mirror of https://github.com/FreeCol/freecol.git
3969 lines
163 KiB
Java
3969 lines
163 KiB
Java
/**
|
|
* Copyright (C) 2002-2019 The FreeCol Team
|
|
*
|
|
* This file is part of FreeCol.
|
|
*
|
|
* FreeCol is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* FreeCol is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package net.sf.freecol.server.control;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map.Entry;
|
|
import java.util.Random;
|
|
import java.util.Set;
|
|
import java.util.function.Function;
|
|
import java.util.function.Predicate;
|
|
import java.util.logging.Level;
|
|
import java.util.logging.Logger;
|
|
import java.util.stream.Collectors;
|
|
|
|
import net.sf.freecol.FreeCol;
|
|
import net.sf.freecol.common.debug.FreeColDebugger;
|
|
import net.sf.freecol.common.i18n.Messages;
|
|
import net.sf.freecol.common.model.Ability;
|
|
import net.sf.freecol.common.model.AbstractGoods;
|
|
import net.sf.freecol.common.model.AbstractUnit;
|
|
import net.sf.freecol.common.model.BuildableType;
|
|
import net.sf.freecol.common.model.Colony;
|
|
import net.sf.freecol.common.model.CombatModel.CombatResult;
|
|
import net.sf.freecol.common.model.Constants.IndianDemandAction;
|
|
import net.sf.freecol.common.model.DiplomaticTrade;
|
|
import net.sf.freecol.common.model.DiplomaticTrade.TradeStatus;
|
|
import net.sf.freecol.common.model.Disaster;
|
|
import net.sf.freecol.common.model.Europe;
|
|
import net.sf.freecol.common.model.Europe.MigrationType;
|
|
import net.sf.freecol.common.model.ExportData;
|
|
import net.sf.freecol.common.model.FoundingFather;
|
|
import net.sf.freecol.common.model.Force;
|
|
import net.sf.freecol.common.model.FreeColGameObject;
|
|
import net.sf.freecol.common.model.FreeColObject;
|
|
import net.sf.freecol.common.model.Game;
|
|
import net.sf.freecol.common.model.Goods;
|
|
import net.sf.freecol.common.model.GoodsContainer;
|
|
import net.sf.freecol.common.model.GoodsLocation;
|
|
import net.sf.freecol.common.model.GoodsType;
|
|
import net.sf.freecol.common.model.HighScore;
|
|
import net.sf.freecol.common.model.HighSeas;
|
|
import net.sf.freecol.common.model.HistoryEvent;
|
|
import net.sf.freecol.common.model.IndianNationType;
|
|
import net.sf.freecol.common.model.IndianSettlement;
|
|
import net.sf.freecol.common.model.Location;
|
|
import net.sf.freecol.common.model.Map;
|
|
import net.sf.freecol.common.model.Market;
|
|
import net.sf.freecol.common.model.Market.Access;
|
|
import net.sf.freecol.common.model.ModelMessage;
|
|
import net.sf.freecol.common.model.ModelMessage.MessageType;
|
|
import net.sf.freecol.common.model.Monarch;
|
|
import net.sf.freecol.common.model.Monarch.MonarchAction;
|
|
import net.sf.freecol.common.model.Nameable;
|
|
import net.sf.freecol.common.model.Nation;
|
|
import net.sf.freecol.common.model.NationSummary;
|
|
import net.sf.freecol.common.model.NativeTrade;
|
|
import net.sf.freecol.common.model.NativeTradeItem;
|
|
import net.sf.freecol.common.model.NativeTrade.NativeTradeAction;
|
|
import net.sf.freecol.common.model.Ownable;
|
|
import net.sf.freecol.common.model.Player;
|
|
import net.sf.freecol.common.model.Player.PlayerType;
|
|
import net.sf.freecol.common.model.Stance;
|
|
import net.sf.freecol.common.model.RandomRange;
|
|
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.StringTemplate;
|
|
import net.sf.freecol.common.model.Tension;
|
|
import net.sf.freecol.common.model.Tile;
|
|
import net.sf.freecol.common.model.TileImprovement;
|
|
import net.sf.freecol.common.model.TileImprovementType;
|
|
import net.sf.freecol.common.model.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.UnitChangeType;
|
|
import net.sf.freecol.common.model.UnitTypeChange;
|
|
import net.sf.freecol.common.model.Unit.UnitState;
|
|
import net.sf.freecol.common.model.UnitLocation;
|
|
import net.sf.freecol.common.model.UnitType;
|
|
import net.sf.freecol.common.model.WorkLocation;
|
|
import net.sf.freecol.common.option.GameOptions;
|
|
import net.sf.freecol.common.networking.ChangeSet;
|
|
import net.sf.freecol.common.networking.ChangeSet.See;
|
|
import net.sf.freecol.common.networking.ChatMessage;
|
|
import net.sf.freecol.common.networking.DiplomacyMessage;
|
|
import net.sf.freecol.common.networking.GameEndedMessage;
|
|
import net.sf.freecol.common.networking.GameStateMessage;
|
|
import net.sf.freecol.common.networking.HighScoresMessage;
|
|
import net.sf.freecol.common.networking.InciteMessage;
|
|
import net.sf.freecol.common.networking.IndianDemandMessage;
|
|
import net.sf.freecol.common.networking.LootCargoMessage;
|
|
import net.sf.freecol.common.networking.Message;
|
|
import net.sf.freecol.common.networking.MonarchActionMessage;
|
|
import net.sf.freecol.common.networking.NationSummaryMessage;
|
|
import net.sf.freecol.common.networking.NativeTradeMessage;
|
|
import net.sf.freecol.common.networking.NewTradeRouteMessage;
|
|
import net.sf.freecol.common.networking.RearrangeColonyMessage.Arrangement;
|
|
import net.sf.freecol.common.networking.ScoutSpeakToChiefMessage;
|
|
import net.sf.freecol.common.networking.SetCurrentPlayerMessage;
|
|
import static net.sf.freecol.common.util.CollectionUtils.*;
|
|
import net.sf.freecol.common.util.LogBuilder;
|
|
import net.sf.freecol.common.util.RandomChoice;
|
|
import static net.sf.freecol.common.util.RandomUtils.*;
|
|
import static net.sf.freecol.common.util.StringUtils.*;
|
|
import net.sf.freecol.common.util.Utils;
|
|
|
|
import net.sf.freecol.server.FreeColServer;
|
|
import net.sf.freecol.server.ai.REFAIPlayer;
|
|
import net.sf.freecol.server.model.DiplomacySession;
|
|
import net.sf.freecol.server.model.LootSession;
|
|
import net.sf.freecol.server.model.MonarchSession;
|
|
import net.sf.freecol.server.model.NativeDemandSession;
|
|
import net.sf.freecol.server.model.NativeTradeSession;
|
|
import net.sf.freecol.server.model.ServerColony;
|
|
import net.sf.freecol.server.model.ServerEurope;
|
|
import net.sf.freecol.server.model.ServerGame;
|
|
import net.sf.freecol.server.model.ServerIndianSettlement;
|
|
import net.sf.freecol.server.model.ServerPlayer;
|
|
import net.sf.freecol.server.model.ServerRegion;
|
|
import net.sf.freecol.server.model.ServerUnit;
|
|
import net.sf.freecol.server.model.Session;
|
|
|
|
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
|
|
|
|
|
/**
|
|
* The main server controller.
|
|
*/
|
|
public final class InGameController extends Controller {
|
|
|
|
private static final Logger logger = Logger.getLogger(InGameController.class.getName());
|
|
|
|
private static final Predicate<Player> coronadoPred = p ->
|
|
p.hasAbility(Ability.SEE_ALL_COLONIES);
|
|
|
|
/** The server random number source. */
|
|
private Random random;
|
|
|
|
/** Debug helpers, do not serialize. */
|
|
private int debugOnlyAITurns = 0;
|
|
private MonarchAction debugMonarchAction = null;
|
|
private ServerPlayer debugMonarchPlayer = null;
|
|
|
|
|
|
/**
|
|
* The constructor to use.
|
|
*
|
|
* @param freeColServer The main server object.
|
|
*/
|
|
public InGameController(FreeColServer freeColServer) {
|
|
super(freeColServer);
|
|
|
|
this.random = null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set the PRNG.
|
|
*
|
|
* @param random The new {@code Random} to use in this controller.
|
|
*/
|
|
public void setRandom(Random random) {
|
|
this.random = random;
|
|
}
|
|
|
|
/**
|
|
* Get the timeout for this game.
|
|
*
|
|
* @return A timeout.
|
|
*/
|
|
private long getTimeout() {
|
|
final boolean single = getFreeColServer().getSinglePlayer();
|
|
return FreeCol.getTimeout(single);
|
|
}
|
|
|
|
|
|
// Debug support
|
|
|
|
/**
|
|
* Gets the number of AI turns to skip through.
|
|
*
|
|
* @return The number of terms to skip.
|
|
*/
|
|
public int getSkippedTurns() {
|
|
return (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS))
|
|
? debugOnlyAITurns : -1;
|
|
}
|
|
|
|
/**
|
|
* Sets the number of AI turns to skip through as a debug helper.
|
|
*
|
|
* @param turns The number of turns to skip through.
|
|
*/
|
|
public void setSkippedTurns(int turns) {
|
|
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)) {
|
|
debugOnlyAITurns = turns;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a monarch action to debug/test.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} whose monarch
|
|
* should act.
|
|
* @param action The {@code MonarchAction} to be taken.
|
|
*/
|
|
public void setMonarchAction(ServerPlayer serverPlayer,
|
|
MonarchAction action) {
|
|
if (FreeColDebugger.isInDebugMode(FreeColDebugger.DebugMode.MENUS)) {
|
|
debugMonarchPlayer = serverPlayer;
|
|
debugMonarchAction = action;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug convenience to step the random number generator.
|
|
*
|
|
* @return The next random number in series, in the range 0-99.
|
|
*/
|
|
public int stepRandom() {
|
|
return randomInt(logger, "step random", random, 100);
|
|
}
|
|
|
|
/**
|
|
* Public version of csAddFoundingFather so it can be used in the
|
|
* test code and DebugMenu.
|
|
*
|
|
* @param player The {@code Player} who gains a father.
|
|
* @param father The {@code FoundingFather} to add.
|
|
*/
|
|
public void addFoundingFather(Player player, FoundingFather father) {
|
|
ChangeSet cs = new ChangeSet();
|
|
((ServerPlayer)player).csAddFoundingFather(father, random, cs);
|
|
cs.addAttribute(See.only(player), "flush", Boolean.TRUE.toString());
|
|
getGame().sendTo(player, cs);
|
|
}
|
|
|
|
/**
|
|
* Public change stance and inform all routine. Mostly used in the
|
|
* test suite, but the AIs also call it.
|
|
*
|
|
* @param player The originating {@code Player}.
|
|
* @param stance The new {@code Stance}.
|
|
* @param other The {@code Player} wrt which the stance changes.
|
|
* @param symmetric If true, change the otherPlayer stance as well.
|
|
*/
|
|
public void changeStance(Player player, Stance stance,
|
|
Player other, boolean symmetric) {
|
|
ChangeSet cs = new ChangeSet();
|
|
if (((ServerPlayer)player).csChangeStance(stance, other, symmetric, cs)) {
|
|
getGame().sendToAll(cs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Change colony owner. Public for DebugUtils.
|
|
*
|
|
* @param colony The {@code ServerColony} to change.
|
|
* @param serverPlayer The {@code ServerPlayer} to change to.
|
|
*/
|
|
public void debugChangeOwner(ServerColony colony,
|
|
ServerPlayer serverPlayer) {
|
|
ChangeSet cs = new ChangeSet();
|
|
final Player owner = colony.getOwner();
|
|
colony.csChangeOwner(serverPlayer, false, cs);//-vis(serverPlayer,owner)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
owner.invalidateCanSeeTiles();//+vis(owner)
|
|
getGame().sendToAll(cs);
|
|
}
|
|
|
|
/**
|
|
* Change unit owner. Public for DebugUtils.
|
|
*
|
|
* @param unit The {@code ServerUnit} to change.
|
|
* @param serverPlayer The {@code ServerPlayer} to change to.
|
|
*/
|
|
public void debugChangeOwner(ServerUnit unit, ServerPlayer serverPlayer) {
|
|
final Player owner = unit.getOwner();
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
((ServerPlayer)owner).csChangeOwner(unit, serverPlayer, null, null,
|
|
cs);//-vis(serverPlayer,owner)
|
|
cs.add(See.perhaps().always(owner), unit.getTile());
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
owner.invalidateCanSeeTiles();//+vis(owner)
|
|
getGame().sendToAll(cs);
|
|
}
|
|
|
|
/**
|
|
* Apply a disaster to a colony. Public for DebugUtils.
|
|
*
|
|
* @param colony The {@code Colony} to apply the disaster to.
|
|
* @param disaster The {@code Disaster} to apply.
|
|
* @return The number of messages generated.
|
|
*/
|
|
public int debugApplyDisaster(ServerColony colony, Disaster disaster) {
|
|
final ServerGame game = getGame();
|
|
final Player owner = colony.getOwner();
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
List<ModelMessage> messages
|
|
= ((ServerPlayer)owner).csApplyDisaster(random, colony, disaster, cs);
|
|
if (!messages.isEmpty()) {
|
|
cs.addGlobalMessage(game, null,
|
|
new ModelMessage(MessageType.DISASTERS,
|
|
"model.disaster.strikes", owner)
|
|
.addName("%colony%", colony.getName())
|
|
.addName("%disaster%", disaster));
|
|
for (ModelMessage message : messages) {
|
|
cs.addGlobalMessage(game, null, message);
|
|
}
|
|
game.sendToAll(cs);
|
|
}
|
|
return messages.size();
|
|
}
|
|
|
|
|
|
// Internal utilities
|
|
|
|
/**
|
|
* Create the Royal Expeditionary Force player corresponding to
|
|
* a given player that is about to rebel.
|
|
*
|
|
* Public for the test suite.
|
|
*
|
|
* FIXME: this should eventually generate changes for the REF player.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} about to rebel.
|
|
* @return The REF player.
|
|
*/
|
|
public ServerPlayer createREFPlayer(ServerPlayer serverPlayer) {
|
|
final Nation refNation = serverPlayer.getNation().getREFNation();
|
|
final Monarch monarch = serverPlayer.getMonarch();
|
|
final ServerPlayer refPlayer = getFreeColServer().makeAIPlayer(refNation);
|
|
final Europe europe = refPlayer.getEurope();
|
|
final Predicate<Tile> exploredPred = t ->
|
|
((!t.isLand() || t.isCoastland() || t.getOwner() == serverPlayer)
|
|
&& t.isExploredBy(serverPlayer));
|
|
// Inherit rebel player knowledge of the seas, coasts, claimed
|
|
// land but not full detailed scouting knowledge.
|
|
Set<Tile> explore = new HashSet<>();
|
|
getGame().getMap().forEachTile(exploredPred, t -> explore.add(t));
|
|
refPlayer.exploreTiles(explore);
|
|
|
|
// Trigger initial placement routine
|
|
refPlayer.setEntryTile(null);
|
|
// Will change, setup only
|
|
Player.makeContact(serverPlayer, refPlayer);
|
|
|
|
// Instantiate the REF in Europe
|
|
Force exf = monarch.getExpeditionaryForce();
|
|
if (!exf.prepareToBoard()) {
|
|
logger.warning("Unable to ensure space for the REF land units.");
|
|
// For now, do not fail completely
|
|
}
|
|
List<Unit> landUnits = refPlayer.createUnits(exf.getLandUnitsList(),
|
|
europe, null);//-vis: safe!map
|
|
List<Unit> navalUnits = refPlayer.createUnits(exf.getNavalUnitsList(),
|
|
europe, random);//-vis: safe!map
|
|
List<Unit> leftOver = refPlayer.loadShips(landUnits, navalUnits,
|
|
random);//-vis: safe!map
|
|
if (!leftOver.isEmpty()) {
|
|
// Should not happen, make this return null one day
|
|
logger.warning("Failed to board REF units: "
|
|
+ join(" ", transform(leftOver, alwaysTrue(),
|
|
FreeColObject::getId)));
|
|
}
|
|
return refPlayer;
|
|
}
|
|
|
|
/**
|
|
* Buy goods from a native settlement.
|
|
*
|
|
* @param unit The {@code Unit} that is buying.
|
|
* @param goods The {@code Goods} to buy.
|
|
* @param price The price to pay.
|
|
* @param sis The {@code ServerIndianSettlement} to give to.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csBuy(Unit unit, Goods goods, int price,
|
|
ServerIndianSettlement sis, ChangeSet cs) {
|
|
final Specification spec = getGame().getSpecification();
|
|
final int alarmBonus = -Math.round(price * 0.001f
|
|
* spec.getPercentage(GameOptions.ALARM_BONUS_BUY));
|
|
final Player owner = unit.getOwner();
|
|
|
|
csVisit((ServerPlayer)owner, sis, 0, cs);
|
|
GoodsLocation.moveGoods(sis, goods.getType(), goods.getAmount(), unit);
|
|
cs.add(See.perhaps(), unit);
|
|
sis.getOwner().modifyGold(price);
|
|
owner.modifyGold(-price);
|
|
sis.csModifyAlarm(owner, alarmBonus, true, cs);
|
|
sis.updateWantedGoods();
|
|
final Tile tile = sis.getTile();
|
|
tile.updateIndianSettlement(owner);
|
|
cs.add(See.only(owner), tile);
|
|
cs.addPartial(See.only(owner), owner,
|
|
"gold", String.valueOf(owner.getGold()));
|
|
logger.finest(owner.getSuffix() + " " + unit + " buys " + goods
|
|
+ " at " + sis.getName() + " for " + price);
|
|
}
|
|
|
|
/**
|
|
* Sell goods to a native settlement.
|
|
*
|
|
* @param unit The {@code Unit} that is selling.
|
|
* @param goods The {@code Goods} to sell.
|
|
* @param price The price to charge.
|
|
* @param sis The {@code ServerIndianSettlement} to sell to.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csSell(Unit unit, Goods goods, int price,
|
|
ServerIndianSettlement sis, ChangeSet cs) {
|
|
final Specification spec = getGame().getSpecification();
|
|
final Player owner = unit.getOwner();
|
|
final int alarmBonus = -Math.round(price * 0.001f
|
|
* spec.getPercentage(GameOptions.ALARM_BONUS_SELL));
|
|
|
|
csVisit((ServerPlayer)owner, sis, 0, cs);
|
|
GoodsLocation.moveGoods(unit, goods.getType(), goods.getAmount(), sis);
|
|
cs.add(See.perhaps(), unit);
|
|
sis.getOwner().modifyGold(-price);
|
|
owner.modifyGold(price);
|
|
sis.csModifyAlarm(owner, alarmBonus, true, cs);
|
|
sis.updateWantedGoods();
|
|
final Tile tile = sis.getTile();
|
|
tile.updateIndianSettlement(owner);
|
|
cs.add(See.only(owner), tile);
|
|
cs.addPartial(See.only(owner), owner,
|
|
"gold", String.valueOf(owner.getGold()));
|
|
cs.addSale(owner, sis, goods.getType(),
|
|
Math.round((float)price/goods.getAmount()));
|
|
logger.finest(owner.getSuffix() + " " + unit + " sells " + goods
|
|
+ " at " + sis.getName() + " for " + price);
|
|
}
|
|
|
|
/**
|
|
* Give goods to a native settlement.
|
|
*
|
|
* @param unit The {@code Unit} that is giving.
|
|
* @param goods The {@code Goods} to give.
|
|
* @param price A price that the natives might have been willing to pay.
|
|
* @param sis The {@code ServerIndianSettlement} to give to.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csGift(Unit unit, Goods goods, int price,
|
|
ServerIndianSettlement sis, ChangeSet cs) {
|
|
final Specification spec = getGame().getSpecification();
|
|
final Player owner = unit.getOwner();
|
|
final int alarmBonus = -Math.round(price * 0.001f
|
|
* spec.getPercentage(GameOptions.ALARM_BONUS_GIFT));
|
|
|
|
csVisit((ServerPlayer)owner, sis, 0, cs);
|
|
GoodsLocation.moveGoods(unit, goods.getType(), goods.getAmount(), sis);
|
|
cs.add(See.perhaps(), unit);
|
|
sis.csModifyAlarm(owner, alarmBonus, true, cs);
|
|
sis.updateWantedGoods();
|
|
final Tile tile = sis.getTile();
|
|
tile.updateIndianSettlement(owner);
|
|
cs.add(See.only(owner), tile);
|
|
logger.finest(owner.getSuffix() + " " + unit + " gives " + goods
|
|
+ " at " + sis.getName() + " worth " + price);
|
|
}
|
|
|
|
/**
|
|
* Visits a native settlement, possibly scouting it full if it is
|
|
* as a result of a scout actually asking to speak to the chief,
|
|
* or for other settlement-contacting events such as missionary
|
|
* actions, demanding tribute, learning skills and trading if the
|
|
* settlementActionsContactChief game option is enabled. It is
|
|
* still unclear what Col1 did here.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is contacting
|
|
* the settlement.
|
|
* @param is The {@code IndianSettlement} to contact.
|
|
* @param scout Positive if this contact is due to a scout asking to
|
|
* speak to the chief, zero if it is another unit, negative if
|
|
* this is from the greeting dialog generation.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csVisit(ServerPlayer serverPlayer, IndianSettlement is,
|
|
int scout, ChangeSet cs) {
|
|
final Player owner = is.getOwner();
|
|
|
|
if (serverPlayer.csContact(owner, cs)) {
|
|
serverPlayer.csNativeFirstContact(owner, null, cs);
|
|
}
|
|
is.setVisited(serverPlayer);
|
|
if (scout > 0 || (scout == 0 && getGame().getSpecification()
|
|
.getBoolean(GameOptions.SETTLEMENT_ACTIONS_CONTACT_CHIEF))) {
|
|
is.setScouted(serverPlayer);
|
|
}
|
|
// Force the settlement tile to become uncached. Should not
|
|
// be necessary but this might mitigate BR#3128.
|
|
is.getTile().seeTile(serverPlayer);
|
|
}
|
|
|
|
/**
|
|
* Launch the REF.
|
|
*
|
|
* @param serverPlayer The REF {@code ServerPlayer}.
|
|
* @param teleport If true, teleport the REF in.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csLaunchREF(ServerPlayer serverPlayer, boolean teleport,
|
|
ChangeSet cs) {
|
|
// Set the REF player entry location from the rebels. Note
|
|
// that the REF units will have their own unit-specific entry
|
|
// locations set by their missions. This just flags that the
|
|
// REF is initialized.
|
|
Player rebel = first(serverPlayer.getRebels());
|
|
if (rebel != null) {
|
|
serverPlayer.setEntryTile(rebel.getEntryTile());
|
|
}
|
|
|
|
if (teleport) { // Teleport in the units.
|
|
List<Unit> naval = transform(serverPlayer.getUnits(), Unit::isNaval);
|
|
Set<Tile> seen = new HashSet<>(naval.size());
|
|
for (Unit u : naval) {
|
|
Tile entry = u.getFullEntryLocation();
|
|
u.setLocation(entry);//-vis(serverPlayer)
|
|
u.setWorkLeft(-1);
|
|
u.setState(Unit.UnitState.ACTIVE);
|
|
if (seen.add(entry)) {
|
|
cs.add(See.only(serverPlayer),
|
|
serverPlayer.exploreForUnit(u));
|
|
cs.add(See.perhaps().except(serverPlayer), entry);
|
|
}
|
|
}
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
} else {
|
|
// Put navy on the high seas, with 1-turn sail time
|
|
for (Unit u : transform(serverPlayer.getUnits(), Unit::isNaval)) {
|
|
u.setWorkLeft(1);
|
|
u.setDestination(u.getFullEntryLocation());
|
|
u.setLocation(u.getOwner().getHighSeas());//-vis: safe!map
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Give independence. Note that the REF player is granting, but
|
|
* most of the changes happen to the newly independent player.
|
|
* hence the special handling.
|
|
*
|
|
* @param serverPlayer The REF {@code ServerPlayer} that is granting.
|
|
* @param independent The newly independent {@code Player}.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csGiveIndependence(ServerPlayer serverPlayer,
|
|
Player independent, ChangeSet cs) {
|
|
serverPlayer.csChangeStance(Stance.PEACE, independent, true, cs);
|
|
independent.changePlayerType(PlayerType.INDEPENDENT);
|
|
Game game = getGame();
|
|
Turn turn = game.getTurn();
|
|
independent.setTax(0);
|
|
independent.reinitialiseMarket();
|
|
HistoryEvent h = new HistoryEvent(turn,
|
|
HistoryEvent.HistoryEventType.INDEPENDENCE, independent);
|
|
|
|
// The score for actual independence is actually a percentage
|
|
// bonus depending on how many other nations are independent.
|
|
// If we ever go for a more complex scoring algorithm it might
|
|
// be better to replace the score int with a Modifier, but for
|
|
// now we just set the score value to the number of
|
|
// independent players other than the one we are granting
|
|
// here, and generate the bonus with a special case in
|
|
// ServerPlayer.updateScore().
|
|
int n = count(game.getLiveEuropeanPlayers(independent),
|
|
p -> p.getPlayerType() == PlayerType.INDEPENDENT);
|
|
h.setScore(n);
|
|
cs.addGlobalHistory(game, h);
|
|
cs.addMessage(independent,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"giveIndependence.announce", independent)
|
|
.addStringTemplate("%ref%", serverPlayer.getNationLabel()));
|
|
|
|
// Who surrenders?
|
|
final Predicate<Unit> surrenderPred = u -> //-vis(both)
|
|
(u.hasTile() && !u.isNaval() && !u.isOnCarrier()
|
|
&& (!u.hasAbility(Ability.REF_UNIT)
|
|
|| u.hasAbility(Ability.CAN_BE_SURRENDERED))
|
|
&& serverPlayer.csChangeOwner(u, independent,
|
|
UnitChangeType.CAPTURE, null, cs));
|
|
List<Unit> surrenderUnits
|
|
= transform(serverPlayer.getUnits(), surrenderPred);
|
|
for (Unit u : surrenderUnits) {
|
|
u.setMovesLeft(0);
|
|
u.setState(Unit.UnitState.ACTIVE);
|
|
cs.add(See.perhaps().always(serverPlayer), u.getTile());
|
|
}
|
|
if (!surrenderUnits.isEmpty()) {
|
|
cs.addMessage(independent,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"giveIndependence.unitsAcquired", independent)
|
|
.addStringTemplate("%units%",
|
|
unitTemplate(", ", surrenderUnits)));
|
|
independent.invalidateCanSeeTiles();//+vis(independent)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
}
|
|
|
|
// Update player type. Again, a pity to have to do a whole
|
|
// player update, but a partial update works for other players.
|
|
cs.addPartial(See.all().except(independent), independent,
|
|
"playerType", String.valueOf(independent.getPlayerType()));
|
|
cs.addGlobalMessage(game, independent,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"giveIndependence.otherAnnounce", independent)
|
|
.addStringTemplate("%nation%", independent.getNationLabel())
|
|
.addStringTemplate("%ref%", serverPlayer.getNationLabel()));
|
|
cs.add(See.only(independent), independent);
|
|
|
|
// Reveal the map on independence.
|
|
cs.add(See.only(independent),
|
|
((ServerPlayer)independent).exploreMap(true));
|
|
}
|
|
|
|
private StringTemplate unitTemplate(String base, List<Unit> units) {
|
|
StringTemplate template = StringTemplate.label(base);
|
|
for (Unit u : units) {
|
|
template.addStringTemplate(u.getLabel(Unit.UnitLabelType.PLAIN));
|
|
}
|
|
return template;
|
|
}
|
|
|
|
/**
|
|
* Resolves a tax raise.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} whose tax is rising.
|
|
* @param taxRaise The amount of tax raise.
|
|
* @param goods The {@code Goods} for a goods party.
|
|
* @param result Whether the tax was accepted or not.
|
|
private void raiseTax(ServerPlayer serverPlayer, int taxRaise, Goods goods,
|
|
boolean result) {
|
|
ChangeSet cs = new ChangeSet();
|
|
serverPlayer.csRaiseTax(taxRaise, goods, result, cs);
|
|
getGame().sendTo(serverPlayer, cs);
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Performs a monarch action.
|
|
*
|
|
* Note that CHANGE_LATE is used so that these actions follow
|
|
* setting the current player, so that it is the players turn when
|
|
* they respond to a monarch action.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} being acted upon.
|
|
* @param action The monarch action.
|
|
* @param cs A {@code ChangeSet} to update.
|
|
*/
|
|
private void csMonarchAction(final ServerPlayer serverPlayer,
|
|
MonarchAction action, ChangeSet cs) {
|
|
final Monarch monarch = serverPlayer.getMonarch();
|
|
final Game game = getGame();
|
|
final Specification spec = game.getSpecification();
|
|
boolean valid = monarch.actionIsValid(action);
|
|
if (!valid) return;
|
|
String messageId = action.getTextKey();
|
|
StringTemplate template;
|
|
MonarchActionMessage message;
|
|
String monarchKey = serverPlayer.getNationId();
|
|
|
|
switch (action) {
|
|
case NO_ACTION:
|
|
break;
|
|
case RAISE_TAX_WAR: case RAISE_TAX_ACT:
|
|
final int taxRaise = monarch.raiseTax(random);
|
|
final Goods goods = serverPlayer.getMostValuableGoods();
|
|
if (goods == null) {
|
|
logger.finest("Ignoring tax raise, no goods to boycott.");
|
|
break;
|
|
}
|
|
template = StringTemplate.template(messageId)
|
|
.addStringTemplate("%goods%", goods.getType().getLabel())
|
|
.addAmount("%amount%", taxRaise);
|
|
if (action == MonarchAction.RAISE_TAX_WAR) {
|
|
template = template.add("%nation%",
|
|
Nation.getRandomNonPlayerNationNameKey(game, random));
|
|
} else if (action == MonarchAction.RAISE_TAX_ACT) {
|
|
template = template.addAmount("%number%",
|
|
randomInt(logger, "Tax act goods", random, 6))
|
|
.addName("%newWorld%", serverPlayer.getNewLandName());
|
|
}
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, template, monarchKey)
|
|
.setTax(taxRaise));
|
|
new MonarchSession(serverPlayer, action, taxRaise, goods).register();
|
|
break;
|
|
case LOWER_TAX_WAR: case LOWER_TAX_OTHER:
|
|
int oldTax = serverPlayer.getTax();
|
|
int taxLower = monarch.lowerTax(random);
|
|
serverPlayer.csSetTax(taxLower, cs);
|
|
template = StringTemplate.template(messageId)
|
|
.addAmount("%difference%", oldTax - taxLower)
|
|
.addAmount("%newTax%", taxLower);
|
|
if (action == MonarchAction.LOWER_TAX_WAR) {
|
|
template = template.add("%nation%",
|
|
Nation.getRandomNonPlayerNationNameKey(game, random));
|
|
} else {
|
|
template = template.addAmount("%number%",
|
|
randomInt(logger, "Lower tax reason", random, 5));
|
|
}
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, template, monarchKey));
|
|
break;
|
|
case WAIVE_TAX:
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action,
|
|
StringTemplate.template(messageId), monarchKey));
|
|
break;
|
|
case ADD_TO_REF:
|
|
AbstractUnit refAdditions = monarch.addToREF(random);
|
|
if (refAdditions == null) break;
|
|
template = StringTemplate.template(messageId)
|
|
.addAmount("%number%", refAdditions.getNumber())
|
|
.addNamed("%unit%", refAdditions.getType(spec));
|
|
cs.add(See.only(serverPlayer), monarch);
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, template, monarchKey));
|
|
break;
|
|
case DECLARE_PEACE:
|
|
List<Player> friends = monarch.collectPotentialFriends();
|
|
if (friends.isEmpty()) break;
|
|
Player friend = getRandomMember(logger, "Choose friend",
|
|
friends, random);
|
|
serverPlayer.csChangeStance(Stance.PEACE, friend, true, cs);
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, StringTemplate
|
|
.template(messageId)
|
|
.addStringTemplate("%nation%", friend.getNationLabel()),
|
|
monarchKey));
|
|
break;
|
|
case DECLARE_WAR:
|
|
List<Player> enemies = monarch.collectPotentialEnemies();
|
|
if (enemies.isEmpty()) break;
|
|
Player enemy = getRandomMember(logger, "Choose enemy",
|
|
enemies, random);
|
|
List<AbstractUnit> warSupport
|
|
= monarch.getWarSupport(enemy, random);
|
|
int warGold = 0;
|
|
if (!warSupport.isEmpty()) {
|
|
serverPlayer.createUnits(warSupport,
|
|
serverPlayer.getEurope(), random);//-vis: safe, Europe
|
|
warGold = spec.getInteger(GameOptions.WAR_SUPPORT_GOLD);
|
|
warGold += (warGold/10) * (randomInt(logger, "War support gold",
|
|
random, 5) - 2);
|
|
serverPlayer.modifyGold(warGold);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()),
|
|
"score", String.valueOf(serverPlayer.getScore()));
|
|
logger.fine("War support v " + enemy.getNation().getSuffix()
|
|
+ " " + warGold + " gold + " + Messages.message(AbstractUnit
|
|
.getListLabel(", ", warSupport)));
|
|
}
|
|
serverPlayer.csChangeStance(Stance.WAR, enemy, true, cs);
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, StringTemplate
|
|
.template((warSupport.isEmpty()) ? messageId
|
|
: "model.monarch.action.declareWarSupported.text")
|
|
.addStringTemplate("%nation%", enemy.getNationLabel())
|
|
.addStringTemplate("%force%",
|
|
AbstractUnit.getListLabel(", ", warSupport))
|
|
.addAmount("%gold%", warGold),
|
|
monarchKey));
|
|
break;
|
|
case SUPPORT_LAND: case SUPPORT_SEA:
|
|
boolean sea = action == MonarchAction.SUPPORT_SEA;
|
|
List<AbstractUnit> support = monarch.getSupport(random, sea);
|
|
if (support.isEmpty()) break;
|
|
serverPlayer.createUnits(support,
|
|
serverPlayer.getEurope(), random);//-vis: safe, Europe
|
|
cs.add(See.only(serverPlayer), serverPlayer.getEurope());
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, StringTemplate
|
|
.template(messageId)
|
|
.addStringTemplate("%addition%",
|
|
AbstractUnit.getListLabel(", ", support)),
|
|
monarchKey));
|
|
break;
|
|
case MONARCH_MERCENARIES:
|
|
final List<AbstractUnit> mercenaries = new ArrayList<>();
|
|
final int mercPrice = monarch.loadMercenaries(random, mercenaries);
|
|
if (mercPrice <= 0) break;
|
|
cs.add(See.only(serverPlayer),
|
|
new MonarchActionMessage(action, StringTemplate
|
|
.template(messageId)
|
|
.addAmount("%gold%", mercPrice)
|
|
.addStringTemplate("%mercenaries%",
|
|
AbstractUnit.getListLabel(", ", mercenaries)),
|
|
monarchKey));
|
|
new MonarchSession(serverPlayer, action, mercenaries, mercPrice).register();
|
|
break;
|
|
case HESSIAN_MERCENARIES:
|
|
final List<AbstractUnit> hessians = new ArrayList<>();
|
|
final int hessianPrice = monarch.loadMercenaries(random, hessians);
|
|
if (hessianPrice <= 0) break;
|
|
serverPlayer.csMercenaries(hessianPrice, hessians, action,
|
|
random, cs);
|
|
break;
|
|
case DISPLEASURE: default:
|
|
logger.warning("Bogus action: " + action);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
// Routines that follow implement the controller response to
|
|
// messages. The convention is to return a change set back to the
|
|
// invoking message handler, but handle changes for other players
|
|
// directly here.
|
|
|
|
|
|
/**
|
|
* Abandon a settlement.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is abandoning.
|
|
* @param settlement The {@code Settlement} to abandon.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet abandonSettlement(ServerPlayer serverPlayer,
|
|
Settlement settlement) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Drop trade routes and create history event before disposing.
|
|
if (settlement instanceof Colony) {
|
|
serverPlayer.csLoseLocation(settlement, cs);
|
|
cs.addHistory(serverPlayer,
|
|
new HistoryEvent(getGame().getTurn(),
|
|
HistoryEvent.HistoryEventType.ABANDON_COLONY, serverPlayer)
|
|
.addName("%colony%", settlement.getName()));
|
|
}
|
|
|
|
// Comprehensive dispose.
|
|
serverPlayer.csDisposeSettlement(settlement, cs);//+vis
|
|
|
|
// FIXME: Player.settlements is still being fixed on the client side.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Ask about learning a skill at a native settlement.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is learning.
|
|
* @param unit The {@code Unit} that is learning.
|
|
* @param is The {@code IndianSettlement} to learn from.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet askLearnSkill(ServerPlayer serverPlayer, Unit unit,
|
|
IndianSettlement is) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
Tile tile = is.getTile();
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
|
|
// Do not update others, nothing to see yet.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Assign a student to a teacher.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param student The student {@code Unit}.
|
|
* @param teacher The teacher {@code Unit}.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet assignTeacher(ServerPlayer serverPlayer, Unit student,
|
|
Unit teacher) {
|
|
Unit oldStudent = teacher.getStudent();
|
|
Unit oldTeacher = student.getTeacher();
|
|
|
|
// Only update units that changed their teaching situation.
|
|
ChangeSet cs = new ChangeSet();
|
|
if (oldTeacher != null) {
|
|
oldTeacher.setStudent(null);
|
|
cs.add(See.only(serverPlayer), oldTeacher);
|
|
}
|
|
if (oldStudent != null) {
|
|
oldStudent.setTeacher(null);
|
|
cs.add(See.only(serverPlayer), oldStudent);
|
|
}
|
|
teacher.setStudent(student);
|
|
teacher.changeWorkType(null);
|
|
student.setTeacher(teacher);
|
|
cs.add(See.only(serverPlayer), student, teacher);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Assign a trade route to a unit.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The unit {@code Unit} to assign to.
|
|
* @param tradeRoute The {@code TradeRoute} to assign.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet assignTradeRoute(ServerPlayer serverPlayer, Unit unit,
|
|
TradeRoute tradeRoute) {
|
|
// If clearing a trade route and the unit is at sea, set
|
|
// the destination to the next stop. Otherwise just clear
|
|
// the destination.
|
|
TradeRouteStop stop;
|
|
unit.setDestination((tradeRoute == null && unit.isAtSea()
|
|
&& (stop = unit.getStop()) != null) ? stop.getLocation()
|
|
: null);
|
|
unit.setTradeRoute(tradeRoute);
|
|
if (tradeRoute != null) {
|
|
List<TradeRouteStop> stops = tradeRoute.getStopList();
|
|
int found = -1;
|
|
for (int i = 0; i < stops.size(); i++) {
|
|
if (Map.isSameLocation(unit.getLocation(),
|
|
stops.get(i).getLocation())) {
|
|
found = i;
|
|
break;
|
|
}
|
|
}
|
|
if (found < 0) found = 0;
|
|
unit.setCurrentStop(found);
|
|
}
|
|
|
|
// Only visible to the player
|
|
return new ChangeSet().add(See.only(serverPlayer), unit);
|
|
}
|
|
|
|
|
|
/**
|
|
* Build a settlement.
|
|
*
|
|
* +til: Resolves many tile appearance changes.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is building.
|
|
* @param unit The {@code Unit} that is building.
|
|
* @param name The new settlement name.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet buildSettlement(ServerPlayer serverPlayer, Unit unit,
|
|
String name) {
|
|
final ServerGame game = getGame();
|
|
final Specification spec = game.getSpecification();
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Build settlement
|
|
Tile tile = unit.getTile();
|
|
Settlement settlement;
|
|
if (Player.ASSIGN_SETTLEMENT_NAME.equals(name)) {
|
|
name = serverPlayer.getSettlementName(random);
|
|
}
|
|
if (serverPlayer.isEuropean()) {
|
|
StringTemplate nation = serverPlayer.getNationLabel();
|
|
settlement = new ServerColony(game, serverPlayer, name, tile);
|
|
for (Tile t : tile.getSurroundingTiles(settlement.getRadius())) {
|
|
t.cacheUnseen();//+til
|
|
}
|
|
Set<Tile> visible = settlement.getVisibleTileSet();
|
|
|
|
// Check new sightings before placing the settlement
|
|
cs.add(See.only(serverPlayer),
|
|
serverPlayer.collectNewTiles(visible));
|
|
// Coronado
|
|
for (Player sp : transform(game.getConnectedPlayers(serverPlayer),
|
|
coronadoPred)) {
|
|
cs.add(See.only(sp), ((ServerPlayer)sp).exploreForSettlement(settlement));//-vis(sp)
|
|
sp.invalidateCanSeeTiles();//+vis(sp)
|
|
cs.addMessage(sp,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"buildColony.others", settlement)
|
|
.addStringTemplate("%nation%", nation)
|
|
.addStringTemplate("%colony%",
|
|
settlement.getLocationLabelFor(sp))
|
|
.addStringTemplate("%region%",
|
|
tile.getRegion().getLabel()));
|
|
}
|
|
|
|
// Place settlement
|
|
serverPlayer.addSettlement(settlement);
|
|
settlement.placeSettlement(false);//-vis(serverPlayer,?),-til
|
|
cs.addHistory(serverPlayer, new HistoryEvent(game.getTurn(),
|
|
HistoryEvent.HistoryEventType.FOUND_COLONY, serverPlayer)
|
|
.addName("%colony%", settlement.getName()));
|
|
|
|
// Remove equipment from founder in case role confuses
|
|
// placement.
|
|
settlement.equipForRole(unit, spec.getDefaultRole(), 0);
|
|
} else {
|
|
IndianNationType nationType
|
|
= (IndianNationType) serverPlayer.getNationType();
|
|
UnitType skill = RandomChoice
|
|
.getWeightedRandom(logger, "Choose skill",
|
|
nationType.generateSkillsForTile(tile),
|
|
random);
|
|
if (skill == null) { // Seasoned Scout
|
|
List<UnitType> scouts = spec
|
|
.getUnitTypesWithAbility(Ability.EXPERT_SCOUT);
|
|
skill = getRandomMember(logger, "Choose scout", scouts, random);
|
|
}
|
|
|
|
settlement = new ServerIndianSettlement(game, serverPlayer, name,
|
|
tile, false, skill, null);
|
|
for (Tile t : tile.getSurroundingTiles(settlement.getRadius())) {
|
|
t.cacheUnseen();//+til
|
|
}
|
|
|
|
// Place settlement
|
|
serverPlayer.addSettlement(settlement);
|
|
settlement.placeSettlement(true);//-vis(serverPlayer),-til
|
|
|
|
for (Player p : getGame().getLivePlayerList(serverPlayer)) {
|
|
((IndianSettlement)settlement).setAlarm(p, (p.isIndian())
|
|
? new Tension(Tension.Level.CONTENT.getLimit())
|
|
: serverPlayer.getTension(p));//-til
|
|
}
|
|
}
|
|
|
|
// Join the settlement.
|
|
unit.setLocation(settlement);//-vis(serverPlayer),-til
|
|
unit.setMovesLeft(0);
|
|
|
|
// Update with settlement tile, and newly owned tiles.
|
|
cs.add(See.perhaps(), settlement.getOwnedTiles());
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
|
|
// Others can see tile changes.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Buy goods in Europe.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is buying.
|
|
* @param type The {@code GoodsType} to buy.
|
|
* @param amount The amount of goods to buy.
|
|
* @param carrier The {@code Unit} to carry the goods.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
private ChangeSet buyGoods(ServerPlayer serverPlayer, GoodsType type,
|
|
int amount, Unit carrier) {
|
|
if (!serverPlayer.canTrade(type, Access.EUROPE)) {
|
|
return serverPlayer.clientError("Can not trade boycotted goods");
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
GoodsContainer container = carrier.getGoodsContainer();
|
|
container.saveState();
|
|
int gold = serverPlayer.getGold();
|
|
int buyAmount = serverPlayer.buyInEurope(random, container, type, amount);
|
|
if (buyAmount < 0) {
|
|
return serverPlayer.clientError("Player " + serverPlayer.getName()
|
|
+ " tried to buy " + amount + " " + type.getSuffix());
|
|
}
|
|
serverPlayer.propagateToEuropeanMarkets(type, -buyAmount, random);
|
|
serverPlayer.csFlushMarket(type, cs);
|
|
carrier.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
cs.add(See.only(serverPlayer), carrier);
|
|
logger.finest(carrier + " bought " + amount + "(" + buyAmount + ")"
|
|
+ " " + type.getSuffix()
|
|
+ " in Europe for " + (serverPlayer.getGold() - gold));
|
|
// Action occurs in Europe, nothing is visible to other players.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Cash in a treasure train.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is cashing in.
|
|
* @param unit The treasure train {@code Unit} to cash in.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet cashInTreasureTrain(ServerPlayer serverPlayer, Unit unit) {
|
|
final ServerGame game = getGame();
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Work out the cash in amount and the message to send.
|
|
int fullAmount = unit.getTreasureAmount();
|
|
int cashInAmount;
|
|
String messageId;
|
|
if (serverPlayer.getPlayerType() == PlayerType.COLONIAL) {
|
|
// Charge transport fee and apply tax
|
|
cashInAmount = (fullAmount - unit.getTransportFee())
|
|
* (100 - serverPlayer.getTax()) / 100;
|
|
messageId = "cashInTreasureTrain.colonial";
|
|
} else {
|
|
// No fee possible, no tax applies.
|
|
cashInAmount = fullAmount;
|
|
messageId = "cashInTreasureTrain.independent";
|
|
}
|
|
|
|
serverPlayer.modifyGold(cashInAmount);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()),
|
|
"score", String.valueOf(serverPlayer.getScore()));
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
messageId, serverPlayer, unit)
|
|
.addAmount("%amount%", fullAmount)
|
|
.addAmount("%cashInAmount%", cashInAmount));
|
|
messageId = (serverPlayer.isRebel()
|
|
|| serverPlayer.getPlayerType() == PlayerType.INDEPENDENT)
|
|
? "cashInTreasureTrain.otherIndependent"
|
|
: "cashInTreasureTrain.otherColonial";
|
|
cs.addGlobalMessage(game, serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
messageId, serverPlayer)
|
|
.addAmount("%amount%", fullAmount)
|
|
.addStringTemplate("%nation%", serverPlayer.getNationLabel()));
|
|
|
|
// Dispose of the unit, only visible to the owner.
|
|
cs.add(See.only(serverPlayer), (FreeColGameObject)unit.getLocation());
|
|
((ServerUnit)unit).csRemove(See.only(serverPlayer),
|
|
null, cs);//-vis: safe in colony
|
|
|
|
// Others can see the cash in message.
|
|
game.sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Change a units state.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to change the state of.
|
|
* @param state The new {@code UnitState}.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet changeState(ServerPlayer serverPlayer, Unit unit,
|
|
UnitState state) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
Tile tile = unit.getTile();
|
|
boolean tileDirty = tile != null && tile.getIndianSettlement() != null;
|
|
if (state == UnitState.FORTIFYING && tile != null) {
|
|
ServerColony colony = (tile.getOwningSettlement() instanceof Colony)
|
|
? (ServerColony) tile.getOwningSettlement()
|
|
: null;
|
|
Player owner = (colony == null) ? null : colony.getOwner();
|
|
if (owner != null
|
|
&& owner != unit.getOwner()
|
|
&& serverPlayer.getStance(owner) != Stance.ALLIANCE
|
|
&& serverPlayer.getStance(owner) != Stance.PEACE) {
|
|
if (colony.isTileInUse(tile)) {
|
|
colony.csEvictUsers(unit, cs);
|
|
}
|
|
if (serverPlayer.getStance(owner) == Stance.WAR) {
|
|
tile.changeOwnership(null, null); // Clear owner if at war
|
|
tileDirty = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
unit.setState(state);
|
|
if (tileDirty) {
|
|
cs.add(See.perhaps(), tile);
|
|
} else {
|
|
cs.add(See.perhaps(), (FreeColGameObject)unit.getLocation());
|
|
}
|
|
|
|
// Others might be able to see the unit.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Change improvement work type.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to change the work type of.
|
|
* @param type The new {@code TileImprovementType} to produce.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet changeWorkImprovementType(ServerPlayer serverPlayer,
|
|
Unit unit,
|
|
TileImprovementType type) {
|
|
Tile tile = unit.getTile();
|
|
TileImprovement improvement = tile.getTileImprovement(type);
|
|
if (improvement == null) { // Create the new improvement.
|
|
improvement = new TileImprovement(getGame(), tile, type, null);
|
|
tile.add(improvement);
|
|
}
|
|
|
|
unit.setWorkImprovement(improvement);
|
|
unit.setState(UnitState.IMPROVING);
|
|
|
|
// Private update of the tile.
|
|
return new ChangeSet().add(See.only(serverPlayer), tile);
|
|
}
|
|
|
|
|
|
/**
|
|
* Change work type.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to change the work type of.
|
|
* @param type The new {@code GoodsType} to produce.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet changeWorkType(ServerPlayer serverPlayer, Unit unit,
|
|
GoodsType type) {
|
|
if (unit.getWorkType() != type) {
|
|
unit.setExperience(0);
|
|
unit.changeWorkType(type);
|
|
}
|
|
|
|
// Private update of the colony.
|
|
return new ChangeSet().add(See.only(serverPlayer), unit.getColony());
|
|
}
|
|
|
|
|
|
/**
|
|
* Chat.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is chatting.
|
|
* @param message The chat message.
|
|
* @param pri A privacy setting, currently a noop.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet chat(ServerPlayer serverPlayer, String message,
|
|
boolean pri) {
|
|
getGame().sendToOthers(serverPlayer,
|
|
ChangeSet.simpleChange(See.all().except(serverPlayer),
|
|
new ChatMessage(serverPlayer, message, false)));
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Choose a founding father.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is choosing.
|
|
* @param ff A {@code FoundingFather} to select.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet chooseFoundingFather(ServerPlayer serverPlayer,
|
|
FoundingFather ff) {
|
|
final List<FoundingFather> offered = serverPlayer.getOfferedFathers();
|
|
|
|
if (!serverPlayer.canRecruitFoundingFather()) {
|
|
return serverPlayer.clientError("Player can not recruit fathers: "
|
|
+ serverPlayer.getId());
|
|
} else if (!offered.contains(ff)) {
|
|
return serverPlayer.clientError("Founding father not offered: "
|
|
+ ff.getId());
|
|
}
|
|
serverPlayer.updateCurrentFather(ff);
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Claim land.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} claiming.
|
|
* @param tile The {@code Tile} to claim.
|
|
* @param settlement The {@code Settlement} to claim for.
|
|
* @param price The price to pay for the land, which must agree
|
|
* with the owner valuation, unless negative which denotes stealing.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet claimLand(ServerPlayer serverPlayer, Tile tile,
|
|
Settlement settlement, int price) {
|
|
final ServerGame sg = getGame();
|
|
ChangeSet cs = new ChangeSet();
|
|
serverPlayer.csClaimLand(tile, settlement, price, cs);
|
|
|
|
if (settlement != null && serverPlayer.isEuropean()) {
|
|
// Define Coronado to make all colony-owned tiles visible
|
|
for (Player sp : transform(sg.getConnectedPlayers(serverPlayer),
|
|
coronadoPred)) {
|
|
((ServerPlayer)sp).exploreTile(tile);
|
|
cs.add(See.only(sp), tile);
|
|
sp.invalidateCanSeeTiles();//+vis(sp)
|
|
}
|
|
}
|
|
|
|
// Others can see the tile.
|
|
sg.sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Clear the specialty of a unit.
|
|
*
|
|
* FIXME: why not clear speciality in the open? You can disband!
|
|
* If we implement this remember to fix the visibility.
|
|
*
|
|
* @param serverPlayer The owner of the unit.
|
|
* @param unit The {@code Unit} to clear the speciality of.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet clearSpeciality(ServerPlayer serverPlayer, Unit unit) {
|
|
UnitTypeChange uc = unit.getUnitChange(UnitChangeType.CLEAR_SKILL);
|
|
if (uc == null) {
|
|
return serverPlayer.clientError("Can not clear unit speciality: "
|
|
+ unit.getId());
|
|
}
|
|
|
|
// There can be some restrictions that may prevent the
|
|
// clearing of the speciality. AFAICT the only ATM is that a
|
|
// teacher can not lose its speciality, but this will need to
|
|
// be revisited if we invent a work location that requires a
|
|
// particular unit type.
|
|
if (unit.getStudent() != null) {
|
|
return serverPlayer.clientError("Can not clear speciality of a teacher.");
|
|
}
|
|
|
|
// Valid, change type.
|
|
unit.changeType(uc.to);//-vis: safe in colony
|
|
|
|
// Update just the unit, others can not see it as this only happens
|
|
// in-colony.
|
|
return new ChangeSet().add(See.only(serverPlayer), unit);
|
|
}
|
|
|
|
|
|
/**
|
|
* Combat. Public for the test suite.
|
|
*
|
|
* @param attackerPlayer The {@code ServerPlayer} who is attacking.
|
|
* @param attacker The {@code FreeColGameObject} that is attacking.
|
|
* @param defender The {@code FreeColGameObject} that is defending.
|
|
* @param crs A list of {@code CombatResult}s defining the result.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet combat(ServerPlayer attackerPlayer,
|
|
FreeColGameObject attacker,
|
|
FreeColGameObject defender,
|
|
List<CombatResult> crs) {
|
|
ChangeSet cs = new ChangeSet();
|
|
try {
|
|
attackerPlayer.csCombat(attacker, defender, crs, random, cs);
|
|
} catch (IllegalStateException e) {
|
|
logger.log(Level.WARNING, "Combat FAIL", e);
|
|
return attackerPlayer.clientError(e.getMessage());
|
|
}
|
|
getGame().sendToOthers(attackerPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Continue playing after winning.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that plays on.
|
|
* @return Null.
|
|
*/
|
|
public ChangeSet continuePlaying(ServerPlayer serverPlayer) {
|
|
final ServerGame game = getGame();
|
|
if (!getFreeColServer().getSinglePlayer()) {
|
|
logger.warning("Can not continue playing in multiplayer!");
|
|
} else if (serverPlayer != game.checkForWinner()) {
|
|
logger.warning("Can not continue playing, as "
|
|
+ serverPlayer.getName()
|
|
+ " has not won the game!");
|
|
} else {
|
|
final Specification spec = game.getSpecification();
|
|
spec.setBoolean(GameOptions.VICTORY_DEFEAT_REF, false);
|
|
spec.setBoolean(GameOptions.VICTORY_DEFEAT_EUROPEANS, false);
|
|
spec.setBoolean(GameOptions.VICTORY_DEFEAT_HUMANS, false);
|
|
logger.info("Disabled victory conditions, as "
|
|
+ serverPlayer.getName()
|
|
+ " has won, but is continuing to play.");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
/**
|
|
* Declare independence.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is declaring.
|
|
* @param nationName The new name for the independent nation.
|
|
* @param countryName The new name for its residents.
|
|
* @return A {@code ChangeSet} containing the response.
|
|
*/
|
|
public ChangeSet declareIndependence(final ServerPlayer serverPlayer,
|
|
String nationName, String countryName) {
|
|
final ServerGame game = getGame();
|
|
final Specification spec = game.getSpecification();
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Cross the Rubicon
|
|
StringTemplate oldNation = serverPlayer.getNationLabel();
|
|
serverPlayer.setIndependentNationName(nationName);
|
|
serverPlayer.setNewLandName(countryName);
|
|
serverPlayer.changePlayerType(PlayerType.REBEL);
|
|
|
|
// Do not add history event to cs as we are going to update the
|
|
// entire player. Likewise clear model messages.
|
|
Turn turn = game.getTurn();
|
|
HistoryEvent h = new HistoryEvent(turn,
|
|
HistoryEvent.HistoryEventType.DECLARE_INDEPENDENCE, serverPlayer);
|
|
final int independenceTurn = spec.getInteger(GameOptions.INDEPENDENCE_TURN);
|
|
h.setScore(Math.max(0, independenceTurn - turn.getNumber()));
|
|
cs.addGlobalHistory(game, h);
|
|
serverPlayer.clearModelMessages();
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"declareIndependence.resolution", serverPlayer));
|
|
|
|
// Dispose of units in or heading to Europe.
|
|
Europe europe = serverPlayer.getEurope();
|
|
StringTemplate seized = StringTemplate.label(", ");
|
|
boolean lost = false;
|
|
for (Unit u : europe.getUnitList()) {
|
|
seized.addStringTemplate(u.getLabel());
|
|
((ServerUnit)u).csRemove(See.only(serverPlayer), null, cs);
|
|
lost = true;
|
|
}
|
|
for (Unit u : transform(serverPlayer.getHighSeas().getUnits(),
|
|
matchKey(europe, Unit::getDestination))) {
|
|
seized.addStringTemplate(u.getLabel());
|
|
((ServerUnit)u).csRemove(See.only(serverPlayer), null, cs);
|
|
lost = true;
|
|
}
|
|
if (lost) {
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.UNIT_LOST,
|
|
"declareIndependence.unitsSeized",
|
|
serverPlayer)
|
|
.addStringTemplate("%units%", seized));
|
|
}
|
|
serverPlayer.csLoseLocation(europe, cs);
|
|
serverPlayer.reinitialiseMarket();
|
|
|
|
// Create the REF.
|
|
ServerPlayer refPlayer = createREFPlayer(serverPlayer);
|
|
cs.addPlayers(Collections.singletonList(refPlayer));
|
|
// Update the intervention force
|
|
final Monarch monarch = serverPlayer.getMonarch();
|
|
monarch.updateInterventionForce();
|
|
String otherKey = Nation.getRandomNonPlayerNationNameKey(game, random);
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"declareIndependence.interventionForce",
|
|
serverPlayer)
|
|
.add("%nation%", otherKey)
|
|
.addAmount("%number%",
|
|
spec.getInteger(GameOptions.INTERVENTION_BELLS)));
|
|
serverPlayer.csChangeStance(Stance.WAR, refPlayer, true, cs);
|
|
|
|
// Generalized continental army muster.
|
|
// Do not use UnitType.getTargetType.
|
|
java.util.Map<UnitType, List<Unit>> unitMap = new HashMap<>();
|
|
for (Colony colony : transform(serverPlayer.getColonies(),
|
|
c -> c.getSoL() > 50)) {
|
|
List<Unit> allUnits = colony.getAllUnitsList();
|
|
int limit = (allUnits.size() + 2) * (colony.getSoL() - 50) / 100;
|
|
|
|
unitMap.clear();
|
|
for (Unit unit : transform(allUnits,
|
|
u -> u.getUnitChange(UnitChangeType.INDEPENDENCE) != null)) {
|
|
appendToMapList(unitMap, unit.getType(), unit);
|
|
}
|
|
for (Entry<UnitType, List<Unit>> entry : unitMap.entrySet()) {
|
|
int n = 0;
|
|
UnitType fromType = entry.getKey();
|
|
UnitType toType = spec.getUnitChange(UnitChangeType.INDEPENDENCE,
|
|
fromType).to;
|
|
List<Unit> units = entry.getValue();
|
|
while (n < limit && !units.isEmpty()) {
|
|
Unit unit = units.remove(0);
|
|
unit.changeType(toType);//-vis
|
|
cs.add(See.only(serverPlayer), unit);
|
|
n++;
|
|
}
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.UNIT_IMPROVED,
|
|
"declareIndependence.continentalArmyMuster",
|
|
serverPlayer, colony)
|
|
.addName("%colony%", colony.getName())
|
|
.addAmount("%number%", n)
|
|
.addNamed("%oldUnit%", fromType)
|
|
.addNamed("%unit%", toType));
|
|
limit -= n;
|
|
}
|
|
}
|
|
|
|
// The most hostile contacted non-warring natives declare war
|
|
// on you and peace with the REF, least hostile contacted
|
|
// natives declare peace on you and war on the REF. If they
|
|
// are the same nation, go to the next most hostile nation
|
|
// that may already be at war.
|
|
final Comparator<Player> comp = Comparator.comparingInt(p ->
|
|
p.getTension(serverPlayer).getValue());
|
|
List<Player> natives = transform(game.getLiveNativePlayers(),
|
|
p -> p.hasContacted(serverPlayer),
|
|
Function.<Player>identity(), comp);
|
|
if (!natives.isEmpty()) {
|
|
Player good = first(natives);
|
|
logger.info("Native ally following independence: " + good);
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"declareIndependence.nativeSupport", good)
|
|
.addStringTemplate("%nation%", good.getNationLabel())
|
|
.add("%ruler%", serverPlayer.getRulerNameKey()));
|
|
int delta;
|
|
switch (good.getStance(serverPlayer)) {
|
|
case ALLIANCE: case PEACE: default:
|
|
delta = 0;
|
|
break;
|
|
case CEASE_FIRE:
|
|
delta = Tension.Level.HAPPY.getLimit()
|
|
- good.getTension(serverPlayer).getValue();
|
|
break;
|
|
case WAR:
|
|
delta = Tension.Level.CONTENT.getLimit()
|
|
- good.getTension(serverPlayer).getValue();
|
|
break;
|
|
}
|
|
((ServerPlayer)good).csModifyTension(serverPlayer, delta, cs);
|
|
Player.makeContact(good, refPlayer);
|
|
((ServerPlayer)good).csModifyTension(refPlayer,
|
|
Tension.Level.HATEFUL.getLimit(), cs);
|
|
|
|
reverse(natives);
|
|
Player bad = null;
|
|
for (Player p : natives) {
|
|
if (p == good
|
|
|| p.getStance(serverPlayer) == Stance.ALLIANCE) break;
|
|
bad = p;
|
|
if (!p.atWarWith(serverPlayer)) break;
|
|
}
|
|
logger.info("Native enemy following independence: " + bad);
|
|
if (bad != null) {
|
|
switch (bad.getStance(serverPlayer)) {
|
|
case PEACE: case CEASE_FIRE:
|
|
delta = Tension.Level.HATEFUL.getLimit()
|
|
- bad.getTension(serverPlayer).getValue();
|
|
break;
|
|
case WAR: default:
|
|
delta = 0;
|
|
break;
|
|
}
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"declareIndependence.nativeHostile", bad)
|
|
.addStringTemplate("%nation%", bad.getNationLabel()));
|
|
if (delta != 0) {
|
|
((ServerPlayer)bad).csModifyTension(serverPlayer, delta, cs);
|
|
}
|
|
Player.makeContact(bad, refPlayer);
|
|
((ServerPlayer)bad).csModifyTension(refPlayer,
|
|
-bad.getTension(refPlayer).getValue(), cs);
|
|
}
|
|
}
|
|
|
|
// Make the mercenary force offer
|
|
List<AbstractUnit> mercs = new ArrayList<>();
|
|
int mercPrice = monarch.loadMercenaryForce(random, mercs);
|
|
if (mercPrice > 0) {
|
|
serverPlayer.csMercenaries(mercPrice, mercs,
|
|
Monarch.MonarchAction.HESSIAN_MERCENARIES, random, cs);
|
|
logger.info("Mercenary force offer on declaration ("
|
|
+ Messages.message(AbstractUnit.getListLabel(", ", mercs))
|
|
+ ") for " + mercPrice);
|
|
} else {
|
|
logger.info("Mercenary force offer on declaration not affordable.");
|
|
}
|
|
|
|
|
|
// Pity to have to update such a heavy object as the player,
|
|
// but we do this, at most, once per player. Other players
|
|
// only need a partial player update and the stance change.
|
|
// Put the stance change after the name change so that the
|
|
// other players see the new nation name declaring war. The
|
|
// REF is hardwired to declare war on rebels so there is no
|
|
// need to adjust its stance or tension.
|
|
cs.addPartial(See.all().except(serverPlayer), serverPlayer,
|
|
"playerType", String.valueOf(serverPlayer.getPlayerType()),
|
|
"independentNationName", serverPlayer.getIndependentNationName(),
|
|
"newLandName", serverPlayer.getNewLandName());
|
|
cs.addGlobalMessage(game, serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"declareIndependence.announce",
|
|
serverPlayer)
|
|
.addStringTemplate("%oldNation%", oldNation)
|
|
.addStringTemplate("%newNation%", serverPlayer.getNationLabel())
|
|
.add("%ruler%", serverPlayer.getRulerNameKey()));
|
|
cs.add(See.only(serverPlayer), serverPlayer);
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
|
|
// Now that everything is ready, we can dispose of Europe.
|
|
cs.addRemove(See.only(serverPlayer), null, europe);//-vis: not on map
|
|
europe.dispose();
|
|
// Do not clean up the Monarch, it contains the intervention force
|
|
|
|
game.sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Decline to investigate strange mounds.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param tile The {@code Tile} where the mounds are.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet declineMounds(ServerPlayer serverPlayer, Tile tile) {
|
|
tile.cacheUnseen();//+til
|
|
tile.removeLostCityRumour();//-til
|
|
|
|
// Others might see rumour disappear
|
|
ChangeSet cs = new ChangeSet();
|
|
cs.add(See.perhaps(), tile);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Delete a trade route.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} to delete a trade
|
|
* route for.
|
|
* @param tradeRoute The {@code TradeRoute} to delete.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet deleteTradeRoute(ServerPlayer serverPlayer,
|
|
TradeRoute tradeRoute) {
|
|
List<Unit> dropped = serverPlayer.removeTradeRoute(tradeRoute);
|
|
|
|
// Trade route change is entirely internal
|
|
ChangeSet cs = new ChangeSet();
|
|
cs.add(See.only(serverPlayer), serverPlayer); // FIXME: big update
|
|
if (!dropped.isEmpty()) cs.add(See.only(serverPlayer), dropped);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Deliver gift to settlement.
|
|
* Note that this includes both European and native gifts.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is delivering.
|
|
* @param unit The {@code Unit} that is delivering.
|
|
* @param settlement The {@code Settlement} to deliver to.
|
|
* @param goods The {@code Goods} to deliver.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet deliverGiftToSettlement(ServerPlayer serverPlayer,
|
|
Unit unit, Settlement settlement,
|
|
Goods goods) {
|
|
NativeTradeSession session
|
|
= Session.lookup(NativeTradeSession.class, unit, settlement);
|
|
if (session == null) {
|
|
return serverPlayer.clientError("Trying to deliver gift without opening a session");
|
|
}
|
|
NativeTrade nt = session.getNativeTrade();
|
|
if (!nt.getGift()) {
|
|
return serverPlayer.clientError("Trying to deliver gift in a session where gift giving is not allowed: " + unit + " " + settlement + " " + session);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
Tile tile = settlement.getTile();
|
|
GoodsLocation.moveGoods(unit, goods.getType(), goods.getAmount(), settlement);
|
|
cs.add(See.perhaps(), unit);
|
|
if (settlement instanceof ServerIndianSettlement) {
|
|
ServerIndianSettlement sis = (ServerIndianSettlement)settlement;
|
|
final int alarmBonus = -Math.round(sis.getPriceToBuy(goods)
|
|
* 0.001f * getGame().getSpecification()
|
|
.getPercentage(GameOptions.ALARM_BONUS_GIFT));
|
|
|
|
csVisit(serverPlayer, sis, 0, cs);
|
|
sis.csModifyAlarm(serverPlayer, alarmBonus, true, cs);
|
|
sis.updateWantedGoods();
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
}
|
|
nt.setGift(true);
|
|
|
|
// Inform the receiver of the gift.
|
|
ModelMessage m = new ModelMessage(MessageType.GIFT_GOODS,
|
|
"deliverGift.goods",
|
|
settlement, goods.getType())
|
|
.addStringTemplate("%player%", serverPlayer.getNationLabel())
|
|
.addNamed("%type%", goods)
|
|
.addAmount("%amount%", goods.getAmount())
|
|
.addName("%settlement%", settlement.getName());
|
|
cs.addMessage(serverPlayer, m);
|
|
final Player receiver = settlement.getOwner();
|
|
if (receiver.isConnected() && settlement instanceof Colony) {
|
|
cs.add(See.only(receiver), unit);
|
|
cs.add(See.only(receiver), settlement);
|
|
cs.addMessage(receiver, m);
|
|
}
|
|
logger.info("Gift delivered by unit: " + unit.getId()
|
|
+ " to settlement: " + settlement.getName());
|
|
|
|
// Others can see unit capacity, receiver gets it own items.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Demand a tribute from a native settlement.
|
|
*
|
|
* FIXME: Move TURNS_PER_TRIBUTE magic number to the spec.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} demanding the tribute.
|
|
* @param unit The {@code Unit} that is demanding the tribute.
|
|
* @param is The {@code IndianSettlement} demanded of.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet demandTribute(ServerPlayer serverPlayer, Unit unit,
|
|
IndianSettlement is) {
|
|
ChangeSet cs = new ChangeSet();
|
|
final int TURNS_PER_TRIBUTE = 5;
|
|
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
|
|
Player indianPlayer = is.getOwner();
|
|
int gold = 0;
|
|
int year = getGame().getTurn().getNumber();
|
|
RandomRange gifts = is.getType().getGifts();
|
|
if (is.getLastTribute() + TURNS_PER_TRIBUTE < year
|
|
&& gifts != null) {
|
|
switch (indianPlayer.getTension(serverPlayer).getLevel()) {
|
|
case HAPPY: case CONTENT:
|
|
gold = Math.min(gifts.getAmount("Tribute", random, true) / 10,
|
|
100);
|
|
break;
|
|
case DISPLEASED:
|
|
gold = Math.min(gifts.getAmount("Tribute", random, true) / 20,
|
|
100);
|
|
break;
|
|
case ANGRY: case HATEFUL:
|
|
default:
|
|
gold = 0; // No tribute for you.
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Increase tension whether we paid or not. Apply tension
|
|
// directly to the settlement and let propagation work.
|
|
((ServerIndianSettlement)is).csModifyAlarm(serverPlayer,
|
|
Tension.TENSION_ADD_NORMAL, true, cs);
|
|
is.setLastTribute(year);
|
|
ModelMessage m;
|
|
if (gold > 0) {
|
|
indianPlayer.modifyGold(-gold);
|
|
serverPlayer.modifyGold(gold);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()),
|
|
"score", String.valueOf(serverPlayer.getScore()));
|
|
m = new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"scoutSettlement.tributeAgree",
|
|
unit, is)
|
|
.addAmount("%amount%", gold);
|
|
} else {
|
|
m = new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"scoutSettlement.tributeDisagree",
|
|
unit, is);
|
|
}
|
|
cs.addMessage(serverPlayer, m);
|
|
final Tile tile = is.getTile();
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
|
|
// Do not update others, this is all private.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Denounce an existing mission.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is denouncing.
|
|
* @param unit The {@code Unit} denouncing.
|
|
* @param is The {@code IndianSettlement} containing the mission
|
|
* to denounce.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet denounceMission(ServerPlayer serverPlayer, Unit unit,
|
|
IndianSettlement is) {
|
|
ChangeSet cs = new ChangeSet();
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
|
|
// Determine result
|
|
Unit missionary = is.getMissionary();
|
|
if (missionary == null) {
|
|
return serverPlayer.clientError("Denouncing null missionary");
|
|
}
|
|
final Player enemy = missionary.getOwner();
|
|
double denounce = randomDouble(logger, "Denounce base", random)
|
|
* enemy.getImmigration() / (serverPlayer.getImmigration() + 1);
|
|
if (missionary.hasAbility(Ability.EXPERT_MISSIONARY)) {
|
|
denounce += 0.2;
|
|
}
|
|
if (unit.hasAbility(Ability.EXPERT_MISSIONARY)) {
|
|
denounce -= 0.2;
|
|
}
|
|
|
|
if (denounce < 0.5) { // Success, remove old mission and establish ours
|
|
return establishMission(serverPlayer, unit, is);
|
|
}
|
|
|
|
// Denounce failed
|
|
final Player owner = is.getOwner();
|
|
cs.add(See.only(serverPlayer), is);
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"indianSettlement.mission.noDenounce",
|
|
serverPlayer, unit)
|
|
.addStringTemplate("%nation%", owner.getNationLabel()));
|
|
cs.addMessage(enemy,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"indianSettlement.mission.enemyDenounce",
|
|
enemy, is)
|
|
.addStringTemplate("%enemy%", serverPlayer.getNationLabel())
|
|
.addStringTemplate("%settlement%",
|
|
is.getLocationLabelFor(enemy))
|
|
.addStringTemplate("%nation%", owner.getNationLabel()));
|
|
cs.add(See.perhaps().always(serverPlayer),
|
|
(FreeColGameObject)unit.getLocation());
|
|
((ServerUnit)unit).csRemove(See.perhaps().always(serverPlayer),
|
|
unit.getLocation(), cs);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
|
|
// Others can see missionary disappear
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Diplomacy.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is trading.
|
|
* @param ourUnit The {@code Unit} that is trading.
|
|
* @param otherColony The {@code Colony} to trade with.
|
|
* @param agreement The {@code DiplomaticTrade} to consider.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet diplomacy(ServerPlayer serverPlayer, Unit ourUnit,
|
|
Colony otherColony, DiplomaticTrade agreement) {
|
|
ChangeSet cs = new ChangeSet();
|
|
TradeStatus status = agreement.getStatus();
|
|
DiplomacySession session
|
|
= Session.lookup(DiplomacySession.class,
|
|
DiplomacySession.makeDiplomacySessionKey(ourUnit, otherColony));
|
|
if (session == null) {
|
|
if (status != TradeStatus.PROPOSE_TRADE) {
|
|
return serverPlayer.clientError("Missing uc-diplomacy session for "
|
|
+ ourUnit.getId() + "/" + otherColony.getId()
|
|
+ " with " + agreement);
|
|
}
|
|
session = new DiplomacySession(ourUnit, otherColony, getTimeout());
|
|
session.register();
|
|
logger.info("New diplomacy session: " + session);
|
|
} else {
|
|
logger.info("Continuing diplomacy session: " + session
|
|
+ " from " + ourUnit);
|
|
}
|
|
serverPlayer.csDiplomacy(session, agreement, cs);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
/**
|
|
* Diplomacy.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is trading.
|
|
* @param ourColony Our {@code Colony}.
|
|
* @param otherUnit The other {@code Unit} that is trading.
|
|
* @param agreement The {@code DiplomaticTrade} to consider.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet diplomacy(ServerPlayer serverPlayer, Colony ourColony,
|
|
Unit otherUnit, DiplomaticTrade agreement) {
|
|
ChangeSet cs = new ChangeSet();
|
|
DiplomacySession session
|
|
= Session.lookup(DiplomacySession.class,
|
|
DiplomacySession.makeDiplomacySessionKey(otherUnit, ourColony));
|
|
if (session == null) {
|
|
return serverPlayer.clientError("Missing cu-diplomacy session for "
|
|
+ otherUnit.getId() + "/" + ourColony.getId()
|
|
+ " with " + agreement);
|
|
} else {
|
|
logger.info("Continuing diplomacy session: " + session
|
|
+ " from " + ourColony);
|
|
}
|
|
serverPlayer.csDiplomacy(session, agreement, cs);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Disband a unit.
|
|
*
|
|
* @param serverPlayer The owner of the unit.
|
|
* @param unit The {@code Unit} to disband.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet disbandUnit(ServerPlayer serverPlayer, Unit unit) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Dispose of the unit.
|
|
Location loc = unit.getLocation();
|
|
cs.add(See.perhaps().always(serverPlayer), (FreeColGameObject)loc);
|
|
((ServerUnit)unit).csRemove(See.perhaps().always(serverPlayer),
|
|
loc, cs);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
|
|
// Others can see the unit removal and the space it leaves.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
/**
|
|
* Disconnect the client.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} to disconnect.
|
|
* @return Null, we never reply to a disconnect.
|
|
*/
|
|
public ChangeSet disconnect(ServerPlayer serverPlayer) {
|
|
final FreeColServer freeColServer = getFreeColServer();
|
|
if (serverPlayer != null) {
|
|
freeColServer.removePlayerConnection(serverPlayer);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Disembark unit from a carrier.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} whose unit is
|
|
* embarking.
|
|
* @param serverUnit The {@code ServerUnit} that is disembarking.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet disembarkUnit(ServerPlayer serverPlayer,
|
|
ServerUnit serverUnit) {
|
|
if (serverUnit.isNaval()) {
|
|
return serverPlayer.clientError("Naval unit " + serverUnit.getId()
|
|
+ " can not disembark.");
|
|
}
|
|
Unit carrier = serverUnit.getCarrier();
|
|
if (carrier == null) {
|
|
return serverPlayer.clientError("Unit " + serverUnit.getId()
|
|
+ " is not embarked.");
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
Location newLocation = carrier.getLocation();
|
|
Set<Tile> newTiles = (newLocation.getTile() == null) ? null
|
|
: serverPlayer.collectNewTiles(newLocation.getTile(),
|
|
serverUnit.getLineOfSight());
|
|
serverUnit.setLocation(newLocation);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
serverUnit.setMovesLeft(0); // In Col1 disembark consumes whole move.
|
|
cs.add(See.perhaps(), (FreeColGameObject)newLocation);
|
|
if (newTiles != null) {
|
|
serverPlayer.csSeeNewTiles(newTiles, cs);
|
|
}
|
|
|
|
// Others can (potentially) see the location.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Embark a unit onto a carrier.
|
|
* Checking that the locations are appropriate is not done here.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} embarking.
|
|
* @param serverUnit The {@code ServerUnit} that is embarking.
|
|
* @param carrier The {@code Unit} to embark onto.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet embarkUnit(ServerPlayer serverPlayer,
|
|
ServerUnit serverUnit, Unit carrier) {
|
|
if (serverUnit.isNaval()) {
|
|
return serverPlayer.clientError("Naval unit " + serverUnit.getId()
|
|
+ " can not embark.");
|
|
}
|
|
UnitLocation.NoAddReason reason = carrier.getNoAddReason(serverUnit);
|
|
if (reason != UnitLocation.NoAddReason.NONE) {
|
|
return serverPlayer.clientError("Carrier: " + carrier.getId()
|
|
+ " can not carry " + serverUnit.getId() + ": " + reason);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
serverUnit.csEmbark(carrier, cs);
|
|
|
|
// Others might see the unit disappear, or the carrier capacity.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* A unit migrates from Europe.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} whose unit it will be.
|
|
* @param slot The slot within {@code Europe} to select the unit from.
|
|
* @param type The type of migration occurring.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet emigrate(ServerPlayer serverPlayer, int slot,
|
|
MigrationType type) {
|
|
ChangeSet cs = new ChangeSet();
|
|
serverPlayer.csEmigrate(slot, type, random, cs);
|
|
|
|
// Do not update others, emigration is private.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Ends the turn of the given player.
|
|
*
|
|
* Note: sends messages to other players.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} to end the turn of.
|
|
* @return A {@code ChangeSet} encapsulating the end of turn changes.
|
|
*/
|
|
public ChangeSet endTurn(ServerPlayer serverPlayer) {
|
|
final FreeColServer freeColServer = getFreeColServer();
|
|
final ServerGame serverGame = getGame();
|
|
ServerPlayer winner = serverGame.checkForWinner();
|
|
ServerPlayer current = (ServerPlayer)serverGame.getCurrentPlayer();
|
|
|
|
if (serverPlayer != current) {
|
|
throw new RuntimeException("It is not " + serverPlayer.getName()
|
|
+ "'s turn, it is " + ((current == null) ? "noone"
|
|
: current.getName()) + "'s!");
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
for (;;) {
|
|
logger.finest("Ending turn for " + current.getName());
|
|
current.clearModelMessages();
|
|
|
|
// Check for new turn
|
|
if (serverGame.isNextPlayerInNewTurn()) {
|
|
serverGame.csNextTurn(cs);
|
|
|
|
LogBuilder lb = new LogBuilder(512);
|
|
lb.add("New turn ", serverGame.getTurn(), " for ");
|
|
serverGame.csNewTurn(random, lb, cs);
|
|
lb.shrink(", ");
|
|
lb.log(logger, Level.FINEST);
|
|
if (debugOnlyAITurns > 0) {
|
|
if (--debugOnlyAITurns <= 0) {
|
|
// If this was a debug run, complete it. This will
|
|
// signal the client to save and quit at the next
|
|
// suitable opportunity.
|
|
FreeColDebugger.signalEndDebugRun();
|
|
}
|
|
}
|
|
serverGame.sendToAll(cs); // Flush changes
|
|
cs.clear();
|
|
}
|
|
|
|
if ((current = (ServerPlayer)serverGame.getNextPlayer()) == null) {
|
|
// "can not happen"
|
|
return serverPlayer.clientError("Can not get next player");
|
|
}
|
|
|
|
// Remove dead players and retry
|
|
switch (current.checkForDeath()) {
|
|
case IS_DEFEATED:
|
|
for (Player p : current.getRebels()) {
|
|
csGiveIndependence(current, p, cs);
|
|
}
|
|
// Fall through
|
|
case IS_DEAD:
|
|
current.csWithdraw(cs, null, null);
|
|
logger.info("For " + serverPlayer.getSuffix()
|
|
+ ", " + current.getNation() + " has withdrawn.");
|
|
break;
|
|
case IS_AUTORECRUIT:
|
|
// Need to autorecruit a unit to keep alive.
|
|
current.csEmigrate(0, MigrationType.SURVIVAL, random, cs);
|
|
break;
|
|
case IS_ALIVE: default:
|
|
break;
|
|
}
|
|
if (!cs.isEmpty()) { // Flush changes
|
|
serverGame.sendToAll(cs);
|
|
cs.clear();
|
|
}
|
|
// Do not proceed with a dead players turn
|
|
if (current.isDead()) continue;
|
|
|
|
// Are there humans left?
|
|
// FIXME: see if this can be relaxed so we can run large
|
|
// AI-only simulations.
|
|
List<Player> connected = serverGame.getConnectedPlayers();
|
|
boolean onlyAI = all(connected, Player::isAI);
|
|
if (onlyAI) {
|
|
final Comparator<Player> scoreComp
|
|
= Comparator.comparingInt(Player::getScore).reversed();
|
|
winner = (ServerPlayer)first(sort(connected, scoreComp));
|
|
logger.info("No human player left, winner is: " + winner);
|
|
if (debugOnlyAITurns > 0) { // Complete debug runs
|
|
FreeColDebugger.signalEndDebugRun();
|
|
}
|
|
serverGame.setCurrentPlayer(null);
|
|
|
|
cs.add(See.all(), new GameEndedMessage(winner, false));
|
|
serverGame.sendToAll(cs);
|
|
cs.clear();
|
|
}
|
|
|
|
// Has the current player won?
|
|
// Do not end single player games where an AI has won,
|
|
// that would stop revenge mode.
|
|
if (winner == current
|
|
&& !(freeColServer.getSinglePlayer() && winner.isAI())) {
|
|
boolean highScore = !winner.isAI()
|
|
&& HighScore.newHighScore(winner);
|
|
cs.add(See.all(), new GameEndedMessage(winner, highScore));
|
|
serverGame.sendToAll(cs);
|
|
cs.clear();
|
|
}
|
|
|
|
// Do "new turn"-like actions that need to wait until right
|
|
// before the player is about to move.
|
|
serverGame.setCurrentPlayer(current);
|
|
if (current.isREF() && current.getEntryTile() == null) {
|
|
// Initialize this newly created REF
|
|
// If the teleportREF option is enabled, teleport it in.
|
|
REFAIPlayer refAIPlayer = (REFAIPlayer)freeColServer
|
|
.getAIPlayer(current);
|
|
boolean teleport = serverGame.getSpecification()
|
|
.getBoolean(GameOptions.TELEPORT_REF);
|
|
if (refAIPlayer.initialize(teleport)) {
|
|
csLaunchREF(current, teleport, cs);
|
|
} else {
|
|
logger.severe("REF failed to initialize.");
|
|
}
|
|
}
|
|
current.csStartTurn(random, cs);
|
|
|
|
cs.add(See.all(), new SetCurrentPlayerMessage(current));
|
|
if (current.getPlayerType() == PlayerType.COLONIAL) {
|
|
Monarch monarch = current.getMonarch();
|
|
MonarchAction action = null;
|
|
if (debugMonarchAction != null
|
|
&& current == debugMonarchPlayer) {
|
|
action = debugMonarchAction;
|
|
debugMonarchAction = null;
|
|
debugMonarchPlayer = null;
|
|
logger.finest("Debug monarch action: " + action);
|
|
} else if (monarch != null) {
|
|
action = RandomChoice.getWeightedRandom(logger,
|
|
"Choose monarch action",
|
|
monarch.getActionChoices(), random);
|
|
}
|
|
if (action != null) {
|
|
if (monarch.actionIsValid(action)) {
|
|
logger.finest("Monarch action: " + action);
|
|
csMonarchAction(current, action, cs);
|
|
} else {
|
|
logger.finest("Skipping invalid monarch action: "
|
|
+ action);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prepare to update, with current player last so that it
|
|
// does not immediately start moving and cause further
|
|
// changes which conflict with these updates.
|
|
List<Player> players = serverGame.getConnectedPlayers(current);
|
|
players.add(current);
|
|
|
|
// If this is a debug run, update everyone and continue.
|
|
boolean debugSkip = debugOnlyAITurns > 0
|
|
&& !current.isAI()
|
|
&& freeColServer.getSinglePlayer();
|
|
if (debugSkip) {
|
|
serverGame.sendToList(players, cs);
|
|
cs.clear();
|
|
continue;
|
|
}
|
|
// Flush accumulated changes, returning to serverPlayer.
|
|
players.remove(serverPlayer);
|
|
serverGame.sendToList(players, cs);
|
|
return cs;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Enters revenge mode against those evil AIs.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} entering revenge mode.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet enterRevengeMode(ServerPlayer serverPlayer) {
|
|
if (!getFreeColServer().getSinglePlayer()) {
|
|
return serverPlayer.clientError("Can not enter revenge mode,"
|
|
+ " as this is not a single player game.");
|
|
}
|
|
Game game = getGame();
|
|
List<UnitType> undeads = game.getSpecification()
|
|
.getUnitTypesWithAbility(Ability.UNDEAD);
|
|
List<UnitType> navalUnits = new ArrayList<>();
|
|
List<UnitType> landUnits = new ArrayList<>();
|
|
for (UnitType undead : undeads) {
|
|
if (undead.hasAbility(Ability.NAVAL_UNIT)) {
|
|
navalUnits.add(undead);
|
|
} else if (undead.hasAbility(Ability.MULTIPLE_ATTACKS)) {
|
|
landUnits.add(undead);
|
|
}
|
|
}
|
|
if (navalUnits.isEmpty() || landUnits.isEmpty()) {
|
|
return serverPlayer.clientError("Can not enter revenge mode,"
|
|
+ " because we can not find the undead units.");
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
UnitType navalType = getRandomMember(logger, "Choose undead navy",
|
|
navalUnits, random);
|
|
Tile start = serverPlayer.getEntryTile()
|
|
.getSafeTile(serverPlayer, random);
|
|
Unit theFlyingDutchman
|
|
= new ServerUnit(game, start, serverPlayer,
|
|
navalType);//-vis(serverPlayer)
|
|
UnitType landType = getRandomMember(logger, "Choose undead army",
|
|
landUnits, random);
|
|
Unit undead = new ServerUnit(game, theFlyingDutchman,
|
|
serverPlayer, landType);//-vis
|
|
assert undead != null;
|
|
cs.add(See.only(serverPlayer),
|
|
serverPlayer.exploreForUnit(theFlyingDutchman));
|
|
serverPlayer.setDead(false);
|
|
serverPlayer.changePlayerType(PlayerType.UNDEAD);
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
|
|
// No one likes the undead.
|
|
for (Player p : transform(game.getLivePlayers(serverPlayer),
|
|
p2 -> serverPlayer.hasContacted(p2))) {
|
|
serverPlayer.csChangeStance(Stance.WAR, p, true, cs);
|
|
}
|
|
|
|
// Revenge begins
|
|
game.setCurrentPlayer(serverPlayer);
|
|
cs.add(See.all(), new SetCurrentPlayerMessage(serverPlayer));
|
|
|
|
// Others can tell something has happened to the player,
|
|
// and possibly see the units.
|
|
cs.add(See.all(), serverPlayer);
|
|
cs.add(See.perhaps(), start);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Equip a unit for a specific role.
|
|
* Currently the unit is either in Europe or in a settlement.
|
|
* Might one day allow the unit to be on a tile co-located with
|
|
* an equipment-bearing wagon.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to equip.
|
|
* @param role The {@code Role} to equip for.
|
|
* @param roleCount The role count.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet equipForRole(ServerPlayer serverPlayer, Unit unit,
|
|
Role role, int roleCount) {
|
|
ChangeSet cs = new ChangeSet();
|
|
boolean ret = false;
|
|
if (unit.isInEurope()) {
|
|
ServerEurope serverEurope = (ServerEurope)serverPlayer.getEurope();
|
|
ret = serverEurope.csEquipForRole(unit, role, roleCount,
|
|
random, cs);
|
|
} else if (unit.getColony() != null) {
|
|
ServerColony serverColony = (ServerColony)unit.getColony();
|
|
ret = serverColony.csEquipForRole(unit, role, roleCount,
|
|
random, cs);
|
|
} else if (unit.getIndianSettlement() != null) {
|
|
ServerIndianSettlement sis
|
|
= (ServerIndianSettlement)unit.getIndianSettlement();
|
|
ret = sis.csEquipForRole(unit, role, roleCount, random, cs);
|
|
} else {
|
|
return serverPlayer.clientError("Unsuitable equip location for: "
|
|
+ unit.getId());
|
|
}
|
|
if (!ret) return null;
|
|
|
|
if (unit.getInitialMovesLeft() != unit.getMovesLeft()) {
|
|
unit.setMovesLeft(0);
|
|
}
|
|
Unit carrier = unit.getCarrier();
|
|
if (carrier != null
|
|
&& carrier.getInitialMovesLeft() != carrier.getMovesLeft()
|
|
&& carrier.getMovesLeft() != 0) {
|
|
carrier.setMovesLeft(0);
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Establish a new mission.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is establishing.
|
|
* @param unit The missionary {@code Unit}.
|
|
* @param is The {@code IndianSettlement} to establish at.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet establishMission(ServerPlayer serverPlayer, Unit unit,
|
|
IndianSettlement is) {
|
|
ChangeSet cs = new ChangeSet();
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
|
|
// Result depends on tension wrt this settlement.
|
|
// Establish if at least not angry.
|
|
final Tension tension = is.getAlarm(serverPlayer);
|
|
Location loc = unit.getLocation();
|
|
switch (tension.getLevel()) {
|
|
case HATEFUL: case ANGRY:
|
|
cs.add(See.perhaps().always(serverPlayer), (FreeColGameObject)loc);
|
|
((ServerUnit)unit).csRemove(See.perhaps().always(serverPlayer),
|
|
loc, cs);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
break;
|
|
|
|
case HAPPY: case CONTENT: case DISPLEASED:
|
|
ServerIndianSettlement sis = (ServerIndianSettlement)is;
|
|
if (is.hasMissionary()) sis.csKillMissionary(Boolean.FALSE, cs);
|
|
|
|
// Always show the tile the unit was on
|
|
cs.add(See.perhaps().always(serverPlayer), unit.getTile());
|
|
|
|
sis.csChangeMissionary(unit, cs);//+vis(serverPlayer)
|
|
break;
|
|
}
|
|
|
|
// Add the descriptive message.
|
|
final StringTemplate nation = is.getOwner().getNationLabel();
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"indianSettlement.mission." + tension.getKey(),
|
|
serverPlayer, unit)
|
|
.addStringTemplate("%nation%", nation));
|
|
|
|
// Others can see missionary disappear and settlement acquire
|
|
// mission.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle first contact between European players.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} making contact.
|
|
* @param ourUnit The {@code Unit} making contact (may be null).
|
|
* @param ourColony The {@code Colony} making contact (may be null).
|
|
* @param otherUnit The other {@code Unit} making contact (may be null).
|
|
* @param otherColony The other {@code Colony} making contact (may be null).
|
|
* @param agreement The {@code DiplomaticTrade} to consider.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet europeanFirstContact(ServerPlayer serverPlayer,
|
|
Unit ourUnit, Colony ourColony,
|
|
Unit otherUnit, Colony otherColony,
|
|
DiplomaticTrade agreement) {
|
|
String err = "Missing contact diplomacy session for ";
|
|
DiplomacySession ds;
|
|
boolean compatible = false;
|
|
if (ourColony != null) {
|
|
ds = DiplomacySession.findContactSession(otherUnit, ourColony);
|
|
if (ds == null) return serverPlayer.clientError(err
|
|
+ ourColony.getId() + " and " + otherUnit.getId());
|
|
compatible = ds.isCompatible(ourColony, otherUnit);
|
|
} else if (otherUnit != null) {
|
|
ds = DiplomacySession.findContactSession(ourUnit, otherUnit);
|
|
if (ds == null) return serverPlayer.clientError(err
|
|
+ ourUnit.getId() + " and " + otherUnit.getId());
|
|
compatible = ds.isCompatible(ourUnit, otherUnit);
|
|
} else {
|
|
ds = DiplomacySession.findContactSession(ourUnit, otherColony);
|
|
if (ds == null) return serverPlayer.clientError(err
|
|
+ ourUnit.getId() + " and " + otherColony.getId());
|
|
compatible = ds.isCompatible(ourUnit, otherColony);
|
|
}
|
|
logger.info("Continuing " + ((compatible) ? "" : "in")
|
|
+ "compatible contact session: " + ds.getKey());
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
if (compatible) { // Update the other player
|
|
serverPlayer.csDiplomacy(ds, agreement, cs);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the game state.
|
|
*
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet gameState() {
|
|
final FreeColServer freeColServer = getFreeColServer();
|
|
return ChangeSet.simpleChange((Player)null,
|
|
new GameStateMessage(freeColServer.getServerState()));
|
|
}
|
|
|
|
|
|
/**
|
|
* Gets the list of high scores.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} querying the scores.
|
|
* @param key A score category key.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet getHighScores(ServerPlayer serverPlayer, String key) {
|
|
return ChangeSet.simpleChange(serverPlayer,
|
|
new HighScoresMessage(key, HighScore.loadHighScores()));
|
|
}
|
|
|
|
|
|
/**
|
|
* Incite a settlement against an enemy.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is inciting.
|
|
* @param unit The missionary {@code Unit} inciting.
|
|
* @param is The {@code IndianSettlement} to incite.
|
|
* @param enemy The {@code Player} to be incited against.
|
|
* @param gold The amount of gold in the bribe.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet incite(ServerPlayer serverPlayer, Unit unit,
|
|
IndianSettlement is, Player enemy, int gold) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
Tile tile = is.getTile();
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
|
|
// How much gold will be needed?
|
|
Player nativePlayer = is.getOwner();
|
|
int payingValue = nativePlayer.getTension(serverPlayer).getValue();
|
|
int targetValue = nativePlayer.getTension(enemy).getValue();
|
|
int goldToPay = (payingValue > targetValue) ? 10000 : 5000;
|
|
goldToPay += 20 * (payingValue - targetValue);
|
|
goldToPay = Math.max(goldToPay, 650);
|
|
|
|
// Try to incite?
|
|
if (gold < 0) { // Initial inquiry
|
|
cs.add(See.only(serverPlayer),
|
|
new InciteMessage(unit, is, enemy, goldToPay));
|
|
} else if (gold < goldToPay || !serverPlayer.checkGold(gold)) {
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"missionarySettlement.inciteGoldFail",
|
|
serverPlayer, is)
|
|
.addStringTemplate("%player%",
|
|
enemy.getNationLabel())
|
|
.addAmount("%amount%", goldToPay));
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
} else {
|
|
// Success. Raise the tension for the native player with respect
|
|
// to the European player. Let resulting stance changes happen
|
|
// naturally in the AI player turn/s.
|
|
((ServerPlayer)nativePlayer).csModifyTension(enemy,
|
|
Tension.WAR_MODIFIER, cs);//+til
|
|
((ServerPlayer)enemy).csModifyTension(serverPlayer,
|
|
Tension.TENSION_ADD_WAR_INCITER, cs);//+til
|
|
serverPlayer.modifyGold(-gold);
|
|
nativePlayer.modifyGold(gold);
|
|
cs.addMessage(serverPlayer,
|
|
new ModelMessage(MessageType.FOREIGN_DIPLOMACY,
|
|
"missionarySettlement.inciteSuccess",
|
|
nativePlayer)
|
|
.addStringTemplate("%native%", nativePlayer.getNationLabel())
|
|
.addStringTemplate("%enemy%", enemy.getNationLabel()));
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
}
|
|
|
|
// Others might include enemy.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Indians making demands of a colony.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that sent the message.
|
|
* @param unit The {@code Unit} making the demands.
|
|
* @param colony The {@code Colony} that is demanded of.
|
|
* @param type The {@code GoodsType} being demanded, null
|
|
* implies gold.
|
|
* @param amount The amount of goods/gold being demanded.
|
|
* @param result The demand result (null initially).
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet indianDemand(final ServerPlayer serverPlayer, Unit unit,
|
|
Colony colony, GoodsType type, int amount,
|
|
IndianDemandAction result) {
|
|
final Player victim = colony.getOwner();
|
|
NativeDemandSession session = Session.lookup(NativeDemandSession.class,
|
|
unit, colony);
|
|
ChangeSet cs = new ChangeSet();
|
|
if (serverPlayer.isIndian()) {
|
|
if (session != null) {
|
|
return serverPlayer.clientError("Repeated native demand: "
|
|
+ unit.getId() + "," + colony.getId());
|
|
}
|
|
session = new NativeDemandSession(unit, colony, type, amount,
|
|
getTimeout());
|
|
session.register();
|
|
logger.info("Native demand(begin) " + session.getKey() + ": "
|
|
+ serverPlayer.getName() + " unit " + unit
|
|
+ " demands " + amount + " " + ((type == null) ? "gold" : type)
|
|
+ " from " + colony.getName());
|
|
cs.add(See.only(victim),
|
|
new IndianDemandMessage(unit, colony, type, amount));
|
|
} else {
|
|
if (session == null) {
|
|
return serverPlayer.clientError("Replying to missing demand: "
|
|
+ unit.getId() + "," + colony.getId());
|
|
}
|
|
logger.info("Native demand(" + result + ") " + session.getKey()
|
|
+ ": " + serverPlayer.getName() + " unit " + unit
|
|
+ " demands " + amount + " " + ((type == null) ? "gold" : type)
|
|
+ " from " + colony.getName());
|
|
session.complete(result == IndianDemandAction.INDIAN_DEMAND_ACCEPT,
|
|
cs);
|
|
}
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Join a colony.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} that is joining.
|
|
* @param colony The {@code Colony} to join.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet joinColony(ServerPlayer serverPlayer, Unit unit,
|
|
Colony colony) {
|
|
final Specification spec = getGame().getSpecification();
|
|
ChangeSet cs = new ChangeSet();
|
|
Set<Tile> ownedTiles = colony.getOwnedTiles();
|
|
Tile tile = colony.getTile();
|
|
|
|
// Join.
|
|
tile.cacheUnseen();//+til
|
|
unit.setLocation(colony);//-vis: safe/colony,-til
|
|
unit.setMovesLeft(0);
|
|
colony.equipForRole(unit, spec.getDefaultRole(), 0);
|
|
|
|
// Update with colony tile, and tiles now owned.
|
|
cs.add(See.only(serverPlayer), tile);
|
|
for (Tile t : transform(tile.getSurroundingTiles(1, colony.getRadius()),
|
|
t2 -> (t2.getOwningSettlement() == colony
|
|
&& !ownedTiles.contains(t2)))) {
|
|
cs.add(See.perhaps(), t);
|
|
}
|
|
|
|
// Others might see a tile ownership change.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Learn a skill at an IndianSettlement.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is learning.
|
|
* @param unit The {@code Unit} that is learning.
|
|
* @param is The {@code IndianSettlement} to learn from.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet learnFromIndianSettlement(ServerPlayer serverPlayer,
|
|
Unit unit, IndianSettlement is) {
|
|
// Sanity checks.
|
|
final Specification spec = getGame().getSpecification();
|
|
final UnitType skill = is.getLearnableSkill();
|
|
if (skill == null) {
|
|
return serverPlayer.clientError("No skill to learn at "
|
|
+ is.getName());
|
|
}
|
|
if (unit.getUnitChange(UnitChangeType.NATIVES, skill) == null) {
|
|
return serverPlayer.clientError("Unit " + unit
|
|
+ " can not learn skill " + skill + " at " + is.getName());
|
|
}
|
|
|
|
// Try to learn
|
|
ChangeSet cs = new ChangeSet();
|
|
unit.setMovesLeft(0);
|
|
csVisit(serverPlayer, is, 0, cs);
|
|
Location loc = unit.getLocation();
|
|
switch (is.getAlarm(serverPlayer).getLevel()) {
|
|
case HATEFUL: // Killed, might be visible to other players.
|
|
cs.add(See.perhaps().always(serverPlayer), (FreeColGameObject)loc);
|
|
((ServerUnit)unit).csRemove(See.perhaps().always(serverPlayer),
|
|
loc, cs);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
break;
|
|
case ANGRY: // Learn nothing, not even a pet update
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
break;
|
|
default:
|
|
// Teach the unit, and expend the skill if necessary.
|
|
// Do a full information update as the unit is in the settlement.
|
|
unit.changeType(skill);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
cs.add(See.perhaps(), unit);
|
|
if (!is.isCapital()
|
|
&& !(is.hasMissionary(serverPlayer)
|
|
&& spec.getBoolean(GameOptions.ENHANCED_MISSIONARIES))) {
|
|
is.setLearnableSkill(null);
|
|
}
|
|
break;
|
|
}
|
|
Tile tile = is.getTile();
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
|
|
// Others always see the unit, it may have died or been taught.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Load goods.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is loading.
|
|
* @param loc The {@code Location} where the goods are.
|
|
* @param goodsType The {@code GoodsType} to load.
|
|
* @param amount The amount of goods to load.
|
|
* @param carrier The {@code Unit} to load.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet loadGoods(ServerPlayer serverPlayer, Location loc,
|
|
GoodsType goodsType, int amount, Unit carrier) {
|
|
if (loc instanceof Europe) {
|
|
if (carrier.isInEurope()) {
|
|
return buyGoods(serverPlayer, goodsType, amount, carrier);
|
|
} else {
|
|
return serverPlayer.clientError("Carrier not in Europe: " + loc);
|
|
}
|
|
}
|
|
// All loading locations other than Europe are GoodsLocations
|
|
if (!(loc instanceof GoodsLocation)) {
|
|
return serverPlayer.clientError("Not a goods location: " + loc);
|
|
}
|
|
GoodsLocation gl = (GoodsLocation)loc;
|
|
if (!carrier.isAtLocation(loc)) {
|
|
return serverPlayer.clientError("Carrier not at location: " + loc);
|
|
}
|
|
if (carrier.getLoadableAmount(goodsType) < amount) {
|
|
return serverPlayer.clientError("Too much goods");
|
|
}
|
|
if (gl.getGoodsCount(goodsType) < amount) {
|
|
return serverPlayer.clientError("Not enough goods ("
|
|
+ gl.getGoodsCount(goodsType) + " < " + amount
|
|
+ " " + goodsType.getSuffix() + ") at " + gl);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
GoodsLocation.moveGoods(gl, goodsType, amount, carrier);
|
|
logger.finest(Messages.message(loc.getLocationLabel())
|
|
+ " loaded " + amount + " " + goodsType.getSuffix()
|
|
+ " onto " + carrier);
|
|
cs.add(See.only(serverPlayer), gl.getGoodsContainer());
|
|
cs.add(See.only(serverPlayer), carrier.getGoodsContainer());
|
|
if (carrier.getInitialMovesLeft() != carrier.getMovesLeft()) {
|
|
carrier.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), carrier,
|
|
"movesLeft", String.valueOf(carrier.getMovesLeft()));
|
|
}
|
|
if (gl instanceof Unit) {
|
|
Unit dst = (Unit)gl;
|
|
if (dst.getInitialMovesLeft() != dst.getMovesLeft()) {
|
|
dst.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), dst,
|
|
"movesLeft", String.valueOf(dst.getMovesLeft()));
|
|
}
|
|
}
|
|
|
|
// Invisible in settlement
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Loot cargo.
|
|
*
|
|
* Note loser is passed by identifier, as by the time we get here
|
|
* the unit may have been sunk.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the winner.
|
|
* @param winner The {@code Unit} that looting.
|
|
* @param loserId The object identifier of the {@code Unit}
|
|
* that is looted.
|
|
* @param loot The {@code Goods} to loot.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet lootCargo(ServerPlayer serverPlayer, Unit winner,
|
|
String loserId, List<Goods> loot) {
|
|
LootSession session = Session.lookup(LootSession.class,
|
|
winner.getId(), loserId);
|
|
if (session == null) {
|
|
return serverPlayer.clientError("Bogus looting!");
|
|
}
|
|
if (!winner.hasSpaceLeft()) {
|
|
return serverPlayer.clientError("No space to loot to: "
|
|
+ winner.getId());
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
List<Goods> available = session.getCapture();
|
|
if (loot == null) { // Initial inquiry
|
|
cs.add(See.only(serverPlayer),
|
|
new LootCargoMessage(winner, loserId, available));
|
|
} else {
|
|
for (Goods g : loot) {
|
|
if (!available.contains(g)) {
|
|
return serverPlayer.clientError("Invalid loot: " + g);
|
|
}
|
|
available.remove(g);
|
|
if (!winner.canAdd(g)) {
|
|
return serverPlayer.clientError("Loot failed: " + g);
|
|
}
|
|
winner.add(g);
|
|
}
|
|
|
|
// Others can see cargo capacity change.
|
|
session.complete(cs);
|
|
cs.add(See.perhaps(), winner);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Respond to a monarch action.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is to respond.
|
|
* @param action The {@code MonarchAction} to respond to.
|
|
* @param result The player response.
|
|
* @return A {@code ChangeSet} containing the response.
|
|
*/
|
|
public ChangeSet monarchAction(ServerPlayer serverPlayer,
|
|
MonarchAction action, boolean result) {
|
|
MonarchSession session = Session.lookup(MonarchSession.class,
|
|
serverPlayer.getId(), "");
|
|
if (session == null) {
|
|
return serverPlayer.clientError("Bogus monarch action: " + action);
|
|
} else if (action != session.getAction()) {
|
|
return serverPlayer.clientError("Session action mismatch, "
|
|
+ session.getAction() + " expected: " + action);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
session.complete(result, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Move a unit.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is moving.
|
|
* @param unit The {@code ServerUnit} to move.
|
|
* @param newTile The {@code Tile} to move to.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet move(ServerPlayer serverPlayer, ServerUnit unit,
|
|
Tile newTile) {
|
|
ChangeSet cs = new ChangeSet();
|
|
unit.csMove(newTile, random, cs);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Move a unit across the high seas.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to move.
|
|
* @param destination The {@code Location} to move to.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet moveTo(ServerPlayer serverPlayer, Unit unit,
|
|
Location destination) {
|
|
ChangeSet cs = new ChangeSet();
|
|
HighSeas highSeas = serverPlayer.getHighSeas();
|
|
Location current = unit.getDestination();
|
|
List<Location> destinations = highSeas.getDestinations();
|
|
boolean others = false; // Notify others?
|
|
boolean invalid = false; // Not a highSeas move?
|
|
|
|
if (!unit.getType().canMoveToHighSeas()) {
|
|
invalid = true;
|
|
} else if (destination instanceof Europe) {
|
|
if (!destinations.contains(destination)) {
|
|
return serverPlayer.clientError("HighSeas does not connect to: "
|
|
+ destination.getId()
|
|
+ " in " + highSeas.destinationsToString());
|
|
} else if (unit.getLocation() == highSeas) {
|
|
if (!(current instanceof Europe)) {
|
|
// Changed direction
|
|
unit.setWorkLeft(unit.getSailTurns()
|
|
- unit.getWorkLeft() + 1);
|
|
}
|
|
unit.setDestination(destination);
|
|
cs.add(See.only(serverPlayer), unit, highSeas);
|
|
} else if (unit.hasTile()) {
|
|
Tile tile = unit.getTile();
|
|
unit.setEntryLocation(tile);
|
|
unit.setWorkLeft(unit.getSailTurns());
|
|
unit.setDestination(destination);
|
|
unit.setMovesLeft(0);
|
|
unit.setLocation(highSeas);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
cs.addDisappear(serverPlayer, tile, unit);
|
|
cs.add(See.only(serverPlayer), tile, highSeas);
|
|
others = true;
|
|
} else {
|
|
invalid = true;
|
|
}
|
|
boolean hasMap = false;
|
|
for (Location dest : destinations) {
|
|
if (dest instanceof Map) {
|
|
hasMap = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!hasMap) {
|
|
Map map = getGame().getMap();
|
|
destinations.add(map);
|
|
}
|
|
} else if (destination instanceof Map) {
|
|
if (!destinations.contains(destination)) {
|
|
return serverPlayer.clientError("HighSeas does not connect to: "
|
|
+ destination.getId()
|
|
+ " in " + highSeas.destinationsToString());
|
|
} else if (unit.getLocation() == highSeas) {
|
|
if (current != destination && (current == null
|
|
|| current.getTile() == null
|
|
|| current.getTile().getMap() != destination)) {
|
|
// Changed direction
|
|
unit.setWorkLeft(unit.getSailTurns()
|
|
- unit.getWorkLeft() + 1);
|
|
}
|
|
unit.setDestination(destination);
|
|
cs.add(See.only(serverPlayer), highSeas);
|
|
} else if (unit.getLocation() instanceof Europe) {
|
|
Europe europe = (Europe) unit.getLocation();
|
|
unit.setWorkLeft(unit.getSailTurns());
|
|
unit.setDestination(destination);
|
|
unit.setMovesLeft(0);
|
|
unit.setLocation(highSeas);//-vis: safe!map
|
|
cs.add(See.only(serverPlayer), europe, highSeas);
|
|
} else {
|
|
invalid = true;
|
|
}
|
|
} else if (destination instanceof Settlement) {
|
|
Tile tile = destination.getTile();
|
|
if (!destinations.contains(tile.getMap())) {
|
|
return serverPlayer.clientError("HighSeas does not connect to: "
|
|
+ destination.getId() + "/" + tile.getMap().getId()
|
|
+ " in " + highSeas.destinationsToString());
|
|
} else if (unit.getLocation() == highSeas) {
|
|
// Direction is somewhat moot, so just reset.
|
|
unit.setWorkLeft(unit.getSailTurns());
|
|
unit.setDestination(destination);
|
|
cs.add(See.only(serverPlayer), highSeas);
|
|
} else if (unit.getLocation() instanceof Europe) {
|
|
Europe europe = (Europe) unit.getLocation();
|
|
unit.setWorkLeft(unit.getSailTurns());
|
|
unit.setDestination(destination);
|
|
unit.setMovesLeft(0);
|
|
unit.setLocation(highSeas);//-vis: safe!map
|
|
cs.add(See.only(serverPlayer), europe, highSeas);
|
|
} else {
|
|
invalid = true;
|
|
}
|
|
} else {
|
|
return serverPlayer.clientError("Bogus moveTo destination: "
|
|
+ destination.getId());
|
|
}
|
|
if (invalid) {
|
|
return serverPlayer.clientError("Invalid moveTo: unit=" + unit.getId()
|
|
+ " from=" + unit.getLocation().getId()
|
|
+ " to=" + destination.getId());
|
|
}
|
|
|
|
if (others) getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get a nation summary.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} to make the summary for.
|
|
* @param player The {@code Player} to summarize.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet nationSummary(ServerPlayer serverPlayer, Player player) {
|
|
return ChangeSet.simpleChange(serverPlayer,
|
|
new NationSummaryMessage(player,
|
|
new NationSummary(player, serverPlayer)));
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle first contact between European and native player.
|
|
*
|
|
* Note that we check for a diplomacy session, but only bother in
|
|
* the case of tile!=null as that is the only possibility for some
|
|
* benefit.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} making contact.
|
|
* @param other The native {@code Player} to contact.
|
|
* @param tile A {@code Tile} on offer at first landing.
|
|
* @param result Whether the initial peace treaty was accepted.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet nativeFirstContact(ServerPlayer serverPlayer,
|
|
Player other, Tile tile,
|
|
boolean result) {
|
|
ChangeSet cs = new ChangeSet();
|
|
DiplomacySession session = null;
|
|
if (tile != null) {
|
|
Unit u = tile.getFirstUnit();
|
|
Settlement s = tile.getOwningSettlement();
|
|
if (u != null && s != null) {
|
|
session = DiplomacySession.findContactSession(u, s);
|
|
}
|
|
}
|
|
if (result) {
|
|
if (tile != null) {
|
|
if (session == null) {
|
|
return serverPlayer.clientError("No diplomacy for: "
|
|
+ tile.getId());
|
|
}
|
|
tile.cacheUnseen();//+til
|
|
tile.changeOwnership(serverPlayer, null);//-til
|
|
cs.add(See.perhaps(), tile);
|
|
}
|
|
} else {
|
|
// Consider not accepting the treaty to be an insult and
|
|
// ban missions.
|
|
((ServerPlayer)other).csModifyTension(serverPlayer,
|
|
Tension.TENSION_ADD_MAJOR, cs);//+til
|
|
((ServerPlayer)other).addMissionBan(serverPlayer);
|
|
}
|
|
if (session != null) session.complete(result, cs);
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* A native unit delivers its gift to a colony.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is delivering.
|
|
* @param unit The {@code Unit} that is delivering.
|
|
* @param colony The {@code Colony} to deliver to.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet nativeGift(ServerPlayer serverPlayer,
|
|
Unit unit, Colony colony) {
|
|
final Goods goods = first(unit.getGoodsList());
|
|
if (goods == null) {
|
|
return serverPlayer.clientError("No gift to deliver: "
|
|
+ unit.getId());
|
|
}
|
|
final Player otherPlayer = colony.getOwner();
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
GoodsLocation.moveGoods(unit, goods.getType(), goods.getAmount(), colony);
|
|
cs.add(See.perhaps(), unit);
|
|
|
|
// Inform the receiver of the gift.
|
|
ModelMessage m = new ModelMessage(MessageType.GIFT_GOODS,
|
|
"deliverGift.goods",
|
|
colony, goods.getType())
|
|
.addStringTemplate("%player%", serverPlayer.getNationLabel())
|
|
.addNamed("%type%", goods)
|
|
.addAmount("%amount%", goods.getAmount())
|
|
.addName("%settlement%", colony.getName());
|
|
cs.addMessage(otherPlayer, m);
|
|
cs.add(See.only(otherPlayer), colony);
|
|
logger.info("Gift delivered by unit: " + unit.getId()
|
|
+ " to colony " + colony.getName() + ": " + goods);
|
|
|
|
// Others might see unit capacity?
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle native trade sessions.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is trading.
|
|
* @param action The {@code NativeTradeAction} to perform.
|
|
* @param nt The {@code NativeTrade} underway.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
@SuppressFBWarnings(value="SF_SWITCH_FALLTHROUGH")
|
|
public ChangeSet nativeTrade(ServerPlayer serverPlayer,
|
|
NativeTradeAction action, NativeTrade nt) {
|
|
final Unit unit = nt.getUnit();
|
|
final IndianSettlement is = nt.getIndianSettlement();
|
|
final Player otherPlayer = (serverPlayer.owns(unit))
|
|
? is.getOwner() : unit.getOwner();
|
|
|
|
// Server view of the transaction is kept in the NativeTradeSession,
|
|
// which is always updated with what the native player says, and
|
|
// only partially update with what the human player says.
|
|
NativeTradeSession session = Session.lookup(NativeTradeSession.class,
|
|
unit, is);
|
|
if (action.isEuropean() != serverPlayer.isEuropean()) {
|
|
return serverPlayer.clientError(((action.isEuropean())
|
|
? "European" : "Native")
|
|
+ " player expected for " + action
|
|
+ ": " + serverPlayer.getSuffix());
|
|
} else if (action == NativeTradeAction.OPEN && session != null) {
|
|
return serverPlayer.clientError("Session already open for: " + nt);
|
|
} else if (action != NativeTradeAction.OPEN && session == null) {
|
|
return serverPlayer.clientError("No session found for: " + nt);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
NativeTradeItem item;
|
|
switch (action) {
|
|
case OPEN: // Open a new session if possible
|
|
if (unit.getMovesLeft() <= 0) {
|
|
return serverPlayer.clientError("Unit " + unit.getId()
|
|
+ " has no moves left.");
|
|
}
|
|
// Register a session and ask the natives to price the goods.
|
|
nt = NativeTradeSession.openSession(nt);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case CLOSE: // Just close the session
|
|
session.complete(cs);
|
|
break;
|
|
|
|
case BUY: // Check goods (not whole item, price might be wrong), forward
|
|
item = nt.getItem();
|
|
nt.mergeFrom(session.getNativeTrade());
|
|
if (item == null) {
|
|
return serverPlayer.clientError("Null purchase: " + nt);
|
|
} else if (!nt.canBuy()) {
|
|
return serverPlayer.clientError("Can not buy: " + nt);
|
|
} else if (find(nt.getSettlementToUnit(),
|
|
item.goodsMatcher()) == null) {
|
|
return serverPlayer.clientError("Item missing for "
|
|
+ action + ": " + nt);
|
|
} else if (!serverPlayer.checkGold(item.getPrice())) {
|
|
return serverPlayer.clientError("Player can not afford item: "
|
|
+ nt);
|
|
}
|
|
nt.setItem(item);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case SELL: // Check goods, forward
|
|
item = nt.getItem();
|
|
nt.mergeFrom(session.getNativeTrade());
|
|
if (item == null) {
|
|
return serverPlayer.clientError("Null sale: " + nt);
|
|
} else if (item.priceIsSet() && !nt.canSell()) {
|
|
return serverPlayer.clientError("Can not sell: " + nt);
|
|
} else if (find(nt.getUnitToSettlement(),
|
|
item.goodsMatcher()) == null) {
|
|
return serverPlayer.clientError("Item missing for "
|
|
+ action + ": " + nt);
|
|
}
|
|
nt.setItem(item);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case GIFT: // Check goods, forward
|
|
item = nt.getItem();
|
|
nt.mergeFrom(session.getNativeTrade());
|
|
if (item == null) {
|
|
return serverPlayer.clientError("Null gift: " + nt);
|
|
} else if (!nt.canGift()) {
|
|
return serverPlayer.clientError("Can not gift: " + nt);
|
|
} else if (find(nt.getUnitToSettlement(),
|
|
item.goodsMatcher()) == null) {
|
|
return serverPlayer.clientError("Item missing for "
|
|
+ action + ": " + nt);
|
|
}
|
|
nt.setItem(item);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case ACK_OPEN: // Natives are prepared to trade, inform player
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
// Set unit moves to zero to avoid cheating. If no
|
|
// action is taken, the moves will be restored when
|
|
// closing the session.
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(otherPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
break;
|
|
|
|
case ACK_BUY_HAGGLE: case ACK_SELL_HAGGLE: case NAK_GOODS:
|
|
// Successful haggle or polite refusal of gift
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case ACK_BUY: // Buy succeeded, update goods, inform player
|
|
item = nt.getItem();
|
|
csBuy(unit, item.getGoods(), item.getPrice(),
|
|
(ServerIndianSettlement)is, cs);
|
|
nt.setBuy(false);
|
|
nt.addToUnit(item);
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
session.getNativeTrade().setBuy(false);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case ACK_SELL: // Sell succeeded, update goods, inform player
|
|
item = nt.getItem();
|
|
csSell(unit, item.getGoods(), item.getPrice(),
|
|
(ServerIndianSettlement)is, cs);
|
|
nt.setSell(false);
|
|
nt.removeFromUnit(item);
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
session.getNativeTrade().setSell(false);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case ACK_GIFT: // Gift succeeded, update goods, inform player
|
|
item = nt.getItem();
|
|
csGift(unit, item.getGoods(), item.getPrice(),
|
|
(ServerIndianSettlement)is, cs);
|
|
nt.setGift(false);
|
|
nt.removeFromUnit(item);
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
session.getNativeTrade().setGift(false);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
break;
|
|
|
|
case NAK_HAGGLE: case NAK_NOSALE: // Fail, close
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(otherPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
// Fall through
|
|
case NAK_INVALID: case NAK_HOSTILE:
|
|
session.getNativeTrade().mergeFrom(nt);
|
|
cs.add(See.only(otherPlayer),
|
|
new NativeTradeMessage(action, nt));
|
|
session.complete(cs);
|
|
break;
|
|
|
|
default:
|
|
return serverPlayer.clientError("Bogus action: " + action);
|
|
}
|
|
logger.fine("Native trade(" + downCase(action.toString()) + ": " + nt);
|
|
|
|
// Update the other player if needed
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Create a new trade route for a player.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that needs a new route.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet newTradeRoute(ServerPlayer serverPlayer) {
|
|
return ChangeSet.simpleChange(serverPlayer,
|
|
new NewTradeRouteMessage(serverPlayer.newTradeRoute()));
|
|
}
|
|
|
|
|
|
/**
|
|
* Pay arrears.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param type The {@code GoodsType} to pay the arrears for.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet payArrears(ServerPlayer serverPlayer, GoodsType type) {
|
|
int arrears = serverPlayer.getArrears(type);
|
|
if (arrears <= 0) {
|
|
return serverPlayer.clientError("No arrears for pay for: "
|
|
+ type.getId());
|
|
} else if (!serverPlayer.checkGold(arrears)) {
|
|
return serverPlayer.clientError("Not enough gold to pay arrears for: "
|
|
+ type.getId());
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
Market market = serverPlayer.getMarket();
|
|
serverPlayer.modifyGold(-arrears);
|
|
market.setArrears(type, 0);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
cs.add(See.only(serverPlayer), market.getMarketData(type));
|
|
// Arrears payment is private.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Pay for a building.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the
|
|
* colony.
|
|
* @param colony The {@code Colony} that is building.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet payForBuilding(ServerPlayer serverPlayer, Colony colony) {
|
|
if (!getGame().getSpecification()
|
|
.getBoolean(GameOptions.PAY_FOR_BUILDING)) {
|
|
return serverPlayer.clientError("Pay for building is disabled");
|
|
}
|
|
|
|
BuildableType build = colony.getCurrentlyBuilding();
|
|
if (build == null) {
|
|
return serverPlayer.clientError("Colony " + colony.getId()
|
|
+ " is not building anything!");
|
|
}
|
|
List<AbstractGoods> required = colony.getRequiredGoods(build);
|
|
int price = colony.priceGoodsForBuilding(required);
|
|
if (!serverPlayer.checkGold(price)) {
|
|
return serverPlayer.clientError("Insufficient funds to pay for build.");
|
|
}
|
|
|
|
// Save the correct final gold for the player, as we are going to
|
|
// use buy() below, but it deducts the normal uninflated price for
|
|
// the goods being bought. We restore this correct amount later.
|
|
int savedGold = serverPlayer.modifyGold(-price);
|
|
serverPlayer.modifyGold(price);
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
GoodsContainer container = colony.getGoodsContainer();
|
|
container.saveState();
|
|
for (AbstractGoods ag : required) {
|
|
GoodsType type = ag.getType();
|
|
int amount = ag.getAmount();
|
|
if (type.isStorable()) {
|
|
// FIXME: should also check canTrade(type, Access.?)
|
|
if ((amount = serverPlayer.buyInEurope(random, container,
|
|
type, amount)) < 0) {
|
|
return serverPlayer.clientError("Can not buy " + amount
|
|
+ " " + type + " for " + build);
|
|
}
|
|
serverPlayer.csFlushMarket(type, cs);
|
|
} else {
|
|
container.addGoods(type, amount);
|
|
}
|
|
}
|
|
colony.invalidateCache();
|
|
|
|
// Nothing to see for others, colony internal.
|
|
serverPlayer.setGold(savedGold);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
cs.add(See.only(serverPlayer), container);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Put outside colony.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to be put out.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet putOutsideColony(ServerPlayer serverPlayer, Unit unit) {
|
|
Tile tile = unit.getTile();
|
|
Colony colony = unit.getColony();
|
|
if (unit.isInColony()) tile.cacheUnseen();//+til
|
|
unit.setLocation(tile);//-vis: safe/colony,-til if in colony
|
|
|
|
// Full tile update for the player, the rest get their limited
|
|
// view of the colony so that population changes.
|
|
ChangeSet cs = new ChangeSet();
|
|
cs.add(See.only(serverPlayer), tile);
|
|
cs.add(See.perhaps().except(serverPlayer), colony);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Rearrange a colony.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is querying.
|
|
* @param colony The {@code Colony} to rearrange.
|
|
* @param arrangements A list of {@code Arrangement}s to apply.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
@SuppressFBWarnings(value="SF_SWITCH_FALLTHROUGH")
|
|
public ChangeSet rearrangeColony(ServerPlayer serverPlayer, Colony colony,
|
|
List<Arrangement> arrangements) {
|
|
final Role defaultRole = getGame().getSpecification().getDefaultRole();
|
|
Tile tile = colony.getTile();
|
|
tile.cacheUnseen();//+til
|
|
|
|
// Move everyone out of the way and stockpile their equipment.
|
|
for (Arrangement a : arrangements) {
|
|
a.unit.setLocation(tile);//-til
|
|
if (!a.unit.hasDefaultRole()) {
|
|
colony.equipForRole(a.unit, defaultRole, 0);
|
|
}
|
|
}
|
|
|
|
List<Arrangement> todo = new ArrayList<>(arrangements);
|
|
while (!todo.isEmpty()) {
|
|
Arrangement a = todo.remove(0);
|
|
if (a.loc == tile) continue;
|
|
WorkLocation wl = (WorkLocation)a.loc;
|
|
// Adding to wl can fail, and in the worst case there
|
|
// might be a circular dependency. If the move can
|
|
// succeed, do it, but if not, retry.
|
|
switch (wl.getNoAddReason(a.unit)) {
|
|
case NONE:
|
|
a.unit.setLocation(wl);
|
|
// Fall through
|
|
case ALREADY_PRESENT:
|
|
if (a.unit.getWorkType() != a.work) {
|
|
a.unit.changeWorkType(a.work);
|
|
}
|
|
break;
|
|
case CAPACITY_EXCEEDED:
|
|
todo.add(todo.size(), a);
|
|
break;
|
|
default:
|
|
logger.warning("Bad move for " + a.unit + " to " + wl);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Collect roles that cause a change, ordered by simplest change
|
|
for (Arrangement a : transform(arrangements,
|
|
a -> a.role != defaultRole && a.role != a.unit.getRole(),
|
|
Function.<Arrangement>identity(),
|
|
Arrangement::roleComparison)) {
|
|
if (!colony.equipForRole(a.unit, a.role, a.roleCount)) {
|
|
return serverPlayer.clientError("Failed to equip "
|
|
+ a.unit.getId() + " for role " + a.role
|
|
+ " at " + colony);
|
|
}
|
|
}
|
|
|
|
// Just update the whole tile, including for other players
|
|
// which might see colony population change.
|
|
return new ChangeSet().add(See.perhaps(), tile);
|
|
}
|
|
|
|
|
|
/**
|
|
* Rename an object.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is naming.
|
|
* @param object The {@code Nameable} to rename.
|
|
* @param newName The new name.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet renameObject(ServerPlayer serverPlayer, Nameable object,
|
|
String newName) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
if (object instanceof Settlement) {
|
|
((Settlement)object).getTile().cacheUnseen();//+til
|
|
}
|
|
object.setName(newName);//-til?
|
|
FreeColGameObject fcgo = (FreeColGameObject)object;
|
|
cs.addPartial(See.all(), fcgo, "name", newName);
|
|
|
|
// Others may be able to see the name change.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Handle a player retiring.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is retiring.
|
|
* @return A {@code ChangeSet} containing the response.
|
|
*/
|
|
public ChangeSet retire(ServerPlayer serverPlayer) {
|
|
boolean highScore = HighScore.newHighScore(serverPlayer);
|
|
ChangeSet cs = new ChangeSet();
|
|
serverPlayer.csWithdraw(cs, null, null); // Clean up the player.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
cs.addAttribute(See.only(serverPlayer),
|
|
"highScore", Boolean.toString(highScore));
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Scout a native settlement, that is, the contacting action
|
|
* that generates the greeting dialog.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is scouting.
|
|
* @param unit The scout {@code Unit}.
|
|
* @param is The {@code IndianSettlement} to scout.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet scoutIndianSettlement(ServerPlayer serverPlayer,
|
|
Unit unit, IndianSettlement is) {
|
|
final Player owner = is.getOwner();
|
|
ChangeSet cs = new ChangeSet();
|
|
Tile tile = is.getTile();
|
|
|
|
csVisit(serverPlayer, is, -1, cs);
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
cs.add(See.only(serverPlayer), new NationSummaryMessage(owner,
|
|
new NationSummary(owner, serverPlayer)));
|
|
|
|
// This is private.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Speak to the chief at a native settlement.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is scouting.
|
|
* @param unit The scout {@code Unit}.
|
|
* @param is The {@code IndianSettlement} to scout.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet scoutSpeakToChief(ServerPlayer serverPlayer,
|
|
Unit unit, IndianSettlement is) {
|
|
ChangeSet cs = new ChangeSet();
|
|
Tile tile = is.getTile();
|
|
boolean tileDirty = is.setVisited(serverPlayer);
|
|
String result;
|
|
|
|
// Hateful natives kill the scout right away.
|
|
Tension tension = is.getAlarm(serverPlayer);
|
|
if (tension.getLevel() == Tension.Level.HATEFUL) {
|
|
Location loc = unit.getLocation();
|
|
cs.add(See.perhaps().always(serverPlayer), (FreeColGameObject)loc);
|
|
((ServerUnit)unit).csRemove(See.perhaps().always(serverPlayer),
|
|
loc, cs);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
result = "die";
|
|
} else {
|
|
// Otherwise player gets to visit, and learn about the settlement.
|
|
List<UnitType> scoutTypes = getGame().getSpecification()
|
|
.getUnitTypesWithAbility(Ability.EXPERT_SCOUT);
|
|
UnitType scoutSkill = first(scoutTypes);
|
|
int radius = unit.getLineOfSight();
|
|
UnitType skill = is.getLearnableSkill();
|
|
int rnd = randomInt(logger, "scouting", random, 10);
|
|
if (is.hasAnyScouted()) {
|
|
// Do nothing if already spoken to.
|
|
result = "nothing";
|
|
} else if (scoutSkill != null && unit.getType() != scoutSkill
|
|
&& ((skill != null && skill.hasAbility(Ability.EXPERT_SCOUT))
|
|
|| rnd == 0)) {
|
|
// If the scout can be taught to be an expert it will be.
|
|
unit.changeType(scoutSkill);//-vis(serverPlayer)
|
|
serverPlayer.invalidateCanSeeTiles();//+vis(serverPlayer)
|
|
result = "expert";
|
|
} else {
|
|
// Choose tales 1/3 of the time, or if there are no beads.
|
|
RandomRange gifts = is.getType().getGifts();
|
|
int gold = (gifts == null) ? 0
|
|
: gifts.getAmount("Base beads amount", random, true);
|
|
if (gold <= 0 || rnd <= 3) {
|
|
radius = Math.max(radius, IndianSettlement.TALES_RADIUS);
|
|
result = "tales";
|
|
} else {
|
|
if (unit.hasAbility(Ability.EXPERT_SCOUT)) {
|
|
gold = (gold * 11) / 10; // FIXME: magic number
|
|
}
|
|
serverPlayer.modifyGold(gold);
|
|
is.getOwner().modifyGold(-gold);
|
|
result = Integer.toString(gold);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()),
|
|
"score", String.valueOf(serverPlayer.getScore()));
|
|
}
|
|
}
|
|
|
|
// Have now spoken to the chief.
|
|
csVisit(serverPlayer, is, 1, cs);
|
|
tileDirty = true;
|
|
|
|
// Update settlement tile with new information, and any
|
|
// newly visible tiles, possibly with enhanced radius.
|
|
Set<Tile> tiles = transform(tile.getSurroundingTiles(1, radius),
|
|
t -> !serverPlayer.canSee(t) && (t.isLand() || t.isShore()),
|
|
Function.<Tile>identity(), Collectors.toSet());
|
|
cs.add(See.only(serverPlayer), serverPlayer.exploreTiles(tiles));
|
|
|
|
// If the unit was promoted, update it completely, otherwise just
|
|
// update moves and possibly gold+score.
|
|
unit.setMovesLeft(0);
|
|
if ("expert".equals(result)) {
|
|
cs.add(See.perhaps(), unit);
|
|
} else {
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
}
|
|
}
|
|
if (tileDirty) {
|
|
tile.updateIndianSettlement(serverPlayer);
|
|
cs.add(See.only(serverPlayer), tile);
|
|
}
|
|
|
|
// Always add result.
|
|
cs.add(See.only(serverPlayer),
|
|
new ScoutSpeakToChiefMessage(unit, is, result));
|
|
|
|
// Other players may be able to see unit disappearing, or
|
|
// learning.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Sell goods in Europe.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is selling.
|
|
* @param type The {@code GoodsType} to sell.
|
|
* @param amount The amount of goods to sell.
|
|
* @param carrier The {@code Unit} carrying the goods.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
private ChangeSet sellGoods(ServerPlayer serverPlayer, GoodsType type,
|
|
int amount, Unit carrier) {
|
|
ChangeSet cs = new ChangeSet();
|
|
GoodsContainer container = carrier.getGoodsContainer();
|
|
container.saveState();
|
|
if (serverPlayer.canTrade(type, Access.EUROPE)) {
|
|
int gold = serverPlayer.getGold();
|
|
int sellAmount = serverPlayer.sellInEurope(random, container,
|
|
type, amount);
|
|
if (sellAmount < 0) {
|
|
return serverPlayer.clientError("Player " + serverPlayer.getName()
|
|
+ " tried to sell " + amount + " " + type.getSuffix());
|
|
}
|
|
serverPlayer.csFlushMarket(type, cs);
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
logger.finest(carrier + " sold " + amount + "(" + sellAmount + ")"
|
|
+ " " + type.getSuffix()
|
|
+ " in Europe for " + (serverPlayer.getGold() - gold));
|
|
} else {
|
|
// Dumping goods in Europe
|
|
GoodsLocation.moveGoods(carrier, type, amount, null);
|
|
logger.finest(carrier + " dumped " + amount
|
|
+ " " + type.getSuffix() + " in Europe");
|
|
}
|
|
carrier.setMovesLeft(0);
|
|
cs.add(See.only(serverPlayer), carrier);
|
|
// Action occurs in Europe, nothing is visible to other players.
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set build queue.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the colony.
|
|
* @param colony The {@code Colony} to set the queue of.
|
|
* @param queue The new build queue.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setBuildQueue(ServerPlayer serverPlayer, Colony colony,
|
|
List<BuildableType> queue) {
|
|
BuildableType current = colony.getCurrentlyBuilding();
|
|
colony.setBuildQueue(queue);
|
|
if (getGame().getSpecification()
|
|
.getBoolean(GameOptions.CLEAR_HAMMERS_ON_CONSTRUCTION_SWITCH)
|
|
&& current != colony.getCurrentlyBuilding()) {
|
|
for (AbstractGoods ag : transform(current.getRequiredGoods(),
|
|
g -> !g.getType().isStorable())) {
|
|
colony.removeGoods(ag.getType());
|
|
}
|
|
}
|
|
colony.invalidateCache();
|
|
|
|
// Only visible to player.
|
|
ChangeSet cs = new ChangeSet();
|
|
cs.add(See.only(serverPlayer), colony);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a unit stop.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to set the destination for.
|
|
* @param index The stop index.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setCurrentStop(ServerPlayer serverPlayer, Unit unit,
|
|
int index) {
|
|
TradeRoute tr = unit.getTradeRoute();
|
|
if (tr == null) {
|
|
return serverPlayer.clientError("Unit has no trade route to set stop for.");
|
|
} else if (index < 0 || index >= tr.getStopCount()) {
|
|
return serverPlayer.clientError("Stop index out of range [0.."
|
|
+ tr.getStopCount() + "]: " + index);
|
|
}
|
|
|
|
unit.setCurrentStop(index);
|
|
|
|
// Others can not see a stop change.
|
|
return new ChangeSet().add(See.only(serverPlayer), unit);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set a unit destination.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to set the destination for.
|
|
* @param destination The {@code Location} to set as destination.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setDestination(ServerPlayer serverPlayer, Unit unit,
|
|
Location destination) {
|
|
if (unit.getTradeRoute() != null) {
|
|
// Override destination to bring the unit to port.
|
|
if (destination == null && unit.isAtSea()) {
|
|
destination = unit.getStop().getLocation();
|
|
}
|
|
unit.setTradeRoute(null);
|
|
}
|
|
unit.setDestination(destination);
|
|
|
|
// Others can not see a destination change.
|
|
return new ChangeSet().add(See.only(serverPlayer), unit);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set goods levels.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the colony.
|
|
* @param colony The {@code Colony} to set the goods levels in.
|
|
* @param exportData The new {@code ExportData}.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setGoodsLevels(ServerPlayer serverPlayer, Colony colony,
|
|
ExportData exportData) {
|
|
colony.setExportData(exportData);
|
|
return new ChangeSet().add(See.only(serverPlayer), colony);
|
|
}
|
|
|
|
|
|
/**
|
|
* Set land name.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} who landed.
|
|
* @param unit The {@code Unit} that has come ashore.
|
|
* @param name The new land name.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setNewLandName(ServerPlayer serverPlayer, Unit unit,
|
|
String name) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
// Special case of a welcome from an adjacent native unit,
|
|
// offering the land the landing unit is on if a peace treaty
|
|
// is accepted.
|
|
serverPlayer.setNewLandName(name);
|
|
|
|
// Update the name and note the history.
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"newLandName", name);
|
|
Turn turn = serverPlayer.getGame().getTurn();
|
|
HistoryEvent h = new HistoryEvent(turn,
|
|
HistoryEvent.HistoryEventType.DISCOVER_NEW_WORLD, serverPlayer)
|
|
.addName("%name%", name);
|
|
cs.addHistory(serverPlayer, h);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set region name.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} discovering.
|
|
* @param unit The {@code Unit} that is discovering.
|
|
* @param region The {@code Region} to discover.
|
|
* @param name The new region name.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet setNewRegionName(ServerPlayer serverPlayer, Unit unit,
|
|
Region region, String name) {
|
|
final Game game = getGame();
|
|
ServerRegion serverRegion = (ServerRegion)region;
|
|
// Discoverer is set when unit moves in.
|
|
if (!Utils.equals(region.getDiscoverer(), unit.getId())) {
|
|
return serverPlayer.clientError("Discoverer mismatch, "
|
|
+ region.getDiscoverer() + " expected, "
|
|
+ unit.getId() + " provided.");
|
|
}
|
|
ChangeSet cs = new ChangeSet();
|
|
serverRegion.csDiscover(serverPlayer, unit, game.getTurn(), name, cs);
|
|
|
|
// Others do find out about region name changes.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Spy on a settlement.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is spying.
|
|
* @param unit The {@code Unit} that is spying.
|
|
* @param settlement The {@code Settlement} to spy on.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet spySettlement(ServerPlayer serverPlayer, Unit unit,
|
|
Settlement settlement) {
|
|
ChangeSet cs = new ChangeSet();
|
|
|
|
logger.info("Spy settlement for " + unit.getId()
|
|
+ " at " + settlement.getId() + "(" + settlement.getName() + ")");
|
|
cs.addSpy(unit, settlement);
|
|
unit.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), unit,
|
|
"movesLeft", String.valueOf(unit.getMovesLeft()));
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Train a unit in Europe.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is demanding.
|
|
* @param type The {@code UnitType} to train.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet trainUnitInEurope(ServerPlayer serverPlayer,
|
|
UnitType type) {
|
|
Europe europe = serverPlayer.getEurope();
|
|
if (europe == null) {
|
|
return serverPlayer.clientError("No Europe to train in.");
|
|
}
|
|
int price = europe.getUnitPrice(type);
|
|
if (price <= 0) {
|
|
return serverPlayer.clientError("Bogus price: " + price);
|
|
} else if (!serverPlayer.checkGold(price)) {
|
|
return serverPlayer.clientError("Not enough gold ("
|
|
+ serverPlayer.getGold() + " < " + price
|
|
+ ") to train " + type);
|
|
}
|
|
|
|
final Game game = getGame();
|
|
final Specification spec = game.getSpecification();
|
|
Role role = (spec.getBoolean(GameOptions.EQUIP_EUROPEAN_RECRUITS))
|
|
? type.getDefaultRole()
|
|
: spec.getDefaultRole();
|
|
Unit unit = new ServerUnit(game, europe, serverPlayer, type,
|
|
role);//-vis: safe, Europe
|
|
unit.setName(serverPlayer.getNameForUnit(type, random));
|
|
serverPlayer.modifyGold(-price);
|
|
((ServerEurope)europe).increasePrice(type, price);
|
|
|
|
// Only visible in Europe
|
|
ChangeSet cs = new ChangeSet();
|
|
cs.addPartial(See.only(serverPlayer), serverPlayer,
|
|
"gold", String.valueOf(serverPlayer.getGold()));
|
|
cs.add(See.only(serverPlayer), europe);
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Unload goods.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that is unloading.
|
|
* @param goodsType The {@code GoodsType} to unload.
|
|
* @param amount The amount of goods to unload.
|
|
* @param carrier The {@code Unit} to unload.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet unloadGoods(ServerPlayer serverPlayer, GoodsType goodsType,
|
|
int amount, Unit carrier) {
|
|
if (carrier.getGoodsCount(goodsType) < amount) {
|
|
return serverPlayer.clientError("Too few goods");
|
|
}
|
|
if (carrier.isInEurope()) {
|
|
return sellGoods(serverPlayer, goodsType, amount, carrier);
|
|
}
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
Settlement settlement = carrier.getSettlement();
|
|
if (settlement != null) {
|
|
GoodsLocation.moveGoods(carrier, goodsType, amount, settlement);
|
|
logger.finest(carrier
|
|
+ " unloaded " + amount + " " + goodsType.getSuffix()
|
|
+ " to " + settlement.getName());
|
|
cs.add(See.only(serverPlayer), settlement.getGoodsContainer());
|
|
cs.add(See.only(serverPlayer), carrier.getGoodsContainer());
|
|
if (carrier.getInitialMovesLeft() != carrier.getMovesLeft()) {
|
|
carrier.setMovesLeft(0);
|
|
cs.addPartial(See.only(serverPlayer), carrier,
|
|
"movesLeft", String.valueOf(carrier.getMovesLeft()));
|
|
}
|
|
} else { // Dump of goods onto a tile
|
|
GoodsLocation.moveGoods(carrier, goodsType, amount, null);
|
|
logger.finest(carrier + " dumped " + amount
|
|
+ " " + goodsType.getSuffix() + " to " + carrier.getLocation());
|
|
cs.add(See.perhaps(), (FreeColGameObject)carrier.getLocation());
|
|
// Others might see a capacity change.
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
}
|
|
return cs;
|
|
}
|
|
|
|
|
|
/**
|
|
* Update a trade route for a player.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} to set trade
|
|
* routes for.
|
|
* @param tradeRoute An uninterned {@code TradeRoute} to update.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet updateTradeRoute(ServerPlayer serverPlayer,
|
|
TradeRoute tradeRoute) {
|
|
final Game game = getGame();
|
|
String name;
|
|
StringTemplate fail;
|
|
TradeRoute tr;
|
|
if (tradeRoute == null || tradeRoute.getId() == null
|
|
|| (name = tradeRoute.getName()) == null) {
|
|
return serverPlayer.clientError("Bogus route");
|
|
} else if ((fail = tradeRoute.verify()) != null) {
|
|
return serverPlayer.clientError(Messages.message(fail));
|
|
} else if ((tr = game.getFreeColGameObject(tradeRoute.getId(),
|
|
TradeRoute.class)) == null) {
|
|
return serverPlayer.clientError("Not an existing trade route: "
|
|
+ tradeRoute.getId());
|
|
}
|
|
tr.copyIn(tradeRoute);
|
|
return new ChangeSet().add(See.only(serverPlayer), tr);
|
|
}
|
|
|
|
|
|
/**
|
|
* Change work location.
|
|
*
|
|
* @param serverPlayer The {@code ServerPlayer} that owns the unit.
|
|
* @param unit The {@code Unit} to change the work location of.
|
|
* @param workLocation The {@code WorkLocation} to change to.
|
|
* @return A {@code ChangeSet} encapsulating this action.
|
|
*/
|
|
public ChangeSet work(ServerPlayer serverPlayer, Unit unit,
|
|
WorkLocation workLocation) {
|
|
final Specification spec = getGame().getSpecification();
|
|
final Colony colony = workLocation.getColony();
|
|
colony.getGoodsContainer().saveState();
|
|
|
|
ChangeSet cs = new ChangeSet();
|
|
Tile tile = workLocation.getWorkTile();
|
|
if (tile != null && tile.getOwningSettlement() != colony) {
|
|
// Claim known free land (because canAdd() succeeded).
|
|
serverPlayer.csClaimLand(tile, colony, 0, cs);
|
|
}
|
|
|
|
colony.equipForRole(unit, spec.getDefaultRole(), 0);
|
|
|
|
// Check for upgrade.
|
|
UnitTypeChange uc = unit.getUnitChange(UnitChangeType.ENTER_COLONY);
|
|
if (uc != null && uc.appliesTo(unit)) {
|
|
unit.changeType(uc.to);//-vis: safe in colony
|
|
}
|
|
|
|
// Change the location.
|
|
// We could avoid updating the whole tile if we knew that this
|
|
// was definitely a move between locations and no student/teacher
|
|
// interaction occurred.
|
|
if (!unit.isInColony()) unit.getColony().getTile().cacheUnseen();//+til
|
|
unit.setLocation(workLocation);//-vis: safe/colony,-til if not in colony
|
|
cs.add(See.perhaps(), colony.getTile());
|
|
// Others can see colony change size
|
|
getGame().sendToOthers(serverPlayer, cs);
|
|
return cs;
|
|
}
|
|
}
|