/**
* 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.client.control;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.freecol.FreeCol;
import net.sf.freecol.client.ClientOptions;
import net.sf.freecol.client.FreeColClient;
import net.sf.freecol.client.gui.GUI;
import net.sf.freecol.client.gui.i18n.Messages;
import net.sf.freecol.client.gui.panel.ChoiceItem;
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.Building;
import net.sf.freecol.common.model.Colony;
import net.sf.freecol.common.model.ColonyTile;
import net.sf.freecol.common.model.ColonyWas;
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.EuropeWas;
import net.sf.freecol.common.model.Event;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.Goods;
import net.sf.freecol.common.model.GoodsContainer;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.HighScore;
import net.sf.freecol.common.model.IndianSettlement;
import net.sf.freecol.common.model.Limit;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.LostCityRumour;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Map.Direction;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.ModelMessage.MessageType;
import net.sf.freecol.common.model.Nameable;
import net.sf.freecol.common.model.NationSummary;
import net.sf.freecol.common.model.Ownable;
import net.sf.freecol.common.model.PathNode;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Player.NoClaimReason;
import net.sf.freecol.common.model.Player.PlayerType;
import net.sf.freecol.common.model.Player.Stance;
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.Tile;
import net.sf.freecol.common.model.TileImprovementType;
import net.sf.freecol.common.model.TradeRoute;
import net.sf.freecol.common.model.TradeRoute.Stop;
import net.sf.freecol.common.model.TransactionListener;
import net.sf.freecol.common.model.Turn;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.Unit.UnitState;
import net.sf.freecol.common.model.UnitType;
import net.sf.freecol.common.model.UnitTypeChange.ChangeType;
import net.sf.freecol.common.model.UnitWas;
import net.sf.freecol.common.model.WorkLocation;
import net.sf.freecol.common.networking.NetworkConstants;
import net.sf.freecol.common.networking.ServerAPI;
import net.sf.freecol.common.option.BooleanOption;
import net.sf.freecol.server.FreeColServer;
import org.freecolandroid.debug.FCLog;
import org.freecolandroid.repackaged.javax.swing.ImageIcon;
import org.freecolandroid.repackaged.javax.swing.SwingUtilities;
import org.freecolandroid.ui.FreeColDialogFragment.DialogListener;
import org.freecolandroid.ui.InputDialogFragment.InputDialogListener;
/**
* The controller that will be used while the game is played.
*/
public final class InGameController implements NetworkConstants {
private static final Logger logger = Logger.getLogger(InGameController.class.getName());
private final FreeColClient freeColClient;
private final short UNIT_LAST_MOVE_DELAY = 300;
// Selecting next unit depends on mode--- either from the active list,
// from the going-to list, or flush going-to and end the turn.
private final int MODE_NEXT_ACTIVE_UNIT = 0;
private final int MODE_EXECUTE_GOTO_ORDERS = 1;
private final int MODE_END_TURN = 2;
private int moveMode = MODE_NEXT_ACTIVE_UNIT;
private int turnsPlayed = 0;
/** The most recently saved game file, or <b>null</b>. */
private File lastSaveGameFile;
private static FileFilter FSG_FILTER = new FileFilter() {
public boolean accept(File file) {
return file.isFile() && file.getName().endsWith(".fsg");
}
};
// A map of messages to be ignored.
private HashMap<String, Integer> messagesToIgnore
= new HashMap<String, Integer>();
private GUI gui;
/**
* The constructor to use.
*
* @param freeColClient The main controller.
*/
public InGameController(FreeColClient freeColClient, GUI gui) {
this.freeColClient = freeColClient;
this.gui = gui;
// TODO: fetch value of lastSaveGameFile from a persistent client value
// lastSaveGameFile = new File(freeColClient.getClientOptions().getString(null));
}
// Public configuration.
/**
* Informs this controller that a game has been newly loaded.
*/
public void setGameConnected () {
turnsPlayed = 0;
}
/**
* Sets the "debug mode" to be active or not. Calls
* {@link FreeCol#setInDebugMode(boolean)} and reinitialize the
* <code>FreeColMenuBar</code>.
*
* @param debug Set to <code>true</code> to enable debug mode.
*/
public void setInDebugMode(boolean debug) {
FreeCol.setInDebugMode(debug);
logger.info("Debug mode set to " + debug);
gui.updateMenuBar();
}
// Private utilities
/**
* Meaningfully named access to the ServerAPI.
*
* @return The ServerAPI.
*/
private ServerAPI askServer() {
return freeColClient.askServer();
}
/**
* Gets the specification for the current game.
*
* @return The current game specification.
*/
private Specification getSpecification() {
return freeColClient.getGame().getSpecification();
}
/**
* Require that it is this client's player's turn.
* Put up the notYourTurn message if not.
*
* @return True if it is our turn.
*/
private boolean requireOurTurn() {
if (!freeColClient.currentPlayerIsMyPlayer()) {
gui.showInformationMessage("notYourTurn");
return false;
}
return true;
}
/**
* Check if an attack results in a transition from peace or cease fire to
* war and, if so, warn the player.
*
* @param attacker The potential attacking <code>Unit</code>.
* @param target The target <code>Tile</code>.
* @return True to attack, false to abort.
*/
private boolean confirmHostileAction(Unit attacker, Tile target) {
if (attacker.hasAbility(Ability.PIRACY)) {
// Privateers can attack and remain at peace
return true;
}
Player enemy;
if (target.getSettlement() != null) {
enemy = target.getSettlement().getOwner();
} else if (target == attacker.getTile()) {
// Fortify on tile owned by another nation
enemy = target.getOwner();
if (enemy == null) return true;
} else {
Unit defender = target.getDefendingUnit(attacker);
if (defender == null) {
logger.warning("Attacking, but no defender - will try!");
return true;
}
if (defender.hasAbility(Ability.PIRACY)) {
// Privateers can be attacked and remain at peace
return true;
}
enemy = defender.getOwner();
}
String messageID = null;
switch (attacker.getOwner().getStance(enemy)) {
case WAR:
logger.finest("Player at war, no confirmation needed");
return true;
case UNCONTACTED: case PEACE:
messageID = "model.diplomacy.attack.peace";
break;
case CEASE_FIRE:
messageID = "model.diplomacy.attack.ceaseFire";
break;
case ALLIANCE:
messageID = "model.diplomacy.attack.alliance";
break;
}
return gui.showConfirmDialog(attacker.getTile(),
StringTemplate.template(messageID)
.addStringTemplate("%nation%", enemy.getNationName()),
"model.diplomacy.attack.confirm", "cancel");
}
/**
* Shows the pre-combat dialog if enabled, allowing the user to
* view the odds and possibly cancel the attack.
*
* @param attacker The attacking <code>Unit</code>.
* @param tile The target <code>Tile</code>.
* @return True to attack, false to abort.
*/
private boolean confirmPreCombat(Unit attacker, Tile tile) {
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_PRECOMBAT)) {
Settlement settlement = tile.getSettlement();
// Don't tell the player how a settlement is defended!
FreeColGameObject defender = (settlement != null) ? settlement
: tile.getDefendingUnit(attacker);
return gui.showPreCombatDialog(attacker, defender,
tile);
}
return true;
}
/**
* Convenience function to find an adjacent settlement. Intended
* to be called in contexts where we are expecting a settlement to
* be there, such as when handling a particular move type.
*
* @param tile The <code>Tile</code> to start at.
* @param direction The <code>Direction</code> to step.
* @return A settlement on the adjacent tile if any.
*/
private Settlement getSettlementAt(Tile tile, Direction direction) {
return tile.getNeighbourOrNull(direction).getSettlement();
}
/**
* Convenience function to find the nation controlling an adjacent
* settlement. Intended to be called in contexts where we are
* expecting a settlement or unit to be there, such as when
* handling a particular move type.
*
* @param tile The <code>Tile</code> to start at.
* @param direction The <code>Direction</code> to step.
* @return The name of the nation controlling a settlement on the
* adjacent tile if any.
*/
private StringTemplate getNationAt(Tile tile, Direction direction) {
Tile newTile = tile.getNeighbourOrNull(direction);
Player player = null;
if (newTile.getSettlement() != null) {
player = newTile.getSettlement().getOwner();
} else if (newTile.getFirstUnit() != null) {
player = newTile.getFirstUnit().getOwner();
} else { // should not happen
player = freeColClient.getGame().getUnknownEnemy();
}
return player.getNationName();
}
// Utilities to handle the transitions between the active unit,
// execute-orders and end-turn controller states.
/**
* Actually do the goto orders operation.
*
* @return True if all goto orders have been performed and no units
* reached their destination and are free to move again.
*/
private boolean doExecuteGotoOrders() {
// Ensure the goto mode sticks.
if (moveMode < MODE_EXECUTE_GOTO_ORDERS) {
moveMode = MODE_EXECUTE_GOTO_ORDERS;
}
// Process all units.
boolean result = true;
Player player = freeColClient.getMyPlayer();
while (player.hasNextGoingToUnit()) {
Unit unit = player.getNextGoingToUnit();
gui.setActiveUnit(unit);
// Give the player a chance to deal with any problems
// shown in a popup before pressing on with more moves.
if (gui.isShowingSubPanel()) {
gui.requestFocusForSubPanel();
return false;
}
// Move the unit as much as possible
if (moveToDestination(unit)) result = false;
nextModelMessage();
}
gui.setActiveUnit(null);
return result;
}
/**
* Really end the turn.
*/
private void doEndTurn() {
// Clear active unit if any.
gui.setActiveUnit(null);
// Unskip all skipped, some may have been faked in-client.
// Server-side skipped units are set active in csNewTurn.
for (Unit unit : freeColClient.getMyPlayer().getUnits()) {
if (unit.getState() == UnitState.SKIPPED) {
unit.setState(UnitState.ACTIVE);
}
}
// Restart the selection cycle.
moveMode = MODE_NEXT_ACTIVE_UNIT;
turnsPlayed++;
// Inform the server of end of turn.
askServer().endTurn();
}
// Trade route support.
/**
* Follows a trade route, doing load/unload actions, moving the unit,
* and updating the stop and destination.
*
* @param unit The <code>Unit</code> on the route.
* @return True if the unit should keep moving, which can only
* happen if the trade route is found to be broken and the
* unit is thrown off it.
*/
private boolean followTradeRoute(Unit unit) {
Player player = unit.getOwner();
TradeRoute tr = unit.getTradeRoute();
String name = tr.getName();
List<ModelMessage> messages = new ArrayList<ModelMessage>();
boolean detailed = freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_GOODS_MOVEMENT);
List<Stop> stops = tr.getStops();
Stop stop;
boolean result = false;
for (;;) {
stop = unit.getStop();
// Complain and return if the stop is no longer valid.
if (!TradeRoute.isStopValid(unit, stop)) {
messages.add(new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT,
"traderoute.broken", unit)
.addName("%route%", name));
clearOrders(unit);
result = true;
break;
}
// Is the unit at the stop already?
boolean atStop;
if (stop.getLocation() instanceof Europe) {
atStop = unit.isInEurope();
} else if (stop.getLocation() instanceof Colony) {
atStop = unit.getTile() == stop.getLocation().getTile();
} else {
throw new IllegalStateException("Bogus stop location: "
+ (FreeColGameObject) stop.getLocation());
}
if (atStop) {
// Anything to unload?
unloadUnitAtStop(unit, (detailed) ? messages : null);
// Anything to load?
loadUnitAtStop(unit, (detailed) ? messages : null);
// If the un/load consumed the moves, break now before
// updating the stop. This allows next move to arrive
// here again having taken a second shot at
// un/loading, but this time should not have consumed
// the moves.
if (unit.getMovesLeft() <= 0) break;
// Update the stop.
int index = unit.validateCurrentStop();
askServer().updateCurrentStop(unit);
// Check if the server reset this stop as the current one.
// This means there is no work to do anywhere in the whole
// trade route. Skip the unit if so.
int next = unit.validateCurrentStop();
if (next == index) {
Location loc = stop.getLocation();
if (detailed) {
messages.add(new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT,
"traderoute.noWork", unit)
.addName("%route%", name)
.addStringTemplate("%unit%",
Messages.getLabel(unit))
.addStringTemplate("%location%",
loc.getLocationNameFor(player)));
}
unit.setState(UnitState.SKIPPED);
break;
}
// Check for and notify of missing stops.
if (detailed) {
for (;;) {
if (++index >= stops.size()) index = 0;
if (index == next) break;
Location loc = stops.get(index).getLocation();
messages.add(new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT,
"traderoute.skipStop", unit)
.addName("%route%", name)
.addStringTemplate("%unit%",
Messages.getLabel(unit))
.addStringTemplate("%location%",
loc.getLocationNameFor(player)));
}
}
continue; // Stop was updated, loop.
}
// Not at stop, give up if no moves left.
if (unit.getMovesLeft() <= 0
|| unit.getState() == UnitState.SKIPPED) {
break;
}
// Find a path to the stop. Skip if none.
Location destination = stop.getLocation();
PathNode path = (destination instanceof Europe)
? unit.findPathToEurope()
: unit.findPath(destination.getTile());
if (path == null) {
StringTemplate dest = destination.getLocationNameFor(player);
messages.add(new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT,
"traderoute.noPath", unit)
.addName("%route%", name)
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addStringTemplate("%location%", dest));
unit.setState(UnitState.SKIPPED);
break;
}
// Try to follow the path.
// Ignore the result, check for unload before returning.
followPath(unit, path);
}
for (ModelMessage m : messages) player.addModelMessage(m);
return result;
}
/**
* Work out what goods to load onto a unit at a stop, and load them.
*
* @param unit The <code>Unit</code> to load.
* @param messages An optional list of messages to update.
* @return True if goods were loaded.
*/
private boolean loadUnitAtStop(Unit unit, List<ModelMessage> messages) {
// Copy the list of goods types to load at this stop.
Stop stop = unit.getStop();
List<GoodsType> goodsTypesToLoad
= new ArrayList<GoodsType>(stop.getCargo());
boolean ret = false;
// First handle partial loads.
// For each cargo the unit is already carrying, and which is
// not to be unloaded at this stop, check if the cargo is
// completely full and if not, try to fill to capacity.
Colony colony = unit.getColony();
Location loc = (unit.isInEurope()) ? unit.getOwner().getEurope()
: colony;
Game game = freeColClient.getGame();
for (Goods goods : unit.getGoodsList()) {
GoodsType type = goods.getType();
int index, toLoad;
if ((toLoad = GoodsContainer.CARGO_SIZE - goods.getAmount()) > 0
&& (index = goodsTypesToLoad.indexOf(type)) >= 0) {
int present, atStop;
if (unit.isInEurope()) {
present = atStop = Integer.MAX_VALUE;
} else {
present = colony.getGoodsContainer().getGoodsCount(type);
atStop = colony.getExportAmount(type);
}
if (atStop > 0) {
Goods cargo = new Goods(game, loc, type,
Math.min(toLoad, atStop));
if (loadGoods(cargo, unit)) {
if (messages != null) {
messages.add(getLoadGoodsMessage(unit, type,
cargo.getAmount(), present,
atStop, toLoad));
}
ret = true;
}
} else if (present > 0) {
if (messages != null) {
messages.add(getLoadGoodsMessage(unit, type,
0, present, 0, toLoad));
}
}
// Do not try to load this goods type again. Either
// it has already succeeded, or it can not ever
// succeed because there is nothing available.
goodsTypesToLoad.remove(index);
}
}
// Then fill any remaining empty cargo slots.
for (GoodsType type : goodsTypesToLoad) {
if (unit.getSpaceLeft() <= 0) break; // Full
int toLoad = GoodsContainer.CARGO_SIZE;
int present, atStop;
if (unit.isInEurope()) {
present = atStop = Integer.MAX_VALUE;
} else {
present = colony.getGoodsContainer().getGoodsCount(type);
atStop = colony.getExportAmount(type);
}
if (atStop > 0) {
Goods cargo = new Goods(game, loc, type,
Math.min(toLoad, atStop));
if (loadGoods(cargo, unit)) {
if (messages != null) {
messages.add(getLoadGoodsMessage(unit, type,
cargo.getAmount(), present, atStop, toLoad));
}
ret = true;
}
} else if (present > 0) {
if (messages != null) {
messages.add(getLoadGoodsMessage(unit, type,
0, present, 0, toLoad));
}
}
}
return ret;
}
/**
* Gets a message describing a goods loading.
*
* @param unit The <code>Unit</code> that is loading.
* @param type The <code>GoodsType</code> the type of goods being loaded.
* @param amount The amount of goods loaded.
* @param present The amount of goods already at the location.
* @param atStop The amount of goods available to load.
* @param toLoad The amount of goods the unit could load.
* @return A model message describing the load.
*/
private ModelMessage getLoadGoodsMessage(Unit unit, GoodsType type,
int amount, int present,
int atStop, int toLoad) {
Player player = unit.getOwner();
Location loc = unit.getLocation();
String route = unit.getTradeRoute().getName();
String key = null;
int more = 0;
if (toLoad < atStop) {
key = "traderoute.loadImportLimited";
more = atStop - toLoad;
} else if (present > atStop && toLoad > atStop) {
key = "traderoute.loadExportLimited";
more = present - atStop;
} else {
key = "traderoute.load";
}
return new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT, key, unit)
.addName("%route%", route)
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addStringTemplate("%location%", loc.getLocationNameFor(player))
.addAmount("%amount%", amount)
.add("%goods%", type.getNameKey())
.addAmount("%more%", more);
}
/**
* Work out what goods to unload from a unit at a stop, and unload them.
*
* @param unit The <code>Unit</code> to unload.
* @param messages A list of messages to update.
* @return True if something was unloaded.
*/
private boolean unloadUnitAtStop(Unit unit, List<ModelMessage> messages) {
Colony colony = unit.getColony();
Stop stop = unit.getStop();
final List<GoodsType> goodsTypesToLoad = stop.getCargo();
boolean ret = false;
// Unload everything that is on the carrier but not listed to
// be loaded at this stop.
Game game = freeColClient.getGame();
for (Goods goods : new ArrayList<Goods>(unit.getGoodsList())) {
GoodsType type = goods.getType();
if (goodsTypesToLoad.contains(type)) continue; // Keep this cargo.
int atStop = (colony == null) ? Integer.MAX_VALUE // Europe
: colony.getImportAmount(type);
int toUnload = goods.getAmount();
if (toUnload > atStop) {
String locName = colony.getName();
String overflow = Integer.toString(toUnload - atStop);
int option = freeColClient.getClientOptions()
.getInteger(ClientOptions.UNLOAD_OVERFLOW_RESPONSE);
switch (option) {
case ClientOptions.UNLOAD_OVERFLOW_RESPONSE_ASK:
StringTemplate template
= StringTemplate.template("traderoute.warehouseCapacity")
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addName("%colony%", locName)
.addName("%amount%", overflow)
.add("%goods%", goods.getNameKey());
if (!gui.showConfirmDialog(colony.getTile(), template,
"yes", "no")) {
toUnload = atStop;
}
break;
case ClientOptions.UNLOAD_OVERFLOW_RESPONSE_NEVER:
toUnload = atStop;
break;
case ClientOptions.UNLOAD_OVERFLOW_RESPONSE_ALWAYS:
break;
default:
logger.warning("Illegal UNLOAD_OVERFLOW_RESPONSE: "
+ Integer.toString(option));
break;
}
}
// Try to unload.
Goods cargo = (goods.getAmount() == toUnload) ? goods
: new Goods(game, unit, type, toUnload);
if (unloadGoods(cargo, unit, colony)) {
if (messages != null) {
messages.add(getUnloadGoodsMessage(unit, type,
cargo.getAmount(), atStop, goods.getAmount(),
toUnload));
}
ret = true;
}
}
return ret;
}
/**
* Gets a message describing a goods unloading.
*
* @param unit The <code>Unit</code> that is unloading.
* @param type The <code>GoodsType</code> the type of goods being unloaded.
* @param amount The amount of goods unloaded.
* @param present The amount of goods already carried by the unit.
* @param atStop The amount of goods available to unload.
* @param toUnload The amount of goods actually unloaded.
* @return A model message describing the unload.
*/
private ModelMessage getUnloadGoodsMessage(Unit unit, GoodsType type,
int amount, int atStop,
int present, int toUnload) {
String key = null;
int overflow = 0;
if (present == toUnload) {
key = "traderoute.unload";
} else if (toUnload > atStop) {
key = "traderoute.overflow";
overflow = toUnload - atStop;
} else {
key = "traderoute.nounload";
overflow = present - atStop;
}
StringTemplate loc = unit.getLocation().getLocationNameFor(unit.getOwner());
return new ModelMessage(ModelMessage.MessageType.GOODS_MOVEMENT, key,
unit)
.addName("%route%", unit.getTradeRoute().getName())
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addStringTemplate("%location%", loc)
.addAmount("%amount%", amount)
.addAmount("%overflow%", overflow)
.add("%goods%", type.getNameKey());
}
// Server access routines called from multiple places.
/**
* Claim a tile.
*
* @param player The <code>Player</code> that is claiming.
* @param tile The <code>Tile</code> to claim.
* @param colony An optional <code>Colony</code> to own the tile.
* @param price The price required.
* @param offer An offer to pay.
* @return True if the claim succeeded.
*/
private boolean claimTile(Player player, Tile tile, Colony colony,
int price, int offer) {
Player owner = tile.getOwner();
if (price < 0) return false; // not for sale
if (price > 0) { // for sale by natives
if (offer >= price) { // offered more than enough
price = offer;
} else if (offer < 0) { // plan to steal
price = NetworkConstants.STEAL_LAND;
} else {
boolean canAccept = player.checkGold(price);
switch (gui.showClaimDialog(tile, player, price,
owner, canAccept)) {
case CANCEL:
return false;
case ACCEPT: // accepted price
break;
case STEAL:
price = NetworkConstants.STEAL_LAND;
break;
default:
throw new IllegalStateException("showClaimDialog fail");
}
}
} // else price == 0 and we can just proceed
// Ask the server
if (askServer().claimLand(tile, colony, price)
&& player.owns(tile)) {
gui.updateGoldLabel();
return true;
}
return false;
}
/**
* Emigrate a unit from Europe.
*
* @param player The <code>Player</code> that owns the unit.
* @param slot The slot to emigrate from.
*/
private void emigrate(Player player, int slot) {
Europe europe = player.getEurope();
EuropeWas europeWas = new EuropeWas(europe);
if (askServer().emigrate(slot)) {
europeWas.fireChanges();
gui.updateGoldLabel();
}
}
/**
* Follow a path.
*
* @param unit The <code>Unit</code> to move.
* @param path The path to follow.
* @return True if the unit has completed the path and can move further.
*/
private boolean followPath(Unit unit, PathNode path) {
// Traverse the path to the destination.
for (; path != null; path = path.next) {
// Special case for the map edges on maps not
// surrounded by high seas.
if (unit.getDestination() instanceof Europe
&& unit.getTile() != null
&& unit.getTile().canMoveToEurope()) {
moveTo(unit, unit.getDestination());
return false;
}
if (!moveDirection(unit, path.getDirection(), false)) return false;
}
return true;
}
/**
* Load some goods onto a carrier.
*
* @param goods The <code>Goods</code> to load.
* @param carrier The <code>Unit</code> to load onto.
* @return True if the load succeeded.
*/
private boolean loadGoods(Goods goods, Unit carrier) {
if (carrier.isInEurope() && goods.getLocation() instanceof Europe) {
if (!carrier.getOwner().canTrade(goods)) return false;
return buyGoods(goods.getType(), goods.getAmount(), carrier);
}
GoodsType type = goods.getType();
GoodsContainer container = carrier.getGoodsContainer();
int oldAmount = container.getGoodsCount(type);
UnitWas unitWas = new UnitWas(carrier);
Colony colony = carrier.getColony();
ColonyWas colonyWas = (colony == null) ? null : new ColonyWas(colony);
if (askServer().loadCargo(goods, carrier)
&& container.getGoodsCount(type) != oldAmount) {
if (colonyWas != null) colonyWas.fireChanges();
unitWas.fireChanges();
return true;
}
return false;
}
/**
* Unload some goods from a carrier.
*
* @param goods The <code>Goods</code> to unload.
* @param carrier The <code>Unit</code> carrying the goods.
* @param colony The <code>Colony</code> to unload to,
* or null if unloading in Europe.
* @return True if the unload succeeded.
*/
private boolean unloadGoods(Goods goods, Unit carrier, Colony colony) {
if (colony == null && carrier.isInEurope()) {
return (!carrier.getOwner().canTrade(goods)) ? false
: sellGoods(goods);
}
GoodsType type = goods.getType();
GoodsContainer container = carrier.getGoodsContainer();
int oldAmount = container.getGoodsCount(type);
ColonyWas colonyWas = (colony == null) ? null : new ColonyWas(colony);
UnitWas unitWas = new UnitWas(carrier);
if (askServer().unloadCargo(goods)
&& container.getGoodsCount(type) != oldAmount) {
if (colonyWas != null) colonyWas.fireChanges();
unitWas.fireChanges();
return true;
}
return false;
}
// Utilities connected with saving the game
/**
* Creates at least one autosave game file of the currently played
* game in the autosave directory. Does nothing if there is no
* game running.
*
*/
private void autosave_game () {
Game game = freeColClient.getGame();
if (game == null) return;
// unconditional save per round (fix file "last-turn")
String autosave_text
= Messages.message("clientOptions.savegames.autosave.fileprefix");
String filename = autosave_text + "-"
+ Messages.message("clientOptions.savegames.autosave.lastturn")
+ ".fsg";
String beforeFilename = autosave_text + "-"
+ Messages.message("clientOptions.savegames.autosave.beforelastturn")
+ ".fsg";
File autosaveDir = FreeCol.getAutosaveDirectory();
File saveGameFile = new File(autosaveDir, filename);
File beforeSaveFile = new File(autosaveDir, beforeFilename);
// if "last-turn" file exists, shift it to "before-last-turn" file
if (saveGameFile.exists()) {
beforeSaveFile.delete();
saveGameFile.renameTo(beforeSaveFile);
}
saveGame(saveGameFile);
// conditional save after user-set period
ClientOptions options = freeColClient.getClientOptions();
int savegamePeriod = options.getInteger(ClientOptions.AUTOSAVE_PERIOD);
int turnNumber = game.getTurn().getNumber();
if (savegamePeriod <= 1
|| (savegamePeriod != 0 && turnNumber % savegamePeriod == 0)) {
Player player = game.getCurrentPlayer();
String playerNation = player == null ? ""
: Messages.message(player.getNation().getNameKey());
String gid = Integer.toHexString(game.getUUID().hashCode());
filename = Messages.message("clientOptions.savegames.autosave.fileprefix")
+ '-' + gid + "_" + playerNation
+ "_" + getSaveGameString(game.getTurn()) + ".fsg";
saveGameFile = new File(autosaveDir, filename);
saveGame(saveGameFile);
}
}
/**
* Returns a string representation of the given turn suitable for
* savegame files.
*
* @param turn a <code>Turn</code> value
* @return A string with the format: "<i>[season] year</i>".
* Examples: "1602_1_Spring", "1503"...
*/
private String getSaveGameString(Turn turn) {
int year = turn.getYear();
switch (turn.getSeason()) {
case SPRING:
return Integer.toString(year) + "_1_" + Messages.message("spring");
case AUTUMN:
return Integer.toString(year) + "_2_" + Messages.message("autumn");
case YEAR:
default:
return Integer.toString(year);
}
}
/**
* Gets the most recently saved game file, or <b>null</b>. (This
* may be either from a recent arbitrary user operation or an
* autosave function.)
*
* @return The recent save game file
*/
public File getLastSaveGameFile() {
File lastSave = null;
for (File directory : new File[] {
FreeCol.getSaveDirectory(), FreeCol.getAutosaveDirectory() }) {
for (File savegame : directory.listFiles(FSG_FILTER)) {
if (lastSave == null
|| savegame.lastModified() > lastSave.lastModified()) {
lastSave = savegame;
}
}
}
return lastSave;
}
/**
* Opens a dialog where the user should specify the filename and
* loads the game.
*/
public void loadGame(File file) {
// File file = gui.showLoadDialog(FreeCol.getSaveDirectory());
if (file == null) return;
if (!file.isFile()) {
gui.errorMessage("fileNotFound");
return;
}
if (freeColClient.isInGame()
&& !gui.showConfirmDialog("stopCurrentGame.text",
"stopCurrentGame.yes", "stopCurrentGame.no")) {
return;
}
freeColClient.getConnectController().quitGame(true);
gui.setActiveUnit(null);
gui.removeInGameComponents();
freeColClient.getConnectController().loadGame(file);
}
/**
* Reloads a game state which was previously saved via
* <code>quicksaveGame</code>.
*
* @return boolean <b>true</b> if and only if a game was loaded
*/
public boolean quickReload () {
Game game = freeColClient.getGame();
if (game != null) {
String gid = Integer.toHexString(game.getUUID().hashCode());
String filename = "quicksave-" + gid + ".fsg";
File file = new File(FreeCol.getAutosaveDirectory(), filename);
if (file.isFile()) {
// ask user to confirm reload action
boolean ok = true; // canvas.showConfirmDialog(gid, gid, filename);
// perform loading game state if answer == ok
if (ok) {
freeColClient.getConnectController().quitGame(true);
gui.removeInGameComponents();
freeColClient.getConnectController().loadGame(file);
return true;
}
}
}
return false;
}
/**
* Opens a dialog where the user should specify the filename and
* saves the game.
*
* @return True if the game was saved.
*/
public boolean saveGame() {
FCLog.log("Saving game");
Player player = freeColClient.getMyPlayer();
Game game = freeColClient.getGame();
String gid = Integer.toHexString(game.getUUID().hashCode());
String fileName = /* player.getName() + "_" */ gid + "_"
+ Messages.message(player.getNationName()) + "_"
+ getSaveGameString(game.getTurn())
+ ".fsg";
fileName = fileName.replaceAll(" ", "_");
if (freeColClient.canSaveCurrentGame()) {
final File file
= gui.showSaveDialog(FreeCol.getSaveDirectory(), fileName);
if (file != null) {
FCLog.log("Save to " + file.getPath());
FreeCol.setSaveDirectory(file.getParentFile());
return saveGame(file);
}
} else {
FCLog.log("Cannot save current game");
}
return false;
}
/**
* Saves the game to the given file.
*
* @param file The <code>File</code>.
* @return True if the game was saved.
*/
public boolean saveGame(final File file) {
if (!file.getAbsolutePath().endsWith(".fsg")) {
File f = new File(file.getAbsolutePath() + ".fsg");
return saveGame(f);
}
FreeColServer server = freeColClient.getFreeColServer();
boolean result = false;
gui.showStatusPanel(Messages.message("status.savingGame"), true);
try {
server.setActiveUnit(gui.getActiveUnit());
server.saveGame(file, freeColClient.getMyPlayer().getName(),
freeColClient.getClientOptions());
lastSaveGameFile = file;
gui.closeStatusPanel();
result = true;
FCLog.log("Save done");
} catch (IOException e) {
FCLog.log("Failed to save game", e);
gui.errorMessage("couldNotSaveGame");
}
gui.requestFocusInWindow();
return result;
}
/**
* Saves the game to a fix-named file in the autosave directory, which may
* be used for quick-reload.
*
* @return boolean <b>true</b> if and only if the game was saved
*/
public boolean quicksaveGame () {
Game game = freeColClient.getGame();
if (game != null) {
String gid = Integer.toHexString(game.getUUID().hashCode());
String filename = "quicksave-" + gid + ".fsg";
File file = new File(FreeCol.getAutosaveDirectory(), filename);
return saveGame(file);
}
return false;
}
// Utilities for message handling.
/**
* Provides an opportunity to filter the messages delivered to the canvas.
*
* @param message the message that is candidate for delivery to the canvas
* @return true if the message should be delivered
*/
private boolean shouldAllowMessage(ModelMessage message) {
BooleanOption option = freeColClient.getClientOptions()
.getBooleanOption(message);
return (option == null) ? true : option.getValue();
}
private synchronized void startIgnoringMessage(String key, int turn) {
logger.finer("Ignoring model message with key " + key);
messagesToIgnore.put(key, new Integer(turn));
}
private synchronized void stopIgnoringMessage(String key) {
logger.finer("Removing model message with key " + key
+ " from ignored messages.");
messagesToIgnore.remove(key);
}
private synchronized Integer getTurnForMessageIgnored(String key) {
return messagesToIgnore.get(key);
}
/**
* Ignore this ModelMessage from now on until it is not generated
* in a turn.
*
* @param message a <code>ModelMessage</code> value
* @param flag whether to ignore the ModelMessage or not
*/
public synchronized void ignoreMessage(ModelMessage message, boolean flag) {
String key = message.getSourceId();
if (message.getTemplateType() == StringTemplate.TemplateType.TEMPLATE) {
for (String otherkey : message.getKeys()) {
if ("%goods%".equals(otherkey)) {
key += otherkey;
}
break;
}
}
if (flag) {
startIgnoringMessage(key,
freeColClient.getGame().getTurn().getNumber());
} else {
stopIgnoringMessage(key);
}
}
/**
* Displays pending <code>ModelMessage</code>s.
*
* @param allMessages Display all messages or just the undisplayed ones.
*/
public void displayModelMessages(boolean allMessages) {
displayModelMessages(allMessages, false);
}
/**
* Displays pending <code>ModelMessage</code>s.
*
* @param allMessages Display all messages or just the undisplayed ones.
* @param endOfTurn Use a turn report panel if necessary.
*/
public void displayModelMessages(final boolean allMessages,
final boolean endOfTurn) {
Player player = freeColClient.getMyPlayer();
int thisTurn = freeColClient.getGame().getTurn().getNumber();
final ArrayList<ModelMessage> messages = new ArrayList<ModelMessage>();
for (ModelMessage m : ((allMessages) ? player.getModelMessages()
: player.getNewModelMessages())) {
if (shouldAllowMessage(m)) {
if (m.getMessageType() == MessageType.WAREHOUSE_CAPACITY) {
String key = m.getSourceId();
switch (m.getTemplateType()) {
case TEMPLATE:
for (String otherkey : m.getKeys()) {
if ("%goods%".equals(otherkey)) {
key += otherkey;
break;
}
}
break;
default:
break;
}
Integer turn = getTurnForMessageIgnored(key);
if (turn != null && turn.intValue() == thisTurn - 1) {
startIgnoringMessage(key, thisTurn);
m.setBeenDisplayed(true);
continue;
}
}
messages.add(m);
}
// flag all messages delivered as "beenDisplayed".
m.setBeenDisplayed(true);
}
for (Entry<String, Integer> entry : messagesToIgnore.entrySet()) {
if (entry.getValue().intValue() < thisTurn - 1) {
if (logger.isLoggable(Level.FINER)) {
logger.finer("Removing old model message with key "
+ entry.getKey() + " from ignored messages.");
}
stopIgnoringMessage(entry.getKey());
}
}
if (messages.size() > 0) {
final ModelMessage[] a = messages.toArray(new ModelMessage[0]);
Runnable uiTask = new Runnable() {
public void run() {
if (endOfTurn) {
gui.showReportTurnPanel(a);
} else {
gui.showModelMessages(a);
}
}
};
freeColClient.getActionManager().update();
if (SwingUtilities.isEventDispatchThread()) {
uiTask.run();
} else {
try {
SwingUtilities.invokeAndWait(uiTask);
} catch (InterruptedException e) {
logger.log(Level.WARNING, "Message display", e);
} catch (InvocationTargetException e) {
logger.log(Level.WARNING, "Message display", e);
}
}
}
}
/**
* Displays the next <code>ModelMessage</code>.
*/
public void nextModelMessage() {
displayModelMessages(false);
}
// All the routines from here on are called from actions.
// They (usually) then make requests to the server.
/**
* Abandon a colony with no units.
*
* @param colony The <code>Colony</code> to be abandoned.
*/
public void abandonColony(Colony colony) {
if (!requireOurTurn()) return;
Player player = freeColClient.getMyPlayer();
// Sanity check
if (colony == null || !player.owns(colony)
|| colony.getUnitCount() > 0) {
throw new IllegalStateException("Abandon bogus colony");
}
// Proceed to abandon
Tile tile = colony.getTile();
if (askServer().abandonColony(colony)
&& tile.getSettlement() == null) {
player.invalidateCanSeeTiles();
gui.setActiveUnit(null);
gui.setSelectedTile(tile, false);
}
}
/**
* Assigns a unit to a teacher <code>Unit</code>.
*
* @param student an <code>Unit</code> value
* @param teacher an <code>Unit</code> value
*/
public void assignTeacher(Unit student, Unit teacher) {
Player player = freeColClient.getMyPlayer();
if (!requireOurTurn()
|| student == null
|| !player.owns(student)
|| student.getColony() == null
|| !(student.getLocation() instanceof WorkLocation)
|| teacher == null
|| !player.owns(teacher)
|| !student.canBeStudent(teacher)
|| teacher.getColony() == null
|| student.getColony() != teacher.getColony()
|| !teacher.getColony().canTrain(teacher)) {
return;
}
askServer().assignTeacher(student, teacher);
}
/**
* Assigns a trade route to a unit using the trade route dialog.
*
* @param unit The <code>Unit</code> to assign a trade route to.
*/
public void assignTradeRoute(Unit unit) {
TradeRoute oldRoute = unit.getTradeRoute();
TradeRoute route = gui.showTradeRouteDialog(oldRoute, unit.getTile());
if (route == null) return; // Cancelled
// Delete or deassign of trade route removes the route from the unit
route = unit.getTradeRoute();
if (oldRoute != route) assignTradeRoute(unit, route);
}
/**
* Assigns a trade route to a unit.
*
* @param unit The <code>Unit</code> to assign a trade route to.
* @param tradeRoute The <code>TradeRoute</code> to assign.
*/
public void assignTradeRoute(Unit unit, TradeRoute tradeRoute) {
if (askServer().assignTradeRoute(unit, tradeRoute)) {
if ((tradeRoute = unit.getTradeRoute()) != null
&& freeColClient.currentPlayerIsMyPlayer()) {
moveToDestination(unit);
}
}
}
/**
* Boards a specified unit onto a carrier.
* The carrier must be at the same location as the boarding unit.
*
* @param unit The <code>Unit</code> which is to board the carrier.
* @param carrier The carrier to board.
* @return True if the unit boards the carrier.
*/
public boolean boardShip(Unit unit, Unit carrier) {
if (!requireOurTurn()) return false;
// Sanity checks.
if (unit == null) {
logger.warning("unit == null");
return false;
}
if (carrier == null) {
logger.warning("Trying to load onto a non-existent carrier.");
return false;
}
if (unit.isNaval()) {
logger.warning("Trying to load a ship onto another carrier.");
return false;
}
if (unit.isInEurope() != carrier.isInEurope()
|| unit.getTile() != carrier.getTile()) {
logger.warning("Unit and carrier are not co-located.");
return false;
}
// Proceed to board
UnitWas unitWas = new UnitWas(unit);
if (askServer().embark(unit, carrier, null)
&& unit.getLocation() == carrier) {
gui.playSound("sound.event.loadCargo");
unitWas.fireChanges();
nextActiveUnit();
return true;
}
return false;
}
/**
* Use the active unit to build a colony.
*/
public void buildColony() {
if (!requireOurTurn()) return;
final Player player = freeColClient.getMyPlayer();
// Check unit can build, and is on the map.
// Show the colony warnings if required.
final Unit unit = gui.getActiveUnit();
if (unit == null) {
return;
} else if (!unit.canBuildColony()) {
gui.showInformationMessage(unit,
StringTemplate.template("buildColony.badUnit")
.addName("%unit%", unit.getName()));
return;
}
final Tile tile = unit.getTile();
if (tile.getColony() != null) {
askServer().joinColony(unit, tile.getColony());
return;
}
NoClaimReason reason = player.canClaimToFoundSettlementReason(tile);
if (reason != NoClaimReason.NONE
&& reason != NoClaimReason.NATIVES) {
gui.showInformationMessage("noClaimReason."
+ reason.toString().toLowerCase(Locale.US));
return;
}
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_COLONY_WARNINGS)
&& !buildColonyShowWarnings(tile, unit)) {
return;
}
if (tile.getOwner() != null && !player.owns(tile)) {
// Claim tile from other owners before founding a settlement.
// Only native owners that we can steal, buy from, or use a
// bonus center tile exception should be possible by this point.
if (!claimTile(player, tile, null, player.getLandPrice(tile), 0))
return;
// One more check that founding can now proceed.
if (!player.canClaimToFoundSettlement(tile)) return;
}
// Get and check the name.
String name = player.getSettlementName();
if (Player.ASSIGN_SETTLEMENT_NAME.equals(name)) {
player.installSettlementNames(Messages.getSettlementNames(player),
null);
name = player.getSettlementName();
}
gui.showInputDialog(tile,
StringTemplate.key("nameColony.text"), name,
"nameColony.yes", "nameColony.no", true, new InputDialogListener() {
@Override
public void onPositiveSelected(String name) {
if (player.getSettlement(name) != null) {
// Colony name must be unique.
gui.showInformationMessage(tile,
StringTemplate.template("nameColony.notUnique")
.addName("%name%", name));
return;
}
if (askServer().buildColony(name, unit)
&& tile.getSettlement() != null) {
player.invalidateCanSeeTiles();
gui.playSound("sound.event.buildingComplete");
gui.setActiveUnit(null);
gui.setSelectedTile(tile, false);
// Check units present for treasure cash-in as they are now
// suddenly in-colony.
for (Unit unitInTile : tile.getUnitList()) {
checkCashInTreasureTrain(unitInTile);
}
}
}
@Override
public void onNegativeSelected() {
// Do nothing
}
});
}
/**
* A colony is proposed to be built. Show warnings if this has
* disadvantages.
*
* @param tile The <code>Tile</code> on which the colony is to be built.
* @param unit The <code>Unit</code> which is to build the colony.
*/
private boolean buildColonyShowWarnings(Tile tile, Unit unit) {
boolean landLocked = true;
boolean ownedByEuropeans = false;
boolean ownedBySelf = false;
boolean ownedByIndians = false;
java.util.Map<GoodsType, Integer> goodsMap
= new HashMap<GoodsType, Integer>();
for (GoodsType goodsType : getSpecification().getGoodsTypeList()) {
if (goodsType.isFoodType()) {
int potential = 0;
if (tile.getType().isPrimaryGoodsType(goodsType)) {
potential = tile.potential(goodsType, null);
}
goodsMap.put(goodsType, new Integer(potential));
} else if (goodsType.isBuildingMaterial()) {
while (goodsType.isRefined()) {
goodsType = goodsType.getRawMaterial();
}
int potential = 0;
if (tile.getType().isSecondaryGoodsType(goodsType)) {
potential = tile.potential(goodsType, null);
}
goodsMap.put(goodsType, new Integer(potential));
}
}
for (Tile newTile: tile.getSurroundingTiles(1)) {
if (!newTile.isLand()) {
landLocked = false;
}
for (Entry<GoodsType, Integer> entry : goodsMap.entrySet()) {
entry.setValue(entry.getValue().intValue()
+ newTile.potential(entry.getKey(), null));
}
Player tileOwner = newTile.getOwner();
if (unit.getOwner() == tileOwner) {
if (newTile.getOwningSettlement() != null) {
// we are using newTile
ownedBySelf = true;
} else {
for (Tile ownTile: newTile.getSurroundingTiles(1)) {
Colony colony = ownTile.getColony();
if (colony != null
&& colony.getOwner() == unit.getOwner()) {
// newTile can be used from an own colony
ownedBySelf = true;
break;
}
}
}
} else if (tileOwner != null && tileOwner.isEuropean()) {
ownedByEuropeans = true;
} else if (tileOwner != null) {
ownedByIndians = true;
}
}
int food = 0;
for (Entry<GoodsType, Integer> entry : goodsMap.entrySet()) {
if (entry.getKey().isFoodType()) {
food += entry.getValue().intValue();
}
}
ArrayList<ModelMessage> messages = new ArrayList<ModelMessage>();
if (landLocked) {
messages.add(new ModelMessage(ModelMessage.MessageType.MISSING_GOODS,
"buildColony.landLocked", unit,
getSpecification().getGoodsType("model.goods.fish")));
}
if (food < 8) {
messages.add(new ModelMessage(ModelMessage.MessageType.MISSING_GOODS,
"buildColony.noFood", unit,
getSpecification().getPrimaryFoodType()));
}
for (Entry<GoodsType, Integer> entry : goodsMap.entrySet()) {
if (!entry.getKey().isFoodType() && entry.getValue().intValue() < 4) {
messages.add(new ModelMessage(ModelMessage.MessageType.MISSING_GOODS,
"buildColony.noBuildingMaterials",
unit, entry.getKey())
.add("%goods%", entry.getKey().getNameKey()));
}
}
if (ownedBySelf) {
messages.add(new ModelMessage(ModelMessage.MessageType.WARNING,
"buildColony.ownLand", unit));
}
if (ownedByEuropeans) {
messages.add(new ModelMessage(ModelMessage.MessageType.WARNING,
"buildColony.EuropeanLand", unit));
}
if (ownedByIndians) {
messages.add(new ModelMessage(ModelMessage.MessageType.WARNING,
"buildColony.IndianLand", unit));
}
if (messages.isEmpty()) return true;
ModelMessage[] modelMessages
= messages.toArray(new ModelMessage[messages.size()]);
return gui.showConfirmDialog(unit.getTile(),
modelMessages, "buildColony.yes", "buildColony.no");
}
/**
* Buy goods in Europe.
* The amount of goods is adjusted to the space in the carrier.
*
* @param type The type of goods to buy.
* @param amount The amount of goods to buy.
* @param carrier The <code>Unit</code> acting as carrier.
* @return True if the purchase succeeds.
*/
public boolean buyGoods(GoodsType type, int amount, Unit carrier) {
if (!requireOurTurn()) return false;
// Sanity checks. Should not happen!
Player player = freeColClient.getMyPlayer();
if (type == null) {
throw new NullPointerException("Goods type must not be null.");
} else if (carrier == null) {
throw new NullPointerException("Carrier must not be null.");
} else if (!player.owns(carrier)) {
throw new IllegalArgumentException("Carrier owned by someone else.");
} else if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive.");
} else if (!player.canTrade(type)) {
throw new IllegalArgumentException("Goods are boycotted.");
}
// Size check, if there are spare holds they can be filled, but...
int toBuy = GoodsContainer.CARGO_SIZE;
if (carrier.getSpaceLeft() <= 0) {
// ...if there are no spare holds, we can only fill a hold
// already partially filled with this type, otherwise fail.
int partial = carrier.getGoodsContainer().getGoodsCount(type)
% GoodsContainer.CARGO_SIZE;
if (partial == 0) return false;
toBuy -= partial;
}
if (amount < toBuy) toBuy = amount;
// Check that the purchase is funded.
Market market = player.getMarket();
if (!player.checkGold(market.getBidPrice(type, toBuy))) {
gui.errorMessage("notEnoughGold");
return false;
}
// Try to purchase.
int oldAmount = carrier.getGoodsContainer().getGoodsCount(type);
int price = market.getCostToBuy(type);
UnitWas unitWas = new UnitWas(carrier);
if (askServer().buyGoods(carrier, type, toBuy)
&& carrier.getGoodsContainer().getGoodsCount(type) != oldAmount) {
gui.playSound("sound.event.loadCargo");
unitWas.fireChanges();
for (TransactionListener l : market.getTransactionListener()) {
l.logPurchase(type, toBuy, price);
}
gui.updateGoldLabel();
nextModelMessage();
return true;
}
// Purchase failed for some reason.
return false;
}
/**
* Changes the state of this <code>Unit</code>.
*
* @param unit The <code>Unit</code>
* @param state The state of the unit.
*/
public void changeState(Unit unit, UnitState state) {
if (!requireOurTurn()) return;
if (!unit.checkSetState(state)) {
return; // Don't bother (and don't log, this is not exceptional)
}
// Check if this is a hostile fortification, and give the player
// a chance to confirm.
Player player = freeColClient.getMyPlayer();
if (state == UnitState.FORTIFYING && unit.isOffensiveUnit()
&& !unit.hasAbility(Ability.PIRACY)) {
Tile tile = unit.getTile();
if (tile != null && tile.getOwningSettlement() != null) {
Player enemy = tile.getOwningSettlement().getOwner();
if (player != enemy
&& player.getStance(enemy) != Stance.ALLIANCE) {
if (!confirmHostileAction(unit, tile)) return; // Aborted
}
}
}
if (askServer().changeState(unit, state)) {
if (!gui.isShowingSubPanel()
&& (unit.getMovesLeft() == 0
|| unit.getState() == UnitState.SENTRY
|| unit.getState() == UnitState.SKIPPED)) {
nextActiveUnit();
} else {
gui.refresh();
}
}
}
/**
* Changes the work type of this <code>Unit</code>.
*
* @param unit The <code>Unit</code>
* @param improvementType a <code>TileImprovementType</code> value
*/
public void changeWorkImprovementType(Unit unit,
TileImprovementType improvementType) {
if (!requireOurTurn()) return;
if (!unit.checkSetState(UnitState.IMPROVING)
|| improvementType.isNatural()) {
return; // Don't bother (and don't log, this is not exceptional)
}
Player player = freeColClient.getMyPlayer();
Tile tile = unit.getTile();
if (!player.owns(tile)) {
if (!claimTile(player, tile, null, player.getLandPrice(tile), 0)
|| !player.owns(tile)) return;
}
if (askServer().changeWorkImprovementType(unit,
improvementType)) {
// Redisplay should work
}
nextActiveUnit();
}
/**
* Changes the work type of this <code>Unit</code>.
*
* @param unit The <code>Unit</code>
* @param workType The new <code>GoodsType</code> to produce.
*/
public void changeWorkType(Unit unit, GoodsType workType) {
if (!requireOurTurn()) return;
UnitWas unitWas = new UnitWas(unit);
if (askServer().changeWorkType(unit, workType)) {
unitWas.fireChanges();
}
}
/**
* Check if a unit is a treasure train, and if it should be cashed in.
* Transfers the gold carried by this unit to the {@link Player owner}.
*
* @param unit The <code>Unit</code> to be checked.
* @return True if the unit was cashed in (and disposed).
*/
public boolean checkCashInTreasureTrain(Unit unit) {
if (!unit.canCarryTreasure() || !unit.canCashInTreasureTrain()
|| !requireOurTurn()) {
return false; // Fail quickly if just not a candidate.
}
boolean cash;
Tile tile = unit.getTile();
Europe europe = unit.getOwner().getEurope();
if (europe == null || unit.isInEurope()) {
cash = true; // No need to check for transport.
} else {
int fee = unit.getTransportFee();
StringTemplate template;
if (fee == 0) {
template = StringTemplate.template("cashInTreasureTrain.free");
} else {
int percent = getSpecification()
.getInteger("model.option.treasureTransportFee");
template = StringTemplate.template("cashInTreasureTrain.pay")
.addAmount("%fee%", percent);
}
cash = gui.showConfirmDialog(unit.getTile(), template,
"cashInTreasureTrain.yes", "cashInTreasureTrain.no");
}
// Update if cash in succeeds.
UnitWas unitWas = new UnitWas(unit);
if (cash && askServer().cashInTreasureTrain(unit)
&& unit.isDisposed()) {
gui.playSound("sound.event.cashInTreasureTrain");
unitWas.fireChanges();
gui.updateGoldLabel();
nextActiveUnit(tile);
return true;
}
return false;
}
/**
* Claim a tile.
*
* @param tile The <code>Tile</code> to claim.
* @param colony An optional <code>Colony</code> to own the tile.
* @param offer An offer to pay.
* @return True if the claim succeeded.
*/
public boolean claimLand(Tile tile, Colony colony, int offer) {
if (!requireOurTurn()) return false;
Player player = freeColClient.getMyPlayer();
int price = ((colony != null) ? player.canClaimForSettlement(tile)
: player.canClaimForImprovement(tile)) ? 0
: player.getLandPrice(tile);
return claimTile(player, tile, colony, price, offer);
}
/**
* Clears the goto orders of the given unit by setting its destination
* to null.
*
* @param unit The <code>Unit</code> to clear the destination for.
*/
public void clearGotoOrders(Unit unit) {
if (unit != null && unit.getDestination() != null) {
setDestination(unit, null);
}
}
/**
* Clears the orders of the given unit.
* Make the unit active and set a null destination and trade route.
*
* @param unit The <code>Unit</code> to clear the orders of
* @return boolean <b>true</b> if the orders were cleared
*/
public boolean clearOrders(Unit unit) {
if (!requireOurTurn() || unit == null
|| !unit.checkSetState(UnitState.ACTIVE)) return false;
if (unit.getState() == UnitState.IMPROVING
&& !gui.showConfirmDialog(unit.getTile(),
StringTemplate.template("model.unit.confirmCancelWork")
.addAmount("%turns%", unit.getWorkTurnsLeft()),
"yes", "no")) {
return false;
}
if (unit.getTradeRoute() != null) assignTradeRoute(unit, null);
clearGotoOrders(unit);
return askServer().changeState(unit, UnitState.ACTIVE);
}
/**
* Clear the speciality of a Unit, making it a Free Colonist.
*
* @param unit The <code>Unit</code> to clear the speciality of.
*/
public void clearSpeciality(Unit unit) {
if (!requireOurTurn()) return;
UnitType oldType = unit.getType();
UnitType newType = oldType.getTargetType(ChangeType.CLEAR_SKILL,
unit.getOwner());
if (newType == null) {
gui.showInformationMessage(unit,
StringTemplate.template("clearSpeciality.impossible")
.addStringTemplate("%unit%", Messages.getLabel(unit)));
return;
}
Tile tile = (gui.isShowingSubPanel()) ? null : unit.getTile();
if (!gui.showConfirmDialog(tile,
StringTemplate.template("clearSpeciality.areYouSure")
.addStringTemplate("%oldUnit%", Messages.getLabel(unit))
.add("%unit%", newType.getNameKey()),
"yes", "no")) {
return;
}
// Try to clear.
if (askServer().clearSpeciality(unit)
&& unit.getType() == newType) {
// Would expect to need to do:
// unit.firePropertyChange(Unit.UNIT_TYPE_CHANGE,
// oldType, newType);
// but this routine is only called out of UnitLabel, where the
// unit icon is always updated anyway.
}
nextActiveUnit();
}
/**
* Declares independence for the home country.
*/
public void declareIndependence() {
if (!requireOurTurn()) return;
Game game = freeColClient.getGame();
Player player = freeColClient.getMyPlayer();
// Check for adequate support.
Event event = getSpecification()
.getEvent("model.event.declareIndependence");
for (Limit limit : event.getLimits()) {
if (!limit.evaluate(player)) {
gui.showInformationMessage(StringTemplate
.template(limit.getDescriptionKey())
.addAmount("%limit%",
limit.getRightHandSide().getValue(game)));
return;
}
}
if (player.getNewLandName() == null) {
// Can only happen in debug mode.
return;
}
// Confirm intention, and collect nation+country names.
List<String> names = gui.showConfirmDeclarationDialog();
if (names == null
|| names.get(0) == null || names.get(0).length() == 0
|| names.get(1) == null || names.get(1).length() == 0) {
// Empty name => user cancelled.
return;
}
// Ask server.
String nationName = names.get(0);
String countryName = names.get(1);
if (askServer().declareIndependence(nationName, countryName)
&& player.getPlayerType() == PlayerType.REBEL) {
gui.showDeclarationDialog();
freeColClient.getActionManager().update();
nextModelMessage();
}
}
/**
* Disbands the active unit.
*/
public void disbandActiveUnit() {
if (!requireOurTurn()) return;
Unit unit = gui.getActiveUnit();
if (unit == null) return;
Tile tile = (gui.isShowingSubPanel()) ? null
: unit.getTile();
if (!gui.showConfirmDialog(tile,
StringTemplate.key("disbandUnit.text"),
"disbandUnit.yes", "disbandUnit.no")) {
return;
}
// Try to disband
if (askServer().disbandUnit(unit)) {
nextActiveUnit();
}
}
/**
* End the turn command.
*/
public void endTurn() {
if (!requireOurTurn()) return;
// Show the end turn dialog, or not.
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_END_TURN_DIALOG)) {
List<Unit> units = new ArrayList<Unit>();
for (Unit unit : freeColClient.getMyPlayer().getUnits()) {
if (unit.couldMove()) {
units.add(unit);
}
}
if (units.size() > 0) {
gui.showEndTurnDialog(units, new DialogListener() {
@Override
public void onPositiveSelected() {
prepareEndTurn();
}
@Override
public void onNegativeSelected() {
// Do nothing
}
});
} else {
prepareEndTurn();
}
} else {
prepareEndTurn();
}
}
private void prepareEndTurn() {
// Ensure end-turn mode sticks.
if (moveMode < MODE_END_TURN) moveMode = MODE_END_TURN;
// Make sure all goto orders are complete before ending turn.
if (!doExecuteGotoOrders()) return;
doEndTurn();
}
/**
* Change the amount of equipment a unit has.
*
* @param unit The <code>Unit</code>.
* @param type The <code>EquipmentType</code> to equip with.
* @param amount How to change the amount of equipment the unit has.
*/
public void equipUnit(Unit unit, EquipmentType type, int amount) {
if (!requireOurTurn() || amount == 0) return;
Player player = freeColClient.getMyPlayer();
List<AbstractGoods> requiredGoods = type.getGoodsRequired();
Colony colony = null;
if (unit.isInEurope()) {
for (AbstractGoods goods : requiredGoods) {
GoodsType goodsType = goods.getType();
if (!player.canTrade(goodsType) && !payArrears(goodsType)) {
return; // payment failed for some reason
}
}
} else {
colony = unit.getColony();
if (colony == null) {
throw new IllegalStateException("Equip unit not in settlement/Europe");
}
}
ColonyWas colonyWas = (colony == null) ? null : new ColonyWas(colony);
UnitWas unitWas = new UnitWas(unit);
if (askServer().equipUnit(unit, type, amount)) {
if (colonyWas != null) colonyWas.fireChanges();
unitWas.fireChanges();
gui.updateGoldLabel();
}
}
/**
* Execute goto orders command.
*/
public void executeGotoOrders() {
doExecuteGotoOrders();
}
/**
* Retrieves client statistics.
*
* @return A <code>Map</code> containing the client statistics.
*/
public java.util.Map<String, String> getClientStatistics() {
return freeColClient.getGame().getStatistics();
}
/**
* Retrieves high scores from server.
*
* @return The list of high scores.
*/
public List<HighScore> getHighScores() {
return askServer().getHighScores();
}
/**
* Get the nation summary for a player.
*
* @param player The <code>Player</code> to summarize.
* @return A summary of that nation, or null on error.
*/
public NationSummary getNationSummary(Player player) {
return askServer().getNationSummary(player);
}
/**
* Gets a new trade route for a player.
*
* @param player The <code>Player</code> to get a new trade route for.
* @return A new <code>TradeRoute</code>.
*/
public TradeRoute getNewTradeRoute(Player player) {
int n = player.getTradeRoutes().size();
if (askServer().getNewTradeRoute()
&& player.getTradeRoutes().size() == n + 1) {
return player.getTradeRoutes().get(n);
}
return null;
}
/**
* Gathers information about the REF.
*
* @return a <code>List</code> value
*/
public List<AbstractUnit> getREFUnits() {
if (!requireOurTurn()) return Collections.emptyList();
return askServer().getREFUnits();
}
/**
* Retrieves the server statistics.
*
* @return A <code>Map</code> containing the server statistics.
*/
public java.util.Map<String, String> getServerStatistics() {
return askServer().getStatistics();
}
/**
* Leave a ship. The ship must be in harbour.
*
* @param unit The <code>Unit</code> which is to leave the ship.
* @return boolean
*/
public boolean leaveShip(Unit unit) {
if (!requireOurTurn()) {
return false;
}
// Sanity check, and find our carrier before we get off.
if (!(unit.getLocation() instanceof Unit)) {
logger.warning("Unit " + unit.getId() + " is not on a carrier.");
return false;
}
Unit carrier = (Unit) unit.getLocation();
// Proceed to disembark
UnitWas unitWas = new UnitWas(unit);
if (askServer().disembark(unit)
&& unit.getLocation() != carrier) {
checkCashInTreasureTrain(unit);
unitWas.fireChanges();
nextActiveUnit();
return true;
}
return false;
}
/**
* Loads a cargo onto a carrier.
*
* @param goods The <code>Goods</code> which are going aboard the carrier.
* @param carrier The <code>Unit</code> acting as carrier.
*/
public void loadCargo(Goods goods, Unit carrier) {
if (!requireOurTurn()) return;
// Sanity checks.
if (goods == null) {
throw new IllegalArgumentException("Null goods.");
} else if (goods.getAmount() <= 0) {
throw new IllegalArgumentException("Empty goods.");
} else if (carrier == null) {
throw new IllegalArgumentException("Null carrier.");
} else if (carrier.isInEurope()) {
// empty
} else if (carrier.getColony() == null) {
throw new IllegalArgumentException("Carrier not at colony or Europe.");
}
// Try to load.
if (loadGoods(goods, carrier)) {
gui.playSound("sound.event.loadCargo");
}
}
/**
* Moves the specified unit somewhere that requires crossing the high seas.
* Public because this is called from the TilePopup and the Europe panel.
*
* @param unit The <code>Unit</code> to be moved.
* @param destination The <code>Location</code> to be moved to.
* @throws IllegalArgumentException if destination or unit are null.
*/
public void moveTo(Unit unit, Location destination) {
if (!requireOurTurn()) return;
// Sanity check current state.
if (unit == null || destination == null) {
throw new IllegalArgumentException("moveTo null argument");
} else if (destination instanceof Europe) {
if (unit.isInEurope()) {
gui.playSound("sound.event.illegalMove");
return;
}
} else if (destination instanceof Map) {
if (unit.getTile() != null
&& unit.getTile().getMap() == destination) {
gui.playSound("sound.event.illegalMove");
return;
}
} else if (destination instanceof Settlement) {
if (unit.getTile() != null) {
gui.playSound("sound.event.illegalMove");
return;
}
}
// Autoload emigrants?
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.AUTOLOAD_EMIGRANTS)
&& unit.isInEurope()) {
int spaceLeft = unit.getSpaceLeft();
for (Unit u : unit.getOwner().getEurope().getUnitList()) {
if (spaceLeft <= 0) break;
if (!u.isNaval()
&& u.getState() == UnitState.SENTRY
&& u.getType().getSpaceTaken() <= spaceLeft) {
boardShip(u, unit);
spaceLeft -= u.getType().getSpaceTaken();
}
}
}
if (askServer().moveTo(unit, destination)) {
nextActiveUnit();
}
}
/**
* Moves the active unit in a specified direction. This may result in an
* attack, move... action.
*
* @param direction The direction in which to move the active unit.
*/
public void moveActiveUnit(Direction direction) {
Unit unit = gui.getActiveUnit();
if (unit != null && requireOurTurn()) {
clearGotoOrders(unit);
move(unit, direction);
} // else: nothing: There is no active unit that can be moved.
}
/**
* Moves the specified unit in a specified direction. This may
* result in many different types of action.
*
* @param unit The <code>Unit</code> to be moved.
* @param direction The direction in which to move the unit.
*/
public void move(Unit unit, Direction direction) {
if (!requireOurTurn()) return;
moveDirection(unit, direction, true);
// TODO: check if this is necessary for all actions?
// SwingUtilities.invokeLater(new Runnable() {
// public void run() {
// freeColClient.getActionManager().update();
// gui.updateMenuBar();
// }
// });
//TODO Temporary disabled
}
/**
* Moves the given unit towards its destination/s if possible.
*
* @param unit The <code>Unit</code> to move.
* @return True if the unit reached its destination and has more moves
* to make.
*/
public boolean moveToDestination(Unit unit) {
if (!requireOurTurn()) return false;
if (unit.getTradeRoute() != null) return followTradeRoute(unit);
gui.setActiveUnit(unit);
Player player = freeColClient.getMyPlayer();
Location destination;
while (unit.getMovesLeft() > 0
&& unit.getState() != UnitState.SKIPPED) {
// Look for valid destinations
if ((destination = unit.getDestination()) == null) {
break; // No destination
} else if (destination instanceof Europe) {
if (unit.isInEurope() || unit.isAtSea()) {
break; // Arrived in or between New World and Europe.
}
} else if (destination.getTile() == null) {
break; // Not on the map, findPath* can not help.
} else if (unit.getTile() == destination.getTile()) {
break; // Arrived at on-map destination
}
// Find a path to the destination.
PathNode path = (destination instanceof Europe)
? unit.findPathToEurope()
: unit.findPath(destination.getTile());
// No path, give up.
if (path == null) {
StringTemplate dest = destination.getLocationNameFor(player);
gui.showInformationMessage(unit,
StringTemplate.template("selectDestination.failed")
.addStringTemplate("%destination%", dest));
break;
}
// Try to follow the path.
if (!followPath(unit, path)) break;
}
// Clear ordinary destinations if arrived.
Location location = unit.getDestination();
if (location != null) {
if (unit.getTile() == null) {
if (unit.getLocation() == location) {
clearGotoOrders(unit);
}
} else {
if (unit.getTile() == location.getTile()) {
clearGotoOrders(unit);
// Check cash-in, and if the unit has moves left
// and was not set to SKIPPED by moveDirection,
// then make sure it remains selected to show that
// this unit could continue.
if (!checkCashInTreasureTrain(unit)
&& unit.getMovesLeft() > 0
&& unit.getState() != UnitState.SKIPPED) {
gui.setActiveUnit(unit);
return true;
}
}
}
}
return false;
}
/**
* Move a unit in a given direction.
*
* @param unit The <code>Unit</code> to move.
* @param direction The <code>Direction</code> to move in.
* @param interactive Interactive mode: play sounds and emit errors.
* @return True if the unit can possibly move further.
*/
private boolean moveDirection(Unit unit, Direction direction,
boolean interactive) {
Location destination = unit.getDestination();
// Consider all the move types
switch (unit.getMoveType(direction)) {
case MOVE_HIGH_SEAS:
if (destination == null) {
return moveHighSeas(unit, direction);
} else if (destination instanceof Europe) {
moveTo(unit, destination);
return false;
}
// Fall through
case MOVE:
moveMove(unit, direction);
return unit.getMovesLeft() > 0;
case EXPLORE_LOST_CITY_RUMOUR:
moveExplore(unit, direction);
return false;
case ATTACK_UNIT: case ATTACK_SETTLEMENT:
moveAttack(unit, direction);
return false;
case EMBARK:
moveEmbark(unit, direction);
return false;
case ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST:
moveLearnSkill(unit, direction);
return false;
case ENTER_INDIAN_SETTLEMENT_WITH_SCOUT:
moveScoutIndianSettlement(unit, direction);
return false;
case ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY:
moveUseMissionary(unit, direction);
return false;
case ENTER_FOREIGN_COLONY_WITH_SCOUT:
moveScoutColony(unit, direction);
return false;
case ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS:
moveTrade(unit, direction);
return false;
case MOVE_NO_ACCESS_BEACHED:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessBeached")
.addStringTemplate("%nation%", nation));
}
return false;
case MOVE_NO_ACCESS_CONTACT:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessContact")
.addStringTemplate("%nation%", nation));
}
return false;
case MOVE_NO_ACCESS_GOODS:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessGoods")
.addStringTemplate("%nation%", nation)
.addStringTemplate("%unit%", Messages.getLabel(unit)));
}
return false;
case MOVE_NO_ACCESS_LAND:
if (!moveDisembark(unit, direction)) {
if (interactive) {
gui.playSound("sound.event.illegalMove");
}
}
return false;
case MOVE_NO_ACCESS_SETTLEMENT:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessSettlement")
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addStringTemplate("%nation%", nation));
}
return false;
case MOVE_NO_ACCESS_SKILL:
if (interactive) {
gui.playSound("sound.event.illegalMove");
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessSkill")
.addStringTemplate("%unit%", Messages.getLabel(unit)));
}
return false;
case MOVE_NO_ACCESS_TRADE:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessTrade")
.addStringTemplate("%nation%", nation));
}
return false;
case MOVE_NO_ACCESS_WAR:
if (interactive) {
gui.playSound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessWar")
.addStringTemplate("%nation%", nation));
}
return false;
case MOVE_NO_ACCESS_WATER:
if (interactive) {
gui.playSound("sound.event.illegalMove");
gui.showInformationMessage(unit,
StringTemplate.template("move.noAccessWater")
.addStringTemplate("%unit%", Messages.getLabel(unit)));
}
return false;
case MOVE_NO_ATTACK_MARINE:
if (interactive) {
gui.playSound("sound.event.illegalMove");
gui.showInformationMessage(unit,
StringTemplate.template("move.noAttackWater")
.addStringTemplate("%unit%", Messages.getLabel(unit)));
}
return false;
case MOVE_NO_MOVES:
// The unit may have some moves left, but not enough
// to move to the next node.
unit.setState(UnitState.SKIPPED);
return false;
default:
if (interactive) {
gui.playSound("sound.event.illegalMove");
}
return false;
}
}
/**
* Confirm attack or demand a tribute from a native settlement, following
* an attacking move.
*
* @param unit The <code>Unit</code> to perform the attack.
* @param direction The direction in which to attack.
*/
private void moveAttack(Unit unit, Direction direction) {
clearGotoOrders(unit);
// Extra option with native settlement
Tile tile = unit.getTile();
Tile target = tile.getNeighbourOrNull(direction);
IndianSettlement is = target.getIndianSettlement();
if (is != null && unit.isArmed()) {
switch (gui.showArmedUnitIndianSettlementDialog(is)) {
case CANCEL:
return;
case INDIAN_SETTLEMENT_ATTACK:
break; // Go on to usual attack confirmation.
case INDIAN_SETTLEMENT_TRIBUTE:
moveTribute(unit, direction);
return;
default:
logger.warning("showArmedUnitIndianSettlementDialog failure.");
return;
}
}
// Normal attack confirmation.
if (confirmHostileAction(unit, target)
&& confirmPreCombat(unit, target)) {
askServer().attack(unit, direction);
nextModelMessage();
}
}
/**
* Check the carrier for passengers to disembark, possibly
* snatching a useful result from the jaws of a
* MOVE_NO_ACCESS_LAND failure.
*
* @param unit The carrier containing the unit to disembark.
* @param direction The direction in which to disembark the unit.
* @return True if the disembark "succeeds" (which deliberately includes
* declined disembarks).
*/
private boolean moveDisembark(Unit unit, final Direction direction) {
Tile tile = unit.getTile().getNeighbourOrNull(direction);
if (tile.getFirstUnit() != null
&& tile.getFirstUnit().getOwner() != unit.getOwner()) {
return false; // Can not disembark onto other nation units.
}
// Disembark selected units able to move.
final List<Unit> disembarkable = new ArrayList<Unit>();
unit.setStateToAllChildren(UnitState.ACTIVE);
for (Unit u : unit.getUnitList()) {
if (u.getMoveType(tile).isProgress()) {
disembarkable.add(u);
}
}
if (disembarkable.size() == 0) {
// Did not find any unit that could disembark, fail.
return false;
}
while (disembarkable.size() > 0) {
if (disembarkable.size() == 1) {
if (gui.showConfirmDialog("disembark.text", "yes", "no")) {
move(disembarkable.get(0), direction);
}
break;
}
List<ChoiceItem<Unit>> choices = new ArrayList<ChoiceItem<Unit>>();
for (Unit dUnit : disembarkable) {
choices.add(new ChoiceItem<Unit>(Messages.message(Messages.getLabel(dUnit)), dUnit));
}
if (disembarkable.size() > 1) {
choices.add(new ChoiceItem<Unit>(Messages.message("all"), unit));
}
// Use moveDirection() to disembark units as while the
// destination tile is known to be clear of other player
// units or settlements, it may have a rumour or need
// other special handling.
Unit u = gui.showChoiceDialog(unit.getTile(),
Messages.message("disembark.text"),
Messages.message("disembark.cancel"),
choices);
if (u == null) { // Cancelled, done.
break;
} else if (u == unit) { // Disembark all.
for (Unit dUnit : disembarkable) {
// Guard against loss of control when asking the
// server to move the unit.
try {
moveDirection(dUnit, direction, false);
} finally {
continue;
}
}
return true;
}
moveDirection(u, direction, false);
disembarkable.remove(u);
}
return true;
}
/**
* Embarks the specified unit onto a carrier in a specified direction
* following a move of MoveType.EMBARK.
*
* @param unit The <code>Unit</code> that wishes to embark.
* @param direction The direction in which to embark.
*/
private void moveEmbark(Unit unit, Direction direction) {
clearGotoOrders(unit);
Tile sourceTile = unit.getTile();
Tile destinationTile = sourceTile.getNeighbourOrNull(direction);
Unit carrier = null;
List<ChoiceItem<Unit>> choices = new ArrayList<ChoiceItem<Unit>>();
for (Unit u : destinationTile.getUnitList()) {
if (u.getSpaceLeft() >= unit.getType().getSpaceTaken()) {
String m = Messages.message(Messages.getLabel(u));
choices.add(new ChoiceItem<Unit>(m, u));
carrier = u; // Save a default
}
}
if (choices.size() == 0) {
throw new RuntimeException("Unit " + unit.getId()
+ " found no carrier to embark upon.");
} else if (choices.size() == 1) {
// Use the default
} else {
carrier = gui.showChoiceDialog(unit.getTile(),
Messages.message("embark.text"),
Messages.message("embark.cancel"),
choices);
if (carrier == null) return; // User cancelled
}
// Proceed to embark
if (askServer().embark(unit, carrier, direction)
&& unit.getLocation() == carrier) {
if (carrier.getMovesLeft() > 0) {
gui.setActiveUnit(carrier);
} else {
nextActiveUnit();
}
}
clearGotoOrders(unit);
}
/**
* Confirm exploration of a lost city rumour, following a move of
* MoveType.EXPLORE_LOST_CITY_RUMOUR.
*
* @param unit The <code>Unit</code> that is exploring.
* @param direction The direction of a rumour.
*/
private void moveExplore(Unit unit, Direction direction) {
Tile tile = unit.getTile().getNeighbourOrNull(direction);
if (gui.showConfirmDialog(unit.getTile(),
StringTemplate.key("exploreLostCityRumour.text"),
"exploreLostCityRumour.yes", "exploreLostCityRumour.no")) {
if (tile.getLostCityRumour().getType()
== LostCityRumour.RumourType.MOUNDS
&& !gui.showConfirmDialog(unit.getTile(),
StringTemplate.key("exploreMoundsRumour.text"),
"exploreLostCityRumour.yes", "exploreLostCityRumour.no")) {
askServer().declineMounds(unit, direction);
}
moveMove(unit, direction);
}
}
/**
* Moves a unit onto the "high seas" in a specified direction following
* a move of MoveType.MOVE_HIGH_SEAS.
* This may result in a move to Europe, no move, or an ordinary move.
*
* @param unit The <code>Unit</code> to be moved.
* @param direction The direction in which to move.
*/
private boolean moveHighSeas(Unit unit, Direction direction) {
// Confirm moving to Europe if told to move to a null tile
// (TODO: can this still happen?), or if crossing the boundary
// between coastal and high sea. Otherwise just move.
Tile oldTile = unit.getTile();
Tile newTile = oldTile.getNeighbourOrNull(direction);
if ((newTile == null
|| (!oldTile.canMoveToEurope() && newTile.canMoveToEurope()))
&& gui.showConfirmDialog(oldTile,
StringTemplate.template("highseas.text")
.addAmount("%number%", unit.getSailTurns()),
"highseas.yes", "highseas.no")) {
moveTo(unit, unit.getOwner().getEurope());
nextActiveUnit();
return false;
}
moveMove(unit, direction);
return true;
}
/**
* Move a free colonist to a native settlement to learn a skill following
* a move of MoveType.ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST.
* The colonist does not physically get into the village, it will
* just stay where it is and gain the skill.
*
* @param unit The <code>Unit</code> to learn the skill.
* @param direction The direction in which the Indian settlement lies.
*/
private void moveLearnSkill(Unit unit, Direction direction) {
clearGotoOrders(unit);
// Refresh knowledge of settlement skill. It may have been
// learned by another player.
if (!askServer().askSkill(unit, direction)) {
return;
}
IndianSettlement settlement
= (IndianSettlement) getSettlementAt(unit.getTile(), direction);
UnitType skill = settlement.getLearnableSkill();
if (skill == null) {
gui.showInformationMessage(settlement,
"indianSettlement.noMoreSkill");
} else if (!unit.getType().canBeUpgraded(skill, ChangeType.NATIVES)) {
gui.showInformationMessage(settlement,
StringTemplate.template("indianSettlement.cantLearnSkill")
.addStringTemplate("%unit%", Messages.getLabel(unit))
.add("%skill%", skill.getNameKey()));
} else if (gui.showConfirmDialog(unit.getTile(),
StringTemplate.template("learnSkill.text")
.add("%skill%", skill.getNameKey()),
"learnSkill.yes", "learnSkill.no")) {
if (askServer().learnSkill(unit, direction)) {
if (unit.isDisposed()) {
gui.showInformationMessage(settlement, "learnSkill.die");
nextActiveUnit(unit.getTile());
return;
}
if (unit.getType() != skill) {
gui.showInformationMessage(settlement, "learnSkill.leave");
}
}
}
nextActiveUnit();
}
/**
* Actually move a unit in a specified direction, following a move
* of MoveType.MOVE.
*
* @param unit The <code>Unit</code> to be moved.
* @param direction The direction in which to move the Unit.
*/
private void moveMove(Unit unit, Direction direction) {
// If we are in a colony, or Europe, load sentries.
if (unit.canCarryUnits() && unit.getSpaceLeft() > 0
&& (unit.getColony() != null || unit.isInEurope())) {
for (Unit sentry : unit.getLocation().getUnitList()) {
if (sentry.getState() == UnitState.SENTRY) {
if (sentry.getSpaceTaken() <= unit.getSpaceLeft()) {
boardShip(sentry, unit);
logger.finest("Unit " + unit.toString()
+ " loaded sentry " + sentry.toString());
} else {
logger.finest("Unit " + sentry.toString()
+ " is too big to board " + unit.toString());
}
}
}
}
// Ask the server
UnitWas unitWas = new UnitWas(unit);
if (!askServer().move(unit, direction)) return;
unitWas.fireChanges();
final Tile tile = unit.getTile();
// Perform a short pause on an active unit's last move if
// the option is enabled.
ClientOptions options = freeColClient.getClientOptions();
if (unit.getMovesLeft() <= 0
&& options.getBoolean(ClientOptions.UNIT_LAST_MOVE_DELAY)) {
gui.paintImmediatelyCanvasInItsBounds();
try {
Thread.sleep(UNIT_LAST_MOVE_DELAY);
} catch (InterruptedException e) {} // Ignore
}
// Update the active unit and GUI.
if (unit.isDisposed() || checkCashInTreasureTrain(unit)) {
nextActiveUnit(tile);
} else {
if (tile.getColony() != null
&& unit.isCarrier()
&& unit.getTradeRoute() == null
&& (unit.getDestination() == null
|| unit.getDestination().getTile() == tile.getTile())) {
gui.showColonyPanel(tile.getColony());
}
if (unit.getMovesLeft() == 0) {
nextActiveUnit();
} else {
displayModelMessages(false);
if (!gui.onScreen(tile))
gui.setSelectedTile(tile, false);
}
}
}
/**
* Move to a foreign colony and either attack, negotiate with the
* foreign power or spy on them. Follows a move of
* MoveType.ENTER_FOREIGN_COLONY_WITH_SCOUT.
* TODO: Unify trade and negotiation.
*
* @param unit The unit that will spy, negotiate or attack.
* @param direction The direction in which the foreign colony lies.
*/
private void moveScoutColony(Unit unit, Direction direction) {
Colony colony = (Colony) getSettlementAt(unit.getTile(), direction);
boolean canNeg = colony.getOwner() != unit.getOwner().getREFPlayer();
clearGotoOrders(unit);
switch (gui.showScoutForeignColonyDialog(colony, unit, canNeg)) {
case CANCEL:
break;
case FOREIGN_COLONY_ATTACK:
moveAttack(unit, direction);
break;
case FOREIGN_COLONY_NEGOTIATE:
moveTradeColony(unit, direction);
break;
case FOREIGN_COLONY_SPY:
moveSpy(unit, direction);
break;
default:
throw new IllegalArgumentException("showScoutForeignColonyDialog fail");
}
}
/**
* Move a scout into an Indian settlement to speak with the chief,
* or demand a tribute following a move of
* MoveType.ENTER_INDIAN_SETTLEMENT_WITH_SCOUT.
* The scout does not physically get into the village, it will
* just stay where it is.
*
* @param unit The <code>Unit</code> that is scouting.
* @param direction The direction in which the Indian settlement lies.
*/
private void moveScoutIndianSettlement(Unit unit, Direction direction) {
Tile unitTile = unit.getTile();
Tile tile = unitTile.getNeighbourOrNull(direction);
IndianSettlement settlement = tile.getIndianSettlement();
clearGotoOrders(unit);
// Offer the choices.
NationSummary ns = getNationSummary(settlement.getOwner());
String number = (ns == null) ? "many" : ns.getNumberOfSettlements();
switch (gui.showScoutIndianSettlementDialog(settlement, number)) {
case CANCEL:
return;
case INDIAN_SETTLEMENT_ATTACK:
if (confirmPreCombat(unit, tile)) {
askServer().attack(unit, direction);
}
return;
case INDIAN_SETTLEMENT_SPEAK:
Player player = unit.getOwner();
final int oldGold = player.getGold();
String result = askServer().scoutSpeak(unit, direction);
if (result == null) {
return; // Fail
} else if ("die".equals(result)) {
gui.showInformationMessage(settlement,
"scoutSettlement.speakDie");
nextActiveUnit(unitTile);
return;
} else if ("expert".equals(result)) {
gui.showInformationMessage(settlement,
StringTemplate.template("scoutSettlement.expertScout")
.add("%unit%", unit.getType().getNameKey()));
} else if ("tales".equals(result)) {
gui.showInformationMessage(settlement,
"scoutSettlement.speakTales");
} else if ("beads".equals(result)) {
gui.updateGoldLabel();
gui.showInformationMessage(settlement,
StringTemplate.template("scoutSettlement.speakBeads")
.addAmount("%amount%", player.getGold() - oldGold));
} else if ("nothing".equals(result)) {
gui.showInformationMessage(settlement,
StringTemplate.template("scoutSettlement.speakNothing")
.addStringTemplate("%nation%", player.getNationName()));
} else {
logger.warning("Invalid result from askScoutSpeak: " + result);
}
nextActiveUnit();
break;
case INDIAN_SETTLEMENT_TRIBUTE:
moveTribute(unit, direction);
break;
default:
throw new IllegalArgumentException("showScoutIndianSettlementDialog fail");
}
}
/**
* Spy on a foreign colony.
*
* @param unit The <code>Unit</code> that is spying.
* @param direction The <code>Direction</code> of a colony to spy on.
*/
private void moveSpy(Unit unit, Direction direction) {
if (askServer().spy(unit, direction)) {
nextActiveUnit();
}
}
/**
* Arrive at a settlement with a laden carrier following a move of
* MoveType.ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS.
*
* @param unit The carrier.
* @param direction The direction to the settlement.
*/
private void moveTrade(Unit unit, Direction direction) {
clearGotoOrders(unit);
Settlement settlement = getSettlementAt(unit.getTile(), direction);
if (settlement instanceof Colony) {
moveTradeColony(unit, direction);
} else if (settlement instanceof IndianSettlement) {
moveTradeIndianSettlement(unit, direction);
} else {
logger.warning("Bogus settlement: " + settlement.getId());
}
}
/**
* Initiates a negotiation with a foreign power. The player
* creates a DiplomaticTrade with the NegotiationDialog. The
* DiplomaticTrade is sent to the other player. If the other
* player accepts the offer, the trade is concluded. If not, this
* method returns, since the next offer must come from the other
* player.
*
* @param unit The <code>Unit</code> negotiating.
* @param direction The direction of a settlement to negotiate with.
*/
private void moveTradeColony(Unit unit, Direction direction) {
Settlement settlement = getSettlementAt(unit.getTile(), direction);
if (settlement == null) return;
// Can not negotiate with the REF.
Player player = unit.getOwner();
if (settlement.getOwner() == player.getREFPlayer()) {
throw new IllegalStateException("Unit tried to negotiate with REF");
}
ModelMessage m = null;
String nation
= Messages.message(settlement.getOwner().getNationName());
DiplomaticTrade ourAgreement = null;
DiplomaticTrade agreement = null;
TradeStatus status;
for (;;) {
ourAgreement = gui.showNegotiationDialog(unit, settlement, agreement);
if (ourAgreement == null) {
if (agreement == null) break;
agreement.setStatus(TradeStatus.REJECT_TRADE);
} else {
agreement = ourAgreement;
}
if (agreement.getStatus() != TradeStatus.PROPOSE_TRADE) {
askServer().diplomacy(unit, settlement, agreement);
gui.updateMenuBar();
break;
}
agreement = askServer().diplomacy(unit, settlement, agreement);
status = (agreement == null) ? TradeStatus.REJECT_TRADE
: agreement.getStatus();
switch (status) {
case PROPOSE_TRADE:
continue; // counter proposal, try again
case ACCEPT_TRADE:
m = new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"negotiationDialog.offerAccepted",
settlement)
.addName("%nation%", nation);
break;
case REJECT_TRADE:
m = new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"negotiationDialog.offerRejected",
settlement)
.addName("%nation%", nation);
break;
default:
throw new IllegalStateException("Bogus trade status" + status);
}
player.addModelMessage(m);
break; // only counter proposals should loop
}
nextActiveUnit();
}
/**
* Trading with the natives, including buying, selling and
* delivering gifts. (Deliberate use of Settlement rather than
* IndianSettlement throughout these routines as some unification
* with colony trading is anticipated, and the native AI already
* uses the same DeliverGiftMessage to deliver gifts to Colonies).
*
* @param unit The <code>Unit</code> that is a carrier containing goods.
* @param direction The direction the unit could move in order to enter a
* <code>Settlement</code>.
* @exception IllegalArgumentException if the unit is not a carrier, or if
* there is no <code>Settlement</code> in the given
* direction.
* @see Settlement
*/
private void moveTradeIndianSettlement(Unit unit, Direction direction) {
Settlement settlement = getSettlementAt(unit.getTile(), direction);
boolean[] results;
boolean done = false;
while (!done) {
results = askServer().openTransactionSession(unit, settlement);
if (results == null) break;
// The session tracks buy/sell/gift events and disables
// them when one happens. So only offer such options if
// the session allows it and the carrier is in good shape.
boolean buy = results[0] && unit.getSpaceLeft() > 0;
boolean sel = results[1] && unit.getGoodsCount() > 0;
boolean gif = results[2] && unit.getGoodsCount() > 0;
if (!buy && !sel && !gif) break;
switch (gui.showIndianSettlementTradeDialog(settlement,
buy, sel, gif)) {
case CANCEL:
done = true;
break;
case BUY:
attemptBuyFromSettlement(unit, settlement);
break;
case SELL:
attemptSellToSettlement(unit, settlement);
break;
case GIFT:
attemptGiftToSettlement(unit, settlement);
break;
default:
throw new IllegalArgumentException("showIndianSettlementTradeDialog fail");
}
}
askServer().closeTransactionSession(unit, settlement);
if (unit.getMovesLeft() > 0) { // May have been restored if no trade
gui.setActiveUnit(unit);
} else {
nextActiveUnit();
}
}
/**
* Displays an appropriate trade failure message.
*
* @param fail The failure state.
* @param settlement The <code>Settlement</code> that failed to trade.
* @param goods The <code>Goods</code> that failed to trade.
*/
private void showTradeFail(int fail, Settlement settlement, Goods goods) {
switch (fail) {
case NO_TRADE_GOODS:
gui.showInformationMessage(settlement,
StringTemplate.template("trade.noTradeGoods")
.add("%goods%", goods.getNameKey()));
return;
case NO_TRADE_HAGGLE:
gui.showInformationMessage(settlement, "trade.noTradeHaggle");
break;
case NO_TRADE_HOSTILE:
gui.showInformationMessage(settlement, "trade.noTradeHostile");
break;
case NO_TRADE: // Proposal was refused
default:
gui.showInformationMessage(settlement, "trade.noTrade");
break;
}
}
/**
* User interaction for buying from the natives.
*
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
*/
private void attemptBuyFromSettlement(Unit unit, Settlement settlement) {
Player player = freeColClient.getMyPlayer();
Goods goods = null;
// Get list of goods for sale
List<Goods> forSale
= askServer().getGoodsForSaleInSettlement(unit, settlement);
for (;;) {
if (forSale.isEmpty()) {
// There is nothing to sell to the player
gui.showInformationMessage(settlement,
"trade.nothingToSell");
return;
}
// Choose goods to buy
goods = gui.showSimpleChoiceDialog(unit.getTile(),
"buyProposition.text", "buyProposition.nothing", forSale);
if (goods == null) break; // Trade aborted by the player
int gold = -1; // Initially ask for a price
for (;;) {
gold = askServer().buyProposition(unit, settlement,
goods, gold);
if (gold <= 0) {
showTradeFail(gold, settlement, goods);
return;
}
// Show dialog for buy proposal
boolean canBuy = player.checkGold(gold);
switch (gui.showBuyDialog(unit, settlement, goods, gold,
canBuy)) {
case CANCEL: // User cancelled
return;
case BUY: // Accept price, make purchase
if (askServer().buyFromSettlement(unit,
settlement, goods, gold)) {
gui.updateGoldLabel(); // Assume success
}
return;
case HAGGLE: // Try to negotiate a lower price
gold = gold * 9 / 10;
break;
default:
throw new IllegalStateException("showBuyDialog fail");
}
}
}
}
/**
* User interaction for selling to the natives.
*
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
*/
private void attemptSellToSettlement(Unit unit, Settlement settlement) {
Goods goods = null;
for (;;) {
// Choose goods to sell
goods = gui.showSimpleChoiceDialog(unit.getTile(),
"sellProposition.text", "sellProposition.nothing",
unit.getGoodsList());
if (goods == null) break; // Trade aborted by the player
int gold = -1; // Initially ask for a price
for (;;) {
gold = askServer().sellProposition(unit, settlement,
goods, gold);
if (gold <= 0) {
showTradeFail(gold, settlement, goods);
return;
}
// Show dialog for sale proposal
switch (gui.showSellDialog(unit, settlement, goods, gold)) {
case CANCEL:
return;
case SELL: // Accepted price, make the sale
if (askServer().sellToSettlement(unit,
settlement, goods, gold)) {
gui.updateGoldLabel(); // Assume success
}
return;
case HAGGLE: // Ask for more money
gold = (gold * 11) / 10;
break;
case GIFT: // Decide to make a gift of the goods
askServer().deliverGiftToSettlement(unit,
settlement, goods);
return;
default:
throw new IllegalStateException("showSellDialog fail");
}
}
}
}
/**
* User interaction for delivering a gift to the natives.
*
* @param unit The <code>Unit</code> that is trading.
* @param settlement The <code>Settlement</code> that is trading.
*/
private void attemptGiftToSettlement(Unit unit, Settlement settlement) {
Goods goods = gui.showSimpleChoiceDialog(unit.getTile(),
"gift.text", "cancel", unit.getGoodsList());
if (goods != null) {
askServer().deliverGiftToSettlement(unit, settlement, goods);
}
}
/**
* Demand a tribute.
*
* @param unit The <code>Unit</code> to perform the attack.
* @param direction The direction in which to attack.
*/
private void moveTribute(Unit unit, Direction direction) {
if (askServer().demandTribute(unit, direction)) {
// Assume tribute paid
gui.updateGoldLabel();
nextActiveUnit();
}
}
/**
* Move a missionary into a native settlement, following a move of
* MoveType.ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY.
*
* @param unit The <code>Unit</code> that will enter the settlement.
* @param direction The direction in which the Indian settlement lies.
*/
private void moveUseMissionary(Unit unit, Direction direction) {
IndianSettlement settlement
= (IndianSettlement) getSettlementAt(unit.getTile(), direction);
Unit missionary = settlement.getMissionary();
boolean canEstablish = missionary == null;
boolean canDenounce = missionary != null
&& missionary.getOwner() != unit.getOwner();
clearGotoOrders(unit);
// Offer the choices.
switch (gui.showUseMissionaryDialog(unit, settlement,
canEstablish, canDenounce)) {
case CANCEL:
return;
case ESTABLISH_MISSION:
if (askServer().missionary(unit, direction, false)) {
if (settlement.getMissionary() == unit) {
gui.playSound("sound.event.missionEstablished");
}
nextActiveUnit();
}
break;
case DENOUNCE_HERESY:
if (askServer().missionary(unit, direction, true)) {
if (settlement.getMissionary() == unit) {
gui.playSound("sound.event.missionEstablished");
}
nextModelMessage();
nextActiveUnit();
}
break;
case INCITE_INDIANS:
List<Player> enemies = new ArrayList<Player>(freeColClient
.getGame().getLiveEuropeanPlayers());
Player player = freeColClient.getMyPlayer();
enemies.remove(player);
Player enemy = gui.showSimpleChoiceDialog(unit.getTile(),
"missionarySettlement.inciteQuestion",
"missionarySettlement.cancel",
enemies);
if (enemy == null) return;
int gold = askServer().incite(unit, direction, enemy, -1);
if (gold < 0) {
// protocol fail
} else if (!player.checkGold(gold)) {
gui.showInformationMessage(settlement,
StringTemplate.template("missionarySettlement.inciteGoldFail")
.add("%player%", enemy.getName())
.addAmount("%amount%", gold));
} else {
if (gui.showConfirmDialog(unit.getTile(),
StringTemplate.template("missionarySettlement.inciteConfirm")
.add("%player%", enemy.getName())
.addAmount("%amount%", gold),
"yes", "no")) {
if (askServer().incite(unit, direction, enemy, gold) >= 0) {
gui.updateGoldLabel();
}
}
nextActiveUnit();
}
break;
default:
logger.warning("showUseMissionaryDialog fail");
break;
}
}
// end move-consequents
/**
* Makes a new unit active.
*/
public void nextActiveUnit() {
nextActiveUnit(null);
}
/**
* Makes a new unit active if any, or focus on a tile (useful if the
* current unit just died).
* Displays any new <code>ModelMessage</code>s with
* {@link #nextModelMessage}.
*
* @param tile The <code>Tile</code> to select if no new unit can
* be made active.
*/
public void nextActiveUnit(Tile tile) {
if (!requireOurTurn()) return;
// Always flush outstanding messages first.
nextModelMessage();
//if (canvas.isShowingSubPanel()) {
// canvas.getShowingSubPanel().requestFocus();
// return;
//}
// Flush any outstanding orders once the mode is raised.
if (moveMode >= MODE_EXECUTE_GOTO_ORDERS
&& !doExecuteGotoOrders()) {
return;
}
// Look for active units.
Player player = freeColClient.getMyPlayer();
Unit unit = gui.getActiveUnit();
if (unit != null && !unit.isDisposed() && unit.getMovesLeft() > 0
&& unit.getState() != UnitState.SKIPPED) {
return; // Current active unit has more moves to do.
}
if (player.hasNextActiveUnit()) {
gui.setActiveUnit(player.getNextActiveUnit());
return; // Successfully found a unit to display
}
// No active units left. Do the goto orders.
if (!doExecuteGotoOrders()) return;
// If not already ending the turn, use the fallback tile if
// supplied, then check for automatic end of turn, otherwise
// just select nothing and wait.
gui.setActiveUnit(null);
ClientOptions options = freeColClient.getClientOptions();
if (moveMode >= MODE_END_TURN) {
endTurn();
} else if (tile != null) {
gui.setSelectedTile(tile, false);
} else if (options.getBoolean(ClientOptions.AUTO_END_TURN)) {
endTurn();
}
}
/**
* Pays the tax arrears on this type of goods.
*
* @param type The type of goods for which to pay arrears.
* @return True if the arrears were paid.
*/
public boolean payArrears(GoodsType type) {
if (!requireOurTurn()) return false;
Player player = freeColClient.getMyPlayer();
int arrears = player.getArrears(type);
if (arrears <= 0) return false;
if (!player.checkGold(arrears)) {
gui.showInformationMessage(StringTemplate.template("model.europe.cantPayArrears")
.addAmount("%amount%", arrears));
return false;
}
if (gui.showConfirmDialog(null,
StringTemplate.template("model.europe.payArrears")
.addAmount("%amount%", arrears),
"ok", "cancel")
&& askServer().payArrears(type)
&& player.canTrade(type)) {
gui.updateGoldLabel();
return true;
}
return false;
}
/**
* Buys the remaining hammers and tools for the {@link Building} currently
* being built in the given <code>Colony</code>.
*
* @param colony The {@link Colony} where the building should be bought.
*/
public void payForBuilding(Colony colony) {
if (!requireOurTurn()) return;
if (!colony.canPayToFinishBuilding()) {
gui.errorMessage("notEnoughGold");
return;
}
int price = colony.getPriceForBuilding();
if (!gui.showConfirmDialog(null,
StringTemplate.template("payForBuilding.text")
.addAmount("%amount%", price),
"payForBuilding.yes", "payForBuilding.no")) {
return;
}
ColonyWas colonyWas = new ColonyWas(colony);
if (askServer().payForBuilding(colony)
&& colony.getPriceForBuilding() == 0) {
colonyWas.fireChanges();
gui.updateGoldLabel();
}
}
/**
* Puts the specified unit outside the colony.
*
* @param unit The <code>Unit</code>
* @return <i>true</i> if the unit was successfully put outside the colony.
*/
public boolean putOutsideColony(Unit unit) {
if (!requireOurTurn()) return false;
Colony colony = unit.getColony();
if (colony == null) {
throw new IllegalStateException("Unit is not in colony.");
} else if (!colony.canReducePopulation()) {
return false;
}
ColonyWas colonyWas = new ColonyWas(colony);
UnitWas unitWas = new UnitWas(unit);
if (askServer().putOutsideColony(unit)) {
colonyWas.fireChanges();
unitWas.fireChanges();
return true;
}
return false;
}
/**
* Recruit a unit from a specified index in Europe.
*
* @param index The index in Europe to recruit from ([0..2]).
*/
public void recruitUnitInEurope(int index) {
if (!requireOurTurn()) return;
Player player = freeColClient.getMyPlayer();
if (!player.checkGold(player.getRecruitPrice())) {
gui.errorMessage("notEnoughGold");
return;
}
emigrate(player, index + 1);
}
/**
* Renames a <code>Nameable</code>.
* Apparently this can be done while it is not your turn.
*
* @param object The object to rename.
*/
public void rename(Nameable object) {
Player player = freeColClient.getMyPlayer();
if (!(object instanceof Ownable)
|| !player.owns((Ownable) object)) {
return;
}
String name = null;
if (object instanceof Colony) {
Colony colony = (Colony) object;
name = gui.showInputDialog(colony.getTile(),
StringTemplate.key("renameColony.text"), colony.getName(),
"renameColony.yes", "renameColony.no", true);
if (name == null) {
// User cancelled, 0-length invalid.
return;
} else if (colony.getName().equals(name)) {
// No change
return;
} else if (player.getSettlement(name) != null) {
// Colony name must be unique.
gui.showInformationMessage((Colony) object,
StringTemplate.template("nameColony.notUnique")
.addName("%name%", name));
return;
}
} else if (object instanceof Unit) {
Unit unit = (Unit) object;
name = gui.showInputDialog(unit.getTile(),
StringTemplate.key("renameUnit.text"), unit.getName(),
"renameUnit.yes", "renameUnit.no", false);
if (name == null) return; // User cancelled, 0-length clears name.
} else {
logger.warning("Tried to rename an unsupported Nameable: "
+ object.toString());
return;
}
askServer().rename((FreeColGameObject) object, name);
}
/**
* Selects a destination for this unit. Europe and the player's
* colonies are valid destinations.
*
* @param unit The unit for which to select a destination.
*/
public void selectDestination(Unit unit) {
Location destination = gui.showSelectDestinationDialog(unit);
if (destination == null) return; // user aborted
if (setDestination(unit, destination)
&& freeColClient.currentPlayerIsMyPlayer()) {
if (destination instanceof Europe) {
if (unit.getTile() != null
&& unit.getTile().canMoveToEurope()) {
moveTo(unit, destination);
} else {
moveToDestination(unit);
}
} else {
if (unit.isInEurope()) {
moveTo(unit, destination);
} else {
moveToDestination(unit);
}
}
}
}
/**
* Sells goods in Europe.
*
* @param goods The goods to be sold.
* @return True if the sale succeeds.
*/
public boolean sellGoods(Goods goods) {
if (!requireOurTurn()) return false;
// Sanity checks.
Player player = freeColClient.getMyPlayer();
if (goods == null) {
throw new NullPointerException("Goods must not be null.");
}
Unit carrier = null;
if (goods.getLocation() instanceof Unit) {
carrier = (Unit) goods.getLocation();
}
if (carrier == null) {
throw new IllegalStateException("Goods not on carrier.");
} else if (!carrier.isInEurope()) {
throw new IllegalStateException("Goods not on carrier in Europe.");
} else if (!player.canTrade(goods)) {
throw new IllegalStateException("Goods are boycotted.");
}
// Try to sell. Remember a bunch of stuff first so the transaction
// can be logged.
Market market = player.getMarket();
GoodsType type = goods.getType();
int amount = goods.getAmount();
int price = market.getPaidForSale(type);
int tax = player.getTax();
int oldAmount = carrier.getGoodsContainer().getGoodsCount(type);
UnitWas unitWas = new UnitWas(carrier);
if (askServer().sellGoods(goods, carrier)
&& carrier.getGoodsContainer().getGoodsCount(type) != oldAmount) {
gui.playSound("sound.event.sellCargo");
unitWas.fireChanges();
for (TransactionListener l : market.getTransactionListener()) {
l.logSale(type, amount, price, tax);
}
gui.updateGoldLabel();
nextModelMessage();
return true;
}
// Sale failed for some reason.
return false;
}
/**
* Sends a public chat message.
*
* @param chat The text of the message.
*/
public void sendChat(String chat) {
askServer().chat(chat);
}
/**
* Changes the current construction project of a <code>Colony</code>.
*
* @param colony The <code>Colony</code>
* @param buildQueue List of <code>BuildableType</code>
*/
public void setBuildQueue(Colony colony, List<BuildableType> buildQueue) {
if (!requireOurTurn()) return;
ColonyWas colonyWas = new ColonyWas(colony);
if (askServer().setBuildQueue(colony, buildQueue)) {
colonyWas.fireChanges();
}
}
/**
* Set a player to be the new current player.
*
* @param player
* The <code>Player</code> to be the new current player.
*/
public void setCurrentPlayer(Player player) {
logger.finest("Entering client setCurrentPlayer: " + player.getName());
Game game = freeColClient.getGame();
game.setCurrentPlayer(player);
if (freeColClient.getMyPlayer().equals(player)
&& freeColClient.getFreeColServer() != null) {
freeColClient.getGUI().closeStatusPanel();
// auto-save the game (if it isn't newly loaded)
if (turnsPlayed > 0) {
autosave_game();
}
player.invalidateCanSeeTiles();
// Check for emigration.
while (player.checkEmigrate()) {
if (player.hasAbility("model.ability.selectRecruit")
&& player.getEurope().recruitablesDiffer()) {
int index = gui.showEmigrationPanel(false);
emigrate(player, index + 1);
} else {
emigrate(player, 0);
}
}
// GUI management.
if (!freeColClient.isSingleplayer()) {
gui.playSound("sound.anthem." + player.getNationID());
}
displayModelMessages(true, true);
} else {
// Another player is moving
StringTemplate t = StringTemplate.template("waitingFor")
.addStringTemplate("%nation%", player.getNationName());
String message = Messages.message(t);
ImageIcon coatOfArmsIcon = freeColClient.getGUI().getImageLibrary()
.getCoatOfArmsImageIcon(player.getNation());
freeColClient.getGUI().showStatusPanel(message, coatOfArmsIcon);
}
logger.finest("Exiting client setCurrentPlayer: " + player.getName());
}
/**
* Set the destination of the given unit.
*
* @param unit The <code>Unit</code> to direct.
* @param destination The destination <code>Location</code>.
* @return True if the destination was set.
* @see Unit#setDestination(Location)
*/
public boolean setDestination(Unit unit, Location destination) {
if (unit.getTradeRoute() != null) {
StringTemplate template
= StringTemplate.template("traderoute.reassignRoute")
.addStringTemplate("%unit%", Messages.getLabel(unit))
.addName("%route%", unit.getTradeRoute().getName());
if (!gui.showConfirmDialog(unit.getTile(), template,
"yes", "no")) return false;
}
return askServer().setDestination(unit, destination)
&& unit.getDestination() == destination;
}
/**
* Sets the export settings of the custom house.
*
* @param colony The colony with the custom house.
* @param goodsType The goods for which to set the settings.
*/
public void setGoodsLevels(Colony colony, GoodsType goodsType) {
askServer().setGoodsLevels(colony,
colony.getExportData(goodsType));
}
/**
* Sets the trade routes for this player
*
* @param routes The trade routes to set.
*/
public void setTradeRoutes(List<TradeRoute> routes) {
askServer().setTradeRoutes(routes);
}
/**
* Skip a unit.
*/
public void skipActiveUnit() {
changeState(gui.getActiveUnit(), UnitState.SKIPPED);
}
/**
* Trains a unit of a specified type in Europe.
*
* @param unitType The type of unit to be trained.
*/
public void trainUnitInEurope(UnitType unitType) {
if (!requireOurTurn()) return;
Player player = freeColClient.getMyPlayer();
Europe europe = player.getEurope();
if (!player.checkGold(europe.getUnitPrice(unitType))) {
gui.errorMessage("notEnoughGold");
return;
}
EuropeWas europeWas = new EuropeWas(europe);
if (askServer().trainUnitInEurope(unitType)) {
gui.updateGoldLabel();
europeWas.fireChanges();
}
}
/**
* Unload, including dumping cargo.
*
* @param unit The <code>Unit<code> that is dumping.
*/
public void unload(Unit unit) {
if (!requireOurTurn()) return;
// Sanity tests.
if (unit == null) {
throw new IllegalArgumentException("Null unit.");
} else if (!unit.isCarrier()) {
throw new IllegalArgumentException("Unit is not a carrier.");
}
Player player = freeColClient.getMyPlayer();
boolean inEurope = unit.isInEurope();
if (unit.getColony() != null) {
// In colony, unload units and goods.
for (Unit u : unit.getUnitList()) {
leaveShip(u);
}
for (Goods goods : new ArrayList<Goods>(unit.getGoodsList())) {
unloadCargo(goods, false);
}
} else {
if (inEurope) { // In Europe, unload non-boycotted goods
for (Goods goods : new ArrayList<Goods>(unit.getGoodsList())) {
if (player.canTrade(goods)) unloadCargo(goods, false);
}
}
// Goods left here must be dumped.
if (unit.getGoodsCount() > 0) {
List<Goods> goodsList
= gui.showDumpCargoDialog(unit);
if (goodsList != null) {
for (Goods goods : goodsList) {
unloadCargo(goods, true);
}
}
}
}
}
/**
* Unload cargo. If the unit carrying the cargo is not in a
* harbour, or if the given boolean is true, the goods will be
* dumped.
*
* @param goods The <code>Goods<code> to unload.
* @param dump If true, dump the goods.
*/
public void unloadCargo(Goods goods, boolean dump) {
if (!requireOurTurn()) return;
// Sanity tests.
if (goods == null) {
throw new IllegalArgumentException("Null goods.");
} else if (goods.getAmount() <= 0) {
throw new IllegalArgumentException("Empty goods.");
}
Unit carrier = null;
if (!(goods.getLocation() instanceof Unit)) {
throw new IllegalArgumentException("Unload from non-unit.");
}
carrier = (Unit) goods.getLocation();
Colony colony = null;
if (!carrier.isInEurope()) {
if (carrier.getTile() == null) {
throw new IllegalArgumentException("Carrier with null location.");
}
colony = carrier.getColony();
if (!dump && colony == null) {
throw new IllegalArgumentException("Unload is really a dump.");
}
}
// Try to unload. TODO: should there be a sound for this?
unloadGoods(goods, carrier, colony);
}
/**
* Updates a trade route.
*
* @param route The trade route to update.
*/
public void updateTradeRoute(TradeRoute route) {
askServer().updateTradeRoute(route);
}
/**
* Tell a unit to wait.
*/
public void waitActiveUnit() {
gui.setActiveUnit(null);
nextActiveUnit();
}
/**
* Moves a <code>Unit</code> to a <code>WorkLocation</code>.
*
* @param unit The <code>Unit</code>.
* @param workLocation The <code>WorkLocation</code>.
*/
public void work(Unit unit, WorkLocation workLocation) {
if (!requireOurTurn()) return;
Colony colony = workLocation.getColony();
if (workLocation instanceof ColonyTile) {
Tile tile = ((ColonyTile) workLocation).getWorkTile();
if (tile.hasLostCityRumour()) {
gui.showInformationMessage("tileHasRumour");
return;
}
if (tile.getOwner() != unit.getOwner()) {
if (!claimLand(tile, colony, 0)) return;
}
}
// Try to change the work location.
ColonyWas colonyWas = new ColonyWas(colony);
UnitWas unitWas = new UnitWas(unit);
if (askServer().work(unit, workLocation)
&& unit.getLocation() == workLocation) {
colonyWas.fireChanges();
unitWas.fireChanges();
}
}
}