/** * Copyright (C) 2002-2012 The FreeCol Team * * This file is part of FreeCol. * * FreeCol is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * FreeCol is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with FreeCol. If not, see <http://www.gnu.org/licenses/>. */ package net.sf.freecol.server.ai; import java.util.Random; import java.util.logging.Logger; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; import net.sf.freecol.common.model.AbstractGoods; import net.sf.freecol.common.model.EquipmentType; import net.sf.freecol.common.model.Europe; import net.sf.freecol.common.model.Locatable; import net.sf.freecol.common.model.Location; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.Settlement; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.Unit.Role; import net.sf.freecol.server.ai.goal.Goal; import net.sf.freecol.server.ai.mission.BuildColonyMission; import net.sf.freecol.server.ai.mission.CashInTreasureTrainMission; import net.sf.freecol.server.ai.mission.DefendSettlementMission; import net.sf.freecol.server.ai.mission.IdleAtSettlementMission; import net.sf.freecol.server.ai.mission.IndianBringGiftMission; import net.sf.freecol.server.ai.mission.IndianDemandMission; import net.sf.freecol.server.ai.mission.Mission; import net.sf.freecol.server.ai.mission.MissionaryMission; import net.sf.freecol.server.ai.mission.PioneeringMission; import net.sf.freecol.server.ai.mission.PrivateerMission; import net.sf.freecol.server.ai.mission.ScoutingMission; import net.sf.freecol.server.ai.mission.TransportMission; import net.sf.freecol.server.ai.mission.UnitSeekAndDestroyMission; import net.sf.freecol.server.ai.mission.UnitWanderHostileMission; import net.sf.freecol.server.ai.mission.UnitWanderMission; import net.sf.freecol.server.ai.mission.WishRealizationMission; import net.sf.freecol.server.ai.mission.WorkInsideColonyMission; import org.w3c.dom.Element; /** * Objects of this class contains AI-information for a single {@link Unit}. * * <br> * <br> * * The method {@link #doMission()} is called once each turn, by * {@link AIPlayer#startWorking()}, to perform the assigned * <code>Mission</code>. Most of the methods in this class just delegates the * call to that mission. * * @see Mission */ public class AIUnit extends AIObject implements Transportable { private static final Logger logger = Logger.getLogger(AIUnit.class.getName()); /** * The Unit this AIObject contains AI-information for. */ private Unit unit; /** * The mission to which this AI unit has been assigned. */ private Mission mission; /** * The goal this AIUnit belongs to, if one has been assigned. */ private Goal goal = null; /** * The dynamic part of the transport priority. */ private int dynamicPriority; /** * The <code>AIUnit</code> which has this <code>Transportable</code> in * its transport list. */ private AIUnit transport; /** * Creates a new uninitialized <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param id The identifier for the uninitialized unit. */ public AIUnit(AIMain aiMain, String id) { super(aiMain, id); unit = null; mission = null; goal = null; dynamicPriority = 0; transport = null; } /** * Creates a new <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param unit The unit to make an {@link AIObject} for. */ public AIUnit(AIMain aiMain, Unit unit) { this(aiMain, unit.getId()); this.unit = unit; mission = new UnitWanderHostileMission(aiMain, this); uninitialized = getUnit() == null; } /** * Creates a new <code>AIUnit</code> from the given * XML-representation. * * @param aiMain The main AI-object. * @param element The root element for the XML-representation * of a <code>Wish</code>. */ public AIUnit(AIMain aiMain, Element element) { super(aiMain, element); uninitialized = getUnit() == null; } /** * Creates a new <code>AIUnit</code> from the given * XML-representation. * * @param aiMain The main AI-object. * @param in The input stream containing the XML. * @throws XMLStreamException if a problem was encountered * during parsing. */ public AIUnit(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain, in); uninitialized = getUnit() == null; } /** * Disposes this object and any attached mission. */ public void dispose() { getAIOwner().removeAIUnit(this); abortMission("AIUnit-disposed"); setTransport(null); super.dispose(); } /** * Gets this AI object's identifier. * * @return The id of the unit. */ public String getId() { if (unit == null) { logger.warning("Uninitialized AI unit"); return null; } return unit.getId(); } /** * Gets the <code>Unit</code> this <code>AIUnit</code> controls. * * @return The <code>Unit</code>. */ public Unit getUnit() { return unit; } /** * Gets the PRNG to use with this unit. * * @return A <code>Random</code> instance. */ public Random getAIRandom() { return getAIMain().getAIPlayer(unit.getOwner()).getAIRandom(); } /** * Gets the AIPlayer that owns this AIUnit. * * @return The owning AIPlayer. */ public AIPlayer getAIOwner() { return getAIMain().getAIPlayer(unit.getOwner()); } /** * Gets the mission this unit has been assigned. * * @return The <code>Mission</code>. */ public Mission getMission() { return mission; } /** * Moves a unit to the new world. * * @return True if there was no c-s problem. */ public boolean moveToAmerica() { return AIMessage.askMoveTo(this, unit.getOwner().getGame().getMap()); } /** * Moves a unit to Europe. * * @return True if there was no c-s problem. */ public boolean moveToEurope() { return AIMessage.askMoveTo(this, unit.getOwner().getEurope()); } /** * Is this AI unit carrying any cargo (units or goods). * * @return True if the unit has cargo aboard. */ public boolean hasCargo() { return (unit == null) ? false : unit.hasCargo(); } /** * Checks if this unit has been assigned a mission. * * @return <code>true</code> if this unit has a mission. */ public boolean hasMission() { return mission != null; } /** * Aborts a mission. Always use this instead of setMission(null), * and provide a useful reason so that AI mission thrashing can be * tracked down. * * @param why A string describing why the mission is to be aborted * (e.g. "invalid"). */ public void abortMission(String why) { if (mission != null) { if (!mission.isOneTime()) { logger.fine("Mission-ABORT(" + why + "): " + mission); } mission.dispose(); this.mission = null; } } /** * Assigns a mission to unit. The dynamic priority is reset. * Do not call setMission(null), use abortMission above. * * @param mission The new <code>Mission</code>. */ public void setMission(Mission mission) { final Mission oldMission = this.mission; if (mission == oldMission) { return; } else if (oldMission == null) { if (!mission.isOneTime()) { logger.fine("Replacing null mission with " + mission); } } else { String reason = oldMission.invalidReason(); reason = (reason != null) ? "(" + reason + ")" : (oldMission.isOneTime()) ? "(oneTime)" : "(forced)"; if (!(oldMission.isOneTime() && mission.isOneTime())) { logger.fine("Replacing " + reason + " old mission " + oldMission + " with " + mission); } oldMission.dispose(); } this.mission = mission; this.dynamicPriority = 0; } /** * Performs the mission this unit has been assigned. */ public void doMission() { if (mission != null && mission.isValid()) { mission.doMission(); } } /** * Gets the goal of this AI unit. * * @return The goal of this AI unit. */ public Goal getGoal() { return goal; } /** * Sets the goal of this AI unit. * * @param goal The new <code>Goal</code>. */ public void setGoal(Goal goal) { this.goal = goal; } /** * Equips this AI unit for a particular role. * * The unit must be at a location where the required goods are available * (possibly requiring a purchase, which may fail due to lack of gold * or boycotts in effect). * * When multiple equipment types are needed, try them all --- so for * example, if a request is made to equip a unit in Europe as a dragoon * but muskets are boycotted, it may still acquire the horses and end * up as a scout. * * TODO: remove cheat. * * @param r The <code>Role</code> to adopt. * @param cheat Cheat goods purchase in Europe (but *not* boycotts). * @return True if the role change was successful. */ public boolean equipForRole(Role r, boolean cheat) { final Specification spec = getSpecification(); final Player player = unit.getOwner(); Location loc = unit.getLocation(); Europe europe = (loc instanceof Europe) ? (Europe)loc : null; Settlement settlement = loc.getSettlement(); if (settlement == null && europe == null) return false; eq: for (EquipmentType e : r.getRoleEquipment(spec)) { if (!unit.canBeEquippedWith(e)) { // Weed out native/colonial-specific equipment types. continue; } // Check that this should succeed before querying server. if (europe != null) { for (AbstractGoods ag : e.getGoodsRequired()) { if (player.getMarket().getArrears(ag.getType()) > 0) { continue eq; // Boycott prevents purchase. } int cost = player.getMarket().getBidPrice(ag.getType(), ag.getAmount()); if (!player.checkGold(cost)) { if (cheat) { player.modifyGold(cost); } else { continue eq; } } } } else { if (!settlement.canBuildEquipment(e)) continue eq; } // Should now only fail due to comms lossage. AIMessage.askEquipUnit(this, e, 1); } return unit.getRole() == r; } /** * Checks the integrity of this AIUnit. * * @return True if the unit is intact. */ public boolean checkIntegrity() { return super.checkIntegrity() && unit != null && !unit.isDisposed(); } // Transportable interface /** * Gets the number of cargo slots taken by this AI unit. * * @return The number of cargo slots taken. */ public int getSpaceTaken() { return (getUnit() == null) ? 0 : getUnit().getSpaceTaken(); } /** * Returns the source for this <code>Transportable</code>. This is * normally the location of the {@link #getTransportLocatable locatable}. * * @return The source for this <code>Transportable</code>. */ public Location getTransportSource() { return (getUnit() == null || getUnit().isDisposed()) ? null : getUnit().getLocation(); } /** * Returns the destination for this <code>Transportable</code>. * This can either be the target {@link * net.sf.freecol.common.model.Tile} of the transport or the * target for the entire <code>Transportable</code>'s mission. The * target for the tansport is determined by {@link * TransportMission} in the latter case. * * @return The destination for this <code>Transportable</code>. */ public Location getTransportDestination() { return (getUnit() == null || getUnit().isDisposed() || !hasMission()) ? null : mission.getTransportDestination(); } /** * Gets the priority of transporting this <code>Transportable</code> to * it's destination. * * @return The priority of the transport. */ public int getTransportPriority() { if (hasMission()) { return mission.getTransportPriority() + dynamicPriority; } else { return 0; } } /** * Sets the priority of getting the goods to the {@link * #getTransportDestination}. * * @param transportPriority The priority. */ public void setTransportPriority(int transportPriority) { if (hasMission()) { dynamicPriority = transportPriority; } } /** * Increases the transport priority of this <code>Transportable</code>. * This method gets called every turn the <code>Transportable</code> have * not been put on a carrier's transport list. */ public void increaseTransportPriority() { if (hasMission()) { ++dynamicPriority; } } /** * Gets the <code>Locatable</code> which should be transported. * * @return The <code>Locatable</code>. */ public Locatable getTransportLocatable() { return unit; } /** * Gets the carrier responsible for transporting this * <code>Transportable</code>. * * @return The <code>AIUnit</code> which has this * <code>Transportable</code> in it's transport list. This * <code>Transportable</code> has not been scheduled for transport * if this value is <code>null</code>. * */ public AIUnit getTransport() { return transport; } /** * Sets the carrier responsible for transporting this * <code>Transportable</code>. * * @param transport The <code>AIUnit</code> which has this * <code>Transportable</code> in it's transport list. This * <code>Transportable</code> has not been scheduled for * transport if this value is <code>null</code>. * */ public void setTransport(AIUnit transport) { if (this.transport == transport) return; AIUnit oldTransport = this.transport; this.transport = transport; if (oldTransport != null) { // Remove from old carrier: if (oldTransport.getMission() != null && oldTransport.getMission() instanceof TransportMission) { TransportMission tm = (TransportMission) oldTransport.getMission(); if (tm.isOnTransportList(this)) { tm.removeFromTransportList(this); } } } if (transport != null && transport.getMission() instanceof TransportMission && !((TransportMission) transport.getMission()).isOnTransportList(this)) { // Add to new carrier: ((TransportMission) transport.getMission()).addToTransportList(this); } } /** * Aborts the given <code>Wish</code>. * * @param w The <code>Wish</code> to be aborted. */ public void abortWish(Wish w) { if (mission instanceof WishRealizationMission) { // TODO: should we use setMission and dispose the mission as well? mission = null; dynamicPriority = 0; } if (w.getTransportable() == this) { w.dispose(); } } // Serialization /** * Writes this object to an XML stream. * * @param out The target stream. * @throws XMLStreamException if there are any problems writing to the * stream. */ protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException { out.writeStartElement(getXMLElementTagName()); out.writeAttribute(ID_ATTRIBUTE, getId()); if (transport != null) { if (transport.getUnit() == null) { logger.warning("transport.getUnit() == null"); } else if (getAIMain().getAIObject(transport.getId()) == null) { logger.warning("broken reference to transport"); } else if (transport.getMission() != null && transport.getMission() instanceof TransportMission && !((TransportMission) transport.getMission()).isOnTransportList(this)) { logger.warning("We should not be on the transport list."); } else { out.writeAttribute("transport", transport.getUnit().getId()); } } if (mission != null) { if (mission.isValid()) { mission.toXML(out); } else { logger.warning("AI unit with " + mission.invalidReason() + " mission " + mission); } } out.writeEndElement(); } /** * Reads information for this object from an XML stream. * * @param in The input stream with the XML. * @throws XMLStreamException if there are any problems reading from the * stream. */ protected void readAttributes(XMLStreamReader in) throws XMLStreamException { final AIMain aiMain = getAIMain(); String str, tag; str = in.getAttributeValue(null, ID_ATTRIBUTE); unit = aiMain.getGame().getFreeColGameObject(str, Unit.class); str = in.getAttributeValue(null, "transport"); if (str != null) { if ((transport = (AIUnit)aiMain.getAIObject(str)) == null) { transport = new AIUnit(aiMain, str); } } else { transport = null; } } protected void readChild(XMLStreamReader in) throws XMLStreamException { final AIMain aiMain = getAIMain(); String tag = in.getLocalName(); mission = null; if (tag.equals(BuildColonyMission.getXMLElementTagName())) { mission = new BuildColonyMission(aiMain, in); } else if (tag.equals(CashInTreasureTrainMission.getXMLElementTagName())) { mission = new CashInTreasureTrainMission(aiMain, in); } else if (tag.equals(DefendSettlementMission.getXMLElementTagName())) { mission = new DefendSettlementMission(aiMain, in); } else if (tag.equals(IdleAtSettlementMission.getXMLElementTagName()) // @compat 0.10.5 || tag.equals("idleAtColonyMission") // @end compatibility code ) { mission = new IdleAtSettlementMission(aiMain, in); } else if (tag.equals(IndianBringGiftMission.getXMLElementTagName())) { mission = new IndianBringGiftMission(aiMain, in); } else if (tag.equals(IndianDemandMission.getXMLElementTagName())) { mission = new IndianDemandMission(aiMain, in); } else if (tag.equals(MissionaryMission.getXMLElementTagName())) { mission = new MissionaryMission(aiMain, in); } else if (tag.equals(PioneeringMission.getXMLElementTagName()) // @compat 0.10.3 || tag.equals("tileImprovementPlanMission")) { // @end compatibility code mission = new PioneeringMission(aiMain, in); } else if (tag.equals(PrivateerMission.getXMLElementTagName())) { mission = new PrivateerMission(aiMain, in); } else if (tag.equals(ScoutingMission.getXMLElementTagName())) { mission = new ScoutingMission(aiMain, in); } else if (tag.equals(TransportMission.getXMLElementTagName())) { mission = new TransportMission(aiMain, in); } else if (tag.equals(UnitSeekAndDestroyMission.getXMLElementTagName())) { mission = new UnitSeekAndDestroyMission(aiMain, in); } else if (tag.equals(UnitWanderHostileMission.getXMLElementTagName())) { mission = new UnitWanderHostileMission(aiMain, in); } else if (tag.equals(UnitWanderMission.getXMLElementTagName())) { mission = new UnitWanderMission(aiMain, in); } else if (tag.equals(WishRealizationMission.getXMLElementTagName())) { mission = new WishRealizationMission(aiMain, in); } else if (tag.equals(WorkInsideColonyMission.getXMLElementTagName())) { mission = new WorkInsideColonyMission(aiMain, in); } else { throw new IllegalStateException("Unknown AIUnit child: " + tag); } } /** * {@inheritDoc} */ @Override public String toString() { return unit.toString("AIUnit "); } /** * Returns the tag name of the root element representing this object. * * @return "aiUnit" */ public static String getXMLElementTagName() { return "aiUnit"; } }