/**
* 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";
}
}