/** * 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 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.common.networking.Connection; 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.IdleAtColonyMission; 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.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.freecolandroid.xml.stream.XMLStreamConstants; import org.freecolandroid.xml.stream.XMLStreamException; import org.freecolandroid.xml.stream.XMLStreamReader; import org.freecolandroid.xml.stream.XMLStreamWriter; import org.w3c.dom.Element; /** * Objects of this class contains AI-information for a single {@link Unit}. * * <br> * <br> * * The method {@link #doMission(Connection)} 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 FreeColGameObject this AIObject contains AI-information for. */ private Unit unit; /** * The mission this 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 * it's transport list. This <code>Transportable</code> has not been * scheduled for transport if this value is <code>null</code>. */ private AIUnit 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) { super(aiMain, unit.getId()); this.unit = unit; mission = new UnitWanderHostileMission(aiMain, this); } /** * Creates a new <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param element An <code>Element</code> containing an XML-representation * of this object. */ public AIUnit(AIMain aiMain, Element element) { super(aiMain, element.getAttribute(ID_ATTRIBUTE)); readFromXMLElement(element); } /** * Creates a new <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param in The input stream containing the XML. * @throws XMLStreamException if a problem was encountered during parsing. * @see AIObject#readFromXML */ public AIUnit(AIMain aiMain, XMLStreamReader in) throws XMLStreamException { super(aiMain, in.getAttributeValue(null, ID_ATTRIBUTE)); readFromXML(in); } /** * Creates a new <code>AIUnit</code>. * * @param aiMain The main AI-object. * @param id The unique ID of this object. */ public AIUnit(AIMain aiMain, String id) { super(aiMain, id); unit = (Unit) getAIMain().getFreeColGameObject(id); if (unit == null) { logger.warning("Could not find unit: " + id); } uninitialized = true; } /** * 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(); } /** * 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(); } } /** * Gets the <code>Locatable</code> which should be transported. * * @return The <code>Locatable</code>. */ public Locatable getTransportLocatable() { return unit; } /** * 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; } } /** * 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 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); } } /** * Gets the mission this unit has been assigned. * * @return The <code>Mission</code>. */ public Mission getMission() { return mission; } /** * 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) { logger.finest("Aborting old " + why + " " + mission.getClass().getName() + " for unit " + getUnit()); 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 (oldMission != null) { logger.finest("Replacing old " + oldMission.getClass().getName() + " with " + mission.getClass().getName() + " for unit " + getUnit()); oldMission.dispose(); } this.mission = mission; this.dynamicPriority = 0; } /** * Performs the mission this unit has been assigned. * * @param connection The <code>Connection</code> to use when communicating * with the server. */ public void doMission(Connection connection) { if (mission != null && mission.isValid()) mission.doMission(connection); } /** * Disposes this object and any attached mission. */ public void dispose() { abortMission("disposed"); setTransport(null); super.dispose(); } /** * Returns the ID of this <code>AIObject</code>. * * @return The same ID as the <code>Unit</code> this <code>AIObject</code> * controls. */ public String getId() { if (unit != null) { return unit.getId(); } else { logger.warning("unit == null"); return null; } } public void setGoal(Goal g) { goal = g; } public Goal getGoal() { return goal; } /** * Gets the AIPlayer that owns this AIUnit. * * @return The owning AIPlayer. */ public AIPlayer getAIOwner() { return getAIMain().getAIPlayer(unit.getOwner()); } /** * Convenience accessor for the owning player connection. * * @return The connection. */ public Connection getConnection() { return getAIOwner().getConnection(); } /** * 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; } /** * 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 { if (unit == null || unit.isDisposed()) { logger.warning("Dead AIUnit: " + unit); return; } 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()) { logger.warning("Writing invalid mission: " + mission); } mission.toXML(out); } 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 readFromXMLImpl(XMLStreamReader in) throws XMLStreamException { final String inID = in.getAttributeValue(null, ID_ATTRIBUTE); unit = (Unit) getAIMain().getFreeColGameObject(inID); if (unit == null) { logger.warning("Could not find unit: " + inID); } final String transportStr = in.getAttributeValue(null, "transport"); if (transportStr != null) { transport = (AIUnit) getAIMain().getAIObject(transportStr); if (transport == null) { transport = new AIUnit(getAIMain(), transportStr); } } else { transport = null; } if (in.nextTag() != XMLStreamConstants.END_ELEMENT) { if (in.getLocalName().equals(UnitWanderHostileMission.getXMLElementTagName())) { mission = new UnitWanderHostileMission(getAIMain(), in); } else if (in.getLocalName().equals(UnitWanderMission.getXMLElementTagName())) { mission = new UnitWanderMission(getAIMain(), in); } else if (in.getLocalName().equals(IndianBringGiftMission.getXMLElementTagName())) { mission = new IndianBringGiftMission(getAIMain(), in); } else if (in.getLocalName().equals(BuildColonyMission.getXMLElementTagName())) { mission = new BuildColonyMission(getAIMain(), in); } else if (in.getLocalName().equals(IndianDemandMission.getXMLElementTagName())) { mission = new IndianDemandMission(getAIMain(), in); } else if (in.getLocalName().equals(TransportMission.getXMLElementTagName())) { mission = new TransportMission(getAIMain(), in); } else if (in.getLocalName().equals(WishRealizationMission.getXMLElementTagName())) { mission = new WishRealizationMission(getAIMain(), in); } else if (in.getLocalName().equals(UnitSeekAndDestroyMission.getXMLElementTagName())) { mission = new UnitSeekAndDestroyMission(getAIMain(), in); } else if (in.getLocalName().equals(PioneeringMission.getXMLElementTagName()) // @compat 0.10.3 || in.getLocalName().equals("tileImprovementPlanMission")) { // @end compatibility code mission = new PioneeringMission(getAIMain(), in); } else if (in.getLocalName().equals(DefendSettlementMission.getXMLElementTagName())) { mission = new DefendSettlementMission(getAIMain(), in); } else if (in.getLocalName().equals(WorkInsideColonyMission.getXMLElementTagName())) { mission = new WorkInsideColonyMission(getAIMain(), in); } else if (in.getLocalName().equals(ScoutingMission.getXMLElementTagName())) { mission = new ScoutingMission(getAIMain(), in); } else if (in.getLocalName().equals(CashInTreasureTrainMission.getXMLElementTagName())) { mission = new CashInTreasureTrainMission(getAIMain(), in); } else if (in.getLocalName().equals(IdleAtColonyMission.getXMLElementTagName())) { mission = new IdleAtColonyMission(getAIMain(), in); } else if (in.getLocalName().equals(PrivateerMission.getXMLElementTagName())) { mission = new PrivateerMission(getAIMain(), in); } else { logger.warning("Could not find mission-class for: " + in.getLocalName()); mission = new UnitWanderHostileMission(getAIMain(), this); return; } in.nextTag(); } } /** * Returns a <code>String</code>-representation of this object. * * @return A <code>String</code> representing this object for * debugging purposes. */ @Override public String toString() { return "AIUnit@" + hashCode() + "[ " + unit + " ]"; } /** * Returns the tag name of the root element representing this object. * * @return "aiUnit" */ public static String getXMLElementTagName() { return "aiUnit"; } }