/**
* 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.control;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.freecol.FreeCol;
import net.sf.freecol.client.gui.i18n.Messages;
import net.sf.freecol.common.model.Ability;
import net.sf.freecol.common.model.AbstractGoods;
import net.sf.freecol.common.model.AbstractUnit;
import net.sf.freecol.common.model.BuildableType;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.ColonyTile;
import net.sf.freecol.common.model.CombatModel.CombatResult;
import net.sf.freecol.common.model.DiplomaticTrade;
import net.sf.freecol.common.model.DiplomaticTrade.TradeStatus;
import net.sf.freecol.common.model.EquipmentType;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.Europe.MigrationType;
import net.sf.freecol.common.model.ExportData;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GameOptions;
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.HighScore;
import net.sf.freecol.common.model.HighSeas;
import net.sf.freecol.common.model.HistoryEvent;
import net.sf.freecol.common.model.IndianNationType;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.Market.Access;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.Monarch;
import net.sf.freecol.common.model.Monarch.MonarchAction;
import net.sf.freecol.common.model.Nameable;
import net.sf.freecol.common.model.Nation;
import net.sf.freecol.common.model.NationSummary;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Player.PlayerType;
import net.sf.freecol.common.model.Player.Stance;
import net.sf.freecol.common.model.RandomRange;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.StringTemplate;
import net.sf.freecol.common.model.Tension;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TileImprovement;
import net.sf.freecol.common.model.TileImprovementType;
import net.sf.freecol.common.model.TradeItem;
import net.sf.freecol.common.model.TradeRoute;
import net.sf.freecol.common.model.TradeRoute.Stop;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Unit.Role;
import net.sf.freecol.common.model.Unit.UnitState;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.UnitTypeChange;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.model.WorkLocation;
import net.sf.freecol.common.networking.ChatMessage;
import net.sf.freecol.common.networking.ChooseFoundingFatherMessage;
import net.sf.freecol.common.networking.Connection;
import net.sf.freecol.common.networking.DOMMessage;
import net.sf.freecol.common.networking.DiplomacyMessage;
import net.sf.freecol.common.networking.GoodsForSaleMessage;
import net.sf.freecol.common.networking.IndianDemandMessage;
import net.sf.freecol.common.networking.LootCargoMessage;
import net.sf.freecol.common.networking.MonarchActionMessage;
import net.sf.freecol.common.util.Introspector;
import net.sf.freecol.common.util.RandomChoice;
import net.sf.freecol.common.util.Utils;
import net.sf.freecol.server.FreeColServer;
import net.sf.freecol.server.ai.AIPlayer;
import net.sf.freecol.server.ai.REFAIPlayer;
import net.sf.freecol.server.control.ChangeSet.ChangePriority;
import net.sf.freecol.server.control.ChangeSet.See;
import net.sf.freecol.server.model.DiplomacySession;
import net.sf.freecol.server.model.LootSession;
import net.sf.freecol.server.model.ServerColony;
import net.sf.freecol.server.model.ServerEurope;
import net.sf.freecol.server.model.ServerGame;
import net.sf.freecol.server.model.ServerIndianSettlement;
import net.sf.freecol.server.model.ServerPlayer;
import net.sf.freecol.server.model.ServerUnit;
import net.sf.freecol.server.model.TradeSession;
import net.sf.freecol.server.model.TransactionSession;
import org.w3c.dom.Element;
/**
* The main server controller.
*/
public final class InGameController extends Controller {
private static Logger logger = Logger.getLogger(InGameController.class.getName());
// TODO: options, spec?
// Alarm adjustments.
public static final int ALARM_NEW_MISSIONARY = -100;
// Score bonus on declaration of independence.
public static final int SCORE_INDEPENDENCE_DECLARED = 100;
// Score bonus on achieving independence.
public static final int SCORE_INDEPENDENCE_GRANTED = 1000;
// The server random number source.
private final Random random;
// Debug helpers
private int debugOnlyAITurns = 0;
private MonarchAction debugMonarchAction = null;
private ServerPlayer debugMonarchPlayer = null;
/**
* The constructor to use.
*
* @param freeColServer The main server object.
* @param random The pseudo-random number source to use.
*/
public InGameController(FreeColServer freeColServer, Random random) {
super(freeColServer);
this.random = random;
}
/**
* Gets the number of AI turns to skip through.
*
* @return The number of terms to skip.
*/
public int getSkippedTurns() {
return (FreeCol.isInDebugMode()) ? debugOnlyAITurns : -1;
}
/**
* Sets the number of AI turns to skip through as a debug helper.
*
* @param turns The number of turns to skip through.
*/
public void setSkippedTurns(int turns) {
if (FreeCol.isInDebugMode()) {
debugOnlyAITurns = turns;
}
}
/**
* Sets a monarch action to debug/test.
*
* @param player The <code>Player</code> whose monarch should act.
* @param action The <code>MonarchAction</code> to be taken.
*/
public void setMonarchAction(Player player, MonarchAction action) {
if (FreeCol.isInDebugMode()) {
debugMonarchPlayer = (ServerPlayer) player;
debugMonarchAction = action;
}
}
/**
* Debug convenience to step the random number generator.
*
* @return The next random number in series, in the range 0-99.
*/
public int stepRandom() {
return Utils.randomInt(logger, "step random", random, 100);
}
/**
* Public version of the yearly goods adjust (public so it can be
* use in the Market test code). Sends the market and change
* messages to the player.
*
* @param serverPlayer The <code>ServerPlayer</code> whose market
* is to be updated.
*/
public void yearlyGoodsAdjust(ServerPlayer serverPlayer) {
ChangeSet cs = new ChangeSet();
serverPlayer.csYearlyGoodsAdjust(random, cs);
sendElement(serverPlayer, cs);
}
/**
* Public version of csAddFoundingFather so it can be used in the
* test code and DebugMenu.
*
* @param serverPlayer The <code>ServerPlayer</code> who gains a father.
* @param father The <code>FoundingFather</code> to add.
*/
public void addFoundingFather(ServerPlayer serverPlayer,
FoundingFather father) {
ChangeSet cs = new ChangeSet();
serverPlayer.csAddFoundingFather(father, random, cs);
sendElement(serverPlayer, cs);
}
/**
* Public change stance and inform all routine. Mostly used in the
* test suite, but the AIs also call it.
*
* @param player The originating <code>Player</code>.
* @param stance The new <code>Stance</code>.
* @param otherPlayer The <code>Player</code> wrt which the stance changes.
* @param symmetric If true, change the otherPlayer stance as well.
*/
public void changeStance(Player player, Stance stance,
Player otherPlayer, boolean symmetric) {
ChangeSet cs = new ChangeSet();
ServerPlayer serverPlayer = (ServerPlayer) player;
if (serverPlayer.csChangeStance(stance, otherPlayer, symmetric, cs)) {
sendToAll(cs);
}
}
/**
* Gets a nation summary.
*
* @param serverPlayer The <code>ServerPlayer</code> that is querying.
* @param player The <code>Player</code> to summarize.
* @return An <code>Element</code> encapsulating this action.
*/
public NationSummary getNationSummary(ServerPlayer serverPlayer,
Player player) {
return new NationSummary(player, serverPlayer);
}
/**
* Move goods from current location to another.
*
* @param goods The <code>Goods</code> to move.
* @param loc The new <code>Location</code>.
*/
public void moveGoods(Goods goods, Location loc)
throws IllegalStateException {
Location oldLoc = goods.getLocation();
if (oldLoc == null) {
throw new IllegalStateException("Goods in null location.");
} else if (loc == null) {
; // Dumping is allowed
} else if (loc instanceof Unit) {
if (((Unit) loc).isInEurope()) {
if (!(oldLoc instanceof Unit && ((Unit) oldLoc).isInEurope())) {
throw new IllegalStateException("Goods and carrier not both in Europe.");
}
} else if (loc.getTile() == null) {
throw new IllegalStateException("Carrier not on the map.");
} else if (oldLoc instanceof Settlement) {
// Can not be co-located when buying from natives, or
// when natives are demanding goods from a colony.
} else if (loc.getTile() != oldLoc.getTile()) {
throw new IllegalStateException("Goods and carrier not co-located.");
}
} else if (loc instanceof IndianSettlement) {
// Can not be co-located when selling to natives.
} else if (loc instanceof Colony) {
if (oldLoc instanceof Unit
&& ((Unit) oldLoc).getOwner() != ((Colony) loc).getOwner()) {
// Gift delivery
} else if (loc.getTile() != oldLoc.getTile()) {
throw new IllegalStateException("Goods and carrier not both in Colony.");
}
} else if (loc.getGoodsContainer() == null) {
throw new IllegalStateException("New location with null GoodsContainer.");
}
// Save state of the goods container/s, allowing simpler updates.
oldLoc.getGoodsContainer().saveState();
if (loc != null) loc.getGoodsContainer().saveState();
oldLoc.remove(goods);
goods.setLocation(null);
if (loc != null) {
loc.add(goods);
goods.setLocation(loc);
}
}
/**
* Create the Royal Expeditionary Force player corresponding to
* a given player that is about to rebel.
* Public for the test suite.
*
* @param serverPlayer The <code>ServerPlayer</code> about to rebel.
* @return The REF player.
*/
public ServerPlayer createREFPlayer(ServerPlayer serverPlayer) {
Nation refNation = serverPlayer.getNation().getRefNation();
Monarch monarch = serverPlayer.getMonarch();
ServerPlayer refPlayer = getFreeColServer().addAIPlayer(refNation);
refPlayer.setEntryLocation(null); // Trigger initial placement routine
Player.makeContact(serverPlayer, refPlayer); // Will change, setup only
// Instantiate the REF in Europe
List<Unit> landUnits
= refPlayer.createUnits(monarch.getREFLandUnits());
List<Unit> navalUnits
= refPlayer.createUnits(monarch.getREFNavalUnits());
List<Unit> unitsList = new ArrayList<Unit>();
unitsList.addAll(navalUnits);
unitsList.addAll(landUnits);
// Embark the land units. For all land units, find a naval
// unit to carry it. Fill greedily, so as if there is excess
// naval capacity then the naval units at the end of the list
// will tend to be empty or very lightly filled, allowing them
// to defend the whole fleet at full strength against the
// rebel navy.
Collections.shuffle(navalUnits, random);
Collections.shuffle(landUnits, random);
for (Unit unit : landUnits) {
for (Unit carrier : navalUnits) {
if (unit.getSpaceTaken() <= carrier.getSpaceLeft()) {
unit.setLocation(carrier);
continue;
}
}
}
// Send the navy on its way
for (Unit u : navalUnits) {
u.setWorkLeft(1);
u.setDestination(getGame().getMap());
u.setLocation(u.getOwner().getHighSeas());
}
return refPlayer;
}
// Client-server communication utilities
// A handler interface to pass to askFuture().
// This will change from DOMMessage to Message when DOM goes away.
private interface DOMMessageHandler {
public DOMMessage handle(DOMMessage message);
};
private class DOMMessageCallable implements Callable<DOMMessage> {
private Connection connection;
private Game game;
private DOMMessage message;
private DOMMessageHandler handler;
public DOMMessageCallable(Connection connection, Game game,
DOMMessage message,
DOMMessageHandler handler) {
this.connection = connection;
this.game = game;
this.message = message;
this.handler = handler;
}
public DOMMessage call() {
Element reply;
try {
reply = connection.askDumping(message.toXMLElement());
} catch (IOException e) {
return null;
}
if (reply == null) return null;
String tag = reply.getTagName();
tag = "net.sf.freecol.common.networking."
+ tag.substring(0, 1).toUpperCase() + tag.substring(1)
+ "Message";
Class[] types = new Class[] { Game.class, Element.class };
Object[] params = new Object[] { game, reply };
DOMMessage message;
try {
message = (DOMMessage)Introspector.instantiate(tag, types,
params);
} catch (IllegalArgumentException e) {
logger.log(Level.WARNING, "Instantiation fail", e);
message = null;
}
return (message == null) ? null : handler.handle(message);
}
};
// A service to run the futures.
private final ExecutorService executor = Executors.newCachedThreadPool();
/**
* Asks a question of a player in a Future.
*
* @param serverPlayer The <code>ServerPlayer</code> to ask.
* @param question The <code>DOMMessage</code> question.
* @param handler The <code>DOMMessageHandler</code> handler to process
* the reply with.
* @return A future encapsulating the result.
*/
private Future<DOMMessage> askFuture(ServerPlayer serverPlayer,
DOMMessage question,
DOMMessageHandler handler) {
Callable<DOMMessage> callable
= new DOMMessageCallable(serverPlayer.getConnection(), getGame(),
question, handler);
return executor.submit(callable);
}
// A place to stash queries that need to be resolved at some point.
private static final List<FutureQuery> outstandingQueries
= new ArrayList<FutureQuery>();
// Trivial way to associate a future with a runnable to resolve it.
private class FutureQuery {
public Future<DOMMessage> future;
public Runnable runnable;
public FutureQuery(Future<DOMMessage> future, Runnable runnable) {
this.future = future;
this.runnable = runnable;
outstandingQueries.add(this);
}
};
/**
* Resolves and clears any outstanding queries.
*/
public void resolveOutstandingQueries() {
FutureQuery fq;
while (!outstandingQueries.isEmpty()) {
fq = outstandingQueries.remove(0);
if (!fq.future.isDone()) {
if (fq.runnable != null) fq.runnable.run();
fq.future.cancel(true);
}
}
}
/**
* Asks a question that must be answered this turn.
*
* @param serverPlayer The <code>ServerPlayer</code> to ask.
* @param question The <code>DOMMessage</code> question.
* @param handler The <code>DOMMessageHandler</code> handler to process
* the reply with.
* @param runnable An optional <code>Runnable</code> to run if the
* question was not answered.
*/
private void askThisTurn(ServerPlayer serverPlayer, DOMMessage message,
DOMMessageHandler handler, Runnable runnable) {
new FutureQuery(askFuture(serverPlayer, message, handler), runnable);
}
/**
* Asks a question of a player with a timeout.
*
* @param serverPlayer The <code>ServerPlayer</code> to ask.
* @param question The <code>DOMMessage</code> question.
* @return The response to the question, or null if none.
*/
private DOMMessage askTimeout(ServerPlayer serverPlayer,
DOMMessage request) {
Future<DOMMessage> future = askFuture(serverPlayer, request,
new DOMMessageHandler() {
public DOMMessage handle(DOMMessage message) {
return message;
}
});
DOMMessage reply;
try {
boolean single = getFreeColServer().isSingleplayer();
reply = future.get(FreeCol.getFreeColTimeout(single),
TimeUnit.SECONDS);
} catch (TimeoutException te) {
sendElement(serverPlayer,
new ChangeSet().addTrivial(See.only(serverPlayer),
"closeMenus", ChangePriority.CHANGE_NORMAL));
reply = null;
} catch (Exception e) {
reply = null;
logger.log(Level.WARNING, "Exception completing future", e);
}
return reply;
}
/**
* Get a list of all server players, optionally excluding supplied ones.
*
* @param serverPlayers The <code>ServerPlayer</code>s to exclude.
* @return A list of all connected server players, with exclusions.
*/
private List<ServerPlayer> getOtherPlayers(ServerPlayer... serverPlayers) {
List<ServerPlayer> result = new ArrayList<ServerPlayer>();
outer: for (Player otherPlayer : getGame().getPlayers()) {
ServerPlayer enemyPlayer = (ServerPlayer) otherPlayer;
if (!enemyPlayer.isConnected()) continue;
for (ServerPlayer exclude : serverPlayers) {
if (enemyPlayer == exclude) continue outer;
}
result.add(enemyPlayer);
}
return result;
}
/**
* Send a set of changes to all players.
*
* @param cs The <code>ChangeSet</code> to send.
*/
private void sendToAll(ChangeSet cs) {
sendToList(getOtherPlayers(), cs);
}
/**
* Send an update to all players except one.
*
* @param serverPlayer A <code>ServerPlayer</code> to exclude.
* @param cs The <code>ChangeSet</code> encapsulating the update.
*/
private void sendToOthers(ServerPlayer serverPlayer, ChangeSet cs) {
sendToList(getOtherPlayers(serverPlayer), cs);
}
/**
* Send an element to all players except one.
* Deprecated, please avoid if possible.
*
* @param serverPlayer A <code>ServerPlayer</code> to exclude.
* @param element An <code>Element</code> to send.
*/
private void sendToOthers(ServerPlayer serverPlayer, Element element) {
sendToList(getOtherPlayers(serverPlayer), element);
}
/**
* Send an update to a list of players.
*
* @param serverPlayers The <code>ServerPlayer</code>s to send to.
* @param cs The <code>ChangeSet</code> encapsulating the update.
*/
private void sendToList(List<ServerPlayer> serverPlayers, ChangeSet cs) {
for (ServerPlayer s : serverPlayers) sendElement(s, cs);
}
/**
* Send an element to a list of players.
* Deprecated, please avoid if possible.
*
* @param serverPlayers The <code>ServerPlayer</code>s to send to.
* @param element An <code>Element</code> to send.
*/
private void sendToList(List<ServerPlayer> serverPlayers, Element element) {
if (element != null) {
for (ServerPlayer s : serverPlayers) {
askElement(s, element);
}
}
}
/**
* Send an element to a specific player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param cs A <code>ChangeSet</code> to build an <code>Element</code> with.
*/
private void sendElement(ServerPlayer serverPlayer, ChangeSet cs) {
askElement(serverPlayer, cs.build(serverPlayer));
}
/**
* Send an element to a specific player.
* Deprecated, please avoid if possible.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param request An <code>Element</code> containing the update.
*/
private Element askElement(ServerPlayer serverPlayer, Element request) {
Connection connection = serverPlayer.getConnection();
if (request == null || connection == null) return null;
Element reply;
try {
reply = connection.askDumping(request);
} catch (IOException e) {
logger.log(Level.WARNING, "Could not send \""
+ request.getTagName() + "\"-message.", e);
reply = null;
}
return reply;
}
/**
* Speaks to a chief in a native settlement, but only if it is as
* a result of a scout actually asking to speak to the chief, or
* for other settlement-contacting events such as missionary
* actions, demanding tribute, learning skills and trading if the
* settlementActionsContactChief game option is enabled.
* It is still unclear what Col1 did here.
*
* @param serverPlayer The <code>ServerPlayer</code> that is contacting
* the settlement.
* @param is The <code>IndianSettlement</code> to contact.
* @param scout True if this contact is due to a scout asking to
* speak to the chief.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csSpeakToChief(ServerPlayer serverPlayer, IndianSettlement is,
boolean scout, ChangeSet cs) {
serverPlayer.csContact((ServerPlayer) is.getOwner(), null, cs);
is.makeContactSettlement(serverPlayer);
if (scout || getGame().getSpecification()
.getBooleanOption("model.option.settlementActionsContactChief")
.getValue()) {
is.setSpokenToChief(serverPlayer);
}
}
// Routines that follow implement the controller response to
// messages.
// The convention is to return an element to be passed back to the
// client by the invoking message handler.
/**
* Ends the turn of the given player.
*
* @param serverPlayer The <code>ServerPlayer</code> to end the turn of.
* @return Null.
*/
public Element endTurn(ServerPlayer serverPlayer) {
FreeColServer freeColServer = getFreeColServer();
ServerGame game = getGame();
ServerPlayer player = (ServerPlayer) game.getCurrentPlayer();
if (serverPlayer != player) {
throw new IllegalArgumentException("It is not "
+ serverPlayer.getName() + "'s turn, it is "
+ ((player == null) ? "noone" : player.getName()) + "'s!");
}
for (;;) {
player.clearModelMessages();
// Has anyone won?
// Do not end single player games where an AI has won,
// that would stop revenge mode.
Player winner = game.checkForWinner();
if (winner != null
&& !(freeColServer.isSingleplayer() && winner.isAI())) {
ChangeSet cs = new ChangeSet();
cs.addTrivial(See.all(), "gameEnded",
ChangePriority.CHANGE_NORMAL,
"winner", winner.getId());
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
// Are there humans left?
// TODO: see if this can be relaxed so we can run large
// AI-only simulations.
boolean human = false;
for (Player p : game.getPlayers()) {
if (!p.isDead() && !p.isAI()
&& ((ServerPlayer) p).isConnected()) {
human = true;
break;
}
}
if (!human) {
game.setCurrentPlayer(null);
return null;
}
// ATM AI player colony-rearrangements do not use the standard
// c-s model but exploit the fact that the AIs and the server
// are still in the same memory space. This means that
// the colony sizes will be wrongly reported unless we take
// the following explicit action.
ChangeSet cs = new ChangeSet();
if (player.isAI() && player.isEuropean()) {
for (Colony c : player.getColonies()) {
cs.add(See.perhaps().except(player), c);
}
}
// Clean up futures from the current player.
resolveOutstandingQueries();
// Check for new turn
if (game.isNextPlayerInNewTurn()) {
game.csNewTurn(random, cs);
if (debugOnlyAITurns > 0) {
if (--debugOnlyAITurns <= 0) {
// If this was a debug run, complete it. This will
// possibly signal the client to save and quit.
if (FreeCol.getDebugRunTurns() > 0) {
FreeCol.completeDebugRun();
}
}
}
}
if ((player = (ServerPlayer) game.getNextPlayer()) == null) {
// "can not happen"
return DOMMessage.clientError("Can not get next player");
}
if (player.checkForDeath()) { // Remove dead players and retry
player.csWithdraw(cs);
sendToAll(cs);
logger.info(player.getNation() + " is dead.");
continue;
} else if (player.isREF() && player.checkForREFDefeat()) {
for (Player p : player.getRebels()) {
csGiveIndependence(player, (ServerPlayer) p, cs);
}
player.csWithdraw(cs);
sendToAll(cs);
logger.info(player.getNation() + " is defeated.");
continue;
}
// Do "new turn"-like actions that need to wait until right
// before the player is about to move.
game.setCurrentPlayer(player);
if (player.isREF() && player.getEntryLocation() == null) {
// Initialize this newly created REF, determining its
// entry location.
// If the teleportREF option is enabled, teleport it in.
REFAIPlayer refAIPlayer = (REFAIPlayer) freeColServer
.getAIPlayer(player);
boolean teleport = getGame().getSpecification()
.getBoolean(GameOptions.TELEPORT_REF);
Tile entry = refAIPlayer.initialize(teleport);
if (entry == null) {
for (Player p : player.getRebels()) {
entry = p.getEntryLocation().getTile();
break;
}
}
player.setEntryLocation(entry);
logger.info(player.getName() + " will appear at " + entry);
if (teleport) {
for (Unit u : player.getUnits()) {
if (u.isNaval()) {
u.setLocation(entry);
u.setWorkLeft(-1);
u.setState(Unit.UnitState.ACTIVE);
}
}
cs.add(See.perhaps(), entry);
}
}
player.csStartTurn(random, cs);
nextFoundingFather(player);
cs.addTrivial(See.all(), "setCurrentPlayer",
ChangePriority.CHANGE_LATE,
"player", player.getId());
Monarch monarch = player.getMonarch();
if (monarch != null) {
MonarchAction action = null;
if (debugMonarchAction != null
&& player == debugMonarchPlayer) {
action = debugMonarchAction;
debugMonarchAction = null;
debugMonarchPlayer = null;
logger.finest("Debug monarch action: " + action);
} else {
action = RandomChoice.getWeightedRandom(logger,
"Choose monarch action", random,
monarch.getActionChoices());
}
if (action != null) {
if (monarch.actionIsValid(action)) {
logger.finest("Monarch action: " + action);
csMonarchAction(player, action, cs);
} else {
logger.finest("Skipping invalid monarch action: "
+ action);
}
}
}
// First, flush accumulated changes to other players.
// Then, if this is an AI or normal connected player in
// non-debug mode then return the accumulated changes directly,
// otherwise flush them out and retry.
sendToOthers(serverPlayer, cs);
if (player.isAI()
|| (player.isConnected() && debugOnlyAITurns <= 0)) {
return cs.build(serverPlayer);
} else {
sendElement(serverPlayer, cs);
}
}
}
/**
* Queries a player to choose their next founding father in a future.
*
* @param serverPlayer The <code>ServerPlayer</code> to ask.
* @return A <code>Future</code> to encapsulate the query.
*/
private void nextFoundingFather(final ServerPlayer serverPlayer) {
if (!serverPlayer.canRecruitFoundingFather()) return;
if (serverPlayer.getOfferedFathers().isEmpty()) {
serverPlayer.setOfferedFathers(serverPlayer
.getRandomFoundingFathers(random));
}
final List<FoundingFather> ffs = serverPlayer.getOfferedFathers();
if (ffs.isEmpty()) return;
askThisTurn(serverPlayer, new ChooseFoundingFatherMessage(ffs),
new DOMMessageHandler() {
public DOMMessage handle(DOMMessage request) {
ChooseFoundingFatherMessage message
= (ChooseFoundingFatherMessage)request;
FoundingFather ff = message.getResult();
if (ff == null) {
logger.warning("No founding father selected");
} else if (!ffs.contains(ff)) {
logger.warning("Invalid founding father: "
+ ff.getId());
} else {
serverPlayer.setCurrentFather(ff);
serverPlayer.clearOfferedFathers();
logger.info("Selected founding father: " + ff);
}
return null;
}
}, null);
}
/**
* Give independence. Note that the REF player is granting, but
* most of the changes happen to the newly independent player.
* hence the special handling.
*
* @param serverPlayer The REF <code>ServerPlayer</code> that is granting.
* @param independent The newly independent <code>ServerPlayer</code>.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csGiveIndependence(ServerPlayer serverPlayer,
ServerPlayer independent, ChangeSet cs) {
serverPlayer.csChangeStance(Stance.PEACE, independent, true, cs);
independent.setPlayerType(PlayerType.INDEPENDENT);
Game game = getGame();
Turn turn = game.getTurn();
independent.modifyScore(SCORE_INDEPENDENCE_GRANTED - turn.getNumber());
independent.setTax(0);
independent.reinitialiseMarket();
cs.addGlobalHistory(game,
new HistoryEvent(turn, HistoryEvent.EventType.INDEPENDENCE));
cs.addMessage(See.only(independent),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.player.independence", independent)
.addStringTemplate("%ref%", serverPlayer.getNationName()));
// Who surrenders?
List<Unit> surrenderUnits = new ArrayList<Unit>();
for (Unit u : serverPlayer.getUnits()) {
if (!u.isNaval()) surrenderUnits.add(u);
}
if (surrenderUnits.size() > 0) {
for (Unit u : surrenderUnits) {
UnitType downgrade = u.getTypeChange(ChangeType.CAPTURE,
independent);
if (downgrade != null) u.setType(downgrade);
u.setOwner(independent);
// Make sure the former owner is notified!
cs.add(See.perhaps().always(serverPlayer), u);
}
cs.addMessage(See.only(independent),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.player.independence.unitsAcquired", independent)
.addStringTemplate("%units%",
unitTemplate(", ", surrenderUnits)));
}
// Update player type. Again, a pity to have to do a whole
// player update, but a partial update works for other players.
cs.addPartial(See.all().except(independent), independent, "playerType");
cs.addMessage(See.all().except(independent),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"model.player.independence.announce", independent)
.addStringTemplate("%nation%", independent.getNationName())
.addStringTemplate("%ref%", serverPlayer.getNationName()));
cs.add(See.only(independent), independent);
}
private StringTemplate unitTemplate(String base, List<Unit> units) {
StringTemplate template = StringTemplate.label(base);
for (Unit u : units) {
template.addStringTemplate(u.getLabel());
}
return template;
}
private StringTemplate abstractUnitTemplate(String base,
List<AbstractUnit> units) {
StringTemplate template = StringTemplate.label(base);
Specification spec = getGame().getSpecification();
for (AbstractUnit au : units) {
template.addStringTemplate(au.getLabel(spec));
}
return template;
}
private String getNonPlayerNation() {
int nations = Nation.EUROPEAN_NATIONS.length;
int start = Utils.randomInt(logger, "Random nation", random, nations);
for (int index = 0; index < nations; index++) {
String nationId = "model.nation."
+ Nation.EUROPEAN_NATIONS[(start + index) % nations];
if (getGame().getPlayer(nationId) == null) {
return nationId + ".name";
}
}
// this should never happen
return "";
}
/**
* Resolves a tax raise.
*
* @param serverPlayer The <code>ServerPlayer</code> whose tax is rising.
* @param taxRaise The amount of tax raise.
* @param goods The <code>Goods</code> for a goods party.
* @param result Whether the tax was accepted or not.
*/
private void raiseTax(ServerPlayer serverPlayer, int taxRaise, Goods goods,
boolean result) {
ChangeSet cs = new ChangeSet();
serverPlayer.csRaiseTax(taxRaise, goods, result, cs);
sendElement(serverPlayer, cs);
}
/**
* Performs a monarch action.
*
* Note that CHANGE_LATE is used so that these actions follow
* setting the current player, so that it is the players turn when
* they respond to a monarch action.
*
* @param serverPlayer The <code>ServerPlayer</code> being acted upon.
* @param action The monarch action.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csMonarchAction(final ServerPlayer serverPlayer,
MonarchAction action, ChangeSet cs) {
final Monarch monarch = serverPlayer.getMonarch();
boolean valid = monarch.actionIsValid(action);
if (!valid) return;
String messageId = "model.monarch.action." + action.toString();
StringTemplate template;
MonarchActionMessage message;
switch (action) {
case NO_ACTION:
break;
case RAISE_TAX_WAR: case RAISE_TAX_ACT:
final int taxRaise = monarch.raiseTax(random);
final Goods goods = serverPlayer.getMostValuableGoods();
if (goods == null) {
logger.finest("Ignoring tax raise, no goods to boycott.");
break;
}
template = StringTemplate.template("model.monarch.action."
+ action.toString())
.addStringTemplate("%goods%", goods.getType().getLabel(true))
.addAmount("%amount%", taxRaise);
if (action == MonarchAction.RAISE_TAX_WAR) {
template = template.add("%nation%", getNonPlayerNation());
} else if (action == MonarchAction.RAISE_TAX_ACT) {
template = template.addAmount("%number%",
Utils.randomInt(logger, "Tax act goods", random, 6))
.addName("%newWorld%", serverPlayer.getNewLandName());
}
message = new MonarchActionMessage(action, template);
message.setTax(taxRaise);
askThisTurn(serverPlayer, message,
new DOMMessageHandler() {
public DOMMessage handle(DOMMessage message) {
boolean result
= (message instanceof MonarchActionMessage)
? ((MonarchActionMessage)message).getResult()
: false;
raiseTax(serverPlayer, taxRaise, goods, result);
return null;
}
},
new Runnable() {
public void run() {
raiseTax(serverPlayer, taxRaise, goods, false);
}
});
break;
case LOWER_TAX_WAR: case LOWER_TAX_OTHER:
int oldTax = serverPlayer.getTax();
int taxLower = monarch.lowerTax(random);
serverPlayer.csSetTax(taxLower, cs);
template = StringTemplate.template(messageId)
.addAmount("%difference%", oldTax - taxLower)
.addAmount("%newTax%", taxLower);
if (action == MonarchAction.LOWER_TAX_WAR) {
template = template.add("%nation%", getNonPlayerNation());
} else {
template = template.addAmount("%number%",
Utils.randomInt(logger, "Lower tax reason", random, 5));
}
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new MonarchActionMessage(action, template));
break;
case WAIVE_TAX:
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_NORMAL,
new MonarchActionMessage(action,
StringTemplate.template(messageId)));
break;
case ADD_TO_REF:
AbstractUnit refAdditions = monarch.chooseForREF(random);
if (refAdditions == null) break;
monarch.addToREF(refAdditions);
template = StringTemplate.template(messageId)
.addAmount("%number%", refAdditions.getNumber())
.add("%unit%", refAdditions.getUnitType(getGame().getSpecification())
.getNameKey());
cs.add(See.only(serverPlayer), monarch);
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new MonarchActionMessage(action, template));
break;
case DECLARE_WAR:
List<Player> enemies = monarch.collectPotentialEnemies();
if (enemies.isEmpty()) break;
Player enemy = Utils.getRandomMember(logger, "Choose enemy",
enemies, random);
serverPlayer.csChangeStance(Stance.WAR, enemy, true, cs);
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new MonarchActionMessage(action,
StringTemplate.template(messageId)
.addStringTemplate("%nation%", enemy.getNationName())));
break;
case SUPPORT_LAND: case SUPPORT_SEA:
boolean sea = action == MonarchAction.SUPPORT_SEA;
List<AbstractUnit> support = monarch.getSupport(random, sea);
if (support.isEmpty()) break;
serverPlayer.createUnits(support);
cs.add(See.only(serverPlayer), serverPlayer.getEurope());
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new MonarchActionMessage(action,
StringTemplate.template(messageId)
.addStringTemplate("%addition%",
abstractUnitTemplate(", ", support))));
break;
case OFFER_MERCENARIES:
final List<AbstractUnit> mercenaries
= monarch.getMercenaries(random);
if (mercenaries.isEmpty()) break;
final int mercPrice = serverPlayer.priceMercenaries(mercenaries);
message = new MonarchActionMessage(MonarchAction.OFFER_MERCENARIES,
StringTemplate.template("model.monarch.action.OFFER_MERCENARIES")
.addAmount("%gold%", mercPrice)
.addStringTemplate("%mercenaries%",
abstractUnitTemplate(", ", mercenaries)));
askThisTurn(serverPlayer, message,
new DOMMessageHandler() {
public DOMMessage handle(DOMMessage message) {
boolean result
= (message instanceof MonarchActionMessage)
? ((MonarchActionMessage)message).getResult()
: false;
if (result) {
ChangeSet cs = new ChangeSet();
serverPlayer.csAddMercenaries(mercenaries,
mercPrice, cs);
sendElement(serverPlayer, cs);
}
return null;
}
}, null);
break;
case DISPLEASURE: default:
logger.warning("Bogus action: " + action);
break;
}
}
/**
* Check the high scores.
*
* @param serverPlayer The <code>ServerPlayer</code> that is retiring.
* @return An element indicating the players high score state.
*/
public Element checkHighScore(ServerPlayer serverPlayer) {
FreeColServer freeColServer = getFreeColServer();
boolean highScore = freeColServer.newHighScore(serverPlayer);
if (highScore) {
try {
freeColServer.saveHighScores();
} catch (Exception e) {
logger.log(Level.WARNING, "Failed to save high scores", e);
highScore = false;
}
}
ChangeSet cs = new ChangeSet();
cs.addAttribute(See.only(serverPlayer),
"highScore", Boolean.toString(highScore));
return cs.build(serverPlayer);
}
/**
* Handle a player retiring.
*
* @param serverPlayer The <code>ServerPlayer</code> that is retiring.
* @return An element cleaning up the player.
*/
public Element retire(ServerPlayer serverPlayer) {
ChangeSet cs = new ChangeSet();
serverPlayer.csWithdraw(cs); // Clean up the player.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Continue playing after winning.
*
* @param serverPlayer The <code>ServerPlayer</code> that plays on.
* @return Null.
*/
public Element continuePlaying(ServerPlayer serverPlayer) {
ServerGame game = (ServerGame) getGame();
Element reply = null;
if (!getFreeColServer().isSingleplayer()) {
logger.warning("Can not continue playing in multiplayer!");
} else if (serverPlayer != game.checkForWinner()) {
logger.warning("Can not continue playing, as "
+ serverPlayer.getName()
+ " has not won the game!");
} else {
Specification spec = game.getSpecification();
spec.getBooleanOption(GameOptions.VICTORY_DEFEAT_REF)
.setValue(false);
spec.getBooleanOption(GameOptions.VICTORY_DEFEAT_EUROPEANS)
.setValue(false);
spec.getBooleanOption(GameOptions.VICTORY_DEFEAT_HUMANS)
.setValue(false);
// The victory panel is shown after end turn, end turn again
// to start turn of next player.
reply = endTurn((ServerPlayer) game.getCurrentPlayer());
}
return reply;
}
/**
* Cash in a treasure train.
*
* @param serverPlayer The <code>ServerPlayer</code> that is cashing in.
* @param unit The treasure train <code>Unit</code> to cash in.
* @return An <code>Element</code> encapsulating this action.
*/
public Element cashInTreasureTrain(ServerPlayer serverPlayer, Unit unit) {
ChangeSet cs = new ChangeSet();
// Work out the cash in amount and the message to send.
int fullAmount = unit.getTreasureAmount();
int cashInAmount;
String messageId;
if (serverPlayer.getPlayerType() == PlayerType.COLONIAL) {
// Charge transport fee and apply tax
cashInAmount = (fullAmount - unit.getTransportFee())
* (100 - serverPlayer.getTax()) / 100;
messageId = "model.unit.cashInTreasureTrain.colonial";
} else {
// No fee possible, no tax applies.
cashInAmount = fullAmount;
messageId = "model.unit.cashInTreasureTrain.independent";
}
serverPlayer.modifyGold(cashInAmount);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold", "score");
cs.addMessage(See.only(serverPlayer),
new ModelMessage(messageId, serverPlayer, unit)
.addAmount("%amount%", fullAmount)
.addAmount("%cashInAmount%", cashInAmount));
messageId = (serverPlayer.getPlayerType() == PlayerType.REBEL
|| serverPlayer.getPlayerType() == PlayerType.INDEPENDENT)
? "model.unit.cashInTreasureTrain.other.independent"
: "model.unit.cashInTreasureTrain.other.colonial";
cs.addMessage(See.all().except(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
messageId, serverPlayer)
.addAmount("%amount%", fullAmount)
.addStringTemplate("%nation%", serverPlayer.getNationName()));
// Dispose of the unit, only visible to the owner.
cs.add(See.only(serverPlayer), (FreeColGameObject) unit.getLocation());
cs.addDispose(See.only(serverPlayer), null, unit);
// Others can see the cash in message.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Declare independence.
*
* @param serverPlayer The <code>ServerPlayer</code> that is naming.
* @param nationName The new name for the independent nation.
* @param countryName The new name for its residents.
* @return An <code>Element</code> encapsulating this action.
*/
public Element declareIndependence(ServerPlayer serverPlayer,
String nationName, String countryName) {
ChangeSet cs = new ChangeSet();
// Cross the Rubicon
StringTemplate oldNation = serverPlayer.getNationName();
serverPlayer.setIndependentNationName(nationName);
serverPlayer.setNewLandName(countryName);
serverPlayer.setPlayerType(PlayerType.REBEL);
serverPlayer.getFeatureContainer().addAbility(new Ability("model.ability.independenceDeclared"));
serverPlayer.modifyScore(SCORE_INDEPENDENCE_DECLARED);
// Do not add history event to cs as we are going to update the
// entire player. Likewise clear model messages.
Turn turn = getGame().getTurn();
cs.addGlobalHistory(getGame(), new HistoryEvent(turn,
HistoryEvent.EventType.DECLARE_INDEPENDENCE));
serverPlayer.clearModelMessages();
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"warOfIndependence.independenceDeclared", serverPlayer));
// Dispose of units in Europe.
Europe europe = serverPlayer.getEurope();
StringTemplate seized = StringTemplate.label(", ");
for (Unit u : europe.getUnitList()) {
seized.addStringTemplate(u.getLabel());
}
if (!seized.getReplacements().isEmpty()) {
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.UNIT_LOST,
"model.player.independence.unitsSeized",
serverPlayer)
.addStringTemplate("%units%", seized));
}
// Generalized continental army muster
java.util.Map<UnitType, UnitType> upgrades
= new HashMap<UnitType, UnitType>();
Specification spec = getGame().getSpecification();
for (UnitType unitType : spec.getUnitTypeList()) {
UnitType upgrade = unitType.getTargetType(ChangeType.INDEPENDENCE,
serverPlayer);
if (upgrade != null) {
upgrades.put(unitType, upgrade);
}
}
for (Colony colony : serverPlayer.getColonies()) {
int sol = colony.getSoL();
if (sol > 50) {
java.util.Map<UnitType, List<Unit>> unitMap = new HashMap<UnitType, List<Unit>>();
List<Unit> allUnits = colony.getTile().getUnitList();
allUnits.addAll(colony.getUnitList());
for (Unit unit : allUnits) {
if (upgrades.containsKey(unit.getType())) {
List<Unit> unitList = unitMap.get(unit.getType());
if (unitList == null) {
unitList = new ArrayList<Unit>();
unitMap.put(unit.getType(), unitList);
}
unitList.add(unit);
}
}
for (Entry<UnitType, List<Unit>> entry : unitMap.entrySet()) {
int limit = (entry.getValue().size() + 2) * (sol - 50) / 100;
if (limit > 0) {
for (int index = 0; index < limit; index++) {
Unit unit = entry.getValue().get(index);
if (unit == null) break;
unit.setType(upgrades.get(entry.getKey()));
cs.add(See.only(serverPlayer), unit);
}
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.UNIT_IMPROVED,
"model.player.continentalArmyMuster",
serverPlayer, colony)
.addName("%colony%", colony.getName())
.addAmount("%number%", limit)
.add("%oldUnit%", entry.getKey().getNameKey())
.add("%unit%", upgrades.get(entry.getKey()).getNameKey()));
}
}
}
}
// Create the REF.
ServerPlayer refPlayer = createREFPlayer(serverPlayer);
// Now the REF is ready, we can dispose of the European connection.
serverPlayer.getHighSeas().removeDestination(europe);
cs.addDispose(See.only(serverPlayer), null, europe);
serverPlayer.setEurope(null);
serverPlayer.setMonarch(null);
// Pity to have to update such a heavy object as the player,
// but we do this, at most, once per player. Other players
// only need a partial player update and the stance change.
// Put the stance change after the name change so that the
// other players see the new nation name declaring war. The
// REF is hardwired to declare war on rebels so there is no
// need to adjust its stance or tension.
cs.addPartial(See.all().except(serverPlayer), serverPlayer,
"playerType", "independentNationName", "newLandName");
cs.addMessage(See.all().except(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"declareIndependence.announce",
serverPlayer)
.addStringTemplate("%oldNation%", oldNation)
.addStringTemplate("%newNation%", serverPlayer.getNationName())
.add("%ruler%", serverPlayer.getRulerNameKey()));
cs.add(See.only(serverPlayer), serverPlayer);
serverPlayer.csChangeStance(Stance.WAR, refPlayer, true, cs);
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Rename an object.
*
* @param serverPlayer The <code>ServerPlayer</code> that is naming.
* @param object The <code>Nameable</code> to rename.
* @param newName The new name.
* @return An <code>Element</code> encapsulating this action.
*/
public Element renameObject(ServerPlayer serverPlayer, Nameable object,
String newName) {
ChangeSet cs = new ChangeSet();
object.setName(newName);
FreeColGameObject fcgo = (FreeColGameObject) object;
cs.addPartial(See.all(), fcgo, "name");
// Others may be able to see the name change.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Gets a settlement transaction session, either existing or
* newly opened.
*
* @param serverPlayer The <code>ServerPlayer</code> that is trading.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
* @return An <code>Element</code> encapsulating this action.
*/
public Element getTransaction(ServerPlayer serverPlayer, Unit unit,
Settlement settlement) {
ChangeSet cs = new ChangeSet();
TradeSession session = TransactionSession.lookup(TradeSession.class,
unit, settlement);
if (session == null) {
if (unit.getMovesLeft() <= 0) {
return DOMMessage.clientError("Unit " + unit.getId()
+ " has no moves left.");
}
session = new TradeSession(unit, settlement);
// Sets unit moves to zero to avoid cheating. If no
// action is taken, the moves will be restored when
// closing the session
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
}
// Add just the attributes the client needs.
cs.addAttribute(See.only(serverPlayer), "canBuy",
Boolean.toString(session.getBuy()));
cs.addAttribute(See.only(serverPlayer), "canSell",
Boolean.toString(session.getSell()));
cs.addAttribute(See.only(serverPlayer), "canGift",
Boolean.toString(session.getGift()));
// Others can not see transactions.
return cs.build(serverPlayer);
}
/**
* Close a transaction.
*
* @param serverPlayer The <code>ServerPlayer</code> that is trading.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
* @return An <code>Element</code> encapsulating this action.
*/
public Element closeTransaction(ServerPlayer serverPlayer, Unit unit,
Settlement settlement) {
TradeSession session
= TradeSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("No such transaction session.");
}
ChangeSet cs = new ChangeSet();
// Restore unit movement if no action taken.
if (!session.getActionTaken()) {
unit.setMovesLeft(session.getMovesLeft());
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
}
session.complete(cs);
// Others can not see end of transaction.
return cs.build(serverPlayer);
}
/**
* Get the goods for sale in a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is enquiring.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
* @return An <code>Element</code> encapsulating this action.
*/
public Element getGoodsForSale(ServerPlayer serverPlayer, Unit unit,
Settlement settlement) {
List<Goods> sellGoods = null;
if (settlement instanceof IndianSettlement) {
IndianSettlement indianSettlement = (IndianSettlement) settlement;
AIPlayer aiPlayer = getFreeColServer()
.getAIPlayer(indianSettlement.getOwner());
sellGoods = indianSettlement.getSellGoods(3, unit);
for (Goods goods : sellGoods) {
aiPlayer.registerSellGoods(goods);
}
} else { // Colony might be supported one day?
return DOMMessage.clientError("Bogus settlement");
}
return new GoodsForSaleMessage(unit, settlement, sellGoods)
.toXMLElement();
}
/**
* Price some goods for sale from a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is buying.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
* @param goods The <code>Goods</code> to buy.
* @param price The buyers proposed price for the goods.
* @return An <code>Element</code> encapsulating this action.
*/
public Element buyProposition(ServerPlayer serverPlayer,
Unit unit, Settlement settlement,
Goods goods, int price) {
TradeSession session
= TradeSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("Proposing to buy without opening a transaction session?!");
}
if (!session.getBuy()) {
return DOMMessage.clientError("Proposing to buy in a session where buying is not allowed.");
}
ChangeSet cs = new ChangeSet();
if (settlement instanceof IndianSettlement) {
csSpeakToChief(serverPlayer, (IndianSettlement)settlement,
false, cs);
}
// AI considers the proposition, return with a gold value
AIPlayer ai = getFreeColServer().getAIPlayer(settlement.getOwner());
int gold = ai.buyProposition(unit, settlement, goods, price);
// Others can not see proposals.
cs.addAttribute(See.only(serverPlayer),
"gold", Integer.toString(gold));
return cs.build(serverPlayer);
}
/**
* Price some goods for sale to a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is buying.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
* @param goods The <code>Goods</code> to sell.
* @param price The sellers proposed price for the goods.
* @return An <code>Element</code> encapsulating this action.
*/
public Element sellProposition(ServerPlayer serverPlayer,
Unit unit, Settlement settlement,
Goods goods, int price) {
TradeSession session
= TradeSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("Proposing to sell without opening a transaction session");
}
if (!session.getSell()) {
return DOMMessage.clientError("Proposing to sell in a session where selling is not allowed.");
}
ChangeSet cs = new ChangeSet();
if (settlement instanceof IndianSettlement) {
csSpeakToChief(serverPlayer, (IndianSettlement)settlement,
false, cs);
}
// AI considers the proposition, return with a gold value
AIPlayer ai = getFreeColServer().getAIPlayer(settlement.getOwner());
int gold = ai.sellProposition(unit, settlement, goods, price);
// Others can not see proposals.
cs.addAttribute(See.only(serverPlayer),
"gold", Integer.toString(gold));
return cs.build(serverPlayer);
}
/**
* Buy goods in Europe.
*
* @param serverPlayer The <code>ServerPlayer</code> that is buying.
* @param unit The <code>Unit</code> to carry the goods.
* @param type The <code>GoodsType</code> to buy.
* @param amount The amount of goods to buy.
* @return An <code>Element</code> encapsulating this action.
*/
public Element buyGoods(ServerPlayer serverPlayer, Unit unit,
GoodsType type, int amount) {
if (!serverPlayer.canTrade(type, Access.EUROPE)) {
return DOMMessage.clientError("Can not trade boycotted goods");
}
ChangeSet cs = new ChangeSet();
GoodsContainer container = unit.getGoodsContainer();
container.saveState();
serverPlayer.buy(container, type, amount, random);
serverPlayer.csFlushMarket(type, cs);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
cs.add(See.only(serverPlayer), container);
// Action occurs in Europe, nothing is visible to other players.
return cs.build(serverPlayer);
}
/**
* Sell goods in Europe.
*
* @param serverPlayer The <code>ServerPlayer</code> that is selling.
* @param unit The <code>Unit</code> carrying the goods.
* @param type The <code>GoodsType</code> to sell.
* @param amount The amount of goods to sell.
* @return An <code>Element</code> encapsulating this action.
*/
public Element sellGoods(ServerPlayer serverPlayer, Unit unit,
GoodsType type, int amount) {
if (!serverPlayer.canTrade(type, Access.EUROPE)) {
return DOMMessage.clientError("Can not trade boycotted goods");
}
ChangeSet cs = new ChangeSet();
GoodsContainer container = unit.getGoodsContainer();
container.saveState();
serverPlayer.sell(container, type, amount, random);
serverPlayer.csFlushMarket(type, cs);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
cs.add(See.only(serverPlayer), container);
// Action occurs in Europe, nothing is visible to other players.
return cs.build(serverPlayer);
}
/**
* A unit migrates from Europe.
*
* @param serverPlayer The <code>ServerPlayer</code> whose unit it will be.
* @param slot The slot within <code>Europe</code> to select the unit from.
* @param type The type of migration occurring.
* @return An <code>Element</code> encapsulating this action.
*/
public Element emigrate(ServerPlayer serverPlayer, int slot,
MigrationType type) {
ChangeSet cs = new ChangeSet();
serverPlayer.csEmigrate(slot, type, random, cs);
// Do not update others, emigration is private.
return cs.build(serverPlayer);
}
/**
* Move a unit.
*
* @param serverPlayer The <code>ServerPlayer</code> that is moving.
* @param unit The <code>Unit</code> to move.
* @param newTile The <code>Tile</code> to move to.
* @return An <code>Element</code> encapsulating this action.
*/
public Element move(ServerPlayer serverPlayer, Unit unit, Tile newTile) {
ChangeSet cs = new ChangeSet();
((ServerUnit) unit).csMove(newTile, random, cs);
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Decline to investigate strange mounds.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param tile The <code>Tile</code> where the mounds are.
* @return An <code>Element</code> encapsulating this action.
*/
public Element declineMounds(ServerPlayer serverPlayer, Tile tile) {
tile.removeLostCityRumour();
// Others might see rumour disappear
ChangeSet cs = new ChangeSet();
cs.add(See.perhaps(), tile);
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Set land name.
*
* @param serverPlayer The <code>ServerPlayer</code> who landed.
* @param unit The <code>Unit</code> that has come ashore.
* @param name The new land name.
* @param welcomer An optional <code>ServerPlayer</code> that has offered
* a treaty.
* @param camps An optional number of camps for the welcome message.
* @param accept True if the treaty has been accepted.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setNewLandName(ServerPlayer serverPlayer, Unit unit,
String name, ServerPlayer welcomer, int camps,
boolean accept) {
ChangeSet cs = new ChangeSet();
// Special case of a welcome from an adjacent native unit,
// offering the land the landing unit is on if a peace treaty
// is accepted.
serverPlayer.setNewLandName(name);
if (welcomer != null) {
if (accept) { // Claim land
Tile tile = unit.getTile();
tile.changeOwnership(serverPlayer, null);
cs.add(See.perhaps(), tile);
} else { // Consider not accepting the treaty to be an insult.
cs.add(See.only(null).perhaps(serverPlayer),
welcomer.modifyTension(serverPlayer,
Tension.TENSION_ADD_MAJOR));
}
}
// Update the name and note the history.
cs.addPartial(See.only(serverPlayer), serverPlayer, "newLandName");
Turn turn = serverPlayer.getGame().getTurn();
cs.addHistory(serverPlayer,
new HistoryEvent(turn, HistoryEvent.EventType.DISCOVER_NEW_WORLD)
.addName("%name%", name));
// Only the tile change is not private.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Set region name.
*
* @param serverPlayer The <code>ServerPlayer</code> discovering.
* @param region The <code>Region</code> to discover.
* @param name The new region name.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setNewRegionName(ServerPlayer serverPlayer,
Region region, String name) {
ChangeSet cs = new ChangeSet();
cs.addRegion(serverPlayer, region, name);
// Others do find out about region name changes.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Move a unit across the high seas.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to move.
* @param destination The <code>Location</code> to move to.
* @return An <code>Element</code> encapsulating this action.
*/
public Element moveTo(ServerPlayer serverPlayer, Unit unit,
Location destination) {
ChangeSet cs = new ChangeSet();
HighSeas highSeas = serverPlayer.getHighSeas();
Location current = unit.getDestination();
boolean others = false; // Notify others?
boolean invalid = false; // Not a highSeas move?
if (destination instanceof Europe) {
if (!highSeas.getDestinations().contains(destination)) {
return DOMMessage.clientError("HighSeas does not connect to: "
+ ((FreeColGameObject) destination).getId());
} else if (unit.getLocation() == highSeas) {
if (!(current instanceof Europe)) {
// Changed direction
unit.setWorkLeft(unit.getSailTurns()
- unit.getWorkLeft() + 1);
}
unit.setDestination(destination);
cs.add(See.only(serverPlayer), unit, highSeas);
} else if (unit.getTile() != null) {
Tile tile = unit.getTile();
unit.setEntryLocation(tile);
unit.setWorkLeft(unit.getSailTurns());
unit.setDestination(destination);
unit.setMovesLeft(0);
unit.setLocation(highSeas);
cs.addDisappear(serverPlayer, tile, unit);
cs.add(See.only(serverPlayer), tile, highSeas);
others = true;
} else {
invalid = true;
}
} else if (destination instanceof Map) {
if (!highSeas.getDestinations().contains(destination)) {
return DOMMessage.clientError("HighSeas does not connect to: "
+ ((FreeColGameObject) destination).getId());
} else if (unit.getLocation() == highSeas) {
if (current != destination && (current == null
|| current.getTile() == null
|| current.getTile().getMap() != destination)) {
// Changed direction
unit.setWorkLeft(unit.getSailTurns()
- unit.getWorkLeft() + 1);
}
unit.setDestination(destination);
cs.add(See.only(serverPlayer), highSeas);
} else if (unit.getLocation() instanceof Europe) {
Europe europe = (Europe) unit.getLocation();
unit.setWorkLeft(unit.getSailTurns());
unit.setDestination(destination);
unit.setMovesLeft(0);
unit.setLocation(highSeas);
cs.add(See.only(serverPlayer), europe, highSeas);
} else {
invalid = true;
}
} else if (destination instanceof Settlement) {
Tile tile = destination.getTile();
if (!highSeas.getDestinations().contains(tile.getMap())) {
return DOMMessage.clientError("HighSeas does not connect to: "
+ ((FreeColGameObject) destination).getId());
} else if (unit.getLocation() == highSeas) {
// Direction is somewhat moot, so just reset.
unit.setWorkLeft(unit.getSailTurns());
unit.setDestination(destination);
cs.add(See.only(serverPlayer), highSeas);
} else if (unit.getLocation() instanceof Europe) {
Europe europe = (Europe) unit.getLocation();
unit.setWorkLeft(unit.getSailTurns());
unit.setDestination(destination);
unit.setMovesLeft(0);
unit.setLocation(highSeas);
cs.add(See.only(serverPlayer), europe, highSeas);
} else {
invalid = true;
}
} else {
return DOMMessage.clientError("Bogus moveTo destination: "
+ ((FreeColGameObject) destination).getId());
}
if (invalid) {
return DOMMessage.clientError("Invalid moveTo: unit=" + unit.getId()
+ " from=" + ((FreeColGameObject) unit.getLocation()).getId()
+ " to=" + ((FreeColGameObject) destination).getId());
}
if (others) sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Embark a unit onto a carrier.
* Checking that the locations are appropriate is not done here.
*
* @param serverPlayer The <code>ServerPlayer</code> embarking.
* @param unit The <code>Unit</code> that is embarking.
* @param carrier The <code>Unit</code> to embark onto.
* @return An <code>Element</code> encapsulating this action.
*/
public Element embarkUnit(ServerPlayer serverPlayer, Unit unit,
Unit carrier) {
if (unit.isNaval()) {
return DOMMessage.clientError("Naval unit " + unit.getId()
+ " can not embark.");
}
if (carrier.getSpaceLeft() < unit.getSpaceTaken()) {
return DOMMessage.clientError("No space available for unit "
+ unit.getId() + " to embark.");
}
ChangeSet cs = new ChangeSet();
Location oldLocation = unit.getLocation();
unit.setLocation(carrier);
unit.setMovesLeft(0);
cs.add(See.only(serverPlayer), (FreeColGameObject) oldLocation);
if (carrier.getLocation() != oldLocation) {
cs.add(See.only(serverPlayer), carrier);
}
if (oldLocation instanceof Tile) {
cs.addMove(See.only(serverPlayer), unit, (Tile) oldLocation,
carrier.getTile());
cs.addDisappear(serverPlayer, (Tile) oldLocation, unit);
}
// Others might see the unit disappear, or the carrier capacity.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Disembark unit from a carrier.
*
* @param serverPlayer The <code>ServerPlayer</code> whose unit is
* embarking.
* @param unit The <code>Unit</code> that is disembarking.
* @return An <code>Element</code> encapsulating this action.
*/
public Element disembarkUnit(ServerPlayer serverPlayer, Unit unit) {
if (unit.isNaval()) {
return DOMMessage.clientError("Naval unit " + unit.getId()
+ " can not disembark.");
}
if (!(unit.getLocation() instanceof Unit)) {
return DOMMessage.clientError("Unit " + unit.getId()
+ " is not embarked.");
}
ChangeSet cs = new ChangeSet();
Unit carrier = (Unit) unit.getLocation();
Location newLocation = carrier.getLocation();
List<Tile> newTiles = (newLocation.getTile() == null) ? null
: ((ServerUnit) unit).collectNewTiles(newLocation.getTile());
unit.setLocation(newLocation);
unit.setMovesLeft(0); // In Col1 disembark consumes whole move.
cs.add(See.perhaps(), (FreeColGameObject) newLocation);
if (newTiles != null) {
serverPlayer.csSeeNewTiles(newTiles, cs);
}
// Others can (potentially) see the location.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Combat.
*
* @param attackerPlayer The <code>ServerPlayer</code> who is attacking.
* @param attacker The <code>FreeColGameObject</code> that is attacking.
* @param defender The <code>FreeColGameObject</code> that is defending.
* @param crs A list of <code>CombatResult</code>s defining the result.
* @return An <code>Element</code> encapsulating this action.
*/
public Element combat(ServerPlayer attackerPlayer,
FreeColGameObject attacker,
FreeColGameObject defender,
List<CombatResult> crs) {
ChangeSet cs = new ChangeSet();
try {
attackerPlayer.csCombat(attacker, defender, crs, random, cs);
} catch (Exception e) {
logger.log(Level.WARNING, "Combat FAIL", e);
return DOMMessage.clientError(e.getMessage());
}
sendToOthers(attackerPlayer, cs);
return cs.build(attackerPlayer);
}
/**
* Ask about learning a skill at an IndianSettlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is learning.
* @param unit The <code>Unit</code> that is learning.
* @param settlement The <code>Settlement</code> to learn from.
* @return An <code>Element</code> encapsulating this action.
*/
public Element askLearnSkill(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement) {
ChangeSet cs = new ChangeSet();
csSpeakToChief(serverPlayer, settlement, false, cs);
Tile tile = settlement.getTile();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
// Do not update others, nothing to see yet.
return cs.build(serverPlayer);
}
/**
* Learn a skill at an IndianSettlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is learning.
* @param unit The <code>Unit</code> that is learning.
* @param settlement The <code>Settlement</code> to learn from.
* @return An <code>Element</code> encapsulating this action.
*/
public Element learnFromIndianSettlement(ServerPlayer serverPlayer,
Unit unit,
IndianSettlement settlement) {
// Sanity checks.
UnitType skill = settlement.getLearnableSkill();
if (skill == null) {
return DOMMessage.clientError("No skill to learn at "
+ settlement.getName());
}
if (!unit.getType().canBeUpgraded(skill, ChangeType.NATIVES)) {
return DOMMessage.clientError("Unit " + unit.toString()
+ " can not learn skill " + skill
+ " at " + settlement.getName());
}
// Try to learn
ChangeSet cs = new ChangeSet();
unit.setMovesLeft(0);
csSpeakToChief(serverPlayer, settlement, false, cs);
switch (settlement.getAlarm(serverPlayer).getLevel()) {
case HATEFUL: // Killed, might be visible to other players.
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) unit.getLocation());
cs.addDispose(See.perhaps().always(serverPlayer),
unit.getLocation(), unit);
break;
case ANGRY: // Learn nothing, not even a pet update
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
break;
default:
// Teach the unit, and expend the skill if necessary.
// Do a full information update as the unit is in the settlement.
unit.setType(skill);
if (!settlement.isCapital()
&& !(settlement.getMissionary(serverPlayer) != null
&& getGame().getSpecification()
.getBoolean("model.option.enhancedMissionaries"))) {
settlement.setLearnableSkill(null);
}
break;
}
Tile tile = settlement.getTile();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), unit, tile);
// Others always see the unit, it may have died or been taught.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Demand a tribute from a native settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> demanding the tribute.
* @param unit The <code>Unit</code> that is demanding the tribute.
* @param settlement The <code>IndianSettlement</code> demanded of.
* @return An <code>Element</code> encapsulating this action.
* TODO: Move TURNS_PER_TRIBUTE magic number to the spec.
*/
public Element demandTribute(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement) {
ChangeSet cs = new ChangeSet();
final int TURNS_PER_TRIBUTE = 5;
csSpeakToChief(serverPlayer, settlement, false, cs);
Player indianPlayer = settlement.getOwner();
int gold = 0;
int year = getGame().getTurn().getNumber();
RandomRange gifts = settlement.getType().getGifts(unit);
if (settlement.getLastTribute() + TURNS_PER_TRIBUTE < year
&& gifts != null) {
switch (indianPlayer.getTension(serverPlayer).getLevel()) {
case HAPPY: case CONTENT:
gold = Math.min(gifts.getAmount("Tribute", random, true) / 10,
100);
break;
case DISPLEASED:
gold = Math.min(gifts.getAmount("Tribute", random, true) / 20,
100);
break;
case ANGRY: case HATEFUL:
default:
gold = 0; // No tribute for you.
break;
}
}
// Increase tension whether we paid or not. Apply tension
// directly to the settlement and let propagation work.
cs.add(See.only(serverPlayer),
((ServerIndianSettlement)settlement).modifyAlarm(serverPlayer,
Tension.TENSION_ADD_NORMAL));
settlement.setLastTribute(year);
ModelMessage m;
if (gold > 0) {
indianPlayer.modifyGold(-gold);
serverPlayer.modifyGold(gold);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold", "score");
m = new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"scoutSettlement.tributeAgree",
unit, settlement)
.addAmount("%amount%", gold);
} else {
m = new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"scoutSettlement.tributeDisagree",
unit, settlement);
}
cs.addMessage(See.only(serverPlayer), m);
Tile tile = settlement.getTile();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
// Do not update others, this is all private.
return cs.build(serverPlayer);
}
/**
* Scout a native settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is scouting.
* @param unit The scout <code>Unit</code>.
* @param settlement The <code>IndianSettlement</code> to scout.
* @return An <code>Element</code> encapsulating this action.
*/
public Element scoutIndianSettlement(ServerPlayer serverPlayer,
Unit unit,
IndianSettlement settlement) {
ChangeSet cs = new ChangeSet();
Tile tile = settlement.getTile();
boolean tileDirty = settlement.makeContactSettlement(serverPlayer);
String result;
// Hateful natives kill the scout right away.
Tension tension = settlement.getAlarm(serverPlayer);
if (tension.getLevel() == Tension.Level.HATEFUL) {
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) unit.getLocation());
cs.addDispose(See.perhaps().always(serverPlayer),
unit.getLocation(), unit);
result = "die";
} else {
// Otherwise player gets to visit, and learn about the settlement.
int radius = unit.getLineOfSight();
UnitType skill = settlement.getLearnableSkill();
if (settlement.hasSpokenToChief()) {
// Do nothing if already spoken to.
result = "nothing";
} else if (skill != null
&& skill.hasAbility(Ability.EXPERT_SCOUT)
&& unit.getType().canBeUpgraded(skill,
ChangeType.NATIVES)) {
// If the scout can be taught to be an expert it will be.
// TODO: in the old code the settlement retains the
// teaching ability. WWC1D?
unit.setType(settlement.getLearnableSkill());
result = "expert";
} else {
// Choose tales 1/3 of the time, or if there are no beads.
RandomRange gifts = settlement.getType().getGifts(unit);
int gold = (gifts == null) ? 0
: gifts.getAmount("Base beads amount", random, true);
if (gold <= 0
|| Utils.randomInt(logger, "Tales", random, 3) == 0) {
radius = Math.max(radius, IndianSettlement.TALES_RADIUS);
result = "tales";
} else {
if (unit.hasAbility(Ability.EXPERT_SCOUT)) {
gold = (gold * 11) / 10; // TODO: magic number
}
serverPlayer.modifyGold(gold);
settlement.getOwner().modifyGold(-gold);
result = "beads";
}
}
// Have now spoken to the chief.
csSpeakToChief(serverPlayer, settlement, true, cs);
tileDirty = true;
// Update settlement tile with new information, and any
// newly visible tiles, possibly with enhanced radius.
for (Tile t : tile.getSurroundingTiles(radius)) {
if (!serverPlayer.canSee(t) && (t.isLand() || t.isCoast())) {
serverPlayer.setExplored(t);
cs.add(See.only(serverPlayer), t);
}
}
// If the unit was promoted, update it completely, otherwise just
// update moves and possibly gold+score.
unit.setMovesLeft(0);
if ("expert".equals(result)) {
cs.add(See.perhaps(), unit);
} else {
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
if ("beads".equals(result)) {
cs.addPartial(See.only(serverPlayer), serverPlayer,
"gold", "score");
}
}
}
if (tileDirty) {
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
}
// Always add result.
cs.addAttribute(See.only(serverPlayer), "result", result);
// Other players may be able to see unit disappearing, or
// learning.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Denounce an existing mission.
*
* @param serverPlayer The <code>ServerPlayer</code> that is denouncing.
* @param unit The <code>Unit</code> denouncing.
* @param settlement The <code>IndianSettlement</code> containing the
* mission to denounce.
* @return An <code>Element</code> encapsulating this action.
*/
public Element denounceMission(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement) {
ChangeSet cs = new ChangeSet();
csSpeakToChief(serverPlayer, settlement, false, cs);
// Determine result
Unit missionary = settlement.getMissionary();
if (missionary == null) {
return DOMMessage.clientError("Denouncing null missionary");
}
ServerPlayer enemy = (ServerPlayer) missionary.getOwner();
double denounce = Utils.randomDouble(logger, "Denounce base", random)
* enemy.getImmigration() / (serverPlayer.getImmigration() + 1);
if (missionary.hasAbility("model.ability.expertMissionary")) {
denounce += 0.2;
}
if (unit.hasAbility("model.ability.expertMissionary")) {
denounce -= 0.2;
}
if (denounce < 0.5) { // Success, remove old mission and establish ours
return establishMission(serverPlayer, unit, settlement);
}
// Denounce failed
Player owner = settlement.getOwner();
cs.add(See.only(serverPlayer), settlement);
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"indianSettlement.mission.noDenounce",
serverPlayer, unit)
.addStringTemplate("%nation%", owner.getNationName()));
cs.addMessage(See.only(enemy),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"indianSettlement.mission.enemyDenounce",
enemy, settlement)
.addStringTemplate("%enemy%", serverPlayer.getNationName())
.addName("%settlement%", settlement.getNameFor(enemy))
.addStringTemplate("%nation%", owner.getNationName()));
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) unit.getLocation());
cs.addDispose(See.perhaps().always(serverPlayer),
unit.getLocation(), unit);
// Others can see missionary disappear
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Establish a new mission.
*
* @param serverPlayer The <code>ServerPlayer</code> that is establishing.
* @param unit The missionary <code>Unit</code>.
* @param settlement The <code>IndianSettlement</code> to establish at.
* @return An <code>Element</code> encapsulating this action.
*/
public Element establishMission(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement) {
ChangeSet cs = new ChangeSet();
csSpeakToChief(serverPlayer, settlement, false, cs);
Unit missionary = settlement.getMissionary();
if (missionary != null) {
ServerPlayer enemy = (ServerPlayer) missionary.getOwner();
enemy.csKillMissionary(settlement, cs);
}
// Result depends on tension wrt this settlement.
// Establish if at least not angry.
switch (settlement.getAlarm(serverPlayer).getLevel()) {
case HATEFUL: case ANGRY:
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) unit.getLocation());
cs.addDispose(See.perhaps().always(serverPlayer),
unit.getLocation(), unit);
break;
case HAPPY: case CONTENT: case DISPLEASED:
cs.add(See.perhaps().always(serverPlayer), unit.getTile());
unit.setLocation(null);
unit.setMovesLeft(0);
cs.add(See.only(serverPlayer), unit);
settlement.changeMissionary(unit);
settlement.setConvertProgress(0);
List<FreeColGameObject> modifiedSettlements
= ((ServerIndianSettlement)settlement).modifyAlarm(serverPlayer,
ALARM_NEW_MISSIONARY);
modifiedSettlements.remove(settlement);
if (!modifiedSettlements.isEmpty()) {
cs.add(See.only(serverPlayer), modifiedSettlements);
}
break;
}
Tile tile = settlement.getTile();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.perhaps().always(serverPlayer), tile);
String messageId = "indianSettlement.mission."
+ settlement.getAlarm(serverPlayer).getKey();
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
messageId, serverPlayer, unit)
.addStringTemplate("%nation%", settlement.getOwner().getNationName()));
// Others can see missionary disappear and settlement acquire
// mission.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Incite a settlement against an enemy.
*
* @param serverPlayer The <code>ServerPlayer</code> that is inciting.
* @param unit The missionary <code>Unit</code> inciting.
* @param settlement The <code>IndianSettlement</code> to incite.
* @param enemy The <code>Player</code> to be incited against.
* @param gold The amount of gold in the bribe.
* @return An <code>Element</code> encapsulating this action.
*/
public Element incite(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement,
Player enemy, int gold) {
ChangeSet cs = new ChangeSet();
Tile tile = settlement.getTile();
csSpeakToChief(serverPlayer, settlement, false, cs);
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
// How much gold will be needed?
ServerPlayer enemyPlayer = (ServerPlayer) enemy;
Player nativePlayer = settlement.getOwner();
int payingValue = nativePlayer.getTension(serverPlayer).getValue();
int targetValue = nativePlayer.getTension(enemyPlayer).getValue();
int goldToPay = (payingValue > targetValue) ? 10000 : 5000;
goldToPay += 20 * (payingValue - targetValue);
goldToPay = Math.max(goldToPay, 650);
// Try to incite?
if (gold < 0) { // Initial enquiry.
cs.addAttribute(See.only(serverPlayer),
"gold", Integer.toString(goldToPay));
} else if (gold < goldToPay || !serverPlayer.checkGold(gold)) {
cs.addMessage(See.only(serverPlayer),
new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"indianSettlement.inciteGoldFail",
serverPlayer, settlement)
.addStringTemplate("%player%", enemyPlayer.getNationName())
.addAmount("%amount%", goldToPay));
cs.addAttribute(See.only(serverPlayer), "gold", "0");
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
} else {
// Success. Raise the tension for the native player with respect
// to the European player. Let resulting stance changes happen
// naturally in the AI player turn/s.
cs.add(See.only(null).perhaps(enemyPlayer),
nativePlayer.modifyTension(enemyPlayer,
Tension.WAR_MODIFIER));
cs.add(See.only(null).perhaps(serverPlayer),
enemyPlayer.modifyTension(serverPlayer,
Tension.TENSION_ADD_WAR_INCITER));
cs.addAttribute(See.only(serverPlayer),
"gold", Integer.toString(gold));
serverPlayer.modifyGold(-gold);
nativePlayer.modifyGold(gold);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
}
// Others might include enemy.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Set a unit destination.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to set the destination for.
* @param destination The <code>Location</code> to set as destination.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setDestination(ServerPlayer serverPlayer, Unit unit,
Location destination) {
if (unit.getTradeRoute() != null) unit.setTradeRoute(null);
unit.setDestination(destination);
// Others can not see a destination change.
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
/**
* Set current stop of a unit to the next valid stop if any.
*
* @param serverPlayer The <code>ServerPlayer</code> the unit belongs to.
* @param unit The <code>Unit</code> to update.
* @return An <code>Element</code> encapsulating this action.
*/
public Element updateCurrentStop(ServerPlayer serverPlayer, Unit unit) {
// Check if there is a valid current stop?
int current = unit.validateCurrentStop();
if (current < 0) { // No valid stop
unit.setTradeRoute(null);
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
List<Stop> stops = unit.getTradeRoute().getStops();
int next = current;
for (;;) {
if (++next >= stops.size()) next = 0;
if (next == current) return null; // No work at any stop, stay put.
Stop nextStop = stops.get(next);
if (((ServerUnit) unit).hasWorkAtStop(nextStop)) break;
logger.finest("Unit " + unit
+ " in trade route " + unit.getTradeRoute().getName()
+ " found no work at stop: " + (FreeColGameObject) nextStop.getLocation());
}
// Next is the updated stop.
// Could do just a partial update of currentStop if we did not
// also need to set the unit destination.
unit.setCurrentStop(next);
// Others can not see a stop change.
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
/**
* Buy from a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is buying.
* @param unit The <code>Unit</code> that will carry the goods.
* @param settlement The <code>IndianSettlement</code> to buy from.
* @param goods The <code>Goods</code> to buy.
* @param amount How much gold to pay.
* @return An <code>Element</code> encapsulating this action.
*/
public Element buyFromSettlement(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement,
Goods goods, int amount) {
ChangeSet cs = new ChangeSet();
csSpeakToChief(serverPlayer, settlement, false, cs);
TradeSession session
= TradeSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("Trying to buy without opening a transaction session");
}
if (!session.getBuy()) {
return DOMMessage.clientError("Trying to buy in a session where buying is not allowed.");
}
if (unit.getSpaceLeft() <= 0) {
return DOMMessage.clientError("Unit is full, unable to buy.");
}
// Check that this is the agreement that was made
AIPlayer ai = getFreeColServer().getAIPlayer(settlement.getOwner());
int returnGold = ai.buyProposition(unit, settlement, goods, amount);
if (returnGold != amount) {
return DOMMessage.clientError("This was not the price we agreed upon! Cheater?");
}
if (!serverPlayer.checkGold(amount)) { // Check this is funded.
return DOMMessage.clientError("Insufficient gold to buy.");
}
// Valid, make the trade.
moveGoods(goods, unit);
cs.add(See.perhaps(), unit);
Player settlementPlayer = settlement.getOwner();
Tile tile = settlement.getTile();
settlement.updateWantedGoods();
settlementPlayer.modifyGold(amount);
serverPlayer.modifyGold(-amount);
cs.add(See.only(serverPlayer),
((ServerIndianSettlement)settlement).modifyAlarm(serverPlayer,
-amount / 50));
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
session.setBuy();
logger.finest(serverPlayer.getName() + " " + unit + " buys " + goods
+ " at " + settlement.getName() + " for " + amount);
// Others can see the unit capacity.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Sell to a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is selling.
* @param unit The <code>Unit</code> carrying the goods.
* @param settlement The <code>IndianSettlement</code> to sell to.
* @param goods The <code>Goods</code> to sell.
* @param amount How much gold to expect.
* @return An <code>Element</code> encapsulating this action.
*/
public Element sellToSettlement(ServerPlayer serverPlayer, Unit unit,
IndianSettlement settlement,
Goods goods, int amount) {
ChangeSet cs = new ChangeSet();
csSpeakToChief(serverPlayer, settlement, false, cs);
TradeSession session
= TransactionSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("Trying to sell without opening a transaction session");
}
if (!session.getSell()) {
return DOMMessage.clientError("Trying to sell in a session where selling is not allowed.");
}
// Check that the gold is the agreed amount
AIPlayer ai = getFreeColServer().getAIPlayer(settlement.getOwner());
int returnGold = ai.sellProposition(unit, settlement, goods, amount);
if (returnGold != amount) {
return DOMMessage.clientError("This was not the price we agreed upon! Cheater?");
}
// Valid, make the trade.
moveGoods(goods, settlement);
cs.add(See.perhaps(), unit);
Player settlementPlayer = settlement.getOwner();
settlementPlayer.modifyGold(-amount);
serverPlayer.modifyGold(amount);
cs.add(See.only(serverPlayer),
((ServerIndianSettlement)settlement).modifyAlarm(serverPlayer,
-amount / 500));
Tile tile = settlement.getTile();
settlement.updateWantedGoods();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
session.setSell();
cs.addSale(serverPlayer, settlement, goods.getType(),
(int) Math.round((float) amount / goods.getAmount()));
logger.finest(serverPlayer.getName() + " " + unit + " sells " + goods
+ " at " + settlement.getName() + " for " + amount);
// Others can see the unit capacity.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Deliver gift to settlement.
* Note that this includes both European and native gifts.
*
* @param serverPlayer The <code>ServerPlayer</code> that is delivering.
* @param unit The <code>Unit</code> that is delivering.
* @param goods The <code>Goods</code> to deliver.
* @return An <code>Element</code> encapsulating this action.
*/
public Element deliverGiftToSettlement(ServerPlayer serverPlayer,
Unit unit, Settlement settlement,
Goods goods) {
TradeSession session
= TransactionSession.lookup(TradeSession.class, unit, settlement);
if (session == null) {
return DOMMessage.clientError("Trying to deliver gift without opening a session");
}
if (!session.getGift()) {
return DOMMessage.clientError("Trying to deliver gift in a session where gift giving is not allowed: " + unit + " " + settlement + " " + session);
}
ChangeSet cs = new ChangeSet();
Tile tile = settlement.getTile();
moveGoods(goods, settlement);
cs.add(See.perhaps(), unit);
if (settlement instanceof IndianSettlement) {
IndianSettlement is = (IndianSettlement) settlement;
csSpeakToChief(serverPlayer, is, false, cs);
cs.add(See.only(serverPlayer),
((ServerIndianSettlement)is).modifyAlarm(serverPlayer,
-is.getPriceToBuy(goods) / 50));
is.updateWantedGoods();
tile.updatePlayerExploredTile(serverPlayer, true);
cs.add(See.only(serverPlayer), tile);
}
session.setGift();
// Inform the receiver of the gift.
ModelMessage m = new ModelMessage(ModelMessage.MessageType.GIFT_GOODS,
"model.unit.gift",
settlement, goods.getType())
.addStringTemplate("%player%", serverPlayer.getNationName())
.add("%type%", goods.getNameKey())
.addAmount("%amount%", goods.getAmount())
.addName("%settlement%", settlement.getName());
cs.addMessage(See.only(serverPlayer), m);
ServerPlayer receiver = (ServerPlayer) settlement.getOwner();
if (receiver.isConnected() && settlement instanceof Colony) {
cs.add(See.only(receiver), unit);
cs.add(See.only(receiver), settlement);
cs.addMessage(See.only(receiver), m);
}
logger.info("Gift delivered by unit: " + unit.getId()
+ " to settlement: " + settlement.getName());
// Others can see unit capacity, receiver gets it own items.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Load cargo.
*
* @param serverPlayer The <code>ServerPlayer</code> that is loading.
* @param unit The <code>Unit</code> to load.
* @param goods The <code>Goods</code> to load.
* @return An <code>Element</code> encapsulating this action.
*/
public Element loadCargo(ServerPlayer serverPlayer, Unit unit,
Goods goods) {
ChangeSet cs = new ChangeSet();
Location oldLocation = goods.getLocation();
goods.adjustAmount();
moveGoods(goods, unit);
boolean moved = false;
if (unit.getInitialMovesLeft() != unit.getMovesLeft()) {
unit.setMovesLeft(0);
moved = true;
}
Unit oldUnit = null;
if (oldLocation instanceof Unit) {
oldUnit = (Unit) oldLocation;
if (oldUnit.getInitialMovesLeft() != oldUnit.getMovesLeft()) {
oldUnit.setMovesLeft(0);
} else {
oldUnit = null;
}
}
// Update the goodsContainers only if within a settlement,
// otherwise update the shared location so that others can
// see capacity changes.
if (unit.getSettlement() != null) {
cs.add(See.only(serverPlayer), oldLocation.getGoodsContainer());
cs.add(See.only(serverPlayer), unit.getGoodsContainer());
if (moved) {
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
}
if (oldUnit != null) {
cs.addPartial(See.only(serverPlayer), oldUnit, "movesLeft");
}
} else {
cs.add(See.perhaps(), (FreeColGameObject) unit.getLocation());
}
// Others might see capacity change.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Unload cargo.
*
* @param serverPlayer The <code>ServerPlayer</code> that is unloading.
* @param unit The <code>Unit</code> to unload.
* @param goods The <code>Goods</code> to unload.
* @return An <code>Element</code> encapsulating this action.
*/
public Element unloadCargo(ServerPlayer serverPlayer, Unit unit,
Goods goods) {
ChangeSet cs = new ChangeSet();
Location loc;
Settlement settlement = null;
if (unit.isInEurope()) { // Must be a dump of boycotted goods
loc = null;
} else if (unit.getTile() == null) {
return DOMMessage.clientError("Unit not on the map.");
} else if (unit.getSettlement() != null) {
settlement = unit.getTile().getSettlement();
loc = settlement;
} else { // Dump of goods onto a tile
loc = null;
}
goods.adjustAmount();
moveGoods(goods, loc);
boolean moved = false;
if (unit.getInitialMovesLeft() != unit.getMovesLeft()) {
unit.setMovesLeft(0);
moved = true;
}
if (settlement != null) {
cs.add(See.only(serverPlayer), settlement.getGoodsContainer());
cs.add(See.only(serverPlayer), unit.getGoodsContainer());
if (moved) cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
} else {
cs.add(See.perhaps(), (FreeColGameObject) unit.getLocation());
}
// Others might see a capacity change.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Clear the specialty of a unit.
*
* @param serverPlayer The owner of the unit.
* @param unit The <code>Unit</code> to clear the speciality of.
* @return An <code>Element</code> encapsulating this action.
*/
public Element clearSpeciality(ServerPlayer serverPlayer, Unit unit) {
UnitType newType = unit.getTypeChange(ChangeType.CLEAR_SKILL,
serverPlayer);
if (newType == null) {
return DOMMessage.clientError("Can not clear unit speciality: "
+ unit.getId());
}
// There can be some restrictions that may prevent the
// clearing of the speciality. AFAICT the only ATM is that a
// teacher can not lose its speciality, but this will need to
// be revisited if we invent a work location that requires a
// particular unit type.
if (unit.getStudent() != null) {
return DOMMessage.clientError("Can not clear speciality of a teacher.");
}
// Valid, change type.
unit.setType(newType);
// Update just the unit, others can not see it as this only happens
// in-colony. TODO: why not clear speciality in the open,
// you can disband...
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
/**
* Disband a unit.
*
* @param serverPlayer The owner of the unit.
* @param unit The <code>Unit</code> to disband.
* @return An <code>Element</code> encapsulating this action.
*/
public Element disbandUnit(ServerPlayer serverPlayer, Unit unit) {
ChangeSet cs = new ChangeSet();
// Dispose of the unit.
cs.add(See.perhaps().always(serverPlayer),
(FreeColGameObject) unit.getLocation());
cs.addDispose(See.perhaps().always(serverPlayer),
unit.getLocation(), unit);
// Others can see the unit removal and the space it leaves.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Build a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is building.
* @param unit The <code>Unit</code> that is building.
* @param name The new settlement name.
* @return An <code>Element</code> encapsulating this action.
*/
public Element buildSettlement(ServerPlayer serverPlayer, Unit unit,
String name) {
ChangeSet cs = new ChangeSet();
Game game = serverPlayer.getGame();
// Build settlement
Tile tile = unit.getTile();
Settlement settlement;
if (Player.ASSIGN_SETTLEMENT_NAME.equals(name)) {
name = serverPlayer.getSettlementName();
if (Player.ASSIGN_SETTLEMENT_NAME.equals(name)) {
// Load settlement names on demand.
serverPlayer.installSettlementNames(Messages
.getSettlementNames(serverPlayer), random);
name = serverPlayer.getSettlementName();
}
}
if (serverPlayer.isEuropean()) {
settlement = new ServerColony(game, serverPlayer, name, tile);
serverPlayer.addSettlement(settlement);
settlement.placeSettlement(serverPlayer.isAI());
} else {
IndianNationType nationType
= (IndianNationType) serverPlayer.getNationType();
UnitType skill = RandomChoice.getWeightedRandom(logger,
"Choose skill", random,
nationType.generateSkillsForTile(tile));
if (skill == null) { // Seasoned Scout
List<UnitType> scouts = getGame().getSpecification()
.getUnitTypesWithAbility(Ability.EXPERT_SCOUT);
skill = Utils.getRandomMember(logger, "Choose scout",
scouts, random);
}
settlement = new ServerIndianSettlement(game, serverPlayer, name,
tile, false, skill,
new HashSet<Player>(),
null);
serverPlayer.addSettlement(settlement);
settlement.placeSettlement(true);
// TODO: its lame that the settlement starts with no contacts
}
// Join. Remove equipment first in case role confuses placement.
((ServerUnit)unit).csRemoveEquipment(settlement,
new HashSet<EquipmentType>(unit.getEquipment().keySet()),
0, random, cs);
unit.setLocation(settlement);
unit.setMovesLeft(0);
// Update with settlement tile, and newly owned tiles.
List<FreeColGameObject> tiles = new ArrayList<FreeColGameObject>();
tiles.addAll(settlement.getOwnedTiles());
cs.add(See.perhaps(), tiles);
cs.addHistory(serverPlayer, new HistoryEvent(game.getTurn(),
HistoryEvent.EventType.FOUND_COLONY)
.addName("%colony%", settlement.getName()));
// Also send any tiles that can now be seen because the colony
// can perhaps see further than the founding unit.
if (settlement.getLineOfSight() > unit.getLineOfSight()) {
tiles.clear();
for (Tile t : tile.getSurroundingTiles(unit.getLineOfSight()+1,
settlement.getLineOfSight())) {
if (!tiles.contains(t)) tiles.add(t);
}
cs.add(See.only(serverPlayer), tiles);
}
// Others can see tile changes.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Join a colony.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> that is joining.
* @param colony The <code>Colony</code> to join.
* @return An <code>Element</code> encapsulating this action.
*/
public Element joinColony(ServerPlayer serverPlayer, Unit unit,
Colony colony) {
ChangeSet cs = new ChangeSet();
List<Tile> ownedTiles = colony.getOwnedTiles();
Tile tile = colony.getTile();
// Join.
unit.setLocation(colony);
unit.setMovesLeft(0);
((ServerUnit)unit).csRemoveEquipment(colony,
new HashSet<EquipmentType>(unit.getEquipment().keySet()),
0, random, cs);
// Update with colony tile, and tiles now owned.
cs.add(See.only(serverPlayer), tile);
for (Tile t : tile.getSurroundingTiles(colony.getRadius())) {
if (t.getOwningSettlement() == colony && !ownedTiles.contains(t)) {
cs.add(See.perhaps(), t);
}
}
// Others might see a tile ownership change.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Abandon a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is abandoning.
* @param settlement The <code>Settlement</code> to abandon.
* @return An <code>Element</code> encapsulating this action.
*/
public Element abandonSettlement(ServerPlayer serverPlayer,
Settlement settlement) {
ChangeSet cs = new ChangeSet();
// Collect the tiles the settlement owns before disposing,
// except the center tile as that is in the dispose below.
for (Tile t : settlement.getOwnedTiles()) {
if (t != settlement.getTile()) cs.add(See.perhaps(), t);
}
// Create history event before disposing.
if (settlement instanceof Colony) {
cs.addHistory(serverPlayer,
new HistoryEvent(getGame().getTurn(),
HistoryEvent.EventType.ABANDON_COLONY)
.addName("%colony%", settlement.getName()));
}
// Now do the dispose.
cs.add(See.perhaps().always(serverPlayer), settlement.getTile());
cs.addDispose(See.perhaps().always(serverPlayer),
settlement.getTile(), settlement);
// TODO: Player.settlements is still being fixed on the client side.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Claim land.
*
* @param serverPlayer The <code>ServerPlayer</code> claiming.
* @param tile The <code>Tile</code> to claim.
* @param settlement The <code>Settlement</code> to claim for.
* @param price The price to pay for the land, which must agree
* with the owner valuation, unless negative which denotes stealing.
* @return An <code>Element</code> encapsulating this action.
*/
public Element claimLand(ServerPlayer serverPlayer, Tile tile,
Settlement settlement, int price) {
ChangeSet cs = new ChangeSet();
serverPlayer.csClaimLand(tile, settlement, price, cs);
// Others can see the tile.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Accept a diplomatic trade. Handles the transfers of TradeItems.
*
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> to trade with.
* @param agreement The <code>DiplomacyTrade</code> agreement.
* @param cs A <code>ChangeSet</code> to update.
*/
private void csAcceptTrade(Unit unit, Settlement settlement,
DiplomaticTrade agreement, ChangeSet cs) {
ServerPlayer srcPlayer = (ServerPlayer) agreement.getSender();
ServerPlayer dstPlayer = (ServerPlayer) agreement.getRecipient();
for (TradeItem tradeItem : agreement.getTradeItems()) {
// Check trade carefully before committing.
if (!tradeItem.isValid()) {
logger.warning("Trade with invalid tradeItem: "
+ tradeItem.toString());
continue;
}
ServerPlayer source = (ServerPlayer) tradeItem.getSource();
if (source != srcPlayer && source != dstPlayer) {
logger.warning("Trade with invalid source: "
+ ((source == null) ? "null" : source.getId()));
continue;
}
ServerPlayer dest = (ServerPlayer) tradeItem.getDestination();
if (dest != srcPlayer && dest != dstPlayer) {
logger.warning("Trade with invalid destination: "
+ ((dest == null) ? "null" : dest.getId()));
continue;
}
// Collect changes for updating. Not very OO but
// TradeItem should not know about server internals.
// Take care to show items that change hands to the *old*
// owner too.
Stance stance = tradeItem.getStance();
if (stance != null
&& !source.csChangeStance(stance, dest, true, cs)) {
logger.warning("Stance trade failure");
}
Colony colony = tradeItem.getColony();
if (colony != null) {
ServerPlayer former = (ServerPlayer) colony.getOwner();
colony.changeOwner(dest);
List<FreeColGameObject> tiles
= new ArrayList<FreeColGameObject>();
tiles.addAll(colony.getOwnedTiles());
cs.add(See.perhaps().always(former), tiles);
}
int gold = tradeItem.getGold();
if (gold > 0) {
source.modifyGold(-gold);
dest.modifyGold(gold);
cs.addPartial(See.only(source), source, "gold", "score");
cs.addPartial(See.only(dest), dest, "gold", "score");
}
Goods goods = tradeItem.getGoods();
if (goods != null) {
moveGoods(goods, settlement);
cs.add(See.only(source), unit);
cs.add(See.only(dest), settlement.getGoodsContainer());
}
Unit newUnit = tradeItem.getUnit();
if (newUnit != null) {
ServerPlayer former = (ServerPlayer) newUnit.getOwner();
unit.setOwner(dest);
cs.add(See.perhaps().always(former), newUnit);
}
}
}
/**
* Diplomatic trades.
* Note that when an agreement is accepted we always process the
* agreement that was sent, not what was returned, cutting off a
* possibility for cheating.
*
* @param serverPlayer The <code>ServerPlayer</code> that is trading.
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> to trade with.
* @param agreement The <code>DiplomaticTrade</code> to consider.
* @return An <code>Element</code> encapsulating this action.
*/
public Element diplomaticTrade(ServerPlayer serverPlayer, Unit unit,
Settlement settlement,
DiplomaticTrade agreement) {
ChangeSet cs = new ChangeSet();
ServerPlayer otherPlayer = (ServerPlayer) settlement.getOwner();
DiplomacySession session
= TransactionSession.lookup(DiplomacySession.class,
unit, settlement);
TradeStatus status;
Player.makeContact(serverPlayer, otherPlayer);
switch (status = agreement.getStatus()) {
case PROPOSE_TRADE:
if (session == null) {
Unit.MoveType type = unit.getMoveType(settlement.getTile());
if (type != Unit.MoveType.ENTER_FOREIGN_COLONY_WITH_SCOUT) {
return DOMMessage.clientError("Unable to enter "
+ settlement.getId() + ": " + type.whyIllegal());
}
session = new DiplomacySession(unit, settlement);
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
}
session.setAgreement(agreement);
break;
case ACCEPT_TRADE:
if (session == null) {
return DOMMessage.clientError("Accept-trade with no session");
}
agreement = session.getAgreement();
agreement.setStatus(status);
csAcceptTrade(unit, settlement, agreement, cs);
break;
case REJECT_TRADE:
if (session == null) {
return DOMMessage.clientError("Reject-trade with no session");
}
agreement = session.getAgreement();
agreement.setStatus(status);
break;
default:
return DOMMessage.clientError("Bogus trade status: " + status);
}
DOMMessage reply = askTimeout(otherPlayer,
new DiplomacyMessage(unit, settlement, agreement));
DiplomaticTrade theirAgreement = (reply instanceof DiplomacyMessage)
? ((DiplomacyMessage)reply).getAgreement()
: null;
if (status != TradeStatus.PROPOSE_TRADE) {
session.complete(cs);
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
status = (theirAgreement == null) ? TradeStatus.REJECT_TRADE
: theirAgreement.getStatus();
switch (status) {
case PROPOSE_TRADE:
session.setAgreement(agreement = theirAgreement);
break;
case ACCEPT_TRADE:
agreement.setStatus(status);
csAcceptTrade(unit, settlement, agreement, cs);
session.complete(cs);
break;
case REJECT_TRADE:
agreement.setStatus(status);
session.complete(cs);
break;
default:
logger.warning("Bogus trade status: " + status);
break;
}
// Update *everyone* with the result, *and* return a
// DiplomacyMessage to the originating player because that is
// what ServerAPI.askDiplomacy is expecting.
sendToAll(cs);
return new ChangeSet()
.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new DiplomacyMessage(unit, settlement, agreement))
.build(serverPlayer);
}
/**
* Spy on a settlement.
*
* @param serverPlayer The <code>ServerPlayer</code> that is spying.
* @param unit The <code>Unit</code> that is spying.
* @param settlement The <code>Settlement</code> to spy on.
* @return An <code>Element</code> encapsulating this action.
*/
public Element spySettlement(ServerPlayer serverPlayer, Unit unit,
Settlement settlement) {
ChangeSet cs = new ChangeSet();
cs.addSpy(See.only(serverPlayer), settlement);
unit.setMovesLeft(0);
cs.addPartial(See.only(serverPlayer), unit, "movesLeft");
return cs.build(serverPlayer);
}
/**
* Change work location.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to change the work location of.
* @param workLocation The <code>WorkLocation</code> to change to.
* @return An <code>Element</code> encapsulating this action.
*/
public Element work(ServerPlayer serverPlayer, Unit unit,
WorkLocation workLocation) {
ChangeSet cs = new ChangeSet();
Colony colony = workLocation.getColony();
colony.getGoodsContainer().saveState();
if (workLocation instanceof ColonyTile) {
Tile tile = ((ColonyTile) workLocation).getWorkTile();
if (tile.getOwningSettlement() != colony) {
// Claim known free land (because canAdd() succeeded).
serverPlayer.csClaimLand(tile, colony, 0, cs);
}
}
// Remove any unit equipment
if (!unit.getEquipment().isEmpty()) {
((ServerUnit)unit).csRemoveEquipment(colony,
new HashSet<EquipmentType>(unit.getEquipment().keySet()),
0, random, cs);
}
// Check for upgrade.
UnitType oldType = unit.getType();
UnitTypeChange change = oldType
.getUnitTypeChange(ChangeType.ENTER_COLONY, unit.getOwner());
if (change != null) {
unit.setType(change.getNewUnitType());
}
// Change the location.
// We could avoid updating the whole tile if we knew that this
// was definitely a move between locations and no student/teacher
// interaction occurred.
unit.setLocation(workLocation);
cs.add(See.perhaps(), colony.getTile());
// Others can see colony change size
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Loot cargo.
*
* Note loser is passed by id, as by the time we get here the unit
* may have been sunk.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the winner.
* @param winner The <code>Unit</code> that looting.
* @param loserId The id of the <code>Unit</code> that is looted.
* @param loot The <code>Goods</code> to loot.
* @return An <code>Element</code> encapsulating this action.
*/
public Element lootCargo(ServerPlayer serverPlayer, Unit winner,
String loserId, List<Goods> loot) {
LootSession session = TransactionSession.lookup(LootSession.class,
winner.getId(), loserId);
if (session == null) {
return DOMMessage.clientError("Bogus looting!");
}
if (winner.getSpaceLeft() == 0) {
return DOMMessage.clientError("No space to loot to: "
+ winner.getId());
}
ChangeSet cs = new ChangeSet();
List<Goods> available = session.getCapture();
if (loot == null) { // Initial inquiry
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_LATE,
new LootCargoMessage(winner, loserId, available));
} else {
for (Goods g : loot) {
if (!available.contains(g)) {
return DOMMessage.clientError("Invalid loot: "
+ g.toString());
}
available.remove(g);
if (!winner.canAdd(g)) {
return DOMMessage.clientError("Loot failed: "
+ g.toString());
}
winner.add(g);
}
// Others can see cargo capacity change.
session.complete(cs);
cs.add(See.perhaps(), winner);
sendToOthers(serverPlayer, cs);
}
return cs.build(serverPlayer);
}
/**
* Pay arrears.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param type The <code>GoodsType</code> to pay the arrears for.
* @return An <code>Element</code> encapsulating this action.
*/
public Element payArrears(ServerPlayer serverPlayer, GoodsType type) {
int arrears = serverPlayer.getArrears(type);
if (arrears <= 0) {
return DOMMessage.clientError("No arrears for pay for: "
+ type.getId());
} else if (!serverPlayer.checkGold(arrears)) {
return DOMMessage.clientError("Not enough gold to pay arrears for: "
+ type.getId());
}
ChangeSet cs = new ChangeSet();
Market market = serverPlayer.getMarket();
serverPlayer.modifyGold(-arrears);
market.setArrears(type, 0);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
cs.add(See.only(serverPlayer), market.getMarketData(type));
// Arrears payment is private.
return cs.build(serverPlayer);
}
/**
* Equip a unit.
* Currently the unit is either in Europe or in a settlement.
* Might one day allow the unit to be on a tile co-located with
* an equipment-bearing wagon.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to equip.
* @param type The <code>EquipmentType</code> to equip with.
* @param amount The change in the amount of equipment.
* @return An <code>Element</code> encapsulating this action.
*/
public Element equipUnit(ServerPlayer serverPlayer, Unit unit,
EquipmentType type, int amount) {
Settlement settlement = (unit.getTile() == null) ? null
: unit.getSettlement();
GoodsContainer container = null;
boolean tileDirty = false;
if (unit.isInEurope()) {
// Refuse to trade in boycotted goods
for (AbstractGoods goods : type.getGoodsRequired()) {
GoodsType goodsType = goods.getType();
if (!serverPlayer.canTrade(goodsType)) {
return DOMMessage.clientError("No equip of " + type.getId()
+ " due to boycott of " + goodsType.getId());
}
}
// Will need a fake container to contain the goods to buy
// in Europe. Units may not necessarily have one.
container = new GoodsContainer(getGame(), serverPlayer.getEurope());
} else if (settlement != null) {
// Equipping a unit at work in a colony should remove the unit
// from the work location.
if (unit.getLocation() instanceof WorkLocation) {
unit.setLocation(settlement.getTile());
tileDirty = true;
}
settlement.getGoodsContainer().saveState();
}
ChangeSet cs = new ChangeSet();
List<EquipmentType> remove = null;
// Process adding equipment first, so as to settle what has to
// be removed.
if (amount > 0) {
for (AbstractGoods goods : type.getGoodsRequired()) {
GoodsType goodsType = goods.getType();
int n = amount * goods.getAmount();
if (unit.isInEurope()) {
try {
serverPlayer.buy(container, goodsType, n, random);
serverPlayer.csFlushMarket(goodsType, cs);
} catch (IllegalStateException e) {
return DOMMessage.clientError(e.getMessage());
}
} else if (settlement != null) {
if (settlement.getGoodsCount(goodsType) < n) {
return DOMMessage.clientError("Failed to equip: "
+ unit.getId() + " not enough " + goodsType
+ " in settlement " + settlement.getId());
}
settlement.removeGoods(goodsType, n);
}
}
remove = unit.changeEquipment(type, amount);
amount = 0; // 0 => all, now
} else if (amount < 0) {
remove = new ArrayList<EquipmentType>();
remove.add(type);
amount = -amount;
} else {
return null; // Nothing to do.
}
// Now do removal of equipment.
((ServerUnit)unit).csRemoveEquipment(settlement, remove, amount,
random, cs);
// Nothing for others to see except if the settlement population
// changes.
// If in Europe, we can get away with just updating the unit
// as sell() will have added sales changes. In a settlement,
// the goods container will always be dirty, but the whole tile
// will only need to be updated if the unit moved into it.
if (unit.getInitialMovesLeft() != unit.getMovesLeft()) {
unit.setMovesLeft(0);
}
if (unit.isInEurope()) {
cs.add(See.only(serverPlayer), unit);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
} else if (settlement != null) {
if (tileDirty) {
cs.add(See.perhaps(), settlement.getTile());
} else {
cs.add(See.only(serverPlayer), unit,
settlement.getGoodsContainer());
}
}
return cs.build(serverPlayer);
}
/**
* Pay for a building.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the colony.
* @param colony The <code>Colony</code> that is building.
* @return An <code>Element</code> encapsulating this action.
*/
public Element payForBuilding(ServerPlayer serverPlayer, Colony colony) {
BuildableType build = colony.getCurrentlyBuilding();
if (build == null) {
return DOMMessage.clientError("Colony " + colony.getId()
+ " is not building anything!");
}
HashMap<GoodsType, Integer> required
= colony.getGoodsForBuilding(build);
int price = colony.priceGoodsForBuilding(required);
if (!serverPlayer.checkGold(price)) {
return DOMMessage.clientError("Insufficient funds to pay for build.");
}
// Save the correct final gold for the player, as we are going to
// use buy() below, but it deducts the normal uninflated price for
// the goods being bought. We restore this correct amount later.
int savedGold = serverPlayer.modifyGold(-price);
serverPlayer.modifyGold(price);
ChangeSet cs = new ChangeSet();
GoodsContainer container = colony.getGoodsContainer();
container.saveState();
for (GoodsType type : required.keySet()) {
int amount = required.get(type);
if (type.isStorable()) {
// TODO: should also check canTrade(type, Access.?)
serverPlayer.buy(container, type, amount, random);
serverPlayer.csFlushMarket(type, cs);
} else {
container.addGoods(type, amount);
}
}
colony.invalidateCache();
// Nothing to see for others, colony internal.
serverPlayer.setGold(savedGold);
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
cs.add(See.only(serverPlayer), container);
return cs.build(serverPlayer);
}
/**
* Indians making demands of a colony.
*
* @param serverPlayer The <code>ServerPlayer</code> that is demanding.
* @param unit The <code>Unit</code> making the demands.
* @param colony The <code>Colony</code> that is demanded of.
* @param goods The <code>Goods</code> being demanded.
* @param gold The amount of gold being demanded.
* @return An <code>Element</code> encapsulating this action.
*/
public Element indianDemand(final ServerPlayer serverPlayer, Unit unit,
Colony colony, Goods goods, int gold) {
ServerPlayer victim = (ServerPlayer) colony.getOwner();
int difficulty = getGame().getSpecification()
.getIntegerOption("model.option.nativeDemands").getValue();
ChangeSet cs = new ChangeSet();
DOMMessage reply = askTimeout(victim,
new IndianDemandMessage(unit, colony, goods, gold));
boolean result = (reply instanceof IndianDemandMessage)
? ((IndianDemandMessage)reply).getResult()
: false;
logger.info(serverPlayer.getName() + " unit " + unit
+ " demands " + goods + " goods and " + gold + " gold "
+ " from " + colony.getName() + " accepted: " + result);
IndianDemandMessage message = new IndianDemandMessage(unit, colony,
goods, gold);
message.setResult(result);
cs.add(See.only(serverPlayer), ChangePriority.CHANGE_NORMAL, message);
if (result) {
if (goods != null) {
GoodsContainer colonyContainer = colony.getGoodsContainer();
colonyContainer.saveState();
GoodsContainer unitContainer = unit.getGoodsContainer();
unitContainer.saveState();
moveGoods(goods, unit);
cs.add(See.only(victim), colonyContainer);
//cs.add(See.only(serverPlayer), unitContainer);
}
if (gold > 0) {
victim.modifyGold(-gold);
serverPlayer.modifyGold(gold);
cs.addPartial(See.only(victim), victim, "gold");
//cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
}
cs.add(See.only(null).perhaps(victim),
serverPlayer.modifyTension(victim, -(5 - difficulty) * 50));
}
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Train a unit in Europe.
*
* @param serverPlayer The <code>ServerPlayer</code> that is demanding.
* @param type The <code>UnitType</code> to train.
* @return An <code>Element</code> encapsulating this action.
*/
public Element trainUnitInEurope(ServerPlayer serverPlayer, UnitType type) {
Europe europe = serverPlayer.getEurope();
if (europe == null) {
return DOMMessage.clientError("No Europe to train in.");
}
int price = europe.getUnitPrice(type);
if (price <= 0) {
return DOMMessage.clientError("Bogus price: " + price);
} else if (!serverPlayer.checkGold(price)) {
return DOMMessage.clientError("Not enough gold to train " + type);
}
new ServerUnit(getGame(), europe, serverPlayer, type);
serverPlayer.modifyGold(-price);
((ServerEurope) europe).increasePrice(type, price);
// Only visible in Europe
ChangeSet cs = new ChangeSet();
cs.addPartial(See.only(serverPlayer), serverPlayer, "gold");
cs.add(See.only(serverPlayer), europe);
return cs.build(serverPlayer);
}
/**
* Set build queue.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the colony.
* @param colony The <code>Colony</code> to set the queue of.
* @param queue The new build queue.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setBuildQueue(ServerPlayer serverPlayer, Colony colony,
List<BuildableType> queue) {
colony.setBuildQueue(queue);
// Only visible to player.
ChangeSet cs = new ChangeSet();
cs.add(See.only(serverPlayer), colony);
return cs.build(serverPlayer);
}
/**
* Set goods levels.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the colony.
* @param colony The <code>Colony</code> to set the goods levels in.
* @param exportData The new <code>ExportData</code>.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setGoodsLevels(ServerPlayer serverPlayer, Colony colony,
ExportData exportData) {
colony.setExportData(exportData);
return new ChangeSet().add(See.only(serverPlayer), colony)
.build(serverPlayer);
}
/**
* Put outside colony.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to be put out.
* @return An <code>Element</code> encapsulating this action.
*/
public Element putOutsideColony(ServerPlayer serverPlayer, Unit unit) {
Tile tile = unit.getTile();
Colony colony = unit.getColony();
unit.setLocation(tile);
// Full tile update for the player, the rest get their limited
// view of the colony so that population changes.
ChangeSet cs = new ChangeSet();
cs.add(See.only(serverPlayer), tile);
cs.add(See.perhaps().except(serverPlayer), colony);
return cs.build(serverPlayer);
}
/**
* Change work type.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to change the work type of.
* @param type The new <code>GoodsType</code> to produce.
* @return An <code>Element</code> encapsulating this action.
*/
public Element changeWorkType(ServerPlayer serverPlayer, Unit unit,
GoodsType type) {
if (unit.getWorkType() != type) {
unit.setExperience(0);
unit.setWorkType(type);
}
// Private update of the unit.
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
/**
* Change improvement work type.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to change the work type of.
* @param type The new <code>TileImprovementType</code> to produce.
* @return An <code>Element</code> encapsulating this action.
*/
public Element changeWorkImprovementType(ServerPlayer serverPlayer,
Unit unit,
TileImprovementType type) {
Tile tile = unit.getTile();
TileImprovement improvement = tile.findTileImprovementType(type);
if (improvement == null) { // Create the new improvement.
improvement = new TileImprovement(getGame(), tile, type);
tile.add(improvement);
}
unit.setWorkImprovement(improvement);
unit.setState(UnitState.IMPROVING);
// Private update of the tile.
return new ChangeSet().add(See.only(serverPlayer), tile)
.build(serverPlayer);
}
/**
* Change a units state.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The <code>Unit</code> to change the state of.
* @param state The new <code>UnitState</code>.
* @return An <code>Element</code> encapsulating this action.
*/
public Element changeState(ServerPlayer serverPlayer, Unit unit,
UnitState state) {
ChangeSet cs = new ChangeSet();
if (state == UnitState.FORTIFYING) {
Tile tile = unit.getTile();
ServerColony colony
= (tile.getOwningSettlement() instanceof Colony)
? (ServerColony) tile.getOwningSettlement()
: null;
if (colony != null
&& colony.getOwner() != unit.getOwner()
&& colony.isTileInUse(tile)) {
colony.csEvictUser(unit, cs);
}
}
unit.setState(state);
cs.add(See.perhaps(), unit.getTile());
// Others might be able to see the unit.
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
/**
* Assign a student to a teacher.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param student The student <code>Unit</code>.
* @param teacher The teacher <code>Unit</code>.
* @return An <code>Element</code> encapsulating this action.
*/
public Element assignTeacher(ServerPlayer serverPlayer, Unit student,
Unit teacher) {
Unit oldStudent = teacher.getStudent();
Unit oldTeacher = student.getTeacher();
// Only update units that changed their teaching situation.
ChangeSet cs = new ChangeSet();
if (oldTeacher != null) {
oldTeacher.setStudent(null);
cs.add(See.only(serverPlayer), oldTeacher);
}
if (oldStudent != null) {
oldStudent.setTeacher(null);
cs.add(See.only(serverPlayer), oldStudent);
}
teacher.setStudent(student);
teacher.setWorkType(null);
student.setTeacher(teacher);
cs.add(See.only(serverPlayer), student, teacher);
return cs.build(serverPlayer);
}
/**
* Assign a trade route to a unit.
*
* @param serverPlayer The <code>ServerPlayer</code> that owns the unit.
* @param unit The unit <code>Unit</code> to assign to.
* @param tradeRoute The <code>TradeRoute</code> to assign.
* @return An <code>Element</code> encapsulating this action.
*/
public Element assignTradeRoute(ServerPlayer serverPlayer, Unit unit,
TradeRoute tradeRoute) {
unit.setTradeRoute(tradeRoute);
unit.setDestination(null);
if (tradeRoute != null) {
List<Stop> stops = tradeRoute.getStops();
int found = -1;
for (int i = 0; i < stops.size(); i++) {
if (unit.getLocation() == stops.get(i).getLocation()) {
found = i;
break;
}
}
if (found < 0) found = 0;
unit.setCurrentStop(found);
}
// Only visible to the player
return new ChangeSet().add(See.only(serverPlayer), unit)
.build(serverPlayer);
}
/**
* Set trade routes for a player.
*
* @param serverPlayer The <code>ServerPlayer</code> to set trade
* routes for.
* @param routes The new list of <code>TradeRoute</code>s.
* @return An <code>Element</code> encapsulating this action.
*/
public Element setTradeRoutes(ServerPlayer serverPlayer,
List<TradeRoute> routes) {
serverPlayer.setTradeRoutes(routes);
// Have to update the whole player alas.
return new ChangeSet().add(See.only(serverPlayer), serverPlayer)
.build(serverPlayer);
}
/**
* Get a new trade route for a player.
*
* @param serverPlayer The <code>ServerPlayer</code> to get a trade
* route for.
* @return An <code>Element</code> encapsulating this action.
*/
public Element getNewTradeRoute(ServerPlayer serverPlayer) {
List<TradeRoute> routes
= new ArrayList<TradeRoute>(serverPlayer.getTradeRoutes());
TradeRoute route = new TradeRoute(getGame(), "", serverPlayer);
routes.add(route);
serverPlayer.setTradeRoutes(routes);
return new ChangeSet().addTradeRoute(serverPlayer, route)
.build(serverPlayer);
}
/**
* Get a list of abstract REF units for a player.
*
* @param serverPlayer The <code>ServerPlayer</code> to query the REF of.
* @return An <code>Element</code> encapsulating this action.
*/
public Element getREFUnits(ServerPlayer serverPlayer) {
Game game = getGame();
Specification spec = game.getSpecification();
List<AbstractUnit> units = new ArrayList<AbstractUnit>();
final UnitType defaultType = spec.getDefaultUnitType();
if (serverPlayer.getMonarch() == null) {
ServerPlayer REFPlayer = (ServerPlayer) serverPlayer.getREFPlayer();
java.util.Map<UnitType, EnumMap<Role, Integer>> unitHash
= new HashMap<UnitType, EnumMap<Role, Integer>>();
for (Unit unit : REFPlayer.getUnits()) {
if (unit.isOffensiveUnit()) {
UnitType unitType = defaultType;
if (unit.getType().getOffence() > 0
|| unit.hasAbility(Ability.EXPERT_SOLDIER)) {
unitType = unit.getType();
}
EnumMap<Role, Integer> roleMap = unitHash.get(unitType);
if (roleMap == null) {
roleMap = new EnumMap<Role, Integer>(Role.class);
}
Role role = unit.getRole();
Integer count = roleMap.get(role);
if (count == null) {
roleMap.put(role, new Integer(1));
} else {
roleMap.put(role, new Integer(count.intValue() + 1));
}
unitHash.put(unitType, roleMap);
}
}
for (java.util.Map.Entry<UnitType, EnumMap<Role, Integer>> typeEntry : unitHash.entrySet()) {
for (java.util.Map.Entry<Role, Integer> roleEntry : typeEntry.getValue().entrySet()) {
units.add(new AbstractUnit(typeEntry.getKey(), roleEntry.getKey(), roleEntry.getValue()));
}
}
} else {
units = serverPlayer.getMonarch().getREF();
}
ChangeSet cs = new ChangeSet();
cs.addTrivial(See.only(serverPlayer), "getREFUnits",
ChangePriority.CHANGE_NORMAL);
Element reply = cs.build(serverPlayer);
// TODO: eliminate explicit Element hackery
for (AbstractUnit unit : units) {
reply.appendChild(unit.toXMLElement(serverPlayer,
reply.getOwnerDocument()));
}
return reply;
}
/**
* Gets the list of high scores.
*
* @param serverPlayer The <code>ServerPlayer</code> that is querying.
* @return An <code>Element</code> encapsulating this action.
*/
public Element getHighScores(ServerPlayer serverPlayer) {
ChangeSet cs = new ChangeSet();
cs.addTrivial(See.only(serverPlayer), "getHighScores",
ChangePriority.CHANGE_NORMAL);
Element reply = cs.build(serverPlayer);
for (HighScore score : getFreeColServer().getHighScores()) {
reply.appendChild(score.toXMLElement(serverPlayer,
reply.getOwnerDocument()));
}
return reply;
}
/**
* Chat.
*
* @param serverPlayer The <code>ServerPlayer</code> that is chatting.
* @param message The chat message.
* @param pri A privacy setting, currently a noop.
* @return An <code>Element</code> encapsulating this action.
*/
public Element chat(ServerPlayer serverPlayer, String message,
boolean pri) {
sendToOthers(serverPlayer,
new ChatMessage(serverPlayer, message, false)
.toXMLElement());
return null;
}
/**
* Get the current game statistics.
*
* @return An <code>Element</code> encapsulating this action.
*/
public Element getStatistics(ServerPlayer serverPlayer) {
// Convert statistics map to a list.
java.util.Map<String, String> stats = getGame()
.getStatistics();
stats.putAll(getFreeColServer().getAIMain().getAIStatistics());
List<String> all = new ArrayList<String>();
List<String> keys = new ArrayList<String>(stats.keySet());
Collections.sort(keys);
for (String k : keys) {
all.add(k);
all.add(stats.get(k));
}
// Return as statistics element.
ChangeSet cs = new ChangeSet();
cs.addTrivial(See.only(serverPlayer), "statistics",
ChangePriority.CHANGE_NORMAL,
all.toArray(new String[0]));
return cs.build(serverPlayer);
}
/**
* Enters revenge mode against those evil AIs.
*
* @param serverPlayer The <code>ServerPlayer</code> entering revenge mode.
* @return An <code>Element</code> encapsulating this action.
*/
public Element enterRevengeMode(ServerPlayer serverPlayer) {
if (!getFreeColServer().isSingleplayer()) {
return DOMMessage.clientError("Can not enter revenge mode,"
+ " as this is not a single player game.");
}
Game game = getGame();
List<UnitType> undeads = game.getSpecification()
.getUnitTypesWithAbility("model.ability.undead");
List<UnitType> navalUnits = new ArrayList<UnitType>();
List<UnitType> landUnits = new ArrayList<UnitType>();
for (UnitType undead : undeads) {
if (undead.hasAbility(Ability.NAVAL_UNIT)) {
navalUnits.add(undead);
} else if (undead.hasAbility("model.ability.multipleAttacks")) {
landUnits.add(undead);
}
}
if (navalUnits.size() == 0 || landUnits.size() == 0) {
return DOMMessage.clientError("Can not enter revenge mode,"
+ " because we can not find the undead units.");
}
ChangeSet cs = new ChangeSet();
UnitType navalType = navalUnits.get(Utils.randomInt(logger,
"Choose undead navy", random, navalUnits.size()));
Tile start = ((Tile) serverPlayer.getEntryLocation())
.getSafeTile(serverPlayer, random);
Unit theFlyingDutchman
= new ServerUnit(game, start, serverPlayer, navalType);
UnitType landType = landUnits.get(Utils.randomInt(logger,
"Choose undead army", random, landUnits.size()));
new ServerUnit(game, theFlyingDutchman, serverPlayer, landType);
serverPlayer.setDead(false);
serverPlayer.setPlayerType(PlayerType.UNDEAD);
// No one likes the undead.
for (Player p : game.getPlayers()) {
if (serverPlayer != (ServerPlayer) p
&& serverPlayer.hasContacted(p)) {
serverPlayer.csChangeStance(Stance.WAR, p, true, cs);
}
}
// Others can tell something has happened to the player,
// and possibly see the units.
cs.add(See.all(), serverPlayer);
cs.add(See.perhaps(), start);
sendToOthers(serverPlayer, cs);
return cs.build(serverPlayer);
}
}