/** * Copyright (C) 2002-2022 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 . */ package net.sf.freecol.common.model; import static net.sf.freecol.common.util.CollectionUtils.all; import static net.sf.freecol.common.util.CollectionUtils.alwaysTrue; import static net.sf.freecol.common.util.CollectionUtils.any; import static net.sf.freecol.common.util.CollectionUtils.find; import static net.sf.freecol.common.util.CollectionUtils.first; import static net.sf.freecol.common.util.CollectionUtils.flatten; import static net.sf.freecol.common.util.CollectionUtils.forEachMapEntry; import static net.sf.freecol.common.util.CollectionUtils.matchKey; import static net.sf.freecol.common.util.CollectionUtils.matchKeyEquals; import static net.sf.freecol.common.util.CollectionUtils.toList; import static net.sf.freecol.common.util.CollectionUtils.toListNoNulls; import static net.sf.freecol.common.util.CollectionUtils.transform; import static net.sf.freecol.common.util.StringUtils.capitalize; import static net.sf.freecol.common.util.StringUtils.lastPart; import java.io.StringReader; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.UUID; import java.util.function.Predicate; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Stream; import javax.xml.stream.XMLStreamException; import net.sf.freecol.common.i18n.NameCache; import net.sf.freecol.common.io.FreeColXMLReader; import net.sf.freecol.common.io.FreeColXMLWriter; import net.sf.freecol.common.model.Constants.IntegrityType; import net.sf.freecol.common.model.NationOptions.NationState; import net.sf.freecol.common.option.OptionGroup; import net.sf.freecol.common.util.Introspector; import net.sf.freecol.common.util.LogBuilder; import net.sf.freecol.common.util.Utils; /** * The main component of the game model. */ public class Game extends FreeColGameObject { private static final Logger logger = Logger.getLogger(Game.class.getName()); public static final String TAG = "game"; /** Reasons for logging out. */ public static enum LogoutReason { DEFEATED, // Game over, player lost LOGIN, // Logging in again FIXME: this should go away LOGOUT, // Player quits but game continues MAIN_TITLE, // Complete reset back to the MainPanel NEW_GAME, // Part reset back to the NewPanel QUIT, // Player quits and whole game is over RECONNECT, // Implement reconnect }; /** State for the FCGO iterator, out here because it has to be static. */ private static enum FcgoState { INVALID, VALID, CONSUMED, }; /** Map of all classes with corresponding server classes. */ private static final java.util.Map, Class> serverClasses = new HashMap<>(); static { serverClasses.put(net.sf.freecol.common.model.Building.class, net.sf.freecol.server.model.ServerBuilding.class); serverClasses.put(net.sf.freecol.common.model.Colony.class, net.sf.freecol.server.model.ServerColony.class); serverClasses.put(net.sf.freecol.common.model.ColonyTile.class, net.sf.freecol.server.model.ServerColonyTile.class); serverClasses.put(net.sf.freecol.common.model.Europe.class, net.sf.freecol.server.model.ServerEurope.class); serverClasses.put(net.sf.freecol.common.model.Game.class, net.sf.freecol.server.model.ServerGame.class); serverClasses.put(net.sf.freecol.common.model.IndianSettlement.class, net.sf.freecol.server.model.ServerIndianSettlement.class); serverClasses.put(net.sf.freecol.common.model.Region.class, net.sf.freecol.server.model.ServerRegion.class); serverClasses.put(net.sf.freecol.common.model.Player.class, net.sf.freecol.server.model.ServerPlayer.class); serverClasses.put(net.sf.freecol.common.model.Unit.class, net.sf.freecol.server.model.ServerUnit.class); }; /** * Map of class name to class for the location classes, to speed * up game loading. */ private static final java.util.Map> locationClasses = new HashMap<>(); static { locationClasses.put("Building", net.sf.freecol.common.model.Building.class); locationClasses.put("Colony", net.sf.freecol.common.model.Colony.class); locationClasses.put("ColonyTile", net.sf.freecol.common.model.ColonyTile.class); locationClasses.put("Europe", net.sf.freecol.common.model.Europe.class); locationClasses.put("HighSeas", net.sf.freecol.common.model.HighSeas.class); locationClasses.put("IndianSettlement", net.sf.freecol.common.model.IndianSettlement.class); locationClasses.put("Map", net.sf.freecol.common.model.Map.class); locationClasses.put("Tile", net.sf.freecol.common.model.Tile.class); locationClasses.put("Unit", net.sf.freecol.common.model.Unit.class); }; /** The Specification this game uses. */ private Specification specification = null; /** * The next available identifier that can be given to a new * {@code FreeColGameObject}. */ protected int nextId = 1; /** Game UUID, persistent in savegame files */ private UUID uuid = UUID.randomUUID(); /** The client player name, null in the server. */ private String clientUserName; /** All the players in the game. */ protected final List players = new ArrayList<>(); /** A virtual player to use for enemy privateers. */ private Player unknownEnemy = null; /** The map of the New World. */ protected Map map = null; /** * Areas are collections of tiles that can be identified using the * area's ID. Areas may overlap. */ private java.util.Map areas = new HashMap<>(); /** * The current nation options. Mainly used to see if a player * nation is available. */ private NationOptions nationOptions = null; /** The player whose turn it is. */ protected Player currentPlayer = null; /** The current turn. */ private Turn turn = new Turn(1); /** Whether the War of Spanish Succession has already taken place. */ private boolean spanishSuccession = false; /** Initial active unit identifier. */ private String initialActiveUnitId = null; // Serialization not required below. /** * References to all objects created in this game. * Serialization is not needed directly as these must be completely * within { players, unknownEnemy, map } which are directly serialized. */ protected final HashMap> freeColGameObjects; /** The number of removed FCGOs that should trigger a cache clean. */ private static final int REMOVE_GC_THRESHOLD = 64; /** The number of FCGOs removed since last cache clean. */ private int removeCount = 0; /** * A FreeColGameObjectListener to watch the objects in the game. * Usually this is the AIMain instance. * FIXME: is this better done with a property change listener? */ protected FreeColGameObjectListener freeColGameObjectListener = null; /** * Root constructor for games. * * Game.newInstance uses this so it must be public. */ public Game() { super(); // Use the special FCGO Game-specific constructor // freeColGameObjects has to be in place before we can // call internId() this.freeColGameObjects = new HashMap<>(10000); internId("0"); // Games are always id 0 this.clientUserName = null; this.players.clear(); this.unknownEnemy = null; this.map = null; this.nationOptions = null; this.currentPlayer = null; this.spanishSuccession = false; this.initialActiveUnitId = null; this.specification = null; this.removeCount = 0; this.initialized = true; // Explicit initialization needed for Games } /** * Constructor used by the ServerGame constructor. * * @param specification The {@code Specification} for this game. */ protected Game(Specification specification) { this(); setSpecification(specification); } /** * Creates a new {@code Game} instance from a stream. * * @param game A base {@code Game} (not used here, this is a very * special case). * @param xr The {@code FreeColXMLReader} to read from. * @exception XMLStreamException if an error occurs */ public Game(@SuppressWarnings("unused")Game game, FreeColXMLReader xr) throws XMLStreamException { this(); readFromXML(xr); } /** * Special update handler for the pre-game update. * * When starting a new game the server sends an update that * contains the game map. This must be synchronized so that the * client does not race ahead and launch into the game before the * update completes. * * We used to check integrity here, but 1) the server has already * done that and 2) the unexplored Tiles have no type which makes * a lot of integrity failures. * * @param game The update for this {@code Game}. * @return True if the update succeeds. */ public synchronized boolean preGameUpdate(Game game) { return copyIn(game); } /** * Instantiate an uninitialized FreeColGameObject within a game. * * @param The actual return type. * @param game The {@code Game} to instantiate in. * @param returnClass The required {@code FreeColObject} class. * @param server Create a server object if possible. * @return The new uninitialized object, or null on error. */ public static T newInstance(Game game, Class returnClass, boolean server) { // Do not restrict trying the full (Game,String) constructor // to just server objects as there are simpler FCOs that // implement it (e.g. Goods). if (server) { @SuppressWarnings("unchecked") Class sc = (Class)serverClasses.get(returnClass); if (sc != null) returnClass = sc; } try { return Introspector.instantiate(returnClass, new Class[] { Game.class, String.class }, new Object[] { game, (String)null }); // No intern! } catch (Introspector.IntrospectorException ex) { ; // Allow another try on failure } if (game != null && FreeColSpecObject.class.isAssignableFrom(returnClass)) { try { return Introspector.instantiate(returnClass, new Class[] { Specification.class }, new Object[] { game.getSpecification() }); } catch (Introspector.IntrospectorException ex) { logger.log(Level.WARNING, "newInstance(spec) fail for: " + returnClass.getName(), ex); } } else { // Or just use the trivial constructor try { return Introspector.instantiate(returnClass, new Class[] {}, new Object[] {}); } catch (Introspector.IntrospectorException ex) { logger.log(Level.WARNING, "newInstance(trivial) fail for: " + returnClass.getName(), ex); } } return null; } /** * Get the difficulty level of this game. * * @return An {@code OptionGroup} containing the difficulty settings. */ public final OptionGroup getDifficultyOptionGroup() { return specification.getDifficultyOptionGroup(); } /** * Gets the game options associated with this game. * * @return An {@code OptionGroup} containing the game options. */ public OptionGroup getGameOptions() { return specification.getGameOptions(); } /** * Sets the game options associated with this game. * * @param go An {@code OptionGroup} containing the game options. */ public void setGameOptions(OptionGroup go) { specification.setGameOptions(go); } /** * Gets the map generator options associated with this game. * * @return An {@code OptionGroup} containing the map * generator options. */ public OptionGroup getMapGeneratorOptions() { return specification.getMapGeneratorOptions(); } /** * Sets the map generator options associated with this game. * * @param mgo An {@code OptionGroup} containing the map * generator options. */ public void setMapGeneratorOptions(OptionGroup mgo) { specification.setMapGeneratorOptions(mgo); } /** * Stub for routine only meaningful in the server. * * @return Nothing. */ public int getNextId() { throw new RuntimeException("game.getNextId not implemented: " + this); } /** * Gets the {@code FreeColGameObject} with the given identifier. * * @param id The object identifier. * @return The game object, or null if not found. */ public FreeColGameObject getFreeColGameObject(String id) { if (id == null || id.isEmpty()) return null; final WeakReference ro; synchronized (this.freeColGameObjects) { ro = freeColGameObjects.get(id); } if (ro == null) return null; final FreeColGameObject o = ro.get(); if (o == null) { removeFreeColGameObject(id, "missed"); return null; } return o; } /** * Gets the {@code FreeColGameObject} with the specified * identifier and class. * * @param The actual return type. * @param id The object identifier. * @param returnClass The expected class of the object. * @return The game object, or null if not found. */ public T getFreeColGameObject(String id, Class returnClass) { FreeColGameObject fcgo = getFreeColGameObject(id); try { return returnClass.cast(fcgo); } catch (ClassCastException e) { return null; } } /** * Set the mapping between object identifier and object. * * @param id The object identifier. * @param fcgo The {@code FreeColGameObject} to add to this {@code Game}. */ public void setFreeColGameObject(String id, FreeColGameObject fcgo) { if (id == null || id.isEmpty()) { throw new RuntimeException("Null/empty identifier: " + this); } else if (fcgo == null) { throw new RuntimeException("Null FreeColGameObject: " + id); } final WeakReference wr = new WeakReference<>(fcgo); synchronized (this.freeColGameObjects) { this.freeColGameObjects.put(id, wr); } } /** * Add a new {@code FreeColGameObject} with a given identifier. * * @param id The object identifier. * @param fcgo The {@code FreeColGameObject} to add to this {@code Game}. */ public void addFreeColGameObject(String id, FreeColGameObject fcgo) { if (id == null || id.isEmpty()) { throw new RuntimeException("Null/empty identifier: " + this); } else if (fcgo == null) { throw new RuntimeException("Null FreeColGameObject: " + id); } final FreeColGameObject old = getFreeColGameObject(id); if (old != null) { throw new RuntimeException("Tried to replace FCGO " + id + " : " + old.getClass() + " with " + fcgo.getId() + " : " + fcgo.getClass()); } setFreeColGameObject(id, fcgo); notifySetFreeColGameObject(id, fcgo); } /** * Removes the {@code FreeColGameObject} with the specified * identifier. * * @param id The object identifier. * @param reason A reason to remove the object. * @exception IllegalArgumentException If the identifier is null or empty. */ public void removeFreeColGameObject(String id, String reason) { if (id == null || id.isEmpty()) { throw new RuntimeException("Null/empty identifier: " + this); } logger.finest("removeFCGO/" + reason + ": " + id); notifyRemoveFreeColGameObject(id); synchronized (this.freeColGameObjects) { this.freeColGameObjects.remove(id); } // Garbage collect the FCGOs if enough have been removed. if (++removeCount > REMOVE_GC_THRESHOLD) { synchronized (this.freeColGameObjects) { Iterator iter = getFreeColGameObjectIterator(); while (iter.hasNext()) iter.next(); } removeCount = 0; } } /** * Update a {@code FreeColGameObject} from another. * * @param The type of object to update. * @param other The other object. * @param create If true, create the object if it is missing. * @return The resulting object after update. */ public T update(T other, boolean create) { return (other == null) ? null : update(other, other.getFreeColObjectClass(), create); } /** * Update a {@code FreeColGameObject} from another, optionally allowing * missing objects to be created. * * @param The type of object to update. * @param other The other object. * @param returnClass The expected class of the object. * @param create If true, create missing objects. * @return The resulting object after update. */ private T update(T other, Class returnClass, boolean create) { if (other == null) return null; final String id = other.getId(); FreeColGameObject fcgo = getFreeColGameObject(id); if (fcgo == null) { if (create) { fcgo = newInstance(this, returnClass, false); fcgo.setId(id); } else { // Otherwise this is an error logger.warning("Update of missing object: " + id + "\n" + net.sf.freecol.common.debug.FreeColDebugger.stackTraceToString()); return null; } } T t; try { t = returnClass.cast(fcgo); } catch (ClassCastException cce) { // "Can not happen" throw new RuntimeException("Update class clash: " + fcgo.getClass() + " / " + returnClass, cce); } if (!t.copyIn(other)) { // "Can not happen" throw new RuntimeException("Update copy failed: " + id + " onto " + t); } if (create) t.internId(id); return t; } /** * Convenience wrapper to update several {@code FreeColGameObject}s. * * @param The type of object to update. * @param other The collection of objects to update. * @param create If true, create missing objects. * @return The resulting list of updated objects. */ public List update(Collection other, boolean create) { if (other == null) return null; List ret = new ArrayList<>(); for (T t : other) { T nt = update(t, create); if (nt != null) ret.add(nt); } return ret; } /** * Update a {@code FreeColGameObject} from a reference to it in an update. * * @param The type of object to update. * @param other The other object. * @return The resulting object after update. */ public T updateRef(T other) { return (other == null) ? null : updateRef(other, other.getFreeColObjectClass()); } /** * Update a {@code FreeColGameObject} from a reference to it in an update. * * @param The type of object to update. * @param other The other object. * @param returnClass The expected class of the object. * @return The resulting object after update. */ private T updateRef(T other, Class returnClass) { if (other == null) return null; final String id = other.getId(); return getFreeColGameObject(id, returnClass); } /** * Update several {@code FreeColGameObject}s from a list of * references to it in an update. * * @param The type of object to update. * @param other The other object. * @return The resulting object after update. */ public List updateRef(Collection other) { if (other == null) return null; List ret = new ArrayList<>(); for (T t : other) { T nt = updateRef(t); if (nt != null) ret.add(nt); } return ret; } /** * Update a {@code Location} from a reference to it in an update. * * @param loc The {@code Location}. * @return The resulting location after update. */ public Location updateLocationRef(Location loc) { return (loc == null) ? null : findFreeColLocation(loc.getId()); } /** * Convenience wrapper to find a location (which is an interface, * precluding using the typed version of getFreeColGameObject()) * by identifier. * * Use this routine when the object should already be present in the game. * * @param id The object identifier. * @return The {@code Location} if any. */ public Location findFreeColLocation(String id) { FreeColGameObject fcgo = getFreeColGameObject(id); return (fcgo instanceof Location) ? (Location)fcgo : null; } /** * Gets an {@code Iterator} over every registered * {@code FreeColGameObject}. * * This {@code Iterator} should be iterated once in a while * since it cleans the {@code FreeColGameObject} cache. Very * few routines call this any more, so there is a thresholded call * in removeFreeColGameObject to ensure the cache is still * cleaned. Reconsider this if the situation changes. * * Lock freeColGameObjects when using this. * * @return An {@code Iterator} containing every registered * {@code FreeColGameObject}. */ private Iterator getFreeColGameObjectIterator() { return new Iterator() { /** An iterator over the freeColGameObjects map. */ private final Iterator>> it = freeColGameObjects.entrySet().iterator(); /** Read ahead to this next entry. */ private Entry> readAhead = null; /** State of the readahead value. */ private FcgoState fcgoState = FcgoState.INVALID; @Override public boolean hasNext() { if (this.fcgoState == FcgoState.VALID) return true; while (this.it.hasNext()) { this.readAhead = this.it.next(); if (this.readAhead.getValue().get() != null) { this.fcgoState = FcgoState.VALID; return true; } this.fcgoState = FcgoState.CONSUMED; remove(); } return false; } @Override public FreeColGameObject next() { if (!hasNext()) throw new NoSuchElementException(); FreeColGameObject fcgo = this.readAhead.getValue().get(); this.fcgoState = FcgoState.CONSUMED; return fcgo; } @Override public void remove() { if (this.fcgoState == FcgoState.INVALID) { throw new RuntimeException("No current entry: " + this.fcgoState); } final String key = this.readAhead.getKey(); this.fcgoState = FcgoState.INVALID; this.it.remove(); logger.finest("removeFCGO/expire: " + key); notifyRemoveFreeColGameObject(key); } }; } /** * Get a list of all the {@code FreeColGameObjects}. * * @return A suitable list. */ public List getFreeColGameObjectList() { List ret = new ArrayList<>(); synchronized (this.freeColGameObjects) { Iterator iter = getFreeColGameObjectIterator(); while (iter.hasNext()) ret.add(iter.next()); } return ret; } /** * Gets the unique identifier for this game. * A game UUID persists in save game files. * * @return The game {@code UUID}. */ public UUID getUUID () { return this.uuid; } /** * Get players in the game. * * @param predicate A {@code Predicate} to select suitable players with. * @return A list of {@code Player}s. */ protected List getPlayerList(Predicate predicate) { synchronized (this.players) { return transform(this.players, predicate); } } /** * Get players in the game. * * @param predicate A {@code Predicate} to select suitable players with. * @return The stream of {@code Player}s. */ public Stream getPlayers(Predicate predicate) { synchronized (this.players) { return getPlayerList(predicate).stream(); } } /** * Get a particular player in the game by a predicate. * * @param predicate A {@code Predicate} to select suitable players with. * @return The {@code Player} found or null if not present. */ public Player getPlayer(Predicate predicate) { synchronized (this.players) { return find(this.players, predicate); } } /** * Set the players in the game. * * @param players The new {@code Player}s to add. */ private void setPlayers(List players) { synchronized (this.players) { this.players.clear(); if (players != null) this.players.addAll(players); } } /** * Gets the live player after the given player. * * @param beforePlayer The {@code Player} before the * {@code Player} to be returned. * @return The {@code Player} after the {@code beforePlayer} * in the list which determines the order each player becomes the * current player. * @see #getNextPlayer */ private Player getPlayerAfter(Player beforePlayer) { synchronized (this.players) { if (this.players.isEmpty()) return null; final int start = this.players.indexOf(beforePlayer); int index = start; do { if (++index >= this.players.size()) index = 0; Player player = this.players.get(index); if (!player.isUnknownEnemy() && !player.isDead()) return player; } while (index != start); } return null; } /** * Adds the specified player to the game. * * @param player The {@code Player} to add. */ public void addPlayer(Player player) { synchronized (this.players) { if (!this.players.contains(player)) this.players.add(player); } Nation nation = getSpecification().getNation(player.getNationId()); nationOptions.getNations().put(nation, NationState.NOT_AVAILABLE); } /** * Removes the specified player from the game. * * We do not really remove the full FCGO from the game, just from the * active players list. * * @param player The {@code Player} to remove. * @return True if the player was removed. */ public boolean removePlayer(Player player) { Player newCurrent = (currentPlayer != player) ? null : getPlayerAfter(currentPlayer); synchronized (this.players) { if (!this.players.remove(player)) return false; } Nation nation = getSpecification().getNation(player.getNationId()); nationOptions.getNations().put(nation, NationState.AVAILABLE); player.dispose(); if (newCurrent != null) currentPlayer = newCurrent; return true; } /** * Get the first player in this game. * * @return The first player, or null if none present. */ public Player getFirstPlayer() { synchronized (this.players) { return first(this.players); } } /** * Sort the players list. * * @param comparator The {@code Comparator} to sort with. */ public void sortPlayers(Comparator comparator) { synchronized (this.players) { this.players.sort(comparator); } } /** * Gets a player specified by a name. * * @param name The name identifying the {@code Player}. * @return The {@code Player} or null if none found. */ public Player getPlayerByName(String name) { return getPlayer(matchKeyEquals(name, Player::getName)); } /** * Get a {@code Player} identified by its nation. * * @param nation The {@code Nation} to search for. * @return The {@code Player} of the given nation, or null if * not found. */ public Player getPlayerByNation(Nation nation) { return getPlayerByNationId(nation.getId()); } /** * Get a {@code Player} identified by its nation identifier. * * @param nationId The nation identifier to search for. * @return The {@code Player} of the given nation, or null if * not found. */ public Player getPlayerByNationId(String nationId) { return getPlayer(matchKeyEquals(nationId, Player::getNationId)); } /** * Get a list of the live players in the game, optionally * excluding supplied ones. * * @param exclude The {@code Player}s to exclude. * @return A list of live {@code Player}s, without the excluded ones. */ public List getLivePlayerList(final Player... exclude) { final Predicate livePred = p -> !p.isUnknownEnemy() && !p.isDead() && !any(exclude, matchKey(p)); return getPlayerList(livePred); } /** * Get a stream of the live players in the game, optionally excluding * supplied ones. * * @param exclude The {@code Player}s to exclude. * @return A stream of live {@code Player}s, without the * excluded ones. */ public Stream getLivePlayers(final Player... exclude) { return getLivePlayerList(exclude).stream(); } /** * Get a list of the live European players in this game. * * @param exclude {@code Player}s to exclude. * @return A list of live European {@code Player}s in this game, * without the excluded ones. */ public List getLiveEuropeanPlayerList(final Player... exclude) { final Predicate europeanPred = p -> !p.isUnknownEnemy() && !p.isDead() && p.isEuropean() && !any(exclude, matchKey(p)); return getPlayerList(europeanPred); } /** * Get a stream of the live European players in this game. * * @param exclude {@code Player}s to exclude. * @return A stream of live European {@code Player}s in this game, * without the excluded ones. */ public Stream getLiveEuropeanPlayers(final Player... exclude) { return getLiveEuropeanPlayerList(exclude).stream(); } /** * Get a list of the live native players in this game. * * @param exclude {@code Player}s to exclude. * @return A list of live native {@code Player}s in this game. */ public List getLiveNativePlayerList(final Player... exclude) { final Predicate nativePred = p -> !p.isUnknownEnemy() && !p.isDead() && p.isIndian() && !any(exclude, matchKey(p)); return getPlayerList(nativePred); } /** * Get a stream of the live native players in this game. * * @param exclude {@code Player}s to exclude. * @return A stream of live native {@code Player}s in this game. */ public Stream getLiveNativePlayers(final Player... exclude) { return getLiveNativePlayerList(exclude).stream(); } /** * Gets the next current player. * * @return The {@code Player} whose turn follows the current player. */ public Player getNextPlayer() { return getPlayerAfter(currentPlayer); } /** * Add a list of players to this game. * * Called from the pre and in-game controllers when players change. * ATM we never remove players, even dead ones. Must tolerate player * being both present and not present. * * @param players The list of {@code players} to add. */ public void addPlayers(List players) { List valid = new ArrayList<>(); for (Player p : players) { FreeColGameObject fcgo = getFreeColGameObject(p.getId()); if (fcgo == null) { if ((fcgo = update(p, Player.class, true)) != null) { addPlayer((Player)fcgo); logger.finest("addPlayers added new: " + fcgo); } else { logger.warning("addPlayers create new fail: " + p); } } else if (fcgo instanceof Player) { if (fcgo.copyIn(p)) { logger.finest("addPlayers copied in: " + fcgo); } else { logger.warning("addPlayers copyIn existing fail: " + p); } } else { logger.warning("addPlayers onto non-player: " + fcgo); } } } /** * Gets the unknown enemy player, which is used for privateers. * * @return The unknown enemy {@code Player}. */ public Player getUnknownEnemy() { return this.unknownEnemy; } /** * Sets the unknown enemy player. * * @param player The {@code Player} to serve as the unknown enemy. */ public void setUnknownEnemy(Player player) { this.unknownEnemy = player; } /** * Get the client user name. * * @return The client user name. */ public String getClientUserName() { return this.clientUserName; } /** * Are we executing in a client? * * @return True in a client. */ public boolean isInClient() { return this.clientUserName != null; } /** * Are we executing in the server? * * @return True in the server. */ public boolean isInServer() { return this.clientUserName == null; } /** * Get the client player this thread is operating for. If in the server * there will be none. * * @return The client {@code Player}. */ public Player getClientPlayer() { return (this.clientUserName == null) ? null : getPlayerByName(this.clientUserName); } /** * Is this game in revenge mode? * * @return True if an undead player is present. */ public boolean isInRevengeMode() { return getPlayer(Player::isUndead) != null; } /** * Gets the current player. * * @return The current player. */ public Player getCurrentPlayer() { return this.currentPlayer; } /** * Sets the current player. * * @param newCurrentPlayer The new current {@code Player}. */ public void setCurrentPlayer(Player newCurrentPlayer) { this.currentPlayer = newCurrentPlayer; } /** * Gets the map that is being used in this game. * * @return The game {@code Map}. */ public synchronized Map getMap() { return this.map; } /** * Sets the game map. * * @param newMap The new {@code Map} to use. * @return The old {@code Map}. */ public synchronized Map setMap(Map newMap) { Map oldMap = this.map; this.map = newMap; return oldMap; } /** * Change the map in this game, fixing player destinations. * * @param newMap The new {@code Map} to use. */ public void changeMap(Map newMap) { Map oldMap = setMap(newMap); if (newMap != oldMap) { for (HighSeas hs : transform(getLivePlayers(), alwaysTrue(), Player::getHighSeas, toListNoNulls())) { hs.removeDestination(oldMap); hs.addDestination(newMap); } } } /** * Get the current nation options. * * @return The current {@code NationOptions}. */ public final synchronized NationOptions getNationOptions() { return nationOptions; } /** * Set the current nation options. * * Public for the test suite. * * @param newNationOptions The new {@code NationOptions} value. */ public final synchronized void setNationOptions(final NationOptions newNationOptions) { this.nationOptions = newNationOptions; } /** * Find an available (i.e. vacant) nation. * * @return A vacant {@code Nation} or null if none found. */ public Nation getVacantNation() { Entry entry = find(nationOptions.getNations().entrySet(), matchKey(NationState.AVAILABLE, Entry::getValue)); return (entry == null) ? null : entry.getKey(); } /** * Get the currently available nations. * * @return A list of available {@code Nation}s. */ public final List getVacantNations() { return transform(nationOptions.getNations().entrySet(), matchKey(NationState.AVAILABLE, Entry::getValue), Entry::getKey); } /** * Can a new player be added to this game? * * @return True if a new player can be added. */ public boolean canAddNewPlayer() { return getVacantNation() != null; } /** * Gets the current turn in this game. * * @return The current {@code Turn}. */ public Turn getTurn() { return turn; } /** * Sets the current turn in this game. * * @param newTurn The new {@code Turn} to set. */ public void setTurn(Turn newTurn) { turn = newTurn; } /** * Get the age for the current turn. * * @return The age (0-2). */ public int getAge() { return getSpecification().getAge(turn); } /** * Get the combat model in this game. * * @return The {@code CombatModel}. */ public final CombatModel getCombatModel() { if (specification.hasAbility(Ability.HITPOINTS_COMBAT_MODEL)) { return new HitpointsCombatModel(); } else { return new SimpleCombatModel(); } } /** * Has the Spanish Succession event occured? * * @return True if the Spanish Succession has occurred. */ public final boolean getSpanishSuccession() { return spanishSuccession; } /** * Set the Spanish Succession value. * * @param spanishSuccession The new Spanish Succession value. */ public final void setSpanishSuccession(final boolean spanishSuccession) { this.spanishSuccession = spanishSuccession; } /** * Get the identifier for the initial active unit. * * @return The active unit identifier, if any. */ public String getInitialActiveUnitId() { return this.initialActiveUnitId; } /** * Get the initial active unit. * * @return The initial active {@code Unit} or null if none. */ public Unit getInitialActiveUnit() { return (this.initialActiveUnitId == null) ? null : getFreeColGameObject(this.initialActiveUnitId, Unit.class); } /** * Set the identifier for the initial active unit. * * @param initialActiveUnitId The identifier for the current active unit. */ public void setInitialActiveUnitId(String initialActiveUnitId) { this.initialActiveUnitId = initialActiveUnitId; } /** * Sets the {@code FreeColGameObjectListener} attached to this game. * * @param fcgol The new {@code FreeColGameObjectListener}. */ public void setFreeColGameObjectListener(FreeColGameObjectListener fcgol) { freeColGameObjectListener = fcgol; } /** * Notify a listener (if any) of a new game object. * * @param id The object identifier. * @param fcgo The new {@code FreeColGameObject}. */ public void notifySetFreeColGameObject(String id, FreeColGameObject fcgo) { if (freeColGameObjectListener != null) { freeColGameObjectListener.setFreeColGameObject(id, fcgo); } } /** * Notify a listener (if any) of that a game object has gone. * * @param id The object identifier. */ public void notifyRemoveFreeColGameObject(String id) { if (freeColGameObjectListener != null) { freeColGameObjectListener.removeFreeColGameObject(id); } } /** * Notify a listener (if any) of that a game object has changed owner. * * @param source The {@code FreeColGameObject} that changed owner. * @param oldOwner The old owning {@code Player}. * @param newOwner The new owning {@code Player}. */ public void notifyOwnerChanged(FreeColGameObject source, Player oldOwner, Player newOwner) { if (freeColGameObjectListener != null) { freeColGameObjectListener.ownerChanged(source, oldOwner, newOwner); } } /** * Maintain the player containers for certain ownables. * Mainly useful in the client, informing the player that it has * gained or lost an ownable. * * @param o The {@code Ownable} that may have changed. * @param oldOwner The previous (possible unchanged) owning * {@code Player}. */ public void checkOwners(Ownable o, Player oldOwner) { Player newOwner = o.getOwner(); if (oldOwner == newOwner) return; if (oldOwner != null && oldOwner.removeOwnable(o)) { oldOwner.invalidateCanSeeTiles();//+vis } if (newOwner != null && newOwner.addOwnable(o)) { newOwner.invalidateCanSeeTiles();//+vis } } // Miscellaneous utilities. /** * Checks if all players are ready to launch. * * @return True if all players are ready to launch. */ public boolean allPlayersReadyToLaunch() { return all(getLiveEuropeanPlayerList(), Player::isReady); } /** * Get all the colonies in the game. * * @param player An optional {@code Player} to omit. * @return A stream of all the {@code Colony}s in the game. */ public Stream getAllColonies(Player player) { return flatten(getLiveEuropeanPlayerList(player), Player::getColonies); } /** * Get a list of all the colonies in the game. * * @param player An optional {@code Player} to omit. * @return A list of all the {@code Colony}s in the game. */ public List getAllColoniesList(Player player) { return toList(getAllColonies(player)); } /** * Finds a settlement by name. * * @param name The name of the {@code Settlement}. * @return The {@code Settlement} found, or {@code null} * if there is no known {@code Settlement} with the * specified name (the settlement might not be visible to a client). */ public Settlement getSettlementByName(String name) { return find(flatten(getLivePlayers(), Player::getSettlements), matchKeyEquals(name, Settlement::getName)); } /** * Helper function to get the source object of a message in this game. * * @param message The {@code ModelMessage} to find the object in. * @return The source object. */ public FreeColGameObject getMessageSource(ModelMessage message) { return getFreeColGameObject(message.getSourceId()); } /** * Helper function to get the object to display with a message in * this game. * * @param message The {@code ModelMessage} to find the object in. * @return An object to display. */ public FreeColObject getMessageDisplay(ModelMessage message) { String id = message.getDisplayId(); if (id == null) id = message.getSourceId(); FreeColObject o = getFreeColGameObject(id); if (o == null) { try { o = getSpecification().getType(id); } catch (Exception e) { o = null; // Ignore } } return o; } /** * Gets the statistics of this game. * * @return A {@code Map} of the statistics. */ public java.util.Map getStatistics() { java.util.Map stats = new HashMap<>(); // Memory Utils.garbageCollect(); long free = Runtime.getRuntime().freeMemory()/(1024*1024); long total = Runtime.getRuntime().totalMemory()/(1024*1024); long max = Runtime.getRuntime().maxMemory()/(1024*1024); stats.put("freeMemory", Long.toString(free)); stats.put("totalMemory", Long.toString(total)); stats.put("maxMemory", Long.toString(max)); // Game objects java.util.Map objStats = new HashMap<>(); long disposed = 0; for (FreeColGameObject fcgo : getFreeColGameObjectList()) { String className = fcgo.getClass().getSimpleName(); Long count = objStats.get(className); if (count != null) { count++; objStats.put(className, count); } else { objStats.put(className, 1L); } if (fcgo.isDisposed()) disposed++; } stats.put("disposed", Long.toString(disposed)); forEachMapEntry(objStats, e -> stats.put(e.getKey(), Long.toString(e.getValue()))); return stats; } /** * Get a location class from an identifier. * * @param id The identifier to dissect. * @return The location class. */ public static Class getLocationClass(String id) { return locationClasses.get(capitalize(FreeColObject.getIdTypeByName(id))); } /** * Unserialize from XML to a FreeColObject in this game. * * @param The actual return type. * @param xml The xml serialized version of an object. * @param returnClass The expected object class. * @return The unserialized object. * @exception XMLStreamException if there are any problems reading from * the stream. */ public T unserialize(String xml, Class returnClass) throws XMLStreamException { try { FreeColXMLReader xr = new FreeColXMLReader(new StringReader(xml)); xr.nextTag(); T ret = newInstance(this, returnClass, false); ret.readFromXML(xr); return ret; } catch (Exception ex) { throw new XMLStreamException(ex); } } /** * Generates empty areas that should be made available in the map editor. */ public void generateDefaultAreas() { for (Nation nation : getSpecification().getNations()) { if (nation.isUnknownEnemy()) { continue; } if (nation.getType().isREF()) { continue; } final String nationAreaId = Area.PREFIX_PLAYER_STARTING_POSITION + nation.getId(); if (!areas.containsKey(nationAreaId)) { addArea(new Area(this, nationAreaId, nation.getNameKey())); } } } /** * Gets the starting area for the given nation. * * @param nation The nation to get the area for. * @return The {@code Area}, if it has been defined on the map. It not, * then just {@code null}. */ public Area getNationStartingArea(Nation nation) { final String nationAreaId = Area.PREFIX_PLAYER_STARTING_POSITION + nation.getId(); return areas.get(nationAreaId); } /** * Gets a list of all areas in this game. */ public List getAreas() { return new ArrayList<>(areas.values()); } /** * Adds a new {@code Area} to the game. * @param area The {@code Area} to be added. */ public void addArea(Area area) { areas.put(area.getId(), area); } // Override FreeColGameObject /** * {@inheritDoc} */ @Override public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) { IntegrityType result = super.checkIntegrity(fix, lb); lb.mark(); synchronized (freeColGameObjects) { Iterator iterator = getFreeColGameObjectIterator(); while (iterator.hasNext()) { FreeColGameObject fcgo = iterator.next(); if (fcgo == null) { lb.add(" null-fcgo"); } else if (!fcgo.isInitialized()) { lb.add(" ", fcgo.getId(), "(", lastPart(fcgo.getClass().getName(), "."), ")"); } else { continue; } if (fix) { iterator.remove(); result = result.fix(); } else { result = result.fail(); } } } if (lb.grew("\n Uninitialized game ids: ")) { if (fix) lb.add(" (dropped)"); } Map map = getMap(); if (map != null) { result = result.combine(map.checkIntegrity(fix, lb)); } synchronized (this.players) { for (Player p : this.players) { result = result.combine(p.checkIntegrity(fix, lb)); } } return result; } // Override FreeColObject /** * {@inheritDoc} */ @Override public Specification getSpecification() { return this.specification; } /** * {@inheritDoc} */ @Override public final void setSpecification(Specification specification) { this.specification = specification; } // Override FreeColObject /** * {@inheritDoc} */ @Override public boolean copyIn(T other) { Game o = copyInCast(other, Game.class); if (o == null || !super.copyIn(o)) return false; this.specification = o.getSpecification(); // Do not update nextId, it is not meaningful in the client. this.uuid = o.getUUID(); this.clientUserName = o.getClientUserName(); // Players before map, so the tile owners work addPlayers(o.players); // Allow creation, might be first sight of the map. // Do map early, so map references work changeMap(update(o.getMap(), Map.class, true)); setNationOptions(o.getNationOptions()); this.unknownEnemy = update(o.getUnknownEnemy(), false); this.currentPlayer = updateRef(o.getCurrentPlayer(), Player.class); this.turn = o.getTurn(); this.spanishSuccession = o.getSpanishSuccession(); this.initialActiveUnitId = o.getInitialActiveUnitId(); return true; } // Serialization // Note: The order of the children is really sensitive. // Several fields can not be read without a specification, so it // must be written first if the intent is to use that spec in the // game when it is read again. Similarly we try to fail fast // if required to read those fields if a spec has not shown up. private static final String AREAS_TAG = "areas"; private static final String CIBOLA_TAG = "cibola"; private static final String CLIENT_USER_NAME_TAG = "clientUserName"; private static final String CURRENT_PLAYER_TAG = "currentPlayer"; private static final String INITIAL_ACTIVE_UNIT_ID = "initialActiveUnitId"; private static final String NEXT_ID_TAG = "nextId"; private static final String SPANISH_SUCCESSION_TAG = "spanishSuccession"; private static final String TURN_TAG = "turn"; private static final String UUID_TAG = "UUID"; /** * {@inheritDoc} */ @Override protected void writeAttributes(FreeColXMLWriter xw) throws XMLStreamException { super.writeAttributes(xw); if (xw.validForSave()) { xw.writeAttribute(NEXT_ID_TAG, nextId); } else { Player client = xw.getClientPlayer(); if (client != null) { xw.writeAttribute(CLIENT_USER_NAME_TAG, client.getName()); } } xw.writeAttribute(UUID_TAG, getUUID()); xw.writeAttribute(TURN_TAG, getTurn().getNumber()); xw.writeAttribute(SPANISH_SUCCESSION_TAG, spanishSuccession); if (initialActiveUnitId != null) { xw.writeAttribute(INITIAL_ACTIVE_UNIT_ID, initialActiveUnitId); } if (currentPlayer != null) { xw.writeAttribute(CURRENT_PLAYER_TAG, currentPlayer); } } /** * {@inheritDoc} */ @Override protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException { super.writeChildren(xw); if (this.specification != null) { synchronized (this.specification) { // Specification *must be first* if present. // It is not necessarily present when reading maps, but an // overriding spec is provided there so all should be well. this.specification.toXML(xw); } } for (String cityName : NameCache.getCitiesOfCibola()) { // Preserve existing order xw.writeStartElement(CIBOLA_TAG); xw.writeAttribute(ID_ATTRIBUTE_TAG, cityName); xw.writeEndElement(); } synchronized (this.nationOptions) { this.nationOptions.toXML(xw); } synchronized (this.players) { // Should already be sorted for (Player p : this.players) p.toXML(xw); } Player unknown = getUnknownEnemy(); if (unknown != null) unknown.toXML(xw); Map map = getMap(); if (map != null) { map.toXML(xw); } xw.writeStartElement(AREAS_TAG); for (Area a : areas.values()) { a.toXML(xw); } xw.writeEndElement(); } /** * {@inheritDoc} */ @Override protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException { super.readAttributes(xr); nextId = xr.getAttribute(NEXT_ID_TAG, -1); this.clientUserName = xr.getAttribute(CLIENT_USER_NAME_TAG, (String)null); String str = xr.getAttribute(UUID_TAG, (String)null); if (str == null) { this.uuid = UUID.randomUUID(); } else { try { UUID u = UUID.fromString(str); this.uuid = u; } catch (IllegalArgumentException iae) { ;// Preserve existing uuid } } turn = new Turn(xr.getAttribute(TURN_TAG, 1)); spanishSuccession = xr.getAttribute(SPANISH_SUCCESSION_TAG, false); initialActiveUnitId = xr.getAttribute(INITIAL_ACTIVE_UNIT_ID, (String)null); // currentPlayer handled in readChildren() } /** * {@inheritDoc} */ @Override protected void readChildren(FreeColXMLReader xr) throws XMLStreamException { // Clear containers. NameCache.clearCitiesOfCibola(); players.clear(); unknownEnemy = null; // The current player is special. Defer lookup of the current // player tag until we read the children, because that is // where the players are defined. String current = xr.getAttribute(CURRENT_PLAYER_TAG, (String)null); super.readChildren(xr); currentPlayer = xr.lookup(this, current, Player.class); // Make sure all work locations have rational default production // now that all tiles are defined. for (Colony c : getAllColoniesList(null)) { c.updateProductionTypes(); } } /** * {@inheritDoc} */ @Override protected void readChild(FreeColXMLReader xr) throws XMLStreamException { final Game game = getGame(); final String tag = xr.getLocalName(); if (CIBOLA_TAG.equals(tag)) { String cibola = xr.readId(); // @compat 0.11.3 final String oldPrefix = "lostCityRumour.cityName"; if (cibola.startsWith(oldPrefix)) cibola = "nameCache." + cibola; // end @compat 0.11.3 NameCache.addCityOfCibola(cibola); xr.closeTag(CIBOLA_TAG); } else if (Map.TAG.equals(tag)) { if (this.specification == null) { throw new XMLStreamException("Tried to read " + tag + " with null specification"); } changeMap(xr.readFreeColObject(game, Map.class)); } else if (NationOptions.TAG.equals(tag)) { if (this.specification == null) { throw new XMLStreamException("Tried to read " + tag + " with null specification"); } setNationOptions(new NationOptions(xr, specification)); } else if (Player.TAG.equals(tag)) { if (this.specification == null) { throw new XMLStreamException("Tried to read " + tag + " with null specification"); } Player player = xr.readFreeColObject(game, Player.class); if (player.isUnknownEnemy()) { setUnknownEnemy(player); } else { players.add(player); } } else if (Specification.TAG.equals(tag)) { setSpecification(new Specification(xr)); } else if (AREAS_TAG.equals(tag)) { try { while (xr.moreTags()) { final Area area = xr.readFreeColObject(game, Area.class); areas.put(area.getId(), area); } } catch (XMLStreamException xse) { logger.log(Level.SEVERE, "nextTag failed at " + tag, xse); } } else { super.readChild(xr); } } /** * {@inheritDoc} */ public String getXMLTagName() { return TAG; } // Override Object /** * {@inheritDoc} */ @Override public boolean equals(Object o) { // Two games are not the same just because they have the same // identifier, but to avoid having to check everything in the // Game just insist on object equality for the equals() test, // and accept the basic id-based hashCode(). return this == o; } /** * {@inheritDoc} */ @Override public int hashCode() { return Utils.hashCode(getId()); } }