freecol/src/net/sf/freecol/common/model/Game.java

1888 lines
61 KiB
Java
Raw Normal View History

2007-10-14 14:15:51 +02:00
/**
2022-01-03 01:29:09 +01:00
* Copyright (C) 2002-2022 The FreeCol Team
2007-10-14 14:15:51 +02:00
*
* 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/>.
*/
2004-04-23 00:01:19 +02:00
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;
2016-09-24 03:18:54 +02:00
import java.util.ArrayList;
import java.util.Collection;
2016-09-24 03:18:54 +02:00
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
2009-04-19 09:40:05 +02:00
import java.util.Map.Entry;
2016-09-24 03:18:54 +02:00
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.function.Predicate;
2015-01-26 05:26:20 +01:00
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
2004-04-23 00:01:19 +02:00
import javax.xml.stream.XMLStreamException;
2015-04-14 02:52:27 +02:00
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;
2017-01-22 06:50:25 +01:00
import net.sf.freecol.common.util.Introspector;
2015-01-26 05:26:20 +01:00
import net.sf.freecol.common.util.LogBuilder;
import net.sf.freecol.common.util.Utils;
2004-04-23 00:01:19 +02:00
/**
* The main component of the game model.
*/
2004-04-23 00:01:19 +02:00
public class Game extends FreeColGameObject {
private static final Logger logger = Logger.getLogger(Game.class.getName());
2004-04-23 00:01:19 +02:00
2016-10-10 13:03:19 +02:00
public static final String TAG = "game";
2016-12-17 06:31:56 +01:00
/** 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
2016-12-17 06:31:56 +01:00
};
2015-02-14 07:46:51 +01:00
/** 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<? extends FreeColObject>,
Class<? extends FreeColObject>>
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<String, Class<? extends FreeColGameObject>>
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);
};
2015-02-14 07:46:51 +01:00
2017-05-05 09:07:31 +02:00
/** The Specification this game uses. */
private Specification specification = null;
2010-10-16 13:00:30 +02:00
/**
* The next available identifier that can be given to a new
* {@code FreeColGameObject}.
*/
2013-04-05 12:46:48 +02:00
protected int nextId = 1;
2013-04-05 12:46:48 +02:00
/** Game UUID, persistent in savegame files */
private UUID uuid = UUID.randomUUID();
2004-04-23 00:01:19 +02:00
/** The client player name, null in the server. */
2016-07-25 11:24:57 +02:00
private String clientUserName;
2013-04-05 12:46:48 +02:00
/** All the players in the game. */
2014-11-27 07:23:18 +01:00
protected final List<Player> players = new ArrayList<>();
2013-04-05 12:46:48 +02:00
/** A virtual player to use for enemy privateers. */
2017-10-13 01:42:46 +02:00
private Player unknownEnemy = null;
2004-04-23 00:01:19 +02:00
2013-04-05 12:46:48 +02:00
/** The map of the New World. */
2017-10-06 07:24:36 +02:00
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<String, Area> areas = new HashMap<>();
/**
2013-04-05 12:46:48 +02:00
* The current nation options. Mainly used to see if a player
* nation is available.
*/
2013-04-05 12:46:48 +02:00
private NationOptions nationOptions = null;
/** The player whose turn it is. */
protected Player currentPlayer = null;
2013-04-05 12:46:48 +02:00
/** The current turn. */
private Turn turn = new Turn(1);
2004-04-23 00:01:19 +02:00
/** 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.
*/
2017-10-13 01:42:46 +02:00
protected final HashMap<String, WeakReference<FreeColGameObject>>
2019-02-10 04:43:01 +01:00
freeColGameObjects;
/** The number of removed FCGOs that should trigger a cache clean. */
private static final int REMOVE_GC_THRESHOLD = 64;
2009-05-09 11:12:22 +02:00
/** The number of FCGOs removed since last cache clean. */
private int removeCount = 0;
2008-01-19 14:46:37 +01:00
/**
2013-04-05 12:46:48 +02:00
* A FreeColGameObjectListener to watch the objects in the game.
* Usually this is the AIMain instance.
2014-11-26 05:08:44 +01:00
* FIXME: is this better done with a property change listener?
*/
2013-04-05 12:46:48 +02:00
protected FreeColGameObjectListener freeColGameObjectListener = null;
2012-12-29 13:39:47 +01:00
/**
2017-10-13 01:42:46 +02:00
* Root constructor for games.
*
* Game.newInstance uses this so it must be public.
*/
2017-10-13 01:42:46 +02:00
public Game() {
2019-02-10 04:43:01 +01:00
super(); // Use the special FCGO Game-specific constructor
2016-02-11 00:21:10 +01:00
2019-02-10 04:43:01 +01:00
// freeColGameObjects has to be in place before we can
// call internId()
this.freeColGameObjects = new HashMap<>(10000);
internId("0"); // Games are always id 0
2017-10-13 01:42:46 +02:00
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;
2016-02-11 00:21:10 +01:00
this.removeCount = 0;
2017-10-11 03:16:22 +02:00
2019-02-10 04:43:01 +01:00
this.initialized = true; // Explicit initialization needed for Games
}
/**
* Constructor used by the ServerGame constructor.
2010-10-16 13:00:30 +02:00
*
* @param specification The {@code Specification} for this game.
*/
protected Game(Specification specification) {
this();
2016-02-11 00:21:10 +01:00
setSpecification(specification);
}
2004-04-23 00:01:19 +02:00
2017-01-12 05:09:35 +01:00
/**
* 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);
}
2013-04-05 12:46:48 +02:00
2017-10-16 01:49:49 +02:00
/**
* 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.
*
2019-01-12 07:00:01 +01:00
* 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.
*
2017-10-16 01:49:49 +02:00
* @param game The update for this {@code Game}.
* @return True if the update succeeds.
*/
public synchronized boolean preGameUpdate(Game game) {
2019-01-12 07:00:01 +01:00
return copyIn(game);
2017-10-16 01:49:49 +02:00
}
2016-02-18 00:45:59 +01:00
/**
2017-01-30 07:11:50 +01:00
* Instantiate an uninitialized FreeColGameObject within a game.
2016-02-18 00:45:59 +01:00
*
* @param <T> The actual return type.
2017-01-30 07:11:50 +01:00
* @param game The {@code Game} to instantiate in.
* @param returnClass The required {@code FreeColObject} class.
2016-02-18 00:45:59 +01:00
* @param server Create a server object if possible.
* @return The new uninitialized object, or null on error.
*/
2017-01-30 07:11:50 +01:00
public static <T extends FreeColObject> T newInstance(Game game,
Class<T> returnClass,
boolean server) {
2017-01-22 06:50:25 +01:00
// Do not restrict trying the full (Game,String) constructor
// to just server objects as there are simpler FCOs that
// implement it (e.g. Goods).
2017-01-22 06:50:25 +01:00
if (server) {
@SuppressWarnings("unchecked")
Class<T> sc = (Class<T>)serverClasses.get(returnClass);
if (sc != null) returnClass = sc;
}
try {
return Introspector.instantiate(returnClass,
new Class[] { Game.class, String.class },
2017-01-30 07:11:50 +01:00
new Object[] { game, (String)null }); // No intern!
2017-01-22 06:50:25 +01:00
} catch (Introspector.IntrospectorException ex) {
2017-01-12 05:09:35 +01:00
; // Allow another try on failure
2017-01-22 06:50:25 +01:00
}
2017-01-30 07:11:50 +01:00
if (game != null
&& FreeColSpecObject.class.isAssignableFrom(returnClass)) {
2017-01-22 06:50:25 +01:00
try {
return Introspector.instantiate(returnClass,
new Class[] { Specification.class },
2017-01-30 07:11:50 +01:00
new Object[] { game.getSpecification() });
2017-01-22 06:50:25 +01:00
} 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;
}
2012-06-21 11:07:53 +02:00
/**
2013-04-05 12:46:48 +02:00
* Get the difficulty level of this game.
*
* @return An {@code OptionGroup} containing the difficulty settings.
2012-06-21 11:07:53 +02:00
*/
public final OptionGroup getDifficultyOptionGroup() {
return specification.getDifficultyOptionGroup();
2012-06-21 11:07:53 +02:00
}
/**
* 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);
}
2012-06-21 11:07:53 +02:00
/**
2013-04-05 12:46:48 +02:00
* Gets the map generator options associated with this game.
2012-06-21 11:07:53 +02:00
*
* @return An {@code OptionGroup} containing the map
2013-04-05 12:46:48 +02:00
* generator options.
*/
2013-04-05 12:46:48 +02:00
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);
}
2008-05-27 10:40:16 +02:00
/**
2013-04-05 12:46:48 +02:00
* Stub for routine only meaningful in the server.
2008-05-27 10:40:16 +02:00
*
2013-04-05 12:46:48 +02:00
* @return Nothing.
2008-05-27 10:40:16 +02:00
*/
2017-05-05 09:07:31 +02:00
public int getNextId() {
throw new RuntimeException("game.getNextId not implemented: " + this);
2008-03-15 12:03:11 +01:00
}
2008-05-27 10:40:16 +02:00
/**
* Gets the {@code FreeColGameObject} with the given identifier.
2008-05-27 10:40:16 +02:00
*
* @param id The object identifier.
2013-04-05 12:46:48 +02:00
* @return The game object, or null if not found.
2008-05-27 10:40:16 +02:00
*/
2013-04-05 12:46:48 +02:00
public FreeColGameObject getFreeColGameObject(String id) {
if (id == null || id.isEmpty()) return null;
final WeakReference<FreeColGameObject> 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;
2013-04-05 12:46:48 +02:00
}
return o;
2008-03-15 12:03:11 +01:00
}
2010-10-16 13:00:30 +02:00
/**
* Gets the {@code FreeColGameObject} with the specified
2013-04-05 12:46:48 +02:00
* identifier and class.
*
2019-03-29 02:59:23 +01:00
* @param <T> The actual return type.
* @param id The object identifier.
2013-04-05 12:46:48 +02:00
* @param returnClass The expected class of the object.
* @return The game object, or null if not found.
*/
2013-04-05 12:46:48 +02:00
public <T extends FreeColGameObject> T getFreeColGameObject(String id,
2014-09-01 06:38:13 +02:00
Class<T> returnClass) {
2013-04-05 12:46:48 +02:00
FreeColGameObject fcgo = getFreeColGameObject(id);
try {
return returnClass.cast(fcgo);
} catch (ClassCastException e) {
return null;
2009-04-19 09:40:05 +02:00
}
}
2013-04-05 12:46:48 +02:00
/**
2017-05-29 01:31:21 +02:00
* Set the mapping between object identifier and object.
2013-04-05 12:46:48 +02:00
*
* @param id The object identifier.
2017-05-29 01:31:21 +02:00
* @param fcgo The {@code FreeColGameObject} to add to this {@code Game}.
2013-04-05 12:46:48 +02:00
*/
public void setFreeColGameObject(String id, FreeColGameObject fcgo) {
2014-09-14 09:09:44 +02:00
if (id == null || id.isEmpty()) {
throw new RuntimeException("Null/empty identifier: " + this);
2017-05-29 01:31:21 +02:00
} else if (fcgo == null) {
throw new RuntimeException("Null FreeColGameObject: " + id);
2017-05-29 01:31:21 +02:00
}
final WeakReference<FreeColGameObject> 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);
2013-04-05 12:46:48 +02:00
} else if (fcgo == null) {
throw new RuntimeException("Null FreeColGameObject: " + id);
2013-04-05 12:46:48 +02:00
}
final FreeColGameObject old = getFreeColGameObject(id);
if (old != null) {
2017-05-29 01:31:21 +02:00
throw new RuntimeException("Tried to replace FCGO "
2013-04-05 12:46:48 +02:00
+ id + " : " + old.getClass()
+ " with " + fcgo.getId() + " : " + fcgo.getClass());
}
2017-05-29 01:31:21 +02:00
setFreeColGameObject(id, fcgo);
2013-04-05 12:46:48 +02:00
notifySetFreeColGameObject(id, fcgo);
}
/**
* Removes the {@code FreeColGameObject} with the specified
2013-04-05 12:46:48 +02:00
* identifier.
2010-10-16 13:00:30 +02:00
*
* @param id The object identifier.
* @param reason A reason to remove the object.
2013-04-05 12:46:48 +02:00
* @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);
}
2013-04-05 12:46:48 +02:00
2016-09-24 09:39:47 +02:00
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<FreeColGameObject> iter = getFreeColGameObjectIterator();
while (iter.hasNext()) iter.next();
}
removeCount = 0;
}
}
/**
* Update a {@code FreeColGameObject} from another.
*
2019-03-29 02:59:23 +01:00
* @param <T> The type of object to update.
* @param other The other object.
2019-03-29 02:59:23 +01:00
* @param create If true, create the object if it is missing.
* @return The resulting object after update.
*/
public <T extends FreeColGameObject> 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.
*
2019-03-29 02:59:23 +01:00
* @param <T> 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 extends FreeColGameObject> T update(T other,
Class<T> 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);
2019-02-08 09:57:42 +01:00
} catch (ClassCastException cce) {
// "Can not happen"
throw new RuntimeException("Update class clash: " + fcgo.getClass()
2019-02-08 09:57:42 +01:00
+ " / " + 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.
*
2019-03-29 02:59:23 +01:00
* @param <T> The type of object to update.
* @param other The collection of objects to update.
2017-10-20 08:08:26 +02:00
* @param create If true, create missing objects.
* @return The resulting list of updated objects.
*/
2017-10-20 08:08:26 +02:00
public <T extends FreeColGameObject> List<T> update(Collection<T> other,
boolean create) {
if (other == null) return null;
List<T> ret = new ArrayList<>();
for (T t : other) {
2017-10-20 08:08:26 +02:00
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.
*
2019-03-29 02:59:23 +01:00
* @param <T> The type of object to update.
* @param other The other object.
* @return The resulting object after update.
*/
public <T extends FreeColGameObject> T updateRef(T other) {
return (other == null) ? null
: updateRef(other, other.getFreeColObjectClass());
}
/**
* Update a {@code FreeColGameObject} from a reference to it in an update.
*
2019-03-29 02:59:23 +01:00
* @param <T> 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 extends FreeColGameObject> T updateRef(T other,
Class<T> 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.
*
2019-03-29 02:59:23 +01:00
* @param <T> The type of object to update.
* @param other The other object.
* @return The resulting object after update.
*/
public <T extends FreeColGameObject> List<T> updateRef(Collection<T> other) {
if (other == null) return null;
List<T> 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());
}
2013-04-20 14:28:32 +02:00
/**
* Convenience wrapper to find a location (which is an interface,
* precluding using the typed version of getFreeColGameObject())
* by identifier.
2013-04-20 14:28:32 +02:00
*
* Use this routine when the object should already be present in the game.
*
* @param id The object identifier.
* @return The {@code Location} if any.
2013-04-20 14:28:32 +02:00
*/
public Location findFreeColLocation(String id) {
FreeColGameObject fcgo = getFreeColGameObject(id);
return (fcgo instanceof Location) ? (Location)fcgo : null;
}
2010-10-17 10:35:43 +02:00
/**
* Gets an {@code Iterator} over every registered
* {@code FreeColGameObject}.
2010-10-17 10:35:43 +02:00
*
* This {@code Iterator} should be iterated once in a while
* since it cleans the {@code FreeColGameObject} cache. Very
2015-01-16 00:18:52 +01:00
* 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.
2013-04-05 12:46:48 +02:00
*
* Lock freeColGameObjects when using this.
*
* @return An {@code Iterator} containing every registered
* {@code FreeColGameObject}.
2010-10-17 10:35:43 +02:00
*/
private Iterator<FreeColGameObject> getFreeColGameObjectIterator() {
2013-04-05 12:46:48 +02:00
return new Iterator<FreeColGameObject>() {
2015-01-16 00:18:52 +01:00
/** An iterator over the freeColGameObjects map. */
private final Iterator<Entry<String,
WeakReference<FreeColGameObject>>> it
2013-04-05 12:46:48 +02:00
= freeColGameObjects.entrySet().iterator();
2015-01-16 00:18:52 +01:00
2015-02-14 07:46:51 +01:00
/** Read ahead to this next entry. */
private Entry<String, WeakReference<FreeColGameObject>> readAhead
2015-01-16 00:18:52 +01:00
= null;
2015-02-14 07:46:51 +01:00
/** State of the readahead value. */
private FcgoState fcgoState = FcgoState.INVALID;
2013-04-05 12:46:48 +02:00
2015-04-04 15:21:11 +02:00
@Override
2015-02-14 07:46:51 +01:00
public boolean hasNext() {
if (this.fcgoState == FcgoState.VALID) return true;
2015-01-16 00:18:52 +01:00
while (this.it.hasNext()) {
2015-02-14 07:46:51 +01:00
this.readAhead = this.it.next();
if (this.readAhead.getValue().get() != null) {
this.fcgoState = FcgoState.VALID;
return true;
}
this.fcgoState = FcgoState.CONSUMED;
2015-01-16 00:18:52 +01:00
remove();
}
2015-02-14 07:46:51 +01:00
return false;
}
2015-04-04 15:21:11 +02:00
@Override
2015-02-14 07:46:51 +01:00
public FreeColGameObject next() {
if (!hasNext()) throw new NoSuchElementException();
FreeColGameObject fcgo = this.readAhead.getValue().get();
this.fcgoState = FcgoState.CONSUMED;
return fcgo;
2013-04-05 12:46:48 +02:00
}
2015-04-04 15:21:11 +02:00
@Override
2013-04-05 12:46:48 +02:00
public void remove() {
2015-02-14 07:46:51 +01:00
if (this.fcgoState == FcgoState.INVALID) {
throw new RuntimeException("No current entry: "
+ this.fcgoState);
2015-01-16 00:18:52 +01:00
}
2015-02-14 07:46:51 +01:00
final String key = this.readAhead.getKey();
this.fcgoState = FcgoState.INVALID;
2015-01-16 00:18:52 +01:00
this.it.remove();
2016-09-24 09:39:47 +02:00
logger.finest("removeFCGO/expire: " + key);
2015-01-16 00:18:52 +01:00
notifyRemoveFreeColGameObject(key);
2013-04-05 12:46:48 +02:00
}
};
}
/**
* Get a list of all the {@code FreeColGameObjects}.
*
* @return A suitable list.
*/
public List<FreeColGameObject> getFreeColGameObjectList() {
List<FreeColGameObject> ret = new ArrayList<>();
synchronized (this.freeColGameObjects) {
Iterator<FreeColGameObject> iter = getFreeColGameObjectIterator();
while (iter.hasNext()) ret.add(iter.next());
}
return ret;
}
2010-10-17 10:35:43 +02:00
/**
2013-04-05 12:46:48 +02:00
* Gets the unique identifier for this game.
* A game UUID persists in save game files.
2010-10-17 10:35:43 +02:00
*
* @return The game {@code UUID}.
2010-10-17 10:35:43 +02:00
*/
2013-04-05 12:46:48 +02:00
public UUID getUUID () {
return this.uuid;
2009-03-22 18:27:26 +01:00
}
2010-10-16 13:00:30 +02:00
2008-01-19 14:46:37 +01:00
/**
2017-10-06 05:19:10 +02:00
* Get players in the game.
2008-01-19 14:46:37 +01:00
*
2017-10-06 05:19:10 +02:00
* @param predicate A {@code Predicate} to select suitable players with.
* @return A list of {@code Player}s.
2008-01-19 14:46:37 +01:00
*/
2017-10-06 05:19:10 +02:00
protected List<Player> getPlayerList(Predicate<? super Player> predicate) {
synchronized (this.players) {
return transform(this.players, predicate);
}
}
/**
2017-10-06 05:19:10 +02:00
* Get players in the game.
*
2017-10-06 05:19:10 +02:00
* @param predicate A {@code Predicate} to select suitable players with.
* @return The stream of {@code Player}s.
*/
2017-10-06 05:19:10 +02:00
public Stream<Player> getPlayers(Predicate<? super Player> predicate) {
synchronized (this.players) {
return getPlayerList(predicate).stream();
}
}
/**
2017-10-06 05:19:10 +02:00
* Get a particular player in the game by a predicate.
*
2017-10-06 05:19:10 +02:00
* @param predicate A {@code Predicate} to select suitable players with.
* @return The {@code Player} found or null if not present.
*/
2017-10-06 05:19:10 +02:00
public Player getPlayer(Predicate<? super Player> predicate) {
synchronized (this.players) {
return find(this.players, predicate);
}
}
/**
2017-10-06 05:19:10 +02:00
* Set the players in the game.
*
2017-10-06 05:19:10 +02:00
* @param players The new {@code Player}s to add.
*/
2017-10-06 05:19:10 +02:00
private void setPlayers(List<Player> players) {
synchronized (this.players) {
this.players.clear();
if (players != null) this.players.addAll(players);
2017-10-06 05:19:10 +02:00
}
}
/**
2017-10-06 05:19:10 +02:00
* Gets the live player after the given player.
*
2017-10-06 05:19:10 +02:00
* @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) {
2017-10-18 00:52:57 +02:00
if (!this.players.contains(player)) this.players.add(player);
2017-10-06 05:19:10 +02:00
}
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<Player> 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.
*/
2017-10-06 05:19:10 +02:00
public Player getPlayerByName(String name) {
return getPlayer(matchKeyEquals(name, Player::getName));
2008-01-19 14:46:37 +01:00
}
/**
* 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) {
2017-10-06 05:19:10 +02:00
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<Player> getLivePlayerList(final Player... exclude) {
final Predicate<Player> livePred = p ->
!p.isUnknownEnemy() && !p.isDead() && !any(exclude, matchKey(p));
return getPlayerList(livePred);
}
2015-06-07 06:39:03 +02:00
/**
* Get a stream of the live players in the game, optionally excluding
* supplied ones.
2015-06-07 06:39:03 +02:00
*
* @param exclude The {@code Player}s to exclude.
* @return A stream of live {@code Player}s, without the
* excluded ones.
2015-06-07 06:39:03 +02:00
*/
public Stream<Player> getLivePlayers(final Player... exclude) {
return getLivePlayerList(exclude).stream();
2015-06-07 06:39:03 +02:00
}
2008-01-19 14:46:37 +01:00
/**
* Get a list of the live European players in this game.
2008-01-19 14:46:37 +01:00
*
* @param exclude {@code Player}s to exclude.
* @return A list of live European {@code Player}s in this game,
* without the excluded ones.
2008-01-19 14:46:37 +01:00
*/
public List<Player> getLiveEuropeanPlayerList(final Player... exclude) {
final Predicate<Player> 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<Player> 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<Player> getLiveNativePlayerList(final Player... exclude) {
final Predicate<Player> nativePred = p ->
!p.isUnknownEnemy() && !p.isDead() && p.isIndian()
&& !any(exclude, matchKey(p));
return getPlayerList(nativePred);
2008-01-19 14:46:37 +01:00
}
/**
* 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<Player> getLiveNativePlayers(final Player... exclude) {
return getLiveNativePlayerList(exclude).stream();
}
/**
2013-04-05 12:46:48 +02:00
* Gets the next current player.
*
* @return The {@code Player} whose turn follows the current player.
*/
2013-04-05 12:46:48 +02:00
public Player getNextPlayer() {
return getPlayerAfter(currentPlayer);
2012-04-29 12:04:59 +02:00
}
/**
* 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<Player> players) {
2017-10-06 05:19:10 +02:00
List<Player> 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);
2017-10-06 05:19:10 +02:00
} 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);
2017-10-06 05:19:10 +02:00
}
} else {
logger.warning("addPlayers onto non-player: " + fcgo);
}
}
}
2004-04-23 00:01:19 +02:00
/**
* Gets the unknown enemy player, which is used for privateers.
2010-10-16 13:00:30 +02:00
*
* @return The unknown enemy {@code Player}.
*/
2013-04-05 12:46:48 +02:00
public Player getUnknownEnemy() {
return this.unknownEnemy;
2004-10-11 21:25:19 +02:00
}
2004-04-23 00:01:19 +02:00
/**
* Sets the unknown enemy player.
2010-10-16 13:00:30 +02:00
*
* @param player The {@code Player} to serve as the unknown enemy.
*/
2013-04-05 12:46:48 +02:00
public void setUnknownEnemy(Player player) {
this.unknownEnemy = player;
2004-04-23 00:01:19 +02:00
}
/**
* Get the client user name.
2013-04-05 12:46:48 +02:00
*
* @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;
2012-04-29 12:04:59 +02:00
}
/**
* Are we executing in the server?
2012-04-29 12:04:59 +02:00
*
* @return True in the server.
2012-04-29 12:04:59 +02:00
*/
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);
}
2007-04-22 12:18:13 +02:00
2015-09-29 09:06:55 +02:00
/**
* Is this game in revenge mode?
*
* @return True if an undead player is present.
*/
public boolean isInRevengeMode() {
2017-10-06 05:19:10 +02:00
return getPlayer(Player::isUndead) != null;
2015-09-29 09:06:55 +02:00
}
2004-04-23 00:01:19 +02:00
/**
2013-04-05 12:46:48 +02:00
* Gets the current player.
2010-10-16 13:00:30 +02:00
*
2013-04-05 12:46:48 +02:00
* @return The current player.
*/
2013-04-05 12:46:48 +02:00
public Player getCurrentPlayer() {
2015-02-03 09:25:07 +01:00
return this.currentPlayer;
2013-04-05 12:46:48 +02:00
}
2004-04-23 00:01:19 +02:00
2013-04-05 12:46:48 +02:00
/**
* Sets the current player.
*
* @param newCurrentPlayer The new current {@code Player}.
2013-04-05 12:46:48 +02:00
*/
public void setCurrentPlayer(Player newCurrentPlayer) {
2015-02-03 09:25:07 +01:00
this.currentPlayer = newCurrentPlayer;
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Gets the map that is being used in this game.
2010-10-16 13:00:30 +02:00
*
* @return The game {@code Map}.
*/
2017-10-16 01:50:51 +02:00
public synchronized Map getMap() {
2017-10-06 07:24:36 +02:00
return this.map;
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Sets the game map.
2010-10-16 13:00:30 +02:00
*
* @param newMap The new {@code Map} to use.
* @return The old {@code Map}.
2013-04-05 12:46:48 +02:00
*/
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) {
2016-07-19 03:10:21 +02:00
for (HighSeas hs : transform(getLivePlayers(), alwaysTrue(),
2017-10-06 07:24:36 +02:00
Player::getHighSeas, toListNoNulls())) {
2017-10-16 01:50:51 +02:00
hs.removeDestination(oldMap);
hs.addDestination(newMap);
}
}
2004-04-23 00:01:19 +02:00
}
2009-04-19 09:40:05 +02:00
/**
2013-04-05 12:46:48 +02:00
* Get the current nation options.
2009-04-19 09:40:05 +02:00
*
* @return The current {@code NationOptions}.
2009-04-19 09:40:05 +02:00
*/
2019-03-15 23:36:37 +01:00
public final synchronized NationOptions getNationOptions() {
2009-04-19 09:40:05 +02:00
return nationOptions;
}
/**
* Set the current nation options.
2009-04-19 09:40:05 +02:00
*
2017-09-28 06:14:28 +02:00
* Public for the test suite.
*
* @param newNationOptions The new {@code NationOptions} value.
2009-04-19 09:40:05 +02:00
*/
2019-03-15 23:36:37 +01:00
public final synchronized void setNationOptions(final NationOptions newNationOptions) {
2009-04-19 09:40:05 +02:00
this.nationOptions = newNationOptions;
}
2004-08-16 13:38:51 +02:00
/**
2013-04-05 12:46:48 +02:00
* Find an available (i.e. vacant) nation.
2010-10-16 13:00:30 +02:00
*
* @return A vacant {@code Nation} or null if none found.
*/
public Nation getVacantNation() {
2016-06-25 11:13:28 +02:00
Entry<Nation, NationState> entry
= find(nationOptions.getNations().entrySet(),
2016-06-15 09:22:54 +02:00
matchKey(NationState.AVAILABLE, Entry::getValue));
return (entry == null) ? null : entry.getKey();
2004-08-16 13:38:51 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Get the currently available nations.
2010-10-16 13:00:30 +02:00
*
* @return A list of available {@code Nation}s.
*/
2013-04-05 12:46:48 +02:00
public final List<Nation> getVacantNations() {
2016-01-05 23:31:56 +01:00
return transform(nationOptions.getNations().entrySet(),
2016-06-15 09:22:54 +02:00
matchKey(NationState.AVAILABLE, Entry::getValue),
2016-05-25 09:43:09 +02:00
Entry::getKey);
}
2004-04-23 00:01:19 +02:00
/**
2013-04-05 12:46:48 +02:00
* Can a new player be added to this game?
2010-10-16 13:00:30 +02:00
*
2013-04-05 12:46:48 +02:00
* @return True if a new player can be added.
*/
2013-04-05 12:46:48 +02:00
public boolean canAddNewPlayer() {
return getVacantNation() != null;
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Gets the current turn in this game.
2010-10-16 13:00:30 +02:00
*
* @return The current {@code Turn}.
*/
2013-04-05 12:46:48 +02:00
public Turn getTurn() {
return turn;
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Sets the current turn in this game.
2010-10-16 13:00:30 +02:00
*
* @param newTurn The new {@code Turn} to set.
*/
2013-04-05 12:46:48 +02:00
public void setTurn(Turn newTurn) {
turn = newTurn;
2004-04-23 00:01:19 +02:00
}
2015-05-31 12:49:37 +02:00
/**
* Get the age for the current turn.
*
* @return The age (0-2).
*/
public int getAge() {
2016-09-24 09:39:47 +02:00
return getSpecification().getAge(turn);
2015-05-31 12:49:37 +02:00
}
2004-04-23 00:01:19 +02:00
/**
2013-04-05 12:46:48 +02:00
* Get the combat model in this game.
2010-10-16 13:00:30 +02:00
*
* @return The {@code CombatModel}.
*/
2013-04-05 12:46:48 +02:00
public final CombatModel getCombatModel() {
if (specification.hasAbility(Ability.HITPOINTS_COMBAT_MODEL)) {
return new HitpointsCombatModel();
} else {
return new SimpleCombatModel();
}
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Has the Spanish Succession event occured?
2010-10-16 13:00:30 +02:00
*
2013-04-05 12:46:48 +02:00
* @return True if the Spanish Succession has occurred.
*/
2013-04-05 12:46:48 +02:00
public final boolean getSpanishSuccession() {
return spanishSuccession;
2004-04-23 00:01:19 +02:00
}
2013-04-05 12:46:48 +02:00
/**
* Set the Spanish Succession value.
*
* @param spanishSuccession The new Spanish Succession value.
*/
public final void setSpanishSuccession(final boolean spanishSuccession) {
this.spanishSuccession = spanishSuccession;
2007-09-30 13:00:27 +02:00
}
/**
* Get the identifier for the initial active unit.
*
* @return The active unit identifier, if any.
*/
2017-05-05 09:07:31 +02:00
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;
}
2004-04-23 00:01:19 +02:00
/**
* Sets the {@code FreeColGameObjectListener} attached to this game.
2010-10-16 13:00:30 +02:00
*
* @param fcgol The new {@code FreeColGameObjectListener}.
*/
2013-04-05 12:46:48 +02:00
public void setFreeColGameObjectListener(FreeColGameObjectListener fcgol) {
freeColGameObjectListener = fcgol;
2004-04-23 00:01:19 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Notify a listener (if any) of a new game object.
2010-10-16 13:00:30 +02:00
*
* @param id The object identifier.
* @param fcgo The new {@code FreeColGameObject}.
*/
2013-04-05 12:46:48 +02:00
public void notifySetFreeColGameObject(String id, FreeColGameObject fcgo) {
if (freeColGameObjectListener != null) {
freeColGameObjectListener.setFreeColGameObject(id, fcgo);
}
2004-04-23 00:01:19 +02:00
}
2009-05-09 11:12:22 +02:00
/**
2013-04-05 12:46:48 +02:00
* Notify a listener (if any) of that a game object has gone.
2009-05-09 11:12:22 +02:00
*
* @param id The object identifier.
2009-05-09 11:12:22 +02:00
*/
2013-04-05 12:46:48 +02:00
public void notifyRemoveFreeColGameObject(String id) {
if (freeColGameObjectListener != null) {
freeColGameObjectListener.removeFreeColGameObject(id);
}
2009-05-09 11:12:22 +02:00
}
/**
2013-04-05 12:46:48 +02:00
* Notify a listener (if any) of that a game object has changed owner.
2009-05-09 11:12:22 +02:00
*
* @param source The {@code FreeColGameObject} that changed owner.
* @param oldOwner The old owning {@code Player}.
* @param newOwner The new owning {@code Player}.
2009-05-09 11:12:22 +02:00
*/
2013-04-05 12:46:48 +02:00
public void notifyOwnerChanged(FreeColGameObject source,
Player oldOwner, Player newOwner) {
if (freeColGameObjectListener != null) {
freeColGameObjectListener.ownerChanged(source, oldOwner, newOwner);
}
2009-05-09 11:12:22 +02:00
}
/**
* 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}.
*/
2016-09-24 09:39:47 +02:00
public void checkOwners(Ownable o, Player oldOwner) {
Player newOwner = o.getOwner();
2016-09-24 03:18:54 +02:00
if (oldOwner == newOwner) return;
if (oldOwner != null && oldOwner.removeOwnable(o)) {
oldOwner.invalidateCanSeeTiles();//+vis
}
if (newOwner != null && newOwner.addOwnable(o)) {
newOwner.invalidateCanSeeTiles();//+vis
}
}
2013-04-05 12:46:48 +02:00
// Miscellaneous utilities.
/**
2013-04-05 12:46:48 +02:00
* Checks if all players are ready to launch.
2010-10-16 13:00:30 +02:00
*
2013-04-05 12:46:48 +02:00
* @return True if all players are ready to launch.
*/
2013-04-05 12:46:48 +02:00
public boolean allPlayersReadyToLaunch() {
return all(getLiveEuropeanPlayerList(), Player::isReady);
}
2010-10-16 13:00:30 +02:00
2016-03-20 04:35:18 +01:00
/**
* 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<Colony> 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.
2016-03-20 04:35:18 +01:00
*/
public List<Colony> getAllColoniesList(Player player) {
return toList(getAllColonies(player));
2016-03-20 04:35:18 +01:00
}
/**
2013-04-05 12:46:48 +02:00
* 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
2013-04-05 12:46:48 +02:00
* specified name (the settlement might not be visible to a client).
*/
2015-09-30 12:34:19 +02:00
public Settlement getSettlementByName(String name) {
2016-06-21 12:11:37 +02:00
return find(flatten(getLivePlayers(), Player::getSettlements),
2016-06-15 09:22:54 +02:00
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) {
2012-04-29 12:04:59 +02:00
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();
2012-04-29 12:04:59 +02:00
FreeColObject o = getFreeColGameObject(id);
if (o == null) {
try {
o = getSpecification().getType(id);
2016-09-24 09:39:47 +02:00
} 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<String, String> getStatistics() {
2014-11-27 07:23:18 +01:00
java.util.Map<String, String> 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
2014-11-27 07:23:18 +01:00
java.util.Map<String, Long> 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));
2016-06-25 11:13:28 +02:00
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<? extends FreeColGameObject> getLocationClass(String id) {
return locationClasses.get(capitalize(FreeColObject.getIdTypeByName(id)));
}
/**
* Unserialize from XML to a FreeColObject in this game.
*
* @param <T> 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 extends FreeColObject> T unserialize(String xml,
Class<T> 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<Area> 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
2013-04-05 12:46:48 +02:00
/**
* {@inheritDoc}
2013-04-05 12:46:48 +02:00
*/
@Override
2019-02-16 05:17:42 +01:00
public IntegrityType checkIntegrity(boolean fix, LogBuilder lb) {
IntegrityType result = super.checkIntegrity(fix, lb);
2015-01-26 05:26:20 +01:00
lb.mark();
synchronized (freeColGameObjects) {
Iterator<FreeColGameObject> 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();
2019-02-16 05:17:42 +01:00
result = result.fix();
} else {
2019-02-16 05:17:42 +01:00
result = result.fail();
}
2013-04-05 12:46:48 +02:00
}
}
2017-02-13 05:50:12 +01:00
if (lb.grew("\n Uninitialized game ids: ")) {
2015-01-26 05:26:20 +01:00
if (fix) lb.add(" (dropped)");
}
2016-09-24 09:39:47 +02:00
Map map = getMap();
if (map != null) {
2019-03-26 00:54:00 +01:00
result = result.combine(map.checkIntegrity(fix, lb));
2013-04-05 12:46:48 +02:00
}
2017-10-06 05:19:10 +02:00
synchronized (this.players) {
for (Player p : this.players) {
2019-02-16 05:17:42 +01:00
result = result.combine(p.checkIntegrity(fix, lb));
2017-10-06 05:19:10 +02:00
}
2013-04-05 12:46:48 +02:00
}
return result;
2013-04-05 12:46:48 +02:00
}
2016-02-11 00:21:10 +01:00
// Override FreeColObject
/**
* {@inheritDoc}
*/
@Override
public Specification getSpecification() {
return this.specification;
}
/**
* {@inheritDoc}
*/
@Override
2019-02-10 04:43:01 +01:00
public final void setSpecification(Specification specification) {
2016-02-11 00:21:10 +01:00
this.specification = specification;
}
2017-05-05 09:07:31 +02:00
// Override FreeColObject
/**
* {@inheritDoc}
*/
@Override
public <T extends FreeColObject> boolean copyIn(T other) {
Game o = copyInCast(other, Game.class);
if (o == null || !super.copyIn(o)) return false;
2017-05-05 09:07:31 +02:00
this.specification = o.getSpecification();
// Do not update nextId, it is not meaningful in the client.
2017-05-05 09:07:31 +02:00
this.uuid = o.getUUID();
this.clientUserName = o.getClientUserName();
2017-10-18 00:53:56 +02:00
// Players before map, so the tile owners work
addPlayers(o.players);
2017-10-20 01:54:54 +02:00
2017-10-18 00:53:56 +02:00
// Allow creation, might be first sight of the map.
// Do map early, so map references work
changeMap(update(o.getMap(), Map.class, true));
2017-10-18 00:53:56 +02:00
2019-03-15 23:36:37 +01:00
setNationOptions(o.getNationOptions());
this.unknownEnemy = update(o.getUnknownEnemy(), false);
this.currentPlayer = updateRef(o.getCurrentPlayer(), Player.class);
2017-05-05 09:07:31 +02:00
this.turn = o.getTurn();
this.spanishSuccession = o.getSpanishSuccession();
this.initialActiveUnitId = o.getInitialActiveUnitId();
return true;
}
2016-02-11 00:21:10 +01:00
2012-06-21 11:07:53 +02:00
// 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";
2013-04-19 07:40:31 +02:00
private static final String CIBOLA_TAG = "cibola";
2016-07-25 11:24:57 +02:00
private static final String CLIENT_USER_NAME_TAG = "clientUserName";
2013-04-19 07:40:31 +02:00
private static final String CURRENT_PLAYER_TAG = "currentPlayer";
private static final String INITIAL_ACTIVE_UNIT_ID = "initialActiveUnitId";
2013-04-19 07:40:31 +02:00
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);
2013-04-19 07:40:31 +02:00
if (xw.validForSave()) {
xw.writeAttribute(NEXT_ID_TAG, nextId);
2019-02-14 07:43:31 +01:00
} else {
Player client = xw.getClientPlayer();
if (client != null) {
xw.writeAttribute(CLIENT_USER_NAME_TAG, client.getName());
}
}
2016-09-24 09:39:47 +02:00
xw.writeAttribute(UUID_TAG, getUUID());
2008-05-25 15:37:05 +02:00
2016-09-24 09:39:47 +02:00
xw.writeAttribute(TURN_TAG, getTurn().getNumber());
xw.writeAttribute(SPANISH_SUCCESSION_TAG, spanishSuccession);
2013-04-19 07:40:31 +02:00
if (initialActiveUnitId != null) {
xw.writeAttribute(INITIAL_ACTIVE_UNIT_ID, initialActiveUnitId);
}
2013-04-19 07:40:31 +02:00
if (currentPlayer != null) {
xw.writeAttribute(CURRENT_PLAYER_TAG, currentPlayer);
}
2013-04-19 07:40:31 +02:00
}
/**
* {@inheritDoc}
*/
@Override
protected void writeChildren(FreeColXMLWriter xw) throws XMLStreamException {
super.writeChildren(xw);
2019-03-17 03:16:13 +01:00
if (this.specification != null) {
2019-03-15 23:36:37 +01:00
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);
}
}
2015-04-14 02:52:27 +02:00
for (String cityName : NameCache.getCitiesOfCibola()) {
// Preserve existing order
xw.writeStartElement(CIBOLA_TAG);
2013-04-19 07:40:31 +02:00
xw.writeAttribute(ID_ATTRIBUTE_TAG, cityName);
2013-04-19 07:40:31 +02:00
xw.writeEndElement();
}
2013-04-19 07:40:31 +02:00
2019-03-15 23:36:37 +01:00
synchronized (this.nationOptions) {
this.nationOptions.toXML(xw);
}
2019-03-15 23:36:37 +01:00
synchronized (this.players) { // Should already be sorted
2017-10-06 05:19:10 +02:00
for (Player p : this.players) p.toXML(xw);
}
2016-09-24 09:39:47 +02:00
Player unknown = getUnknownEnemy();
2017-10-06 05:19:10 +02:00
if (unknown != null) unknown.toXML(xw);
2013-04-05 12:46:48 +02:00
2019-03-26 00:54:00 +01:00
Map map = getMap();
if (map != null) {
map.toXML(xw);
}
xw.writeStartElement(AREAS_TAG);
for (Area a : areas.values()) {
a.toXML(xw);
}
xw.writeEndElement();
2013-04-19 07:40:31 +02:00
}
2008-05-25 15:37:05 +02:00
2004-04-23 00:01:19 +02:00
/**
2013-04-19 07:40:31 +02:00
* {@inheritDoc}
*/
@Override
protected void readAttributes(FreeColXMLReader xr) throws XMLStreamException {
super.readAttributes(xr);
nextId = xr.getAttribute(NEXT_ID_TAG, -1);
2016-07-25 11:24:57 +02:00
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
}
}
2013-04-19 07:40:31 +02:00
turn = new Turn(xr.getAttribute(TURN_TAG, 1));
2013-04-19 07:40:31 +02:00
spanishSuccession = xr.getAttribute(SPANISH_SUCCESSION_TAG, false);
initialActiveUnitId = xr.getAttribute(INITIAL_ACTIVE_UNIT_ID,
(String)null);
2017-10-19 05:47:31 +02:00
// currentPlayer handled in readChildren()
}
2013-04-19 07:40:31 +02:00
/**
* {@inheritDoc}
*/
@Override
protected void readChildren(FreeColXMLReader xr) throws XMLStreamException {
2013-04-19 07:40:31 +02:00
// Clear containers.
2015-04-14 02:52:27 +02:00
NameCache.clearCitiesOfCibola();
2013-04-19 07:40:31 +02:00
players.clear();
unknownEnemy = null;
2017-10-19 05:47:31 +02:00
// 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);
2013-04-19 07:40:31 +02:00
super.readChildren(xr);
2013-04-19 07:40:31 +02:00
2017-10-19 05:47:31 +02:00
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();
}
2013-04-19 07:40:31 +02:00
}
/**
* {@inheritDoc}
*/
@Override
protected void readChild(FreeColXMLReader xr) throws XMLStreamException {
final Game game = getGame();
final String tag = xr.getLocalName();
2013-04-19 07:40:31 +02:00
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);
2013-04-19 07:40:31 +02:00
2016-10-10 13:03:19 +02:00
} 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));
2013-04-19 07:40:31 +02:00
2016-10-10 13:03:19 +02:00
} else if (NationOptions.TAG.equals(tag)) {
if (this.specification == null) {
throw new XMLStreamException("Tried to read " + tag
+ " with null specification");
}
2019-03-15 23:36:37 +01:00
setNationOptions(new NationOptions(xr, specification));
2013-04-19 07:40:31 +02:00
2016-10-10 13:03:19 +02:00
} else if (Player.TAG.equals(tag)) {
if (this.specification == null) {
throw new XMLStreamException("Tried to read " + tag
+ " with null specification");
}
2017-01-22 06:54:44 +01:00
Player player = xr.readFreeColObject(game, Player.class);
2013-04-19 07:40:31 +02:00
if (player.isUnknownEnemy()) {
setUnknownEnemy(player);
} else {
players.add(player);
}
2016-10-10 13:03:19 +02:00
} else if (Specification.TAG.equals(tag)) {
2019-03-15 23:36:37 +01:00
setSpecification(new Specification(xr));
2013-04-19 07:40:31 +02:00
} 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);
}
2013-04-19 07:40:31 +02:00
} else {
super.readChild(xr);
2013-04-19 07:40:31 +02:00
}
2004-04-23 00:01:19 +02:00
}
2016-10-10 13:03:19 +02:00
/**
* {@inheritDoc}
*/
public String getXMLTagName() { return TAG; }
// Override Object
2013-06-06 03:33:21 +02:00
/**
* {@inheritDoc}
*/
2015-04-04 15:21:11 +02:00
@Override
2016-10-10 13:03:19 +02:00
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().
2016-10-10 13:03:19 +02:00
return this == o;
}
2013-06-06 03:33:21 +02:00
2004-04-23 00:01:19 +02:00
/**
2016-10-10 13:03:19 +02:00
* {@inheritDoc}
*/
2016-10-10 13:03:19 +02:00
@Override
public int hashCode() {
return Utils.hashCode(getId());
2004-04-23 00:01:19 +02:00
}
}