/** * 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.model; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Random; import java.util.logging.Logger; import net.sf.freecol.common.model.Ability; import net.sf.freecol.common.model.AbstractGoods; import net.sf.freecol.common.model.BuildQueue; import net.sf.freecol.common.model.BuildableType; import net.sf.freecol.common.model.Building; import net.sf.freecol.common.model.BuildingType; import net.sf.freecol.common.model.Colony; import net.sf.freecol.common.model.ColonyTile; import net.sf.freecol.common.model.ExportData; import net.sf.freecol.common.model.Game; import net.sf.freecol.common.model.Goods; import net.sf.freecol.common.model.GoodsContainer; import net.sf.freecol.common.model.GoodsType; import net.sf.freecol.common.model.Market; import net.sf.freecol.common.model.ModelMessage; import net.sf.freecol.common.model.Player; import net.sf.freecol.common.model.ProductionInfo; import net.sf.freecol.common.model.Specification; import net.sf.freecol.common.model.Tile; import net.sf.freecol.common.model.TileImprovement; import net.sf.freecol.common.model.TypeCountMap; import net.sf.freecol.common.model.Unit; import net.sf.freecol.common.model.UnitType; import net.sf.freecol.common.model.UnitTypeChange.ChangeType; import net.sf.freecol.common.model.WorkLocation; import net.sf.freecol.common.util.Utils; import net.sf.freecol.server.control.ChangeSet; import net.sf.freecol.server.control.ChangeSet.See; /** * The server version of a colony. */ public class ServerColony extends Colony implements ServerModelObject { private static final Logger logger = Logger.getLogger(ServerColony.class.getName()); /** * Trivial constructor required for all ServerModelObjects. */ public ServerColony(Game game, String id) { super(game, id); } /** * Creates a new ServerColony. * * @param game The <code>Game</code> in which this object belongs. * @param owner The <code>Player</code> owning this <code>Colony</code>. * @param name The name of the new <code>Colony</code>. * @param tile The location of the <code>Colony</code>. */ public ServerColony(Game game, Player owner, String name, Tile tile) { super(game, owner, name, tile); Specification spec = getSpecification(); setGoodsContainer(new GoodsContainer(game, this)); sonsOfLiberty = 0; oldSonsOfLiberty = 0; established = game.getTurn(); tile.setOwner(owner); if (!tile.hasRoad()) { TileImprovement road = new TileImprovement(game, tile, spec.getTileImprovementType("model.improvement.road")); road.setTurnsToComplete(0); road.setVirtual(true); tile.add(road); } ColonyTile colonyTile = new ServerColonyTile(game, this, tile); colonyTiles.add(colonyTile); for (Tile t : tile.getSurroundingTiles(getRadius())) { colonyTiles.add(new ServerColonyTile(game, this, t)); if (t.getType().isWater()) { landLocked = false; } } // set up default production queues if (landLocked) { buildQueue.add(spec.getBuildingType("model.building.warehouse")); } else { buildQueue.add(spec.getBuildingType("model.building.docks")); getFeatureContainer().addAbility(HAS_PORT); } for (UnitType unitType : spec.getUnitTypesWithAbility("model.ability.bornInColony")) { if (!unitType.getGoodsRequired().isEmpty()) { populationQueue.add(unitType); } } Building building; List<BuildingType> buildingTypes = spec.getBuildingTypeList(); for (BuildingType buildingType : buildingTypes) { if (buildingType.isAutomaticBuild() || isAutomaticBuild(buildingType)) { building = new ServerBuilding(getGame(), this, buildingType); addBuilding(building); } } } /** * Is a goods type needed for a buildable that this colony could * be building. * * @param goodsType The <code>GoodsType</code> to check. * @return True if the goods could be used to build something. */ private boolean neededForBuildableType(GoodsType goodsType) { final Specification spec = getSpecification(); List<BuildableType> buildables = new ArrayList<BuildableType>(); buildables.addAll(spec.getBuildingTypeList()); buildables.addAll(spec.getUnitTypesWithoutAbility("model.ability.person")); for (BuildableType bt : buildables) { if (canBuild(bt)) { for (AbstractGoods ag : bt.getGoodsRequired()) { if (ag.getType() == goodsType) return true; } } } return false; } /** * New turn for this colony. * Try to find out if the colony is going to survive (last colonist does * not starve) before generating lots of production-related messages. * TODO: use the warehouse to store things? * * @param random A <code>Random</code> number source. * @param cs A <code>ChangeSet</code> to update. */ public void csNewTurn(Random random, ChangeSet cs) { logger.finest("ServerColony.csNewTurn, for " + toString()); ServerPlayer owner = (ServerPlayer) getOwner(); Specification spec = getSpecification(); // The AI is prone to removing all units from a colony. // Clean up such cases, to avoid other players seeing the // nonsensical 0-unit colony. if (getUnitCount() <= 0) { logger.warning("Cleaning up 0-unit colony: " + getName()); cs.addDispose(See.perhaps().always(owner), getTile(), this); return; } boolean tileDirty = false; boolean newUnitBorn = false; GoodsContainer container = getGoodsContainer(); container.saveState(); // Check for learning by experience for (WorkLocation workLocation : getCurrentWorkLocations()) { ((ServerModelObject) workLocation).csNewTurn(random, cs); ProductionInfo productionInfo = getProductionInfo(workLocation); if (productionInfo == null) continue; if (!workLocation.isEmpty()) { for (AbstractGoods goods : productionInfo.getProduction()) { UnitType expert = spec.getExpertForProducing(goods.getType()); int experience = goods.getAmount() / workLocation.getUnitCount(); for (Unit unit : workLocation.getUnitList()) { if (goods.getType() == unit.getExperienceType() && unit.getType().canBeUpgraded(expert, ChangeType.EXPERIENCE)) { unit.setExperience(unit.getExperience() + experience); cs.addPartial(See.only(owner), unit, "experience"); } } } } if (workLocation instanceof Building) { // TODO: generalize to other WorkLocations? csCheckMissingInput((Building) workLocation, productionInfo, cs); } } // Check the build queues and build new stuff. If a queue // does a build add it to the built list, so that we can // remove the item built from it *after* applying the // production changes. List<BuildQueue<? extends BuildableType>> built = new ArrayList<BuildQueue<? extends BuildableType>>(); for (BuildQueue<?> queue : new BuildQueue<?>[] { buildQueue, populationQueue }) { ProductionInfo info = getProductionInfo(queue); if (info == null) continue; if (info.getConsumption().isEmpty()) { BuildableType build = queue.getCurrentlyBuilding(); if (build != null) { AbstractGoods needed = new AbstractGoods(); int complete = getTurnsToComplete(build, needed); // Warn if about to fail, or if no useful progress // towards completion is possible. if (complete == -1 && needed.getType() != null) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.MISSING_GOODS, "model.colony.buildableNeedsGoods", this, build) .addName("%colony%", getName()) .add("%buildable%", build.getNameKey()) .addAmount("%amount%", needed.getAmount()) .add("%goodsType%", needed.getType().getNameKey())); } } } else { // Ready to build something. TODO: OO! BuildableType buildable = csNextBuildable(queue, random, cs); if (buildable == null) { ; // It was invalid, ignore. } else if (buildable instanceof UnitType) { Unit newUnit = csBuildUnit(queue, random, cs); if (newUnit.hasAbility(Ability.BORN_IN_COLONY)) { newUnitBorn = true; } built.add(queue); } else if (buildable instanceof BuildingType) { if (csBuildBuilding(queue, cs)) { built.add(queue); // Building *might* have ejected units. tileDirty = true; } } else { throw new IllegalStateException("Bogus buildable: " + buildable); } } } // Apply the accumulated production changes. // Beware that if you try to build something requiring hammers // and tools, as soon as one is updated in the colony the // current production cache is invalidated, and the // recalculated one will see the build as incomplete due to // missing the goods just updated. // Hence the need for a copy of the current production map. TypeCountMap<GoodsType> productionMap = getProductionMap(); for (GoodsType goodsType : productionMap.keySet()) { int net = productionMap.getCount(goodsType); int stored = getGoodsCount(goodsType); if (net + stored <= 0) { removeGoods(goodsType, stored); } else { addGoods(goodsType, net); } // Handle the food situation if (goodsType == spec.getPrimaryFoodType()) { // Check for famine when total primary food goes negative. if (net + stored < 0) { if (getUnitCount() > 1) { Unit victim = Utils.getRandomMember(logger, "Choose starver", getUnitList(), random); cs.addDispose(See.only(owner), null, victim); cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.UNIT_LOST, "model.colony.colonistStarved", this) .addName("%colony%", getName())); } else { // Its dead, Jim. cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.UNIT_LOST, "model.colony.colonyStarved", this) .addName("%colony%", getName())); cs.addDispose(See.perhaps().always(owner), getTile(), this); return; } } else if (net < 0) { int turns = stored / -net; if (turns <= 3 && !newUnitBorn) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WARNING, "model.colony.famineFeared", this) .addName("%colony%", getName()) .addAmount("%number%", turns)); logger.finest("Famine feared in " + getName() + " food=" + stored + " production=" + net + " turns=" + turns); } } } } invalidateCache(); // Now that the goods have been updated it is safe to remove the // built item from its build queue. if (!built.isEmpty()) { for (BuildQueue<? extends BuildableType> queue : built) { switch(queue.getCompletionAction()) { case SHUFFLE: if (queue.size() > 1) { Collections.shuffle(queue.getValues(), random); } break; case REMOVE_EXCEPT_LAST: if (queue.size() == 1 && queue.getCurrentlyBuilding() instanceof UnitType) { // several units can co-exist break; } case REMOVE: default: queue.remove(0); break; } csNextBuildable(queue, random, cs); } tileDirty = true; } // Export goods if custom house is built. // Do not flush price changes yet, as any price change may change // yet again in csYearlyGoodsAdjust. if (hasAbility(Ability.EXPORT)) { boolean gold = false; for (Goods goods : container.getCompactGoods()) { GoodsType type = goods.getType(); ExportData data = getExportData(type); if (data.isExported() && (owner.canTrade(goods, Market.Access.CUSTOM_HOUSE))) { int amount = goods.getAmount() - data.getExportLevel(); if (amount > 0) { owner.sell(container, type, amount, random); gold = true; cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT, "customs.sale", this) .addName("%colony%", getName()) .addAmount("%amount%", amount) .add("%goods%", type.getNameKey())); } } } if (gold) { cs.addPartial(See.only(owner), owner, "gold"); } } // Throw away goods there is no room for, and warn about // levels that will be exceeded next turn int limit = getWarehouseCapacity(); int adjustment = limit / GoodsContainer.CARGO_SIZE; for (Goods goods : container.getCompactGoods()) { GoodsType type = goods.getType(); if (!type.isStorable()) continue; ExportData exportData = getExportData(type); int low = exportData.getLowLevel() * adjustment; int high = exportData.getHighLevel() * adjustment; int amount = goods.getAmount(); int oldAmount = container.getOldGoodsCount(type); if (amount < low && oldAmount >= low && !(type == spec.getPrimaryFoodType() && newUnitBorn)) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WAREHOUSE_CAPACITY, "model.building.warehouseEmpty", this, type) .add("%goods%", type.getNameKey()) .addAmount("%level%", low) .addName("%colony%", getName())); continue; } if (type.limitIgnored()) continue; String messageId = null; int waste = 0; if (amount > limit) { // limit has been exceeded waste = amount - limit; container.removeGoods(type, waste); messageId = "model.building.warehouseWaste"; } else if (amount == limit && oldAmount < limit) { // limit has been reached during this turn messageId = "model.building.warehouseOverfull"; } else if (amount > high && oldAmount <= high) { // high-water-mark has been reached this turn messageId = "model.building.warehouseFull"; } if (messageId != null) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WAREHOUSE_CAPACITY, messageId, this, type) .add("%goods%", type.getNameKey()) .addAmount("%waste%", waste) .addAmount("%level%", high) .addName("%colony%", getName())); } // No problem this turn, but what about the next? if (!(exportData.isExported() && hasAbility(Ability.EXPORT) && owner.canTrade(type, Market.Access.CUSTOM_HOUSE)) && amount <= limit) { int loss = amount + getNetProductionOf(type) - limit; if (loss > 0) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WAREHOUSE_CAPACITY, "model.building.warehouseSoonFull", this, type) .add("%goods%", goods.getNameKey()) .addName("%colony%", getName()) .addAmount("%amount%", loss)); } } } // Check for free buildings for (BuildingType buildingType : spec.getBuildingTypeList()) { if (isAutomaticBuild(buildingType)) { addBuilding(new ServerBuilding(getGame(), this, buildingType)); } } // If the build queue is empty, check that we are not // producing any goods types useful for BuildableTypes, except // if that type is the input to some other form of production. // (Note: isBuildingMaterial is also true for goods used to // produce EquipmentTypes, hence neededForBuildableType). // Such production probably means we forgot to reset the build // queue. Thus, if hammers are being produced it is worth // warning about, but not if producing tools. if (buildQueue.isEmpty()) { for (GoodsType g : spec.getGoodsTypeList()) { if (g.isBuildingMaterial() && !g.isRawMaterial() && getAdjustedNetProductionOf(g) > 0 && neededForBuildableType(g)) { cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.BUILDING_COMPLETED, "model.colony.notBuildingAnything", this) .addName("%colony%", getName())); break; } } } else { } // Update SoL. updateSoL(); if (sonsOfLiberty / 10 != oldSonsOfLiberty / 10) { cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.SONS_OF_LIBERTY, (sonsOfLiberty > oldSonsOfLiberty) ? "model.colony.SoLIncrease" : "model.colony.SoLDecrease", this, spec.getGoodsType("model.goods.bells")) .addAmount("%oldSoL%", oldSonsOfLiberty) .addAmount("%newSoL%", sonsOfLiberty) .addName("%colony%", getName())); ModelMessage govMgtMessage = checkForGovMgtChangeMessage(); if (govMgtMessage != null) { cs.addMessage(See.only(owner), govMgtMessage); } } updateProductionBonus(); // Try to update minimally. if (tileDirty) { cs.add(See.perhaps(), getTile()); } else { cs.add(See.only(owner), this); } } /** * Check a building to see if it is missing input. * * @param building The <code>Building</code> to check. * @param pi The <code>ProductionInfo</code> for the building. * @param cs A <code>ChangeSet</code> to update. */ private void csCheckMissingInput(Building building, ProductionInfo pi, ChangeSet cs) { // Can not happen if the building needs no input or no one is // working there. if (building.canAutoProduce() || building.isEmpty()) return; // Check all goods types produced by this building. If the // output for a type is zero and the maximum production is // non-zero then the building input type must be missing, Emit // a message and return avoiding possible multiple messages // per building. for (AbstractGoods goods : pi.getProduction()) { if (goods.getAmount() > 0) continue; GoodsType type = goods.getType(); for (AbstractGoods g : pi.getMaximumProduction()) { if (g.getType() != type) continue; if (goods.getAmount() >= 0) { type = building.getGoodsInputType(); cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.MISSING_GOODS, "model.building.notEnoughInput", this, type) .add("%inputGoods%", type.getNameKey()) .add("%building%", building.getNameKey()) .addName("%colony%", getName())); return; } break; } } } /** * Build a unit from a build queue. * * @param buildQueue The <code>BuildQueue</code> to find the unit in. * @param random A pseudo-random number source. * @param cs A <code>ChangeSet</code> to update. * @return The unit that was built. */ private Unit csBuildUnit(BuildQueue<? extends BuildableType> buildQueue, Random random, ChangeSet cs) { Unit unit = new ServerUnit(getGame(), getTile(), owner, (UnitType) buildQueue.getCurrentlyBuilding()); if (unit.hasAbility(Ability.BORN_IN_COLONY)) { cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.UNIT_ADDED, "model.colony.newColonist", this, unit) .addName("%colony%", getName())); } else { cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.UNIT_ADDED, "model.colony.unitReady", this, unit) .addName("%colony%", getName()) .addStringTemplate("%unit%", unit.getLabel())); } logger.info("New unit created in " + getName() + ": " + unit); return unit; } /** * Builds a building from a build queue. * * @param buildQueue The <code>BuildQueue</code> to build from. * @param cs A <code>ChangeSet</code> to update. * @return True if the build was successful. */ private boolean csBuildBuilding(BuildQueue<? extends BuildableType> buildQueue, ChangeSet cs) { BuildingType type = (BuildingType) buildQueue.getCurrentlyBuilding(); BuildingType from = type.getUpgradesFrom(); boolean success; if (from == null) { addBuilding(new ServerBuilding(getGame(), this, type)); success = true; } else { Building building = getBuilding(from); success = building.upgrade(); if (!success) { cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.BUILDING_COMPLETED, "colonyPanel.unbuildable", this) .addName("%colony%", getName()) .add("%object%", type.getNameKey())); } } if (success) { tile.updatePlayerExploredTiles(); // See stockade changes cs.addMessage(See.only((ServerPlayer) owner), new ModelMessage(ModelMessage.MessageType.BUILDING_COMPLETED, "model.colony.buildingReady", this) .addName("%colony%", getName()) .add("%building%", type.getNameKey())); if (owner.isAI()) { firePropertyChange(REARRANGE_WORKERS, true, false); } } return success; } /** * Removes a buildable from a build queue, and updates the queue so that * a valid buildable is now being built if possible. * * @param queue The <code>BuildQueue</code> to update. * @param random A pseudo-random number source. * @param cs A <code>ChangeSet</code> to update. * @return The next buildable that can be built, or null if nothing. */ private BuildableType csNextBuildable(BuildQueue<? extends BuildableType> queue, Random random, ChangeSet cs) { Specification spec = getSpecification(); ServerPlayer owner = (ServerPlayer) getOwner(); BuildableType buildable; while ((buildable = queue.getCurrentlyBuilding()) != null) { switch (getNoBuildReason(buildable)) { case NONE: return buildable; case NOT_BUILDING: for (GoodsType goodsType : spec.getGoodsTypeList()) { if (goodsType.isBuildingMaterial() && !goodsType.isStorable() && getProductionOf(goodsType) > 0) { // Production is idle cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WARNING, "model.colony.cannotBuild", this) .addName("%colony%", getName())); } } return null; case POPULATION_TOO_SMALL: cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WARNING, "model.colony.buildNeedPop", this) .addName("%colony%", getName()) .add("%building%", buildable.getNameKey())); break; default: // Are there other warnings to send? cs.addMessage(See.only(owner), new ModelMessage(ModelMessage.MessageType.WARNING, "colonyPanel.unbuildable", this, buildable) .addName("%colony%", getName()) .add("%object%", buildable.getNameKey())); break; } queue.remove(0); } return null; } /** * Evict the users from a tile used by this colony, due to military * action from another unit. * * @param enemyUnit The <code>Unit</code> that has moved in. * @param cs A <code>ChangeSet</code> to update. */ public void csEvictUser(Unit enemyUnit, ChangeSet cs) { ServerPlayer serverPlayer = (ServerPlayer) getOwner(); Tile tile = enemyUnit.getTile(); ServerColonyTile ct = (ServerColonyTile) getColonyTile(tile); if (ct == null || ct.isEmpty()) return; Tile centerTile = getTile(); for (Unit unit : ct.getUnitList()) { unit.setLocation(centerTile); cs.addMessage(See.only(serverPlayer), new ModelMessage(ModelMessage.MessageType.WARNING, "model.colony.workerEvicted", this, this) .addName("%colony%", getName()) .addStringTemplate("%unit%", unit.getLabel()) .addStringTemplate("%location%", tile.getLocationName()) .addStringTemplate("%enemyUnit%", enemyUnit.getLabel())); } cs.add(See.only(serverPlayer), ct); cs.add(See.perhaps(), centerTile); } /** * Returns the tag name of the root element representing this object. * * @return "serverColony" */ public String getServerXMLElementTagName() { return "serverColony"; } }