freecol/src/net/sf/freecol/client/control/InGameController.java

4910 lines
180 KiB
Java

/**
* Copyright (C) 2002-2015 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.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import net.sf.freecol.FreeCol;
import net.sf.freecol.client.ClientOptions;
import net.sf.freecol.client.FreeColClient;
import net.sf.freecol.client.gui.ChoiceItem;
import net.sf.freecol.client.gui.GUI;
import net.sf.freecol.client.gui.option.FreeColActionUI;
import net.sf.freecol.common.debug.DebugUtils;
import net.sf.freecol.common.debug.FreeColDebugger;
import net.sf.freecol.common.i18n.Messages;
import net.sf.freecol.common.i18n.NameCache;
import net.sf.freecol.common.io.FreeColDirectories;
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.TradeContext;
import net.sf.freecol.common.model.DiplomaticTrade.TradeStatus;
import net.sf.freecol.common.model.Europe;
import net.sf.freecol.common.model.Europe.MigrationType;
import net.sf.freecol.common.model.EuropeWas;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GameOptions;
import net.sf.freecol.common.model.GoldTradeItem;
import net.sf.freecol.common.model.Goods;
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.Location;
import net.sf.freecol.common.model.LostCityRumour;
import net.sf.freecol.common.model.Map;
import net.sf.freecol.common.model.Direction;
import net.sf.freecol.common.model.Market;
import net.sf.freecol.common.model.MarketWas;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.ModelMessage.MessageType;
import net.sf.freecol.common.model.Monarch.MonarchAction;
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.Stance;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Role;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Specification;
import net.sf.freecol.common.model.StanceTradeItem;
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.TradeLocation;
import net.sf.freecol.common.model.TradeRoute;
import net.sf.freecol.common.model.TradeRouteStop;
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.util.LogBuilder;
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 static net.sf.freecol.common.util.CollectionUtils.*;
/**
* 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());
/** Actions when an armed unit contacts a settlement. */
public static enum ArmedUnitSettlementAction {
SETTLEMENT_ATTACK,
SETTLEMENT_TRIBUTE,
}
/** Actions when dealing with a boycott. */
public static enum BoycottAction {
PAY_ARREARS,
DUMP_CARGO
}
/** Actions when buying from the natives. */
public static enum BuyAction {
BUY,
HAGGLE
}
/** Actions when claiming land. */
public static enum ClaimAction {
ACCEPT,
STEAL
}
/** Actions with a missionary at a native settlement. */
public static enum MissionaryAction {
ESTABLISH_MISSION,
DENOUNCE_HERESY,
INCITE_INDIANS
}
/** Actions in scouting a colony. */
public static enum ScoutColonyAction {
FOREIGN_COLONY_NEGOTIATE,
FOREIGN_COLONY_SPY,
FOREIGN_COLONY_ATTACK
}
/** Actions in scouting a native settlement. */
public static enum ScoutIndianSettlementAction {
INDIAN_SETTLEMENT_SPEAK,
INDIAN_SETTLEMENT_TRIBUTE,
INDIAN_SETTLEMENT_ATTACK
}
/** Actions when selling to the natives. */
public static enum SellAction {
SELL,
HAGGLE,
GIFT
}
/** Choice of sales action at a native settlement. */
public static enum TradeAction {
BUY,
SELL,
GIFT
}
/**
* 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 static enum MoveMode {
NEXT_ACTIVE_UNIT,
EXECUTE_GOTO_ORDERS,
END_TURN;
public MoveMode maximize(MoveMode m) {
return (this.ordinal() < m.ordinal()) ? m : this;
}
}
private static final short UNIT_LAST_MOVE_DELAY = 300;
/** A template to use as a magic cookie for aborted trades. */
private static final StringTemplate abortTrade
= StringTemplate.template("");
/** The enclosing <code>FreeColClient</code>. */
private final FreeColClient freeColClient;
/** A cache reference to the gui. */
private final GUI gui;
/** Current mode for moving units. */
private MoveMode moveMode = MoveMode.NEXT_ACTIVE_UNIT;
/** A map of messages to be ignored. */
private final HashMap<String, Integer> messagesToIgnore = new HashMap<>();
/** The messages in the last turn report. */
private final List<ModelMessage> turnReportMessages = new ArrayList<>();
/**
* The constructor to use.
*
* @param freeColClient The <code>FreeColClient</code> for the game.
*/
public InGameController(FreeColClient freeColClient) {
this.freeColClient = freeColClient;
this.gui = freeColClient.getGUI();
// FIXME: fetch value of lastSaveGameFile from a persistent
// client value
// lastSaveGameFile = new File(freeColClient.getClientOptions().getString(null));
}
// Simple utilities
/**
* Meaningfully named access to the ServerAPI.
*
* @return The ServerAPI.
*/
private ServerAPI askServer() {
return freeColClient.askServer();
}
/**
* Play a sound.
*
* @param soundKey The sound resource key.
*/
private void sound(String soundKey) {
freeColClient.getSoundController().playSound(soundKey);
}
/**
* 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()) return true;
if (freeColClient.isInGame()) {
gui.showInformationMessage("info.notYourTurn");
}
return false;
}
/**
* Display the colony panel for a colony, and select the unit that just
* arrived there if it is a carrier.
*
* @param colony The <code>Colony</code> to display.
* @param unit An optional <code>Unit</code> to select.
*/
private void colonyPanel(Colony colony, Unit unit) {
gui.showColonyPanel(colony, (unit.isCarrier()) ? unit : null);
}
/**
* 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.hasSettlement()) {
player = newTile.getSettlement().getOwner();
} else if (newTile.getFirstUnit() != null) {
player = newTile.getFirstUnit().getOwner();
} else { // should not happen
player = freeColClient.getGame().getUnknownEnemy();
}
return player.getNationLabel();
}
/**
* Update the GUI and the active unit with a fallback tile.
*
* @param tile An optional fallback <code>Tile</code> to display if
* no active unit is found, useful when the last unit might have
* died.
*/
private void updateGUI(Tile tile) {
if (displayModelMessages(false, false)) {
; // If messages are displayed they probably refer to the
// current unit, so do not update it.
} else if (updateActiveUnit(tile)) {
; // setActiveUnit will update the menu bar
} else {
gui.updateMapControls();
gui.updateMenuBar();
}
}
// Server access routines called from multiple places.
/**
* Ask the server to assign a trade route.
*
* @param unit The <code>Unit</code> to assign to.
* @param tradeRoute The <code>TradeRoute</code> to assign.
* @return True if the assignment succeeds.
*/
private boolean askAssignTradeRoute(Unit unit, TradeRoute tradeRoute) {
if (tradeRoute == unit.getTradeRoute()) return true;
if (tradeRoute != null && unit.getTradeRoute() != null) {
if (!gui.confirmClearTradeRoute(unit)) return false;
}
return askServer().assignTradeRoute(unit, tradeRoute)
&& unit.getTradeRoute() == tradeRoute;
}
/**
* Claim a tile.
*
* @param player The <code>Player</code> that is claiming.
* @param tile The <code>Tile</code> to claim.
* @param claimant The <code>Unit</code> or <code>Colony</code> claiming.
* @param price The price required.
* @return True if the claim succeeded.
*/
private boolean askClaimTile(Player player, Tile tile,
FreeColGameObject claimant, int price) {
final Player owner = tile.getOwner();
if (price < 0) { // not for sale
return false;
} else if (price > 0) { // for sale
ClaimAction act = gui.getClaimChoice(tile, player, price, owner);
if (act == null) return false; // Cancelled
switch (act) {
case ACCEPT: // accepted price
break;
case STEAL:
price = NetworkConstants.STEAL_LAND;
break;
default:
logger.warning("Claim dialog fail: " + act);
return false;
}
} // else price == 0 and we can just proceed to claim
// Ask the server
return askServer().claimTile(tile, claimant, price)
&& player.owns(tile);
}
/**
* 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.
* @return True if the unit now has no destination or trade route.
*/
private boolean askClearGotoOrders(Unit unit) {
if (!askAssignTradeRoute(unit, null)) return false;
if (unit.getDestination() == null) return true;
if (askSetDestination(unit, null)) {
gui.clearGotoPath();
return true;
}
return false;
}
/**
* Embark onto a carrier.
*
* @param unit The <code>Unit</code> to embark.
* @param carrier The carrier <code>Unit</code> to board.
* @return True if boarding succeeded.
*/
private boolean askEmbark(Unit unit, Unit carrier) {
ColonyWas colonyWas = (unit.getColony() != null)
? new ColonyWas(unit.getColony()) : null;
EuropeWas europeWas = (unit.isInEurope())
? new EuropeWas(unit.getOwner().getEurope()) : null;
UnitWas unitWas = new UnitWas(unit);
if (askServer().embark(unit, carrier, null)
&& unit.getLocation() == carrier) {
sound("sound.event.loadCargo");
unitWas.fireChanges();
if (colonyWas != null) colonyWas.fireChanges();
if (europeWas != null) europeWas.fireChanges();
return true;
}
return false;
}
/**
* A unit in Europe emigrates.
*
* This is unusual for an ask* routine in that it uses a *Was
* structure, but it is needed to extract the unit.
*
* @param europe The <code>Europe</code> where the unit appears.
* @param slot The slot to choose, [0..RECRUIT_COUNT].
* @return The new <code>Unit</code> or null on failure.
*/
private Unit askEmigrate(Europe europe, int slot) {
if (europe == null
|| !MigrationType.validMigrantSlot(slot)) return null;
EuropeWas europeWas = new EuropeWas(europe);
Unit newUnit = null;
if (askServer().emigrate(slot)
&& (newUnit = europeWas.getNewUnit()) != null) {
europeWas.fireChanges();
}
return newUnit;
}
/**
* Select all the units to emigrate from Europe. If they are all
* the same they can be picked automatically, but otherwise use
* the emigration dialog. Only to be called if the player is
* allowed to select the unit type (i.e. FoY or has Brewster).
*
* The server contains the count of available FoY-units, and
* maintains the immigration/immigrationRequired amounts, so this
* routine will fail harmlessly if it asks for too much.
*
* @param player The <code>Player</code> that owns the unit.
* @param n The number of units known to be eligible to emigrate.
* @param fountainOfYouth True if this migration if due to a FoY.
*/
private void emigration(Player player, int n, boolean fountainOfYouth) {
final Europe europe = player.getEurope();
if (europe == null) return;
for (; n > 0 || player.checkEmigrate() ; n--) {
if (!allSame(europe.getRecruitables())) {
final int nf = n;
gui.showEmigrationDialog(player, fountainOfYouth,
(Integer value) -> { // Value is a valid slot
emigrate(player,
Europe.MigrationType.convertToMigrantSlot(value),
nf-1, fountainOfYouth);
});
return;
}
Unit u = askEmigrate(europe, Europe.MigrationType.getDefaultSlot());
if (u == null) break; // Give up on failure, try again next turn
player.addModelMessage(player.getEmigrationMessage(u));
}
}
/**
* Load some goods onto a carrier.
*
* @param loc The <code>Location</code> to load from.
* @param type The <code>GoodsType</code> to load.
* @param amount The amount of goods to load.
* @param carrier The <code>Unit</code> to load onto.
* @return True if the load succeeded.
*/
private boolean askLoadGoods(Location loc, GoodsType type, int amount,
Unit carrier) {
TradeLocation trl = carrier.getTradeLocation();
if (trl == null) return false;
// Size check, if there are spare holds they can be filled, but...
int loadable = carrier.getLoadableAmount(type);
if (amount > loadable) amount = loadable;
final Player player = carrier.getOwner();
final Market market = player.getMarket();
MarketWas marketWas = (market != null) ? new MarketWas(player) : null;
if (carrier.isInEurope()) {
// Are the goods boycotted?
if (!player.canTrade(type)) return false;
// Check that the purchase is funded.
if (!player.checkGold(market.getBidPrice(type, amount))) {
gui.showInformationMessage("info.notEnoughGold");
return false;
}
}
// Try to purchase.
int oldAmount = carrier.getGoodsContainer().getGoodsCount(type);
if (askServer().loadGoods(loc, type, amount, carrier)
&& carrier.getGoodsContainer().getGoodsCount(type) != oldAmount) {
if (marketWas != null) marketWas.fireChanges(type, amount);
return true;
}
return false;
}
/**
* Set a destination for a unit.
*
* @param unit The <code>Unit</code> to direct.
* @param destination The destination <code>Location</code>.
* @return True if the destination was set.
*/
private boolean askSetDestination(Unit unit, Location destination) {
return askServer().setDestination(unit, destination)
&& unit.getDestination() == destination;
}
/**
* Unload some goods from a carrier.
*
* @param type The <code>GoodsType</code> to unload.
* @param amount The amount of goods to unload.
* @param carrier The <code>Unit</code> carrying the goods.
* @return True if the unload succeeded.
*/
private boolean askUnloadGoods(GoodsType type, int amount, Unit carrier) {
// Do not check for trade location, unloading can include dumping
// which can happen anywhere
final Player player = freeColClient.getMyPlayer();
final Market market = player.getMarket();
MarketWas marketWas = (market != null) ? new MarketWas(player) : null;
int oldAmount = carrier.getGoodsContainer().getGoodsCount(type);
if (askServer().unloadGoods(type, amount, carrier)
&& carrier.getGoodsContainer().getGoodsCount(type) != oldAmount) {
if (marketWas != null) marketWas.fireChanges(type, -amount);
return true;
}
return false;
}
// Utilities connected with saving the game
/**
* Get the trunk of the save game string.
*
* @param game The <code>Game</code> to query.
* @return The trunk of the file name to use for saved games.
*/
private String getSaveGameString(Game game) {
final Player player = freeColClient.getMyPlayer();
final String gid = Integer.toHexString(game.getUUID().hashCode());
final Turn turn = game.getTurn();
return (/* player.getName() + "_" */ gid
+ "_" + Messages.message(player.getNationLabel())
+ "_" + turn.getSaveGameSuffix()
+ "." + FreeCol.FREECOL_SAVE_EXTENSION)
.replaceAll(" ", "_");
}
/**
* 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 autoSaveGame () {
final Game game = freeColClient.getGame();
if (game == null) return;
// unconditional save per round (fixed file "last-turn")
final ClientOptions options = freeColClient.getClientOptions();
final String prefix = options.getText(ClientOptions.AUTO_SAVE_PREFIX);
final String lastTurnName = prefix + "-"
+ options.getText(ClientOptions.LAST_TURN_NAME)
+ "." + FreeCol.FREECOL_SAVE_EXTENSION;
final String beforeLastTurnName = prefix + "-"
+ options.getText(ClientOptions.BEFORE_LAST_TURN_NAME)
+ "." + FreeCol.FREECOL_SAVE_EXTENSION;
File autoSaveDir = FreeColDirectories.getAutosaveDirectory();
File lastTurnFile = new File(autoSaveDir, lastTurnName);
File beforeLastTurnFile = new File(autoSaveDir, beforeLastTurnName);
// if "last-turn" file exists, shift it to "before-last-turn" file
if (lastTurnFile.exists()) {
beforeLastTurnFile.delete();
lastTurnFile.renameTo(beforeLastTurnFile);
}
saveGame(lastTurnFile);
// conditional save after user-set period
int saveGamePeriod = options.getInteger(ClientOptions.AUTOSAVE_PERIOD);
int turnNumber = game.getTurn().getNumber();
if (saveGamePeriod >= 1 && turnNumber % saveGamePeriod == 0) {
String fileName = prefix + "-" + getSaveGameString(game);
saveGame(new File(autoSaveDir, fileName));
}
}
/**
* Saves the game to the given file.
*
* @param file The <code>File</code>.
* @return True if the game was saved.
*/
private boolean saveGame(final File file) {
final FreeColServer server = freeColClient.getFreeColServer();
boolean result = false;
gui.showStatusPanel(Messages.message("status.savingGame"));
try {
server.setActiveUnit(gui.getActiveUnit());
server.saveGame(file, freeColClient.getClientOptions());
result = true;
} catch (IOException e) {
gui.showErrorMessage(FreeCol.badSave(file));
} finally {
gui.closeStatusPanel();
}
return result;
}
// 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();
}
/**
* Start ignoring a kind of message.
*
* @param key The key for a message to ignore.
* @param turn The current <code>Turn</code>.
*/
private synchronized void startIgnoringMessage(String key, Turn turn) {
messagesToIgnore.put(key, turn.getNumber());
logger.finer("Ignore message start: " + key);
}
/**
* Stop ignoring a kind of message.
*
* @param key The key for a message to stop ignoring.
*/
private synchronized void stopIgnoringMessage(String key) {
messagesToIgnore.remove(key);
logger.finer("Ignore message stop: " + key);
}
/**
* Reap all ignored message keys that are older than the given turn.
*
* @param turn The <code>Turn</code> value to test against.
*/
private synchronized void reapIgnoredMessages(Turn turn) {
Iterator<String> keys = messagesToIgnore.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
if (messagesToIgnore.get(key) < turn.getNumber()) {
keys.remove();
logger.finer("Ignore message reap: " + key);
}
}
}
/**
* See if messages with a given key were ignored last turn. If so,
* continue to ignore them.
*
* @param key The key to check.
* @param turn The current <code>Turn</code>.
* @return True if the message should continue to be ignored.
*/
private synchronized boolean continueIgnoreMessage(String key, Turn turn) {
Integer value;
if (key != null
&& (value = messagesToIgnore.get(key)) != null
&& value + 1 == turn.getNumber()) {
messagesToIgnore.put(key, value + 1);
logger.finer("Ignore message continue: " + key);
return true;
}
return false;
}
/**
* Displays the messages in the current turn report.
*/
public void displayTurnReportMessages() {
gui.showReportTurnPanel(turnReportMessages);
}
/**
* 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.
* @return True if any messages were displayed.
*/
public boolean displayModelMessages(final boolean allMessages,
final boolean endOfTurn) {
final Player player = freeColClient.getMyPlayer();
final Turn thisTurn = freeColClient.getGame().getTurn();
final ArrayList<ModelMessage> messages = new ArrayList<>();
for (ModelMessage m : ((allMessages) ? player.getModelMessages()
: player.getNewModelMessages())) {
if (shouldAllowMessage(m)
&& !continueIgnoreMessage(m.getIgnoredMessageKey(), thisTurn)) {
messages.add(m);
}
m.setBeenDisplayed(true);
}
reapIgnoredMessages(thisTurn);
if (!messages.isEmpty()) {
Runnable uiTask;
if (endOfTurn) {
turnReportMessages.addAll(messages);
uiTask = () -> { displayTurnReportMessages(); };
} else {
uiTask = () -> { gui.showModelMessages(messages); };
}
gui.invokeNowOrWait(uiTask);
}
return !messages.isEmpty();
}
// Utilities to handle the transitions between the active-unit,
// execute-orders and end-turn states.
/**
* 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() {
if (gui.isShowingSubPanel()) return false; // Clear the panel first
final Player player = freeColClient.getMyPlayer();
final Unit active = gui.getActiveUnit();
Unit stillActive = null;
// Ensure the goto mode sticks.
moveMode = moveMode.maximize(MoveMode.EXECUTE_GOTO_ORDERS);
// Deal with the trade route units first.
List<ModelMessage> messages = new ArrayList<>();
while (player.hasNextTradeRouteUnit()) {
Unit unit = player.getNextTradeRouteUnit();
gui.setActiveUnit(unit);
if (moveToDestination(unit, messages)) stillActive = unit;
}
if (!messages.isEmpty()) {
for (ModelMessage m : messages) {
player.addModelMessage(m);
turnReportMessages.add(m);
}
displayModelMessages(false, false);
gui.setActiveUnit((stillActive != null) ? stillActive : active);
return false;
}
// The active unit might also be a going-to unit. Make sure it
// gets processed first. setNextGoingToUnit will fail harmlessly
// if it is not a going-to unit so this is safe.
if (active != null) player.setNextGoingToUnit(active);
// Process all units.
while (player.hasNextGoingToUnit()) {
Unit unit = player.getNextGoingToUnit();
gui.setActiveUnit(unit);
// Move the unit as much as possible
if (moveToDestination(unit, null)) stillActive = unit;
// Might have LCR messages to display
displayModelMessages(false, false);
// 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();
break;
}
}
gui.setActiveUnit((stillActive != null) ? stillActive : active);
return stillActive == null;
}
/**
* End the turn.
*
* @param showDialog Show the end turn dialog?
* @return True if the turn ended.
*/
private boolean doEndTurn(boolean showDialog) {
if (showDialog) {
List<Unit> units = freeColClient.getMyPlayer().getUnits().stream()
.filter(Unit::couldMove).collect(Collectors.toList());
if (!units.isEmpty()) {
// Modal dialog takes over
gui.showEndTurnDialog(units,
(Boolean value) -> {
if (value != null && value) {
endTurn(false);
}
});
return false;
}
}
// Ensure end-turn mode sticks.
moveMode = moveMode.maximize(MoveMode.END_TURN);
// Make sure all goto orders are complete before ending turn.
if (!doExecuteGotoOrders()) return false;
// Check for desync as last thing!
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.DESYNC)
&& DebugUtils.checkDesyncAction(freeColClient)) {
freeColClient.getConnectController().reconnect();
return false;
}
// Clean up lingering menus.
gui.closeMenus();
// 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 = MoveMode.NEXT_ACTIVE_UNIT;
// Clear outdated turn report messages.
turnReportMessages.clear();
// Inform the server of end of turn.
return askServer().endTurn();
}
/**
* 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.
* @return True if the active unit changes.
*/
private boolean updateActiveUnit(Tile tile) {
// Make sure the active unit is done.
final Player player = freeColClient.getMyPlayer();
Unit unit = gui.getActiveUnit();
if (unit != null && unit.couldMove()) return false;
// Flush any outstanding orders once the mode is raised.
if (moveMode != MoveMode.NEXT_ACTIVE_UNIT
&& !doExecuteGotoOrders()) {
return false;
}
// Successfully found a unit to display
if (player.hasNextActiveUnit()) {
gui.setActiveUnit(player.getNextActiveUnit());
return true;
}
// No unit to find.
gui.setActiveUnit(null);
// No active units left. Do the goto orders.
if (!doExecuteGotoOrders()) return true;
// 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.
final ClientOptions options = freeColClient.getClientOptions();
if (tile != null) {
gui.setSelectedTile(tile);
} else if (options.getBoolean(ClientOptions.AUTO_END_TURN)) {
doEndTurn(options.getBoolean(ClientOptions.SHOW_END_TURN_DIALOG));
}
return true;
}
// Movement support.
/**
* Moves the given unit towards its destination/s if possible.
*
* @param unit The <code>Unit</code> to move.
* @param messages An optional list in which to retain any
* trade route <code>ModelMessage</code>s generated.
* @return True if the unit reached its destination, is still alive,
* and has more moves to make.
*/
private boolean moveToDestination(Unit unit, List<ModelMessage> messages) {
Location destination;
if (!requireOurTurn()
|| unit.isAtSea()
|| unit.getMovesLeft() <= 0
|| unit.getState() == UnitState.SKIPPED) {
return false;
} else if (unit.getTradeRoute() != null) {
return followTradeRoute(unit, messages);
} else if ((destination = unit.getDestination()) == null) {
return unit.getMovesLeft() > 0;
}
// Find a path to the destination and try to follow it.
final Player player = freeColClient.getMyPlayer();
PathNode path = unit.findPath(destination);
if (path == null) {
StringTemplate src = unit.getLocation()
.getLocationLabelFor(player);
StringTemplate dst = destination.getLocationLabelFor(player);
StringTemplate template = StringTemplate
.template("info.moveToDestinationFailed")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addStringTemplate("%location%", src)
.addStringTemplate("%destination%", dst);
gui.showInformationMessage(unit, template);
return false;
}
gui.setActiveUnit(unit);
// Clear ordinary destinations if arrived.
if (movePath(unit, path) && unit.isAtLocation(destination)) {
askClearGotoOrders(unit);
Colony colony = (unit.hasTile()) ? unit.getTile().getColony()
: null;
if (colony != null && !checkCashInTreasureTrain(unit)) {
colonyPanel(colony, unit);
}
return unit.couldMove();
}
return false;
}
/**
* Move a unit in a given direction.
*
* Public for the test suite.
*
* @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.
*/
public boolean moveDirection(Unit unit, Direction direction,
boolean interactive) {
// If this move would reach the unit destination but we
// discover that it would be permanently impossible to complete,
// clear the destination.
Unit.MoveType mt = unit.getMoveType(direction);
Location destination = unit.getDestination();
Tile oldTile = unit.getTile();
boolean clearDestination = destination != null
&& oldTile != null
&& Map.isSameLocation(oldTile.getNeighbourOrNull(direction),
destination);
// Consider all the move types.
boolean result = mt.isLegal();
switch (mt) {
case MOVE_HIGH_SEAS:
if (freeColClient.getMyPlayer().getEurope() == null) {
; // do nothing
} else if (destination == null) {
result = moveHighSeas(unit, direction);
break;
} else if (destination instanceof Europe) {
result = moveTo(unit, destination);
break;
}
// Fall through
case MOVE:
result = moveMove(unit, direction);
break;
case EXPLORE_LOST_CITY_RUMOUR:
result = moveExplore(unit, direction);
break;
case ATTACK_UNIT:
result = moveAttack(unit, direction);
break;
case ATTACK_SETTLEMENT:
result = moveAttackSettlement(unit, direction);
break;
case EMBARK:
result = moveEmbark(unit, direction);
break;
case ENTER_INDIAN_SETTLEMENT_WITH_FREE_COLONIST:
result = moveLearnSkill(unit, direction);
break;
case ENTER_INDIAN_SETTLEMENT_WITH_SCOUT:
result = moveScoutIndianSettlement(unit, direction);
break;
case ENTER_INDIAN_SETTLEMENT_WITH_MISSIONARY:
result = moveUseMissionary(unit, direction);
break;
case ENTER_FOREIGN_COLONY_WITH_SCOUT:
result = moveScoutColony(unit, direction);
break;
case ENTER_SETTLEMENT_WITH_CARRIER_AND_GOODS:
result = moveTrade(unit, direction);
break;
// Illegal moves
case MOVE_NO_ACCESS_BEACHED:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessBeached")
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_CONTACT:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessContact")
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_GOODS:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessGoods")
.addStringTemplate("%nation%", nation)
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
}
break;
case MOVE_NO_ACCESS_LAND:
if (!moveDisembark(unit, direction)) {
if (interactive) {
sound("sound.event.illegalMove");
}
}
break;
case MOVE_NO_ACCESS_MISSION_BAN:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessMissionBan")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_SETTLEMENT:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessSettlement")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_SKILL:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessSkill")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
}
break;
case MOVE_NO_ACCESS_TRADE:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessTrade")
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_WAR:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
StringTemplate nation = getNationAt(unit.getTile(), direction);
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessWar")
.addStringTemplate("%nation%", nation));
}
break;
case MOVE_NO_ACCESS_WATER:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
gui.showInformationMessage(unit, StringTemplate
.template("move.noAccessWater")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
}
break;
case MOVE_NO_ATTACK_MARINE:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
gui.showInformationMessage(unit, StringTemplate
.template("move.noAttackWater")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
}
break;
case MOVE_NO_MOVES:
// The unit may have some moves left, but not enough
// to move to the next node. The move is illegal
// this turn, but might not be next turn, so do not cancel the
// destination but set the state to skipped instead.
clearDestination = false;
unit.setState(UnitState.SKIPPED);
break;
case MOVE_NO_TILE:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
gui.showInformationMessage(unit, StringTemplate
.template("move.noTile")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
}
break;
default:
if (interactive || clearDestination) {
sound("sound.event.illegalMove");
}
result = false;
break;
}
if (clearDestination && !unit.isDisposed()) {
askClearGotoOrders(unit);
}
return result;
}
/**
* 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 movePath(Unit unit, PathNode path) {
for (; path != null; path = path.next) {
if (unit.isAtLocation(path.getLocation())) continue;
if (path.getLocation() instanceof Europe) {
if (unit.hasTile()
&& unit.getTile().isDirectlyHighSeasConnected()) {
moveTo(unit, path.getLocation());
} else {
logger.warning("Can not move to Europe from "
+ unit.getLocation()
+ " on path: " + path.fullPathToString());
}
return false;
} else if (path.getLocation() instanceof Tile) {
if (path.getDirection() == null) {
if (unit.isInEurope()) {
moveTo(unit, unit.getGame().getMap());
} else {
logger.warning("Null direction on path: "
+ path.fullPathToString());
}
return false;
} else {
if (!moveDirection(unit, path.getDirection(), false)) {
return false;
}
if (unit.hasTile()
&& unit.getTile().getDiscoverableRegion() != null) {
// Break up the goto to allow region naming to occur,
// BR#2707
return false;
}
}
} else if (path.getLocation() instanceof Unit) {
return moveEmbark(unit, path.getDirection());
} else {
logger.warning("Bad path: " + path.fullPathToString());
}
}
return true;
}
/**
* 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.
* @return True if the unit could move further.
*/
private boolean moveAttack(Unit unit, Direction direction) {
Tile tile = unit.getTile();
Tile target = tile.getNeighbourOrNull(direction);
Unit u = target.getFirstUnit();
if (u == null || unit.getOwner().owns(u)) return false;
askClearGotoOrders(unit);
if (gui.confirmHostileAction(unit, target)
&& gui.confirmPreCombat(unit, target)) {
askServer().attack(unit, direction);
}
// Always return false, as the unit has either attacked and lost
// its remaining moves, or the move can not proceed because it is
// blocked.
return false;
}
/**
* Confirm attack or demand a tribute from a settlement, following
* an attacking move.
*
* @param unit The <code>Unit</code> to perform the attack.
* @param direction The direction in which to attack.
* @return True if the unit could move further.
*/
private boolean moveAttackSettlement(Unit unit, Direction direction) {
Tile tile = unit.getTile();
Tile target = tile.getNeighbourOrNull(direction);
Settlement settlement = target.getSettlement();
if (settlement == null
|| unit.getOwner().owns(settlement)) return false;
ArmedUnitSettlementAction act
= gui.getArmedUnitSettlementChoice(settlement);
if (act == null) return true; // Cancelled
switch (act) {
case SETTLEMENT_ATTACK:
if (gui.confirmHostileAction(unit, target)
&& gui.confirmPreCombat(unit, target)) {
askServer().attack(unit, direction);
Colony col = target.getColony();
if (col != null && unit.getOwner().owns(col)) {
colonyPanel(col, unit);
}
return false;
}
break;
case SETTLEMENT_TRIBUTE:
int amount = (settlement instanceof Colony)
? gui.confirmEuropeanTribute(unit, (Colony)settlement,
getNationSummary(settlement.getOwner()))
: (settlement instanceof IndianSettlement)
? gui.confirmNativeTribute(unit, (IndianSettlement)settlement)
: -1;
if (amount <= 0) return true; // Cancelled
return moveTribute(unit, amount, direction);
default:
logger.warning("showArmedUnitSettlementDialog fail: " + act);
break;
}
return true;
}
/**
* Initiates diplomacy with a foreign power.
*
* @param unit The <code>Unit</code> negotiating.
* @param direction The direction of a settlement to negotiate with.
* @param dt The base <code>DiplomaticTrade</code> agreement to
* begin the negotiation with.
* @return True if the unit can move further.
*/
private boolean moveDiplomacy(Unit unit, Direction direction,
DiplomaticTrade dt) {
Settlement settlement = getSettlementAt(unit.getTile(), direction);
if (settlement == null
|| !(settlement instanceof Colony)) return false;
Colony colony = (Colony)settlement;
// Can not negotiate with the REF.
final Game game = freeColClient.getGame();
final Player player = unit.getOwner();
final Player other = colony.getOwner();
if (other == player.getREFPlayer()) return false;
StringTemplate nation = other.getNationLabel();
while (dt != null) {
// Inform server of current agreement.
dt = askServer().diplomacy(game, unit, colony, dt);
// Returned dt will be null if we sent or the other player
// replied with an accept/reject. Otherwise consider
// counter proposal.
if (dt != null) {
dt = gui.showNegotiationDialog(unit, colony, dt,
dt.getSendMessage(player, colony));
}
}
return false;
}
/**
* 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.
unit.setStateToAllChildren(UnitState.ACTIVE);
final List<Unit> disembarkable = unit.getUnitList().stream()
.filter(u -> u.getMoveType(tile).isProgress())
.collect(Collectors.toList());
if (disembarkable.isEmpty()) return false; // Fail, did not find one
if (disembarkable.size() == 1) {
if (gui.confirm(tile,
StringTemplate.key("disembark.text"),
disembarkable.get(0), "ok", "cancel")) {
moveDirection(disembarkable.get(0), direction, false);
}
} else {
List<ChoiceItem<Unit>> choices = new ArrayList<>();
for (Unit dUnit : disembarkable) {
choices.add(new ChoiceItem<>(dUnit.getDescription(Unit.UnitLabelType.NATIONAL),
dUnit));
}
choices.add(new ChoiceItem<>(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.getChoice(unit.getTile(),
Messages.message("disembark.text"),
unit,
"none", choices);
if (u == null) {
// Cancelled, done.
} 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;
}
}
} else {
moveDirection(u, direction, false);
}
}
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.
* @return True if the unit could move further.
*/
private boolean moveEmbark(Unit unit, Direction direction) {
if (unit.getColony() != null
&& !gui.confirmLeaveColony(unit)) return false;
Tile sourceTile = unit.getTile();
Tile destinationTile = sourceTile.getNeighbourOrNull(direction);
Unit carrier = null;
List<ChoiceItem<Unit>> choices = new ArrayList<>();
for (Unit u : destinationTile.getUnitList()) {
if (u.canAdd(unit)) {
String m = u.getDescription(Unit.UnitLabelType.NATIONAL);
choices.add(new ChoiceItem<>(m, u));
carrier = u; // Save a default
}
}
if (choices.isEmpty()) {
throw new RuntimeException("Unit " + unit.getId()
+ " found no carrier to embark upon.");
} else if (choices.size() == 1) {
// Use the default
} else {
carrier = gui.getChoice(unit.getTile(),
Messages.message("embark.text"),
unit,
"none", choices);
if (carrier == null) return true; // User cancelled
}
// Proceed to embark
askClearGotoOrders(unit);
if (!askServer().embark(unit, carrier, direction)
|| unit.getLocation() != carrier) {
unit.setState(UnitState.SKIPPED);
return false;
}
unit.getOwner().invalidateCanSeeTiles();
return false;
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveExplore(Unit unit, Direction direction) {
Tile tile = unit.getTile().getNeighbourOrNull(direction);
if (!gui.confirm(unit.getTile(),
StringTemplate.key("exploreLostCityRumour.text"), unit,
"exploreLostCityRumour.yes", "exploreLostCityRumour.no")) {
return true;
}
if (tile.getLostCityRumour().getType()== LostCityRumour.RumourType.MOUNDS
&& !gui.confirm(unit.getTile(),
StringTemplate.key("exploreMoundsRumour.text"), unit,
"exploreLostCityRumour.yes", "exploreLostCityRumour.no")) {
askServer().declineMounds(unit, direction);
}
return 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.
* @return True if the unit can move further.
*/
private boolean moveHighSeas(Unit unit, Direction direction) {
// Confirm moving to Europe if told to move to a null tile
// (FIXME: 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.isDirectlyHighSeasConnected()
&& newTile.isDirectlyHighSeasConnected())) {
if (unit.getTradeRoute() != null) {
TradeRouteStop stop = unit.getStop();
if (stop != null && TradeRoute.isStopValid(unit, stop)
&& stop.getLocation() instanceof Europe) {
moveTo(unit, stop.getLocation());
return false;
}
} else if (unit.getDestination() instanceof Europe) {
moveTo(unit, unit.getDestination());
return false;
} else {
if (gui.confirm(oldTile, StringTemplate
.template("highseas.text")
.addAmount("%number%", unit.getSailTurns()),
unit, "highseas.yes", "highseas.no")) {
moveTo(unit, unit.getOwner().getEurope());
return false;
}
}
}
return moveMove(unit, direction);
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveLearnSkill(Unit unit, Direction direction) {
askClearGotoOrders(unit);
// Refresh knowledge of settlement skill. It may have been
// learned by another player.
if (!askServer().askSkill(unit, direction)) return false;
IndianSettlement settlement
= (IndianSettlement)getSettlementAt(unit.getTile(), direction);
UnitType skill = settlement.getLearnableSkill();
if (skill == null) {
gui.showInformationMessage(settlement, "info.noMoreSkill");
} else if (!unit.getType().canBeUpgraded(skill, ChangeType.NATIVES)) {
gui.showInformationMessage(settlement, StringTemplate
.template("info.cantLearnSkill")
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addNamed("%skill%", skill));
} else if (gui.confirm(unit.getTile(), StringTemplate
.template("learnSkill.text")
.addNamed("%skill%", skill),
unit, "learnSkill.yes", "learnSkill.no")) {
if (askServer().learnSkill(unit, direction)) {
if (unit.isDisposed()) {
gui.showInformationMessage(settlement, "learnSkill.die");
return false;
}
if (unit.getType() != skill) {
gui.showInformationMessage(settlement, "learnSkill.leave");
}
}
}
return false;
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveMove(Unit unit, Direction direction) {
final ClientOptions options = freeColClient.getClientOptions();
if (unit.canCarryUnits() && unit.hasSpaceLeft()
&& options.getBoolean(ClientOptions.AUTOLOAD_SENTRIES)) {
// Autoload sentries if selected
List<Unit> waiting = (unit.getColony() != null)
? unit.getTile().getUnitList()
: Collections.<Unit>emptyList();
for (Unit u : waiting) {
if (u.getState() != UnitState.SENTRY
|| !unit.couldCarry(u)) continue;
try {
askEmbark(u, unit);
} finally {
if (u.getLocation() != unit) {
u.setState(UnitState.SKIPPED);
}
continue;
}
}
// Boarding consumed this unit's moves.
if (unit.getMovesLeft() <= 0) return false;
}
// Ask the server
if (!askServer().move(unit, direction)) {
// Can fail due to desynchronization. Skip this unit so
// we do not end up retrying indefinitely.
unit.setState(UnitState.SKIPPED);
return false;
}
unit.getOwner().invalidateCanSeeTiles();
final Tile tile = unit.getTile();
// Perform a short pause on an active unit's last move if
// the option is enabled.
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.
boolean ret = !unit.isDisposed() && !checkCashInTreasureTrain(unit);
if (ret) {
if (tile.getColony() != null && unit.isCarrier()) {
final Colony colony = tile.getColony();
if (unit.getTradeRoute() == null
&& Map.isSameLocation(tile, unit.getDestination())) {
colonyPanel(colony, unit);
}
}
ret = unit.getMovesLeft() > 0;
}
return ret;
}
/**
* 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.
*
* FIXME: Unify trade and negotiation.
*
* @param unit The unit that will spy, negotiate or attack.
* @param direction The direction in which the foreign colony lies.
* @return True if the unit can move further.
*/
private boolean moveScoutColony(Unit unit, Direction direction) {
final Game game = freeColClient.getGame();
Colony colony = (Colony) getSettlementAt(unit.getTile(), direction);
boolean canNeg = colony.getOwner() != unit.getOwner().getREFPlayer();
askClearGotoOrders(unit);
ScoutColonyAction act
= gui.getScoutForeignColonyChoice(colony, unit, canNeg);
if (act == null) return true; // Cancelled
switch (act) {
case FOREIGN_COLONY_ATTACK:
return moveAttackSettlement(unit, direction);
case FOREIGN_COLONY_NEGOTIATE:
Player player = unit.getOwner();
DiplomaticTrade agreement
= new DiplomaticTrade(game, TradeContext.DIPLOMATIC,
player, colony.getOwner(), null, 0);
agreement = gui.showNegotiationDialog(unit, colony,
agreement, agreement.getSendMessage(player, colony));
return (agreement == null
|| agreement.getStatus() == TradeStatus.REJECT_TRADE) ? true
: moveDiplomacy(unit, direction, agreement);
case FOREIGN_COLONY_SPY:
return moveSpy(unit, direction);
default:
logger.warning("showScoutForeignColonyDialog fail: " + act);
break;
}
return true;
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveScoutIndianSettlement(Unit unit, Direction direction) {
Tile unitTile = unit.getTile();
Tile tile = unitTile.getNeighbourOrNull(direction);
IndianSettlement settlement = tile.getIndianSettlement();
Player player = unit.getOwner();
askClearGotoOrders(unit);
// Offer the choices.
String number = askServer().scoutSettlement(unit, direction);
if (number == null) number = Messages.message("many");
ScoutIndianSettlementAction act
= gui.getScoutIndianSettlementChoice(settlement, number);
if (act == null) return true; // Cancelled
switch (act) {
case INDIAN_SETTLEMENT_ATTACK:
if (!gui.confirmPreCombat(unit, tile)) return true;
askServer().attack(unit, direction);
return false;
case INDIAN_SETTLEMENT_SPEAK:
final int oldGold = player.getGold();
String result = askServer().scoutSpeakToChief(unit, direction);
if (result == null) {
return false; // Fail
} else if ("die".equals(result)) {
gui.showInformationMessage(settlement,
"scoutSettlement.speakDie");
return false;
} else if ("expert".equals(result)) {
gui.showInformationMessage(settlement, StringTemplate
.template("scoutSettlement.expertScout")
.addNamed("%unit%", unit.getType()));
} else if ("tales".equals(result)) {
gui.showInformationMessage(settlement,
"scoutSettlement.speakTales");
} else if ("beads".equals(result)) {
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.getNationLabel()));
} else {
logger.warning("Invalid result from askScoutSpeak: " + result);
}
return false;
case INDIAN_SETTLEMENT_TRIBUTE:
return moveTribute(unit, 1, direction);
default:
logger.warning("showScoutIndianSettlementDialog fail: " + act);
break;
}
return true;
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveSpy(Unit unit, Direction direction) {
askServer().spy(unit, direction);
return false;
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveTrade(Unit unit, Direction direction) {
askClearGotoOrders(unit);
Settlement settlement = getSettlementAt(unit.getTile(), direction);
if (settlement instanceof Colony) {
final Game game = freeColClient.getGame();
final Player player = unit.getOwner();
DiplomaticTrade agreement
= new DiplomaticTrade(game, TradeContext.TRADE,
player, settlement.getOwner(), null, 0);
agreement = gui.showNegotiationDialog(unit, settlement,
agreement, agreement.getSendMessage(player, settlement));
return (agreement == null
|| agreement.getStatus() == TradeStatus.REJECT_TRADE) ? true
: moveDiplomacy(unit, direction, agreement);
} else if (settlement instanceof IndianSettlement) {
return moveTradeIndianSettlement(unit, direction);
} else {
throw new RuntimeException("Bogus settlement: "
+ settlement.getId());
}
}
/**
* 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>.
* @see Settlement
* @return True if the unit can move further.
*/
private boolean moveTradeIndianSettlement(Unit unit, Direction direction) {
Settlement settlement = getSettlementAt(unit.getTile(), direction);
StringTemplate baseTemplate = StringTemplate
.template("tradeProposition.welcome")
.addStringTemplate("%nation%",
settlement.getOwner().getNationLabel())
.addName("%settlement%", settlement.getName());
StringTemplate template = baseTemplate;
boolean[] results = askServer()
.openTransactionSession(unit, settlement);
while (results != null) {
// 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.hasSpaceLeft();
boolean sel = results[1] && unit.hasGoodsCargo();
boolean gif = results[2] && unit.hasGoodsCargo();
if (!buy && !sel && !gif) break;
TradeAction act = gui.getIndianSettlementTradeChoice(settlement,
template, buy, sel, gif);
if (act == null) break;
StringTemplate t = null;
switch (act) {
case BUY:
t = attemptBuyFromSettlement(unit, settlement);
if (t == null) {
results[0] = false;
template = baseTemplate;
} else {
template = t;
}
break;
case SELL:
t = attemptSellToSettlement(unit, settlement);
if (t == null) {
results[1] = false;
template = baseTemplate;
} else {
template = t;
}
break;
case GIFT:
t = attemptGiftToSettlement(unit, settlement);
if (t == null) {
results[2] = false;
template = baseTemplate;
} else {
template = t;
}
break;
default:
logger.warning("showIndianSettlementTradeDialog fail: "
+ act);
results = null;
break;
}
if (template == abortTrade) template = baseTemplate;
}
askServer().closeTransactionSession(unit, settlement);
if (unit.getMovesLeft() > 0) gui.setActiveUnit(unit); // No trade?
return false;
}
/**
* 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.
* @return A <code>StringTemplate</code> describing the failure.
*/
private StringTemplate tradeFailMessage(int fail, Settlement settlement,
Goods goods) {
switch (fail) {
case NO_TRADE_GOODS:
return StringTemplate.template("trade.noTradeGoods")
.addNamed("%goods%", goods);
case NO_TRADE_HAGGLE:
return StringTemplate.template("trade.noTradeHaggle");
case NO_TRADE_HOSTILE:
return StringTemplate.template("trade.noTradeHostile");
case NO_TRADE: // Proposal was refused
default:
break;
}
return StringTemplate.template("trade.noTrade")
.addName("%settlement%",
settlement.getLocationLabelFor(freeColClient.getMyPlayer()));
}
/**
* 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.
* @return A <code>StringTemplate</code> containing a message if
* there is problem, or null on success.
*/
private StringTemplate attemptBuyFromSettlement(Unit unit,
Settlement settlement) {
final Game game = freeColClient.getGame();
Player player = freeColClient.getMyPlayer();
Goods goods = null;
// Get list of goods for sale
List<Goods> forSale = askServer()
.getGoodsForSaleInSettlement(game, unit, settlement);
for (;;) {
if (forSale.isEmpty()) { // Nothing to sell to the player
return StringTemplate.template("trade.nothingToSell");
}
// Choose goods to buy
List<ChoiceItem<Goods>> choices = new ArrayList<>();
for (Goods g : forSale) {
String label = Messages.message(g.getLabel(true));
choices.add(new ChoiceItem<>(label, g));
}
goods = gui.getChoice(unit.getTile(),
Messages.message("buyProposition.text"),
settlement,
"nothing", choices);
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) {
return tradeFailMessage(gold, settlement, goods);
}
// Show dialog for buy proposal
boolean canBuy = player.checkGold(gold);
BuyAction act
= gui.getBuyChoice(unit, settlement, goods, gold, canBuy);
if (act == null) break; // User cancelled
switch (act) {
case BUY: // Accept price, make purchase
return (askServer().buyFromSettlement(unit,
settlement, goods, gold)) ? null
: abortTrade;
case HAGGLE: // Try to negotiate a lower price
gold = gold * 9 / 10;
break;
default:
logger.warning("showBuyDialog fail: " + act);
return null;
}
}
}
return abortTrade;
}
/**
* 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.
* @return A <code>StringTemplate</code> containing a message if
* there is problem, or null on success.
*/
private StringTemplate attemptSellToSettlement(Unit unit,
Settlement settlement) {
Goods goods = null;
for (;;) {
// Choose goods to sell
List<ChoiceItem<Goods>> choices = new ArrayList<>();
for (Goods g : unit.getGoodsList()) {
String label = Messages.message(g.getLabel(true));
choices.add(new ChoiceItem<>(label, g));
}
goods = gui.getChoice(unit.getTile(),
Messages.message("sellProposition.text"),
settlement,
"nothing", choices);
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) {
return tradeFailMessage(gold, settlement, goods);
}
// Show dialog for sale proposal
SellAction act = gui.getSellChoice(unit, settlement,
goods, gold);
if (act == null) break; // Cancelled
switch (act) {
case SELL: // Accepted price, make the sale
return (askServer().sellToSettlement(unit, settlement,
goods, gold)) ? null
: abortTrade;
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 abortTrade;
default:
logger.warning("showSellDialog fail: " + act);
return null;
}
}
}
return abortTrade;
}
/**
* 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.
* @return A <code>StringTemplate</code> containing a message if
* there is problem, or null on success.
*/
private StringTemplate attemptGiftToSettlement(Unit unit,
Settlement settlement) {
List<ChoiceItem<Goods>> choices = new ArrayList<>();
for (Goods g : unit.getGoodsList()) {
String label = Messages.message(g.getLabel(true));
choices.add(new ChoiceItem<>(label, g));
}
Goods goods = gui.getChoice(unit.getTile(),
Messages.message("gift.text"),
settlement,
"cancel", choices);
return (goods != null
&& askServer().deliverGiftToSettlement(unit, settlement, goods))
? null
: abortTrade;
}
/**
* Demand a tribute.
*
* @param unit The <code>Unit</code> to perform the attack.
* @param amount An amount of tribute to demand.
* @param direction The direction in which to attack.
* @return True if the unit can move further.
*/
private boolean moveTribute(Unit unit, int amount, Direction direction) {
final Game game = freeColClient.getGame();
Player player = unit.getOwner();
Tile tile = unit.getTile();
Tile target = tile.getNeighbourOrNull(direction);
Settlement settlement = target.getSettlement();
Player other = settlement.getOwner();
// Indians are easy and can use the basic tribute mechanism.
if (settlement.getOwner().isIndian()) {
askServer().demandTribute(unit, direction);
return false;
}
// Europeans might be human players, so we convert to a diplomacy
// dialog.
DiplomaticTrade agreement
= new DiplomaticTrade(game, TradeContext.TRIBUTE, player, other,
null, 0);
agreement.add(new StanceTradeItem(game, player, other, Stance.PEACE));
agreement.add(new GoldTradeItem(game, other, player, amount));
return moveDiplomacy(unit, direction, agreement);
}
/**
* 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.
* @return True if the unit can move further.
*/
private boolean moveUseMissionary(Unit unit, Direction direction) {
IndianSettlement settlement
= (IndianSettlement)getSettlementAt(unit.getTile(), direction);
Player player = unit.getOwner();
boolean canEstablish = !settlement.hasMissionary();
boolean canDenounce = !canEstablish
&& !settlement.hasMissionary(player);
askClearGotoOrders(unit);
// Offer the choices.
MissionaryAction act = gui.getMissionaryChoice(unit, settlement,
canEstablish, canDenounce);
if (act == null) return true;
switch (act) {
case ESTABLISH_MISSION: case DENOUNCE_HERESY:
if (askServer().missionary(unit, direction,
act == MissionaryAction.DENOUNCE_HERESY)
&& settlement.hasMissionary(player)) {
sound("sound.event.missionEstablished");
player.invalidateCanSeeTiles();
}
break;
case INCITE_INDIANS:
List<ChoiceItem<Player>> choices = new ArrayList<>();
for (Player p : freeColClient.getGame().getLiveEuropeanPlayers(player)) {
String label = Messages.message(p.getCountryLabel());
choices.add(new ChoiceItem<>(label, p));
}
Player enemy = gui.getChoice(unit.getTile(),
Messages.message("missionarySettlement.inciteQuestion"),
unit,
"missionarySettlement.cancel", choices);
if (enemy == null) return true;
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.confirm(unit.getTile(), StringTemplate
.template("missionarySettlement.inciteConfirm")
.add("%player%", enemy.getName())
.addAmount("%amount%", gold),
unit, "yes", "no")) {
askServer().incite(unit, direction, enemy, gold);
}
break;
default:
logger.warning("showUseMissionaryDialog fail");
break;
}
return false;
}
// 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.
* @param messages An optional list in which to retain any
* <code>ModelMessage</code>s generated.
* @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, List<ModelMessage> messages) {
final Player player = unit.getOwner();
final TradeRoute tr = unit.getTradeRoute();
final boolean detailed = freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_GOODS_MOVEMENT);
final boolean checkProduction = freeColClient.getClientOptions()
.getBoolean(ClientOptions.STOCK_ACCOUNTS_FOR_PRODUCTION);
final List<TradeRouteStop> stops = unit.getCurrentStops();
boolean result = false;
// If required, accumulate a summary of all the activity of
// this unit on its trade route.
LogBuilder lb = new LogBuilder((detailed && !tr.isSilent()) ? 256
: -1);
lb.mark();
// Validate the whole route.
boolean valid = true;
for (TradeRouteStop trs : stops) {
if (!TradeRoute.isStopValid(unit, trs)) {
lb.add(" ", Messages.message(trs.invalidStopLabel(player)));
valid = false;
}
}
if (!valid) {
clearOrders(unit);
stops.clear();
result = unit.getMovesLeft() > 0;
}
// Try to find work to do on the current list of stops.
while (!stops.isEmpty()) {
TradeRouteStop stop = stops.remove(0);
if (!unit.atStop(stop)) {
// Not at stop, give up if no moves left or the path was
// exhausted on a previous round.
if (unit.getMovesLeft() <= 0
|| unit.getState() == UnitState.SKIPPED) {
lb.add(" ", Messages.message(stop
.getLabelFor("tradeRoute.toStop", player)));
break;
}
// Find a path to the stop, skip if none.
Location destination = stop.getLocation();
PathNode path = unit.findPath(destination);
if (path == null) {
lb.add(" ", Messages.message(stop
.getLabelFor("tradeRoute.pathStop", player)));
unit.setState(UnitState.SKIPPED);
break;
}
// Try to follow the path. If the unit does not reach
// the stop it is finished for now.
movePath(unit, path);
if (!unit.atStop(stop)) {
unit.setState(UnitState.SKIPPED);
break;
}
}
// At the stop, do the work available.
lb.mark();
unloadUnitAtStop(unit, lb); // Anything to unload?
loadUnitAtStop(unit, lb); // Anything to load?
lb.grew(" ", Messages.message(stop.getLabelFor("tradeRoute.atStop",
player)));
// If the un/load consumed the moves, break now before
// updating the stop. This allows next turn to retry
// un/loading, but this time it will not consume the moves.
if (unit.getMovesLeft() <= 0) break;
// Find the next stop with work to do.
TradeRouteStop next = null;
List<TradeRouteStop> moreStops = unit.getCurrentStops();
if (unit.atStop(moreStops.get(0))) moreStops.remove(0);
for (TradeRouteStop trs : moreStops) {
if (trs.hasWork(unit, (!checkProduction) ? 0
: unit.getTurnsToReach(trs.getLocation()))) {
next = trs;
break;
}
}
if (next == null) {
// No work was found anywhere on the trade route,
// so we should skip this unit.
lb.add(" ", Messages.message("tradeRoute.wait"));
unit.setState(UnitState.SKIPPED);
break;
}
// Add a message for any skipped stops.
List<TradeRouteStop> skipped
= tr.getStopSublist(stops.get(0), next);
if (!skipped.isEmpty()) {
StringTemplate t = StringTemplate.label("")
.add("tradeRoute.skipped");
String sep = " ";
for (TradeRouteStop trs : skipped) {
t.addName(sep)
.addStringTemplate(trs.getLocation()
.getLocationLabelFor(player));
sep = ", ";
}
t.addName(".");
lb.add(" ", Messages.message(t));
}
// Bring the next stop to the head of the stops list if it
// is present.
while (!stops.isEmpty() && stops.get(0) != next) {
stops.remove(0);
}
// Set the new stop, skip on error.
if (!askServer().setCurrentStop(unit, tr.getIndex(next))) {
unit.setState(UnitState.SKIPPED);
break;
}
}
if (lb.grew()) {
ModelMessage m = new ModelMessage(MessageType.GOODS_MOVEMENT,
"tradeRoute.prefix", unit)
.addName("%route%", tr.getName())
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addName("%data%", lb.toString());
if (messages != null) {
messages.add(m);
} else {
player.addModelMessage(m);
turnReportMessages.add(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 lb A <code>LogBuilder</code> to update.
* @return True if goods were loaded.
*/
private boolean loadUnitAtStop(Unit unit, LogBuilder lb) {
final TradeLocation trl = unit.getTradeLocation();
if (trl == null) return false;
final TradeRouteStop stop = unit.getStop();
boolean ret = false;
// Get the collapsed list of goods to load at this stop.
List<AbstractGoods> toLoad = stop.getCompactCargo();
// If already at capacity for a goods type, drop it from the
// toLoad list, otherwise reduce its amount by the amount
// already loaded. Handle excess goods.
for (Goods g : unit.getCompactGoods()) {
AbstractGoods ag = AbstractGoods.findByType(g.getType(), toLoad);
if (ag != null) {
int goodsAmount = g.getAmount();
int amount = ag.getAmount() - goodsAmount;
if (amount <= 0) { // At capacity
toLoad.remove(ag);
} else { // Modify amount
ag.setAmount(amount);
}
} else {
// Excess goods on board. They must have failed to
// unload somewhere.
lb.add(" ", Messages.message(StringTemplate
.template("tradeRoute.loadStopBlocked")
.addStringTemplate("%goods%", g.getLabel())));
}
}
// Adjust toLoad with the actual export amount. Some goods
// may not have an export surplus. Add messages for them and
// drop from the toLoad list.
Iterator<AbstractGoods> iterator = toLoad.iterator();
while (iterator.hasNext()) {
AbstractGoods ag = iterator.next();
int amount = stop.getExportAmount(ag.getType(), 0);
if (amount <= 0) {
if (stop.getCargo().contains(ag.getType())) {
// Complain only if this goods type was planned to load
int present = stop.getGoodsCount(ag.getType());
lb.add(" ", getLoadGoodsMessage(ag.getType(), 0, present,
0, -1));
}
iterator.remove();
} else {
ag.setAmount(Math.min(amount, ag.getAmount()));
}
}
// Load the goods.
for (AbstractGoods ag : toLoad) {
GoodsType type = ag.getType();
int demand = ag.getAmount();
ret = askLoadGoods(stop.getLocation(), type, demand, unit);
if (!ret) {
// Assume any failure is due to goods still on board,
// and thus no further loading is likely to succeed.
break;
}
int present = stop.getGoodsCount(type);
int export = stop.getExportAmount(type, 0);
lb.add(" ", getLoadGoodsMessage(type, demand, present,
export, demand));
}
return ret;
}
/**
* Gets a message describing a goods loading.
*
* @param type The <code>GoodsType</code> the type of goods being loaded.
* @param amount The amount of goods actually loaded.
* @param present The amount of goods already at the location.
* @param export The amount of goods available to export.
* @param toLoad The amount of goods the unit should load according to
* the trade route orders.
* @return A summary of the load.
*/
private String getLoadGoodsMessage(GoodsType type,
int amount, int present,
int export, int toLoad) {
String key;
int more;
if (amount == 0) {
key = (present == 0)
// Loaded no %goods% from an empty warehouse.
? "tradeRoute.loadStopNone"
// Loaded no %goods% with %more% more retained...
: "tradeRoute.loadStopNoExport";
more = present;
} else if (toLoad < export) {
key = "tradeRoute.loadStopImport";
// Loaded %amount% %goods% lacking space for %more% more
more = export - toLoad;
} else if (present > export && toLoad > export) {
// Loaded %amount% %goods% with %more% more retained...
key = "tradeRoute.loadStopExport";
more = present - export;
} else {
// Loaded %amount% %goods%
key = "tradeRoute.loadStop";
more = -1; // not displayed
}
return Messages.message(StringTemplate.template(key)
.addAmount("%amount%", amount)
.addNamed("%goods%", type)
.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 lb A <code>LogBuilder</code> to update.
* @return True if something was unloaded.
*/
private boolean unloadUnitAtStop(Unit unit, LogBuilder lb) {
final TradeLocation trl = unit.getTradeLocation();
if (trl == null) return false;
final TradeRouteStop 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 : unit.getCompactGoodsList()) {
GoodsType type = goods.getType();
if (goodsTypesToLoad.contains(type)) continue; // Keep this cargo.
int present = goods.getAmount();
int toUnload = present;
int atStop = trl.getImportAmount(type, 0);
int amount = toUnload;
if (amount > atStop) {
StringTemplate locName = ((Location)trl).getLocationLabel();
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%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addStringTemplate("%colony%", locName)
.addAmount("%amount%", toUnload - atStop)
.addNamed("%goods%", goods);
if (!gui.confirm(unit.getTile(), template,
unit, "yes", "no")) amount = atStop;
break;
case ClientOptions.UNLOAD_OVERFLOW_RESPONSE_NEVER:
amount = atStop;
break;
case ClientOptions.UNLOAD_OVERFLOW_RESPONSE_ALWAYS:
break;
default:
logger.warning("Illegal UNLOAD_OVERFLOW_RESPONSE: "
+ Integer.toString(option));
break;
}
}
// Try to unload.
ret = (amount == 0) ? false : askUnloadGoods(type, amount, unit);
if (ret) {
lb.add(" ", getUnloadGoodsMessage(unit, type, amount,
present, atStop, toUnload));
}
}
return ret;
}
/**
* Gets a message describing a goods unloading.
*
* Normally just state that a certain amount of goods was
* unloaded. Make special mention if the actual unloaded amount
* was short (unloaded &lt; amount), or an overflow is happening
* (amount &gt; atStop) in which case distinguish dumping (amount
* == toUnload) from retaining on board).
*
* @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 requested to be unloaded.
* @param present The amount of goods originally on the unit.
* @param atStop The amount of goods space available at the stop.
* @param toUnload The amount of goods that should be unloaded according
* to the trade route orders.
* @return A summary of the unload.
*/
private String getUnloadGoodsMessage(Unit unit, GoodsType type,
int amount, int present,
int atStop, int toUnload) {
String key = null;
int onBoard = unit.getGoodsCount(type);
int unloaded = present - onBoard;
int more = 0;
if (unloaded < amount) {
// Tried to unload %amount% %goods%, but %more% was unloaded
key = "tradeRoute.unloadStopFail";
more = unloaded;
} else if (amount > atStop) {
if (amount == toUnload) {
// Unloaded %amount% %goods% and dumped %more%.
key = "tradeRoute.unloadStopImport";
more = toUnload - atStop;
} else {
// Unloaded %amount% %goods% with %more% more retained...
key = (amount == 0) ? "tradeRoute.unloadStopNoExport"
: "tradeRoute.unloadStopExport";
more = onBoard;
}
} else {
// Unloaded %amount% %goods%
key = "tradeRoute.unloadStop";
}
return Messages.message(StringTemplate.template(key)
.addAmount("%amount%", amount)
.addAmount("%more%", more)
.addNamed("%goods%", type));
}
// Routines from here on are mostly user commands. That is they
// are called directly as a result of keyboard, menu, mouse or
// panel/dialog actions. Some though are called indirectly after
// a call to the server routes information back through the
// InGameInputHandler. They should all be annotated as such to
// confirm where they can come from.
//
// User command all return a success/failure indication, except if
// the game is stopped. IGIH-initiated routines do not need to.
//
// Successfully executed commands should update the GUI.
/**
* Abandon a colony with no units.
*
* Called from ColonyPanel.closeColonyPanel
*
* @param colony The <code>Colony</code> to be abandoned.
* @return True if the colony was abandoned.
*/
public boolean abandonColony(Colony colony) {
final Player player = freeColClient.getMyPlayer();
if (!requireOurTurn() || colony == null
|| !player.owns(colony) || colony.getUnitCount() > 0)
return false;
// Proceed to abandon
final Tile tile = colony.getTile();
boolean ret = askServer().abandonColony(colony)
&& !tile.hasSettlement();
if (ret) {
player.invalidateCanSeeTiles();
updateGUI(null);
}
return ret;
}
/**
* Animate an attack.
*
* Called from IGIH.animateAttack.
*
* @param attacker The attacking <code>Unit</code>.
* @param defender The defending <code>Unit</code>.
* @param attackerTile The <code>Tile</code> the attack originates from.
* @param defenderTile The <code>Tile</code> the defence takes place on.
* @param success True if the attack succeeds.
*/
public void animateAttack(Unit attacker, Unit defender,
Tile attackerTile, Tile defenderTile,
boolean success) {
// Note: we used to focus the map on the unit even when
// animation is off as long as the center-active-unit option
// was set. However IR#115 requested that if animation is off
// that we display nothing so as to speed up the other player
// moves as much as possible.
if (freeColClient.getAnimationSpeed(attacker.getOwner()) > 0) {
gui.animateUnitAttack(attacker, defender,
attackerTile, defenderTile, success);
}
gui.refresh();
}
/**
* Animate a move.
*
* Called from IGIH.animateMove.
*
* @param unit The <code>Unit</code> that moves.
* @param oldTile The <code>Tile</code> the move begins at.
* @param newTile The <code>Tile</code> the move ends at.
*/
public void animateMove(Unit unit, Tile oldTile, Tile newTile) {
// Note: we used to focus the map on the unit even when
// animation is off as long as the center-active-unit option
// was set. However IR#115 requested that if animation is off
// that we display nothing so as to speed up the other player
// moves as much as possible.
if (freeColClient.getAnimationSpeed(unit.getOwner()) > 0) {
gui.animateUnitMove(unit, oldTile, newTile);
} else if (freeColClient.getMyPlayer().owns(unit)) {
gui.requireFocus(newTile);
}
gui.refresh();
}
/**
* Assigns a student to a teacher.
*
* Called from UnitLabel
*
* @param student The student <code>Unit</code>.
* @param teacher The teacher <code>Unit</code>.
* @return True if the student was assigned.
*/
public boolean assignTeacher(Unit student, Unit teacher) {
final Player player = freeColClient.getMyPlayer();
if (!requireOurTurn()
|| student == null
|| !player.owns(student)
|| student.getColony() == null
|| !student.isInColony()
|| teacher == null
|| !player.owns(teacher)
|| !student.canBeStudent(teacher)
|| teacher.getColony() == null
|| student.getColony() != teacher.getColony()
|| !teacher.getColony().canTrain(teacher))
return false;
UnitWas unitWas = new UnitWas(student);
boolean ret = askServer().assignTeacher(student, teacher)
&& student.getTeacher() == teacher;
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Assigns a trade route to a unit.
*
* Called from EuropePanel.DestinationPanel, TradeRoutePanel(),
* TradeRoutePanel.newRoute
*
* @param unit The <code>Unit</code> to assign a trade route to.
* @param tradeRoute The <code>TradeRoute</code> to assign.
* @return True if the route was successfully assigned.
*/
public boolean assignTradeRoute(Unit unit, TradeRoute tradeRoute) {
if (unit == null) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askAssignTradeRoute(unit, tradeRoute);
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Boards a specified unit onto a carrier.
* The carrier must be at the same location as the boarding unit.
*
* Called from CargoPanel, TilePopup.
*
* @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() || unit == null || unit.isCarrier()
|| carrier == null || !carrier.canCarryUnits()
|| !unit.isAtLocation(carrier.getLocation())) return false;
boolean ret = askEmbark(unit, carrier);
if (ret) {
updateGUI(null);
}
return ret;
}
/**
* Use the active unit to build a colony.
*
* Called from BuildColonyAction.
*
* @param unit The <code>Unit</code> to build the colony.
* @return True if a colony was built.
*/
public boolean buildColony(Unit unit) {
if (!requireOurTurn() || unit == null) return false;
// Check unit, which must be on the map and able to build.
if (unit == null) return false;
final Tile tile = unit.getTile();
if (tile == null) return false;
if (!unit.canBuildColony()) {
gui.showInformationMessage(unit, StringTemplate
.template("buildColony.badUnit")
.addName("%unit%", unit.getName()));
return false;
}
// Join existing colony if present
final Colony colony = tile.getColony();
if (colony != null) {
askServer().joinColony(unit, colony);
updateGUI(null);
colonyPanel(colony, unit);
return false;
}
// Check for other impediments.
final Player player = freeColClient.getMyPlayer();
NoClaimReason reason = player.canClaimToFoundSettlementReason(tile);
switch (reason) {
case NONE:
case NATIVES: // Tile can still be claimed
break;
default:
gui.showInformationMessage(reason.getDescriptionKey());
return false;
}
// Show the warnings if applicable.
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_COLONY_WARNINGS)) {
StringTemplate warnings = tile.getBuildColonyWarnings(unit);
if (!warnings.getReplacements().isEmpty()
&& !gui.confirm(tile, warnings,
unit, "buildColony.yes", "buildColony.no")) {
return false;
}
}
// Get and check the name.
String name = gui.getNewColonyName(player, tile);
if (name == null) return false;
// 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.
UnitWas unitWas = new UnitWas(unit);
boolean ret = player.owns(tile);
if (!ret) {
ret = askClaimTile(player, tile, unit, player.getLandPrice(tile));
if (!ret) NameCache.putSettlementName(player, name);
}
if (ret) {
ret = askServer().buildColony(name, unit)
&& tile.hasSettlement();
if (ret) {
sound("sound.event.buildingComplete");
player.invalidateCanSeeTiles();
unitWas.fireChanges();
// Check units present for treasure cash-in as they are now
// at a colony.
for (Unit u : tile.getUnitList()) checkCashInTreasureTrain(u);
colonyPanel((Colony)tile.getSettlement(), unit);
}
updateGUI(null);
}
return ret;
}
/**
* Buy goods in Europe.
* The amount of goods is adjusted to the space in the carrier.
*
* Called from CargoPanel, TilePopup, loadCargo()
*
* @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() || type == null || amount <= 0
|| carrier == null
|| !carrier.isInEurope()
|| !freeColClient.getMyPlayer().owns(carrier)) return false;
final Europe europe = carrier.getOwner().getEurope();
EuropeWas europeWas = new EuropeWas(europe);
UnitWas unitWas = new UnitWas(carrier);
boolean ret = askLoadGoods(europe, type, amount, carrier);
if (ret) {
sound("sound.event.loadCargo");
europeWas.fireChanges();
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Chat with another player.
*
* Called from IGIH.chat.
*
* @param player The <code>Player</code> to chat with.
* @param message What to say.
* @param pri If true, the message is private.
*/
public void chat(Player player, String message, boolean pri) {
gui.displayChatMessage(player, message, pri);
}
/**
* Changes the state of this <code>Unit</code>.
*
* Called from FortifyAction, SentryAction, TilePopup, UnitLabel
*
* @param unit The <code>Unit</code>
* @param state The state of the unit.
* @return True if the state was changed.
*/
public boolean changeState(Unit unit, UnitState state) {
if (!requireOurTurn() || unit == null) return false;
if (unit.getState() == state) return true;
if (!unit.checkSetState(state)) return false;
// Check if this is a hostile fortification, and give the player
// a chance to confirm.
final 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
&& !gui.confirmHostileAction(unit, tile))
return false; // Aborted
}
}
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().changeState(unit, state)
&& unit.getState() == state;
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Changes the work type of this <code>Unit</code>.
*
* Called from ImprovementAction.
*
* @param unit The <code>Unit</code>
* @param improvementType a <code>TileImprovementType</code> value
* @return True if the improvement was changed.
*/
public boolean changeWorkImprovementType(Unit unit,
TileImprovementType improvementType) {
if (!requireOurTurn() || unit == null || improvementType == null
|| !unit.hasTile()
|| !unit.checkSetState(UnitState.IMPROVING)
|| improvementType.isNatural()) return false;
// May need to claim the tile first
final Player player = freeColClient.getMyPlayer();
final Tile tile = unit.getTile();
UnitWas unitWas = new UnitWas(unit);
boolean ret = player.owns(tile)
|| askClaimTile(player, tile, unit, player.getLandPrice(tile));
if (ret) {
ret = askServer()
.changeWorkImprovementType(unit, improvementType)
&& unit.getWorkImprovement() != null
&& unit.getWorkImprovement().getType() == improvementType;
if (ret) {
unitWas.fireChanges();
}
updateGUI(null);
}
return ret;
}
/**
* Changes the work type of this <code>Unit</code>.
*
* Called from ColonyPanel.tryWork, UnitLabel
*
* @param unit The <code>Unit</code>
* @param workType The new <code>GoodsType</code> to produce.
* @return True if the work type was changed.
*/
public boolean changeWorkType(Unit unit, GoodsType workType) {
if (!requireOurTurn() || unit == null) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().changeWorkType(unit, workType)
&& unit.getWorkType() == workType;
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* 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}.
*
* Called from TilePopup
*
* @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 (!requireOurTurn() || unit == null
|| !unit.canCarryTreasure() || !unit.canCashInTreasureTrain())
return false; // Fail quickly if just not a candidate.
final Tile tile = unit.getTile();
final Europe europe = unit.getOwner().getEurope();
if (europe == null || unit.isInEurope()) {
;// 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(GameOptions.TREASURE_TRANSPORT_FEE);
template = StringTemplate.template("cashInTreasureTrain.pay")
.addAmount("%fee%", percent);
}
if (!gui.confirm(unit.getTile(), template, unit,
"accept", "reject")) return false;
}
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().cashInTreasureTrain(unit)
&& unit.isDisposed();
if (ret) {
sound("sound.event.cashInTreasureTrain");
unitWas.fireChanges();
updateGUI(tile);
}
return ret;
}
/**
* Choose a founding father from an offered list.
*
* Called from GUI.showChooseFoundingFatherDialog
*
* @param ffs A list of <code>FoundingFather</code>s to choose from.
* @param ff The chosen <code>FoundingFather</code> (may be null).
* @return True if a father was chosen.
*/
public boolean chooseFoundingFather(List<FoundingFather> ffs,
FoundingFather ff) {
if (ffs == null) return false;
Player player = freeColClient.getMyPlayer();
player.setCurrentFather(ff);
return askServer().chooseFoundingFather(ffs, ff);
}
/**
* Choose a founding father from an offered list.
*
* Called from IGIH.chooseFoundingFather.
*
* @param ffs A list of <code>FoundingFather</code>s to choose from.
*/
public void chooseFoundingFather(List<FoundingFather> ffs) {
if (ffs == null) return;
gui.showChooseFoundingFatherDialog(ffs,
(FoundingFather ff) -> chooseFoundingFather(ffs, ff));
}
/**
* Claim a tile.
*
* Called from ColonyPanel.ASingleTilePanel, UnitLabel and work()
*
* @param tile The <code>Tile</code> to claim.
* @param claimant The <code>Unit</code> or <code>Colony</code> claiming.
* @return True if the claim succeeded.
*/
public boolean claimTile(Tile tile, FreeColGameObject claimant) {
if (!requireOurTurn() || tile == null
|| claimant == null) return false;
final Player player = freeColClient.getMyPlayer();
final int price = ((claimant instanceof Settlement)
? player.canClaimForSettlement(tile)
: player.canClaimForImprovement(tile))
? 0
: player.getLandPrice(tile);
UnitWas unitWas = (claimant instanceof Unit)
? new UnitWas((Unit)claimant) : null;
boolean ret = askClaimTile(player, tile, claimant, price);
if (ret) {
if (unitWas != null) unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Clears the goto orders of the given unit by setting its destination
* to null.
*
* Called from CanvasMouseListener
*
* @param unit The <code>Unit</code> to clear the destination for.
* @return True if the unit has no destination.
*/
public boolean clearGotoOrders(Unit unit) {
if (!requireOurTurn() || unit == null) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askClearGotoOrders(unit);
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Clears the orders of the given unit.
* Make the unit active and set a null destination and trade route.
*
* Called from ClearOrdersAction, TilePopup, TradeRoutePanel, UnitLabel
*
* @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) return false;
if (unit.getState() == UnitState.IMPROVING
&& !gui.confirm(unit.getTile(), StringTemplate
.template("clearOrders.text")
.addAmount("%turns%", unit.getWorkTurnsLeft()),
unit, "ok", "cancel")) {
return false;
}
UnitWas unitWas = new UnitWas(unit);
boolean ret = askClearGotoOrders(unit)
&& (unit.getState() == UnitState.ACTIVE
|| askServer().changeState(unit, UnitState.ACTIVE));
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Clear the speciality of a Unit, making it a Free Colonist.
*
* Called from UnitLabel
*
* @param unit The <code>Unit</code> to clear the speciality of.
* @return True if the speciality was cleared.
*/
public boolean clearSpeciality(Unit unit) {
if (!requireOurTurn() || unit == null) return false;
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%",
unit.getLabel(Unit.UnitLabelType.NATIONAL)));
return false;
}
final Tile tile = (gui.isShowingSubPanel()) ? null : unit.getTile();
if (!gui.confirm(tile, StringTemplate
.template("clearSpeciality.areYouSure")
.addStringTemplate("%oldUnit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL))
.addNamed("%unit%", newType),
unit, "ok", "cancel")) {
return false;
}
// Try to clear.
// Note that this routine is only called out of UnitLabel,
// where the unit icon is always updated anyway.
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().clearSpeciality(unit)
&& unit.getType() == newType;
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Close any open GUI menus.
*
* Called from IGIH.closeMenus.
*/
public void closeMenus() {
gui.closeMenus();
}
/**
* Declares independence for the home country.
*
* Called from DeclareIndependenceAction
*
* @return True if independence was declared.
*/
public boolean declareIndependence() {
if (!requireOurTurn()) return false;
final Player player = freeColClient.getMyPlayer();
if (player.getNewLandName() == null) {
return false; // Can only happen in debug mode.
}
// Check for adequate support.
StringTemplate declare = player.checkDeclareIndependence();
if (declare != null) {
gui.showInformationMessage(declare);
return false;
}
// Confirm intention, and collect nation+country names.
List<String> names = gui.confirmDeclaration();
if (names == null
|| names.get(0) == null || names.get(0).isEmpty()
|| names.get(1) == null || names.get(1).isEmpty()) {
// Empty name => user cancelled.
return false;
}
// Ask server.
boolean ret = askServer().declareIndependence(names.get(0), names.get(1))
&& player.isRebel();
if (ret) {
gui.showDeclarationPanel();
updateGUI(null);
}
return ret;
}
/**
* Handle a diplomatic offer.
*
* Called from IGIH.diplomacy
*
* @param our Our <code>FreeColGameObject</code> that is negotiating.
* @param other The other <code>FreeColGameObject</code>.
* @param agreement The <code>DiplomaticTrade</code> agreement.
* @return A counter agreement, a rejected agreement, or null if
* the original agreement was already decided.
*/
public DiplomaticTrade diplomacy(FreeColGameObject our,
FreeColGameObject other,
DiplomaticTrade agreement) {
final Player player = freeColClient.getMyPlayer();
final Player otherPlayer = agreement.getOtherPlayer(player);
StringTemplate t, nation = otherPlayer.getNationLabel();
switch (agreement.getStatus()) {
case ACCEPT_TRADE:
boolean visibilityChange = false;
for (Colony c : agreement.getColoniesGivenBy(player)) {
player.removeSettlement(c);//-vis(player)
visibilityChange = true;
}
for (Unit u : agreement.getUnitsGivenBy(player)) {
player.removeUnit(u);//-vis(player)
visibilityChange = true;
}
if (visibilityChange) player.invalidateCanSeeTiles();//+vis(player)
ModelMessage mm
= new ModelMessage(ModelMessage.MessageType.FOREIGN_DIPLOMACY,
"diplomacy.offerAccepted", otherPlayer)
.addStringTemplate("%nation%", nation);
player.addModelMessage(mm);
updateGUI(null);
break;
case REJECT_TRADE:
t = StringTemplate.template("diplomacy.offerRejected")
.addStringTemplate("%nation%", nation);
gui.showInformationMessage(t);
break;
case PROPOSE_TRADE:
t = agreement.getReceiveMessage(otherPlayer);
DiplomaticTrade ourAgreement
= gui.showNegotiationDialog(our, other, agreement, t);
if (ourAgreement == null) {
agreement.setStatus(TradeStatus.REJECT_TRADE);
} else {
agreement = ourAgreement;
}
return agreement;
default:
logger.warning("Bogus trade status: " + agreement.getStatus());
break;
}
return null;
}
/**
* Disbands the active unit.
*
* Called from DisbandUnitAction.
*
* @param unit The <code>Unit</code> to disband.
* @return True if the unit was disbanded.
*/
public boolean disbandUnit(Unit unit) {
if (!requireOurTurn() || unit == null) return false;
if (unit.getColony() != null
&& !gui.confirmLeaveColony(unit)) return false;
final Tile tile = (gui.isShowingSubPanel()) ? null : unit.getTile();
if (!gui.confirm(tile, StringTemplate.key("disbandUnit.text"),
unit, "disbandUnit.yes", "cancel"))
return false;
// Try to disband
boolean ret = askServer().disbandUnit(unit) && unit.isDisposed();
if (ret) {
updateGUI(tile);
}
return ret;
}
/**
* Display the high scores.
*
* Called from IGIH.gameEnded, ReportHighScoresAction
*
* @param high A <code>Boolean</code> whose values indicates whether
* a new high score has been achieved, or no information if null.
* @return True, the high scores were displayed.
*/
public boolean displayHighScores(Boolean high) {
List<HighScore> scores = askServer().getHighScores();
gui.showHighScoresPanel((high == null) ? null
: (high) ? "highscores.yes" : "highscores.no",
scores);
return true;
}
/**
* Displays pending <code>ModelMessage</code>s.
*
* Called from IGIH.displayModelMessagesRunnable
*
* @param allMessages Display all messages or just the undisplayed ones.
* @return True if any messages were displayed.
*/
public boolean displayModelMessages(boolean allMessages) {
return displayModelMessages(allMessages, false);
}
/**
* Emigrate a unit from Europe.
*
* Called from GUI.showEmigrationDialog.
*
* @param player The <code>Player</code> that owns the unit.
* @param slot The slot to emigrate from, [0..RECRUIT_COUNT].
* @param n The number of remaining units known to be eligible to migrate.
* @param foY True if this migration is due to a fountain of youth event.
*/
public void emigrate(Player player, int slot, int n, boolean foY) {
if (player == null || !player.isColonial()
|| !MigrationType.validMigrantSlot(slot)) return;
if (askEmigrate(player.getEurope(), slot) != null) {
emigration(player, n, foY);
}
}
/**
* End the turn command.
*
* Called from EndTurnAction, GUI.showEndTurnDialog
*
* @param showDialog If false, suppress showing the end turn dialog.
* @return True if the turn was ended.
*/
public boolean endTurn(boolean showDialog) {
if (!requireOurTurn()) return false;
return doEndTurn(showDialog && freeColClient.getClientOptions()
.getBoolean(ClientOptions.SHOW_END_TURN_DIALOG));
}
/**
* Change the role-equipment a unit has.
*
* Called from DefaultTransferHandler, QuickActionMenu
*
* @param unit The <code>Unit</code>.
* @param role The <code>Role</code> to assume.
* @param roleCount The role count.
* @return True if the role is taken.
*/
public boolean equipUnitForRole(Unit unit, Role role, int roleCount) {
if (!requireOurTurn() || unit == null || role == null || 0 > roleCount
|| roleCount > role.getMaximumCount()) return false;
if (role == unit.getRole()
&& roleCount == unit.getRoleCount()) return true;
final Player player = freeColClient.getMyPlayer();
final Colony colony = unit.getColony();
ColonyWas colonyWas = (colony != null) ? new ColonyWas(colony) : null;
final Europe europe = player.getEurope();
EuropeWas europeWas = (europe != null) ? new EuropeWas(europe) : null;
final Market market = (europe != null) ? player.getMarket() : null;
MarketWas marketWas = (market != null) ? new MarketWas(player) : null;
int price = -1;
List<AbstractGoods> req = unit.getGoodsDifference(role, roleCount);
if (unit.isInEurope()) {
for (AbstractGoods ag : req) {
GoodsType goodsType = ag.getType();
if (!player.canTrade(goodsType) && !payArrears(goodsType)) {
return false; // payment failed
}
}
price = player.getEurope().priceGoods(req);
if (price < 0 || !player.checkGold(price)) return false;
} else if (colony != null) {
for (AbstractGoods ag : req) {
if (colony.getGoodsCount(ag.getType()) < ag.getAmount()) {
StringTemplate template = StringTemplate
.template("equipUnit.impossible")
.addName("%colony%", colony.getName())
.addNamed("%equipment%", ag.getType())
.addStringTemplate("%unit%",
unit.getLabel(Unit.UnitLabelType.NATIONAL));
gui.showInformationMessage(unit, template);
return false;
}
}
} else {
return false;
}
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().equipUnitForRole(unit, role, roleCount)
&& unit.getRole() == role;
if (ret) {
if (colonyWas != null) colonyWas.fireChanges();
if (europeWas != null) europeWas.fireChanges();
if (marketWas != null) marketWas.fireChanges(req);
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Display an error.
*
* Called from IGIH.error.
*
* @param messageId The i18n-keyname of the error message to display.
* @param message An alternative (possibly non-i18n) message to
* display if the resource specified by <code>messageId</code>
* is unavailable.
*/
public void error(String messageId, String message) {
gui.showErrorMessage(messageId, message);
}
/**
* Execute goto orders command.
*
* Called from ExecuteGotoOrdersAction.
*
* @return True if all goto orders have been performed and no units
* reached their destination and are free to move again.
*/
public boolean executeGotoOrders() {
if (!requireOurTurn()) return false;
return doExecuteGotoOrders();
}
/**
* A player makes first contact with a native player.
*
* Called from GUI.showFirstContactDialog
*
* @param player The <code>Player</code> making contact.
* @param other The native <code>Player</code> being contacted.
* @param tile An optional <code>Tile</code> to offer the player if
* they have made a first landing.
* @param result Whether the initial treaty was accepted.
* @return True if first contact occurs.
*/
public boolean firstContact(Player player, Player other, Tile tile,
boolean result) {
if (player == null || player == null || player == other
|| tile == null) return false;
boolean ret = askServer().firstContact(player, other, tile, result);
if (ret) {
updateGUI(null);
}
return ret;
}
/**
* A player makes first contact with a native player.
*
* Called from IGIH.firstContact.
*
* @param player The <code>Player</code> making contact.
* @param other The native <code>Player</code> being contacted.
* @param tile An optional <code>Tile</code> to offer the player if
* they have made a first landing.
* @param n The number of settlements claimed by the native player.
*/
public void firstContact(Player player, Player other, Tile tile, int n) {
gui.showFirstContactDialog(player, other, tile, n,
(Boolean b) -> firstContact(player, other, tile, b));
}
/**
* Handle a fountain of youth event.
*
* Called from IGIH.fountainOfYouth.
*
* @param n The number of migrants available for selection.
*/
public void fountainOfYouth(int n) {
Player player = freeColClient.getMyPlayer();
final boolean fountainOfYouth = true;
gui.showEmigrationDialog(player, fountainOfYouth,
(Integer value) -> { // Value is a valid slot
emigrate(player,
Europe.MigrationType.convertToMigrantSlot(value),
n-1, fountainOfYouth);
});
}
/**
* Get the nation summary for a player.
*
* Called from DiplomaticTradePanel, ReportForeignAffairsPanel,
* ReportIndianPanel
*
* @param player The <code>Player</code> to summarize.
* @return A summary of that nation, or null on error.
*/
public NationSummary getNationSummary(Player player) {
if (player == null) return null;
return askServer().getNationSummary(player);
}
/**
* Gets a new trade route for a player.
*
* Called from TradeRoutePanel.newRoute
*
* @param player The <code>Player</code> to get a new trade route for.
* @return A new <code>TradeRoute</code>.
*/
public TradeRoute getNewTradeRoute(Player player) {
if (player == null) return null;
final int n = player.getTradeRoutes().size();
return (askServer().getNewTradeRoute()
&& player.getTradeRoutes().size() == n + 1)
? player.getTradeRoutes().get(n)
: null;
}
/**
* Gathers information about the REF.
*
* Called from ReportNavalPanel, ReportMilitaryPanel
*
* @return a <code>List</code> value
*/
public List<AbstractUnit> getREFUnits() {
return (!requireOurTurn()) ? Collections.<AbstractUnit>emptyList()
: askServer().getREFUnits();
}
/**
* Retrieves the server statistics.
*
* Called from StatisticsPanel
*
* @return A <code>Map</code> containing the server statistics.
*/
public java.util.Map<String, String> getServerStatistics() {
return askServer().getStatistics();
}
/**
* Go to a tile.
*
* Called from CanvasMouseListener, TilePopup
*
* @param unit The <code>Unit</code> to move.
* @param tile The <code>Tile</code> to move to.
* @return True if the destination change was successful.
*/
public boolean goToTile(Unit unit, Tile tile) {
if (!requireOurTurn() || unit == null
|| !freeColClient.getMyPlayer().owns(unit)) return false;
if (!gui.confirmClearTradeRoute(unit)) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askSetDestination(unit, tile);
if (ret) {
moveToDestination(unit, null);
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Ignore this ModelMessage from now on until it is not generated
* in a turn.
*
* Called from ReportTurnPanel
*
* @param message a <code>ModelMessage</code> value
* @param flag whether to ignore the ModelMessage or not
* @return True, ignore message status changes can not fail.
*/
public boolean ignoreMessage(ModelMessage message, boolean flag) {
String key;
if (message == null
|| (key = message.getIgnoredMessageKey()) == null) return false;
if (flag) {
final Turn turn = freeColClient.getGame().getTurn();
if (!continueIgnoreMessage(key, turn)) {
startIgnoringMessage(key, turn);
}
} else {
stopIgnoringMessage(key);
}
return true;
}
/**
* Handle a native demand at a colony.
*
* Called from IGIH.indianDemand
*
* @param unit The native <code>Unit</code> making the demand.
* @param colony The <code>Colony</code> demanded of.
* @param type The <code>GoodsType</code> demanded (null means gold).
* @param amount The amount of goods/gold demanded.
* @return Whether the demand was accepted or not.
*/
public boolean indianDemand(Unit unit, Colony colony,
GoodsType type, int amount) {
if (unit == null || colony == null) return false;
final Player player = freeColClient.getMyPlayer();
final int opt = freeColClient.getClientOptions()
.getInteger(ClientOptions.INDIAN_DEMAND_RESPONSE);
boolean accepted;
ModelMessage m = null;
String nation = Messages.message(unit.getOwner().getNationLabel());
if (type == null) {
switch (opt) {
case ClientOptions.INDIAN_DEMAND_RESPONSE_ASK:
accepted = gui.confirm(colony.getTile(), StringTemplate
.template("indianDemand.gold.text")
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount),
unit, "accept", "indianDemand.gold.no");
break;
case ClientOptions.INDIAN_DEMAND_RESPONSE_ACCEPT:
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.gold.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount);
accepted = true;
break;
case ClientOptions.INDIAN_DEMAND_RESPONSE_REJECT:
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.gold.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount);
accepted = false;
break;
default:
throw new RuntimeException("Impossible option value.");
}
} else {
switch (opt) {
case ClientOptions.INDIAN_DEMAND_RESPONSE_ASK:
if (type.isFoodType()) {
accepted = gui.confirm(colony.getTile(),
StringTemplate.template("indianDemand.food.text")
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount),
unit, "indianDemand.food.yes", "indianDemand.food.no");
} else {
accepted = gui.confirm(colony.getTile(),
StringTemplate.template("indianDemand.other.text")
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount)
.addNamed("%goods%", type),
unit, "accept", "indianDemand.other.no");
}
break;
case ClientOptions.INDIAN_DEMAND_RESPONSE_ACCEPT:
if (type.isFoodType()) {
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.food.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount);
} else {
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.other.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount)
.addNamed("%goods%", type);
}
accepted = true;
break;
case ClientOptions.INDIAN_DEMAND_RESPONSE_REJECT:
if (type.isFoodType()) {
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.food.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount);
} else {
m = new ModelMessage(ModelMessage.MessageType.DEMANDS,
"indianDemand.other.text", colony, unit)
.addName("%nation%", nation)
.addName("%colony%", colony.getName())
.addAmount("%amount%", amount)
.addNamed("%goods%", type);
}
accepted = false;
break;
default:
throw new RuntimeException("Impossible option value.");
}
}
if (m != null) {
player.addModelMessage(m);
displayModelMessages(false);
}
return accepted;
}
/**
* Leave a ship. The ship must be in harbour.
*
* Called from CargoPanel, ColonyPanel, EuropePanel.unloadAction,
* UnitLabel
*
* @param unit The <code>Unit</code> which is to leave the ship.
* @return True if the unit left the ship.
*/
public boolean leaveShip(Unit unit) {
Unit carrier;
if (!requireOurTurn() || unit == null
|| (carrier = unit.getCarrier()) == null) return false;
// Proceed to disembark
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().disembark(unit)
&& unit.getLocation() != carrier;
if (ret) {
checkCashInTreasureTrain(unit);
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Loads a cargo onto a carrier.
*
* Called from CargoPanel, ColonyPanel, LoadAction, TilePopup.
*
* @param goods The <code>Goods</code> which are going aboard the carrier.
* @param carrier The <code>Unit</code> acting as carrier.
*/
public boolean loadCargo(Goods goods, Unit carrier) {
if (!requireOurTurn() || goods == null || goods.getAmount() <= 0
|| goods.getLocation() == null
|| carrier == null || !carrier.isCarrier()) return false;
if (goods.getLocation() instanceof Europe) {
return buyGoods(goods.getType(), goods.getAmount(), carrier);
}
UnitWas carrierWas = new UnitWas(carrier);
UnitWas sourceWas = null;
ColonyWas colonyWas = null;
if (goods.getLocation() instanceof Unit) {
Unit source = (Unit)goods.getLocation();
sourceWas = new UnitWas(source);
} else {
Colony colony = carrier.getColony();
if (colony == null) return false;
colonyWas = new ColonyWas(colony);
}
boolean ret = askLoadGoods(goods.getLocation(), goods.getType(),
goods.getAmount(), carrier);
if (ret) {
sound("sound.event.loadCargo");
if (colonyWas != null) colonyWas.fireChanges();
if (sourceWas != null) sourceWas.fireChanges();
carrierWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Opens a dialog where the user should specify the filename and
* loads the game.
*
* Called from OpenAction.
*
* Returns no status as this game is stopped.
*/
public void loadGame() {
File file = gui.showLoadSaveFileDialog();
if (file == null) return;
if (freeColClient.isInGame() && !gui.confirmStopGame()) return;
freeColClient.getConnectController().quitGame(true);
turnReportMessages.clear();
gui.setActiveUnit(null);
gui.removeInGameComponents();
FreeColDirectories.setSavegameFile(file.getPath());
freeColClient.getConnectController().startSavedGame(file, null);
}
/**
* Loot some cargo.
*
* Called from GUI.showCaptureGoodsDialog
*
* @param unit The <code>Unit</code> that is looting.
* @param goods A list of <code>Goods</code> to choose from.
* @param defenderId The identifier of the defender unit (may have sunk).
* @return True if looting occurs.
*/
public boolean lootCargo(Unit unit, List<Goods> goods, String defenderId) {
if (unit == null || goods == null || goods.isEmpty()
|| defenderId == null) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().loot(unit, defenderId, goods);
if (ret) {
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Loot some cargo.
*
* Called from IGIH.lootCargo.
*
* @param unit The <code>Unit</code> that is looting.
* @param goods A list of <code>Goods</code> to choose from.
* @param defenderId The identifier of the defender unit (may have sunk).
*/
public void loot(Unit unit, List<Goods> goods, String defenderId) {
gui.showCaptureGoodsDialog(unit, goods,
(List<Goods> gl) -> lootCargo(unit, gl, defenderId));
}
/**
* Accept or reject a monarch action.
*
* Called from GUI.showMonarchDialog
*
* @param action The <code>MonarchAction</code> performed.
* @param accept If true, accept the action.
* @return True if the monarch was answered.
*/
public boolean monarchAction(MonarchAction action, boolean accept) {
if (action == null) return false;
boolean ret = false;
switch (action) {
case RAISE_TAX_ACT: case RAISE_TAX_WAR:
case MONARCH_MERCENARIES: case HESSIAN_MERCENARIES:
ret = askServer().answerMonarch(action, accept);
break;
default:
break;
}
if (ret) {
updateGUI(null);
}
return ret;
}
/**
* Do a monarch interaction.
*
* Called from IGIH.monarchAction.
*
* @param action The <code>MonarchAction</code> to perform.
* @param template A <code>StringTemplate</code> describing the action.
* @param monarchKey A key for the monarch involved.
*/
public void monarch(MonarchAction action, StringTemplate template,
String monarchKey) {
gui.showMonarchDialog(action, template, monarchKey,
(Boolean b) -> monarchAction(action, b));
}
/**
* Moves the specified unit somewhere that requires crossing the
* high seas.
*
* Called from EuropePanel.DestinationPanel, TilePopup
*
* @param unit The <code>Unit</code> to be moved.
* @param destination The <code>Location</code> to be moved to.
* @return True if the unit can possibly move further.
*/
public boolean moveTo(Unit unit, Location destination) {
if (!requireOurTurn() || unit == null
|| destination == null) return false;
// Sanity check current state.
if (destination instanceof Europe) {
if (unit.isInEurope()) {
sound("sound.event.illegalMove");
return false;
}
} else if (destination instanceof Map) {
if (unit.hasTile() && unit.getTile().getMap() == destination) {
sound("sound.event.illegalMove");
return false;
}
} else if (destination instanceof Settlement) {
if (unit.hasTile()) {
sound("sound.event.illegalMove");
return false;
}
} else {
return false;
}
// Autoload?
boolean update = false;
if (freeColClient.getClientOptions()
.getBoolean(ClientOptions.AUTOLOAD_EMIGRANTS)
&& unit.isInEurope()) {
for (Unit u : unit.getOwner().getEurope().getUnitList()) {
if (!u.isNaval()
&& u.getState() == UnitState.SENTRY
&& unit.canAdd(u)) {
if (askEmbark(u, unit)) update = true;
}
}
}
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().moveTo(unit, destination);
if (ret) {
unitWas.fireChanges();
update = true;
}
if (update) updateGUI(null);
return ret;
}
/**
* Moves the active unit in a specified direction. This may result in an
* attack, move... action.
*
* Called from MoveAction, CornerMapControls
*
* @param unit The <code>Unit</code> to move.
* @param direction The <code>Direction</code> in which to move
* the active unit.
* @return True if the unit may move further.
*/
public boolean moveUnit(Unit unit, Direction direction) {
if (!requireOurTurn() || unit == null
|| direction == null || !unit.hasTile()) return false;
if (!askClearGotoOrders(unit)) return false;
final int unitCount = unit.getUnitCount(),
goodsCount = unit.getGoodsList().size();
final Tile oldTile = unit.getTile();
UnitWas unitWas = new UnitWas(unit);
ColonyWas colonyWas = (unit.getColony() == null) ? null
: new ColonyWas(unit.getColony());
unit.setState(UnitState.ACTIVE);
moveDirection(unit, direction, true);
boolean ret = unit.getTile() != oldTile
|| unitWas.fireChanges();
if (ret) {
if (colonyWas != null) colonyWas.fireChanges();
updateGUI(null);
if (!unit.couldMove() && unit.hasTile()) {
// Show colony panel if unit out of moves
Colony colony = unit.getTile().getColony();
if (colony != null) colonyPanel(colony, unit);
}
}
return ret;
}
/**
* Move the tile cursor.
*
* Called from MoveAction in terrain mode.
*
* @param direction The <code>Direction</code> to move the tile cursor.
* @return True if the tile cursor is moved.
*/
public boolean moveTileCursor(Direction direction) {
if (direction == null) return false;
final Tile tile = gui.getSelectedTile();
if (tile == null) return false;
final Tile newTile = tile.getNeighbourOrNull(direction);
if (newTile == null) return false;
gui.setSelectedTile(newTile);
return true;
}
/**
* A player names the New World.
*
* Called from GUI.showNameNewLandDialog
*
* @param unit The <code>Unit</code> that landed.
* @param name The name to use.
* @return True if the new land was named.
*/
public boolean nameNewLand(Unit unit, String name) {
if (unit == null || name == null) return false;
// Respond to the server.
if (!askServer().newLandName(unit, name)) return false;
// The name is set, bring up the first landing panel.
final Player player = unit.getOwner();
StringTemplate t = StringTemplate.template("event.firstLanding")
.addName("%name%", name);
gui.showEventPanel(Messages.message(t), "image.flavor.event.firstLanding",
null);
// Add tutorial message.
final String key = FreeColActionUI.getHumanKeyStrokeText(freeColClient
.getActionManager().getFreeColAction("buildColonyAction")
.getAccelerator());
player.addModelMessage(new ModelMessage(ModelMessage.MessageType.TUTORIAL,
"buildColony.tutorial", player)
.addName("%colonyKey%", key)
.add("%colonyMenuItem%", "buildColonyAction.name")
.add("%ordersMenuItem%", "menuBar.orders"));
displayModelMessages(false);
return true;
}
/**
* The player names a new region.
*
* Called from newRegionName, GUI.showNameNewRegionDialog
*
* @param tile The <code>Tile</code> within the region.
* @param unit The <code>Unit</code> that has discovered the region.
* @param region The <code>Region</code> to name.
* @param name The name to offer.
* @return True if the new region was named.
*/
public boolean nameNewRegion(final Tile tile, final Unit unit,
final Region region, final String name) {
if (tile == null || unit == null || region == null) return false;
return askServer().newRegionName(region, tile, unit, name);
}
/**
* Ask the player to name the new land.
*
* Called from IGIH.newLandName.
*
* @param defaultName The default name to use.
* @param unit The <code>Unit</code> that has landed.
*/
public void newLandName(String defaultName, Unit unit) {
gui.showNamingDialog(
StringTemplate.key("newLand.text"), defaultName, unit,
(String name) -> {
if (name == null || name.isEmpty()) name = defaultName;
nameNewLand(unit, name);
});
}
/**
* Ask the player to name a new region.
*
* Called from IGIH.newRegionName.
*
* @param region The <code>Region</code> to name.
* @param defaultName The default name to use.
* @param tile The <code>Tile</code> the unit landed at.
* @param unit The <code>Unit</code> that has landed.
*/
public void newRegionName(Region region, String defaultName, Tile tile,
Unit unit) {
if (region.hasName()) {
if (region.isPacific()) {
gui.showEventPanel(Messages.message("event.discoverPacific"),
"image.flavor.event.discoverPacific", null);
}
nameNewRegion(tile, unit, region, defaultName);
} else {
gui.showNamingDialog(
StringTemplate.template("nameRegion.text")
.addStringTemplate("%type%", region.getLabel()),
defaultName, unit,
(String name) -> {
if (name == null || name.isEmpty()) name = defaultName;
nameNewRegion(tile, unit, region, name);
});
}
}
/**
* Switch to a new turn.
*
* Called from IGIH.newTurn
*
* @param turn The turn number.
* @return True if the new turn occurs.
*/
public boolean newTurn(int turn) {
final Game game = freeColClient.getGame();
final Player player = freeColClient.getMyPlayer();
if (turn < 0) {
logger.warning("Bad turn in newTurn: " + turn);
return false;
}
Turn newTurn = new Turn(turn);
game.setTurn(newTurn);
logger.info("New turn: " + newTurn + "/" + turn);
final boolean alert = freeColClient.getClientOptions()
.getBoolean(ClientOptions.AUDIO_ALERTS);
if (alert) sound("sound.event.alertSound");
final Turn currTurn = game.getTurn();
if (currTurn.isFirstSeasonTurn()) {
player.addModelMessage(new ModelMessage(MessageType.WARNING,
"twoTurnsPerYear", player)
.addStringTemplate("%year%", currTurn.getLabel())
.addAmount("%amount%", currTurn.getSeasonNumber()));
}
return true;
}
/**
* Makes a new unit active.
*
* Called from PGC.startGame, ColonyPanel.closeColonyPanel
*
* @return True unless it was not our turn.
*/
public boolean nextActiveUnit() {
if (!requireOurTurn()) return false;
updateGUI(null);
return true;
}
/**
* Displays the next <code>ModelMessage</code>.
*
* Called from CC.reconnect, CargoPanel,
* ColonyPanel.closeColonyPanel, EuropePanel.exitAction,
* EuropePanel.MarketPanel
*
* @return True if any messages were displayed.
*/
public boolean nextModelMessage() {
return displayModelMessages(false, false);
}
/**
* Pays the tax arrears on this type of goods.
*
* Called from CargoPanel, EuropePanel.MarketPanel,
* EuropePanel.unloadAction, QuickActionMenu
*
* @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() || type == null) return false;
Player player = freeColClient.getMyPlayer();
int arrears = player.getArrears(type);
if (arrears <= 0) return false;
if (!player.checkGold(arrears)) {
gui.showInformationMessage(StringTemplate
.template("payArrears.noGold")
.addAmount("%amount%", arrears));
return false;
}
StringTemplate t = StringTemplate.template("payArrears.text")
.addAmount("%amount%", arrears);
if (!gui.confirm(null, t, type, "ok", "cancel")) return false;
boolean ret = askServer().payArrears(type) && player.canTrade(type);
if (ret) {
updateGUI(null);
}
return ret;
}
/**
* Buys the remaining hammers and tools for the {@link Building} currently
* being built in the given <code>Colony</code>.
*
* Called from BuildQueuePanel
*
* @param colony The <code>Colony</code> where the building should be
* bought.
* @return True if the building was bought.
*/
public boolean payForBuilding(Colony colony) {
if (!requireOurTurn() || colony == null) return false;
if (!getSpecification().getBoolean(GameOptions.PAY_FOR_BUILDING)) {
gui.showInformationMessage("payForBuilding.disabled");
return false;
}
if (!colony.canPayToFinishBuilding()) {
gui.showInformationMessage("info.notEnoughGold");
return false;
}
final int price = colony.getPriceForBuilding();
StringTemplate t = StringTemplate.template("payForBuilding.text")
.addAmount("%amount%", price);
if (!gui.confirm(null, t, colony, "yes", "no")) return false;
ColonyWas colonyWas = new ColonyWas(colony);
boolean ret = askServer().payForBuilding(colony)
&& colony.getPriceForBuilding() == 0;
if (ret) {
colonyWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Puts the specified unit outside the colony.
*
* Called from ColonyPanel.OutsideColonyPanel, UnitLabel
*
* @param unit The <code>Unit</code>
* @return True if the unit was successfully put outside the colony.
*/
public boolean putOutsideColony(Unit unit) {
Colony colony;
if (!requireOurTurn() || unit == null
|| (colony = unit.getColony()) == null) return false;
if (!gui.confirmLeaveColony(unit)) return false;
ColonyWas colonyWas = new ColonyWas(colony);
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().putOutsideColony(unit)
&& unit.getLocation() == colony.getTile();
if (ret) {
colonyWas.fireChanges();
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Query whether the user wants to reconnect?
*
* Called from ReconnectAction, IGIH.reconnectRunnable
*
* Returns no status, this game is going away.
*/
public void reconnect() {
if (gui.confirm("reconnect.text", "reconnect.no", "reconnect.yes")) {
logger.finest("Reconnect quit.");
freeColClient.quit();
} else {
logger.finest("Reconnect accepted.");
freeColClient.getConnectController().reconnect();
}
}
/**
* Recruit a unit from a specified index in Europe.
*
* Called from RecruitPanel
*
* @param index The index in Europe to recruit from, [0..RECRUIT_COUNT).
* @return True if a unit was recruited.
*/
public boolean recruitUnitInEurope(int index) {
if (!requireOurTurn()
|| !MigrationType.validMigrantIndex(index)) return false;
final Player player = freeColClient.getMyPlayer();
if (!player.isColonial()) return false;
if (!player.checkGold(player.getRecruitPrice())) {
gui.showInformationMessage("info.notEnoughGold");
return false;
}
Unit newUnit = askEmigrate(player.getEurope(),
MigrationType.migrantIndexToSlot(index));
if (newUnit != null) {
player.setNextActiveUnit(newUnit);
gui.setActiveUnit(newUnit);
updateGUI(null);
}
return newUnit != null;
}
/**
* Remove game objects.
*
* Called from IGIH.remove().
*
* @param objects A list of <code>FreeColGameObject</code>s to remove.
*/
public void remove(List<FreeColGameObject> objects,
FreeColGameObject divert) {
final Player player = freeColClient.getMyPlayer();
boolean visibilityChange = false;
for (FreeColGameObject fcgo : objects) {
if (divert != null) player.divertModelMessages(fcgo, divert);
if (fcgo instanceof Settlement) {
Settlement settlement = (Settlement)fcgo;
if (settlement != null && settlement.getOwner() != null) {
settlement.getOwner().removeSettlement(settlement);
}
visibilityChange = true;//-vis(player)
} else if (fcgo instanceof Unit) {
// Deselect the object if it is the current active unit.
Unit u = (Unit)fcgo;
if (u == gui.getActiveUnit()) gui.setActiveUnit(null);
// Temporary hack until we have real containers.
if (u != null && u.getOwner() != null) {
u.getOwner().removeUnit(u);
}
visibilityChange = true;//-vis(player)
}
// Do just the low level dispose that removes
// reference to this object in the client. The other
// updates should have done the rest.
fcgo.disposeResources();
}
if (visibilityChange) player.invalidateCanSeeTiles();//+vis(player)
gui.refresh();
}
/**
* Renames a <code>Nameable</code>.
*
* Apparently this can be done while it is not your turn.
*
* Called from RenameAction, TilePopup.
*
* @param object The object to rename.
* @return True if the object was renamed.
*/
public boolean rename(Nameable object) {
final Player player = freeColClient.getMyPlayer();
if (!(object instanceof Ownable)
|| !player.owns((Ownable)object)) return false;
String name = null;
if (object instanceof Colony) {
Colony colony = (Colony) object;
name = gui.getInput(colony.getTile(),
StringTemplate.key("renameColony.text"),
colony.getName(), "rename", "cancel");
if (name == null) { // User cancelled
return false;
} else if (name.isEmpty()) { // Zero length invalid
gui.showInformationMessage("info.enterSomeText");
return false;
} else if (colony.getName().equals(name)) { // No change
return false;
} else if (player.getSettlementByName(name) != null) {
// Colony name must be unique.
gui.showInformationMessage((Colony)object, StringTemplate
.template("nameColony.notUnique")
.addName("%name%", name));
return false;
}
} else if (object instanceof Unit) {
Unit unit = (Unit) object;
name = gui.getInput(unit.getTile(),
StringTemplate.key("renameUnit.text"),
unit.getName(), "rename", "cancel");
if (name == null) return false; // User cancelled
} else {
logger.warning("Tried to rename an unsupported Nameable: "
+ object);
return false;
}
return askServer().rename((FreeColGameObject)object, name);
}
/**
* Opens a dialog where the user should specify the filename and
* saves the game.
*
* Called from SaveAction and SaveAndQuitAction.
*
* @return True if the game was saved.
*/
public boolean saveGame() {
if (!freeColClient.canSaveCurrentGame()) return false;
final Game game = freeColClient.getGame();
if (game == null) return false; // Keyboard handling can race init
String fileName = getSaveGameString(game);
File file = gui.showSaveDialog(FreeColDirectories.getSaveDirectory(),
fileName);
if (file == null) return false;
final boolean confirm = freeColClient.getClientOptions()
.getBoolean(ClientOptions.CONFIRM_SAVE_OVERWRITE);
if (!confirm
|| !file.exists()
|| gui.confirm("saveConfirmationDialog.areYouSure.text",
"ok", "cancel")) {
FreeColDirectories.setSavegameFile(file.getPath());
return saveGame(file);
}
return false;
}
/**
* Selects a destination for this unit. Europe and the player's
* colonies are valid destinations.
*
* Called from GotoAction.
*
* @param unit The unit for which to select a destination.
* @return True if the destination change succeeds.
*/
public boolean selectDestination(Unit unit) {
if (!requireOurTurn() || unit == null) return false;
if (!gui.confirmClearTradeRoute(unit)) return false;
Location destination = gui.showSelectDestinationDialog(unit);
if (destination == null) return false;
UnitWas unitWas = new UnitWas(unit);
boolean ret = askSetDestination(unit, destination);
if (ret) {
if (destination instanceof Europe) {
if (unit.hasTile()
&& unit.getTile().isDirectlyHighSeasConnected()) {
moveTo(unit, destination);
} else {
moveToDestination(unit, null);
}
} else {
if (unit.isInEurope()) {
moveTo(unit, destination);
} else {
moveToDestination(unit, null);
}
}
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Sells goods in Europe.
*
* Called from EuropePanel.MarketPanel, EuropePanel.unloadAction,
* unload(), unloadCargo()
*
* @param goods The goods to be sold.
* @return True if the sale succeeds.
*/
public boolean sellGoods(Goods goods) {
if (!requireOurTurn() || goods == null
|| !(goods.getLocation() instanceof Unit)) return false;
final Player player = freeColClient.getMyPlayer();
Unit carrier = (Unit)goods.getLocation();
Europe europe = player.getEurope();
EuropeWas europeWas = new EuropeWas(europe);
UnitWas unitWas = new UnitWas(carrier);
boolean ret = askUnloadGoods(goods.getType(), goods.getAmount(), carrier);
if (ret) {
sound("sound.event.sellCargo");
europeWas.fireChanges();
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Sends a public chat message.
*
* Called from ChatPanel
*
* @param chat The text of the message.
* @return True if the message was sent.
*/
public boolean sendChat(String chat) {
if (chat == null) return false;
return askServer().chat(freeColClient.getMyPlayer(), chat);
}
/**
* Changes the current construction project of a <code>Colony</code>.
*
* Called from BuildQueuePanel
*
* @param colony The <code>Colony</code>
* @param buildQueue List of <code>BuildableType</code>
* @return True if the build queue was changed.
*/
public boolean setBuildQueue(Colony colony,
List<BuildableType> buildQueue) {
if (!requireOurTurn() || colony == null
|| buildQueue == null) return false;
ColonyWas colonyWas = new ColonyWas(colony);
boolean ret = askServer().setBuildQueue(colony, buildQueue);
if (ret) {
colonyWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Set a player to be the new current player.
*
* Called from IGIH.newTurn, IGIH.setCurrentPlayer, CC.login
*
* @param player The <code>Player</code> to be the new current player.
* @return True if the current player changes.
*/
public boolean setCurrentPlayer(Player player) {
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)
&& freeColClient.currentPlayerIsMyPlayer()) {
gui.closeMenus();
}
FreeColDebugger.finishDebugRun(freeColClient, false);
final Game game = freeColClient.getGame();
game.setCurrentPlayer(player);
if (freeColClient.getMyPlayer().equals(player)) {
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.DESYNC)
&& DebugUtils.checkDesyncAction(freeColClient)) {
freeColClient.getConnectController().reconnect();
return false;
}
// Save the game (if it isn't newly loaded)
if (freeColClient.getFreeColServer() != null
&& game.getTurn().getNumber() > 0) autoSaveGame();
// Get turn report out quickly before more message display occurs.
player.removeDisplayedModelMessages();
displayModelMessages(true, true);
player.invalidateCanSeeTiles();
// Check for emigration.
Europe europe = player.getEurope();
if (player.hasAbility(Ability.SELECT_RECRUIT)) {
emigration(player, 0, false);
} else {
while (player.checkEmigrate()) {
askEmigrate(europe,
Europe.MigrationType.getUnspecificSlot());
}
}
// Wake up human!
if (!freeColClient.isSinglePlayer()) {
sound("sound.anthem." + player.getNationId());
}
player.resetIterators();
updateGUI(player.getFallbackTile());
}
return true;
}
/**
* Set a player to be dead.
*
* Called from IGIH.setDead
*
* @param dead The dead <code>Player</code>.
* @return True if the player is marked as dead.
*/
public boolean setDead(Player dead) {
if (dead == null) return false;
final Player player = freeColClient.getMyPlayer();
if (player == dead) {
FreeColDebugger.finishDebugRun(freeColClient, true);
if (freeColClient.isSinglePlayer()) {
if (player.getPlayerType() == Player.PlayerType.RETIRED) {
; // Do nothing, retire routine will quit
} else if (player.getPlayerType() != Player.PlayerType.UNDEAD
&& gui.confirm("defeatedSinglePlayer.text",
"defeatedSinglePlayer.yes", "quit")) {
freeColClient.askServer().enterRevengeMode();
} else {
freeColClient.quit();
}
} else {
if (!gui.confirm("defeated.text", "defeated.yes",
"quit")) freeColClient.quit();
}
} else {
player.setStance(dead, null);
}
return true;
}
/**
* Informs this controller that a game has been newly loaded.
*
* Called from ConnectController.startSavedGame
*
* No status returned to connect controller.
*/
public void setGameConnected () {
final Player player = freeColClient.getMyPlayer();
if (player != null) {
player.refilterModelMessages(freeColClient.getClientOptions());
}
}
/**
* Sets the export settings of the custom house.
*
* Called from WarehouseDialog
*
* @param colony The colony with the custom house.
* @param goodsType The goods for which to set the settings.
* @return True if the levels were set.
*/
public boolean setGoodsLevels(Colony colony, GoodsType goodsType) {
if (colony == null || goodsType == null) return false;
return askServer().setGoodsLevels(colony,
colony.getExportData(goodsType));
}
/**
* Sets the debug mode to include the extra menu commands.
*
* Called from DebugAction
*
* @return True, always succeeds.
*/
public boolean setInDebugMode() {
FreeColDebugger.enableDebugMode(FreeColDebugger.DebugMode.MENUS);
updateGUI(null);
return true;
}
/**
* Notify the player that the stance between two players has changed.
*
* Called from IGIH.setStance
*
* @param stance The changed <code>Stance</code>.
* @param first The first <code>Player</code>.
* @param second The second <code>Player</code>.
* @return True if the stance change succeeds.
*/
public boolean setStance(Stance stance, Player first, Player second) {
if (stance == null || first == null || second == null) return false;
final Player player = freeColClient.getMyPlayer();
Stance old = first.getStance(second);
try {
first.setStance(second, stance);
} catch (IllegalStateException e) {
logger.log(Level.WARNING, "Illegal stance transition", e);
return false;
}
if (player == first && old == Stance.UNCONTACTED) {
sound("sound.event.meet." + second.getNationId());
}
return true;
}
/**
* Sets the trade routes for this player
*
* Called from TradeRoutePanel.deleteTradeRoute
*
* @param routes The trade routes to set.
* @return True if the trade routes were set.
*/
public boolean setTradeRoutes(List<TradeRoute> routes) {
if (routes == null) return false;
return askServer().setTradeRoutes(routes);
}
/**
* Spy on a colony.
*
* Called from IGIH.spyResult.
*
* @param tile The <code>Tile</code> to find the colony on.
* @param recover A <code>Runnable</code> to restore the normal
* player view of the tile when the spying colony panel is closed.
*/
public void spyColony(Tile tile, Runnable recover) {
gui.showSpyColonyPanel(tile, recover);
}
/**
* Trains a unit of a specified type in Europe.
*
* Called from NewUnitPanel
*
* @param unitType The type of unit to be trained.
* @return True if a new unit was trained.
*/
public boolean trainUnitInEurope(UnitType unitType) {
if (!requireOurTurn() || unitType == null) return false;
final Player player = freeColClient.getMyPlayer();
final Europe europe = player.getEurope();
if (!player.checkGold(europe.getUnitPrice(unitType))) {
gui.showInformationMessage("info.notEnoughGold");
return false;
}
EuropeWas europeWas = new EuropeWas(europe);
Unit newUnit = null;
boolean ret = askServer().trainUnitInEurope(unitType)
&& (newUnit = europeWas.getNewUnit()) != null;
if (ret) {
europeWas.fireChanges();
player.setNextActiveUnit(newUnit);
gui.setActiveUnit(newUnit);
updateGUI(null);
}
return ret;
}
/**
* Unload, including dumping cargo.
*
* Called from UnloadAction, UnitLabel
*
* @param unit The <code>Unit</code> that is dumping.
* @return True if the unit unloaded.
*/
public boolean unload(Unit unit) {
if (!requireOurTurn() || unit == null
|| !unit.isCarrier()) return false;
boolean ret = true;
Colony colony = unit.getColony();
if (colony != null) { // In colony, unload units and goods.
for (Unit u : unit.getUnitList()) {
ret = leaveShip(u) && ret;
}
for (Goods goods : unit.getGoodsList()) {
ret = unloadCargo(goods, false) && ret;
}
} else if (unit.isInEurope()) { // In Europe, unload non-boycotted goods
Player player = freeColClient.getMyPlayer();
for (Goods goods : unit.getCompactGoodsList()) {
if (player.canTrade(goods.getType())) {
ret = sellGoods(goods) && ret;
}
}
if (unit.hasGoodsCargo()) { // Goods left here must be dumped.
gui.showDumpCargoDialog(unit,
(List<Goods> goodsList) -> {
for (Goods g : goodsList) unloadCargo(g, true);
});
return false;
}
} else { // Dump goods, units dislike jumping overboard
for (Goods goods : unit.getGoodsList()) {
ret = unloadCargo(goods, false) && ret;
}
}
return ret;
}
/**
* 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.
*
* Called from CargoPanel, ColonyPanel, EuropePanel.MarketPanel,
* GUI.showDumpCargoDialog, QuickActionMenu, unload()
*
* @param goods The <code>Goods</code> to unload.
* @param dump If true, dump the goods.
* @return True if the unload succeeds.
*/
public boolean unloadCargo(Goods goods, boolean dump) {
if (!requireOurTurn() || goods == null
|| goods.getAmount() <= 0
|| !(goods.getLocation() instanceof Unit)) return false;
// Find the carrier
final Unit carrier = (Unit)goods.getLocation();
// Use Europe-specific routine if needed
if (carrier.isInEurope()) return sellGoods(goods);
// Check for a colony
final Colony colony = carrier.getColony();
// Unload
ColonyWas colonyWas = (colony == null) ? null : new ColonyWas(colony);
UnitWas unitWas = new UnitWas(carrier);
boolean ret = askUnloadGoods(goods.getType(), goods.getAmount(), carrier);
if (ret) {
if (!dump) sound("sound.event.unloadCargo");
if (colonyWas != null) colonyWas.fireChanges();
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
/**
* Updates a trade route.
*
* Called from TradeRoutePanel(), TradeRoutePanel.newRoute
*
* @param route The trade route to update.
* @return True if the trade route was updated.
*/
public boolean updateTradeRoute(TradeRoute route) {
if (route == null) return false;
return askServer().updateTradeRoute(route);
}
/**
* The player has won, show the high scores and victory dialog.
*
* Called from IGIH.gameEnded.
*
* @param score If "true", a new high score was reached.
*/
public void victory(String score) {
displayHighScores("true".equalsIgnoreCase(score));
gui.showVictoryDialog((Boolean result) -> victory(result));
}
/**
* The player has won!
*
* Called from GUI.showVictoryDialog
*
* @param quit If true, leave this game and start a new one.
* @return True.
*/
public boolean victory(Boolean quit) {
if (quit) {
freeColClient.newGame(false);
} else {
askServer().continuePlaying();
}
return true;
}
/**
* Tell a unit to wait.
*
* Called from WaitAction.
*
* @return True, this can not fail.
*/
public boolean waitUnit() {
if (!requireOurTurn()) return false;
// Defeat the normal check for whether the current unit can move.
gui.setActiveUnit(null);
updateGUI(null);
return true;
}
/**
* Moves a <code>Unit</code> to a <code>WorkLocation</code>.
*
* Called from ColonyPanel.tryWork, UnitLabel
*
* @param unit The <code>Unit</code>.
* @param workLocation The new <code>WorkLocation</code>.
* @return True if the unit is now working at the new work location.
*/
public boolean work(Unit unit, WorkLocation workLocation) {
if (!requireOurTurn() || unit == null
|| workLocation == null) return false;
StringTemplate template;
if (unit.getStudent() != null
&& !gui.confirmAbandonEducation(unit, false)) return false;
Colony colony = workLocation.getColony();
if (workLocation instanceof ColonyTile) {
Tile tile = ((ColonyTile)workLocation).getWorkTile();
if (tile.hasLostCityRumour()) {
gui.showInformationMessage("tileHasRumour");
return false;
}
if (!unit.getOwner().owns(tile)) {
if (!claimTile(tile, colony)) return false;
}
}
// Try to change the work location.
ColonyWas colonyWas = new ColonyWas(colony);
UnitWas unitWas = new UnitWas(unit);
boolean ret = askServer().work(unit, workLocation)
&& unit.getLocation() == workLocation;
if (ret) {
colonyWas.fireChanges();
unitWas.fireChanges();
updateGUI(null);
}
return ret;
}
}