package games.strategy.engine.data; import java.io.IOException; import java.io.ObjectInputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.swing.SwingUtilities; import games.strategy.engine.data.events.GameDataChangeListener; import games.strategy.engine.data.events.GameMapListener; import games.strategy.engine.data.events.TerritoryListener; import games.strategy.engine.data.properties.GameProperties; import games.strategy.engine.framework.IGameLoader; import games.strategy.engine.framework.message.PlayerListing; import games.strategy.engine.history.History; import games.strategy.thread.LockUtil; import games.strategy.triplea.ResourceLoader; import games.strategy.util.ListenerList; import games.strategy.util.Tuple; import games.strategy.util.Version; /** * Central place to find all the information for a running game. * * <p> * Using this object you can find the territories, connections, production rules, * unit types... * </p> * * <p> * Threading. The game data, and all parts of the game data (such as Territories, Players, Units...) are protected by a * read/write lock. If * you are reading the game data, you should read while you have the read lock as below. * </p> * * <p> * <code> * data.acquireReadLock(); * try * { * //read data here * } * finally * { * data.releaseReadLock(); * } * </code> * The exception is delegates within a start(), end() or any method called from an IGamePlayer through the delegates * remote interface. The * delegate will have a read lock for the duration of those methods. * </p> * * <p> * Non engine code must NOT acquire the games writeLock(). All changes to game Data must be made through a * DelegateBridge or through a * History object. * </p> */ public class GameData implements java.io.Serializable { private static final long serialVersionUID = -2612710634080125728L; public static final String GAME_UUID = "GAME_UUID"; private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); private transient LockUtil lockUtil = LockUtil.INSTANCE; private transient volatile boolean forceInSwingEventThread = false; private String gameName; private Version gameVersion; private int diceSides; private transient ListenerList<TerritoryListener> territoryListeners = new ListenerList<>(); private transient ListenerList<GameDataChangeListener> dataChangeListeners = new ListenerList<>(); private transient ListenerList<GameMapListener> gameMapListeners = new ListenerList<>(); private final AllianceTracker alliances = new AllianceTracker(); // Tracks current relationships between players, this is empty if relationships aren't used private final RelationshipTracker relationships = new RelationshipTracker(this); private final DelegateList delegateList; private final GameMap map = new GameMap(this); private final PlayerList playerList = new PlayerList(this); private final ProductionFrontierList productionFrontierList = new ProductionFrontierList(this); private final ProductionRuleList productionRuleList = new ProductionRuleList(this); private final RepairFrontierList repairFrontierList = new RepairFrontierList(this); private final RepairRuleList repairRuleList = new RepairRuleList(this); private final ResourceList resourceList = new ResourceList(this); private final GameSequence sequence = new GameSequence(this); private final UnitTypeList unitTypeList = new UnitTypeList(this); // Tracks all relationshipTypes that are in the current game, default there will be the SelfRelation and the // NullRelation any other relations are map designer created. private final RelationshipTypeList relationshipTypeList = new RelationshipTypeList(this); private final GameProperties properties = new GameProperties(this); private final UnitsList unitsList = new UnitsList(); private final TechnologyFrontier technologyFrontier = new TechnologyFrontier("allTechsForGame", this); private transient ResourceLoader resourceLoader; private IGameLoader loader; private final History gameHistory = new History(this); private transient volatile boolean testLockIsHeld = false; private final List<Tuple<IAttachment, ArrayList<Tuple<String, String>>>> attachmentOrderAndValues = new ArrayList<>(); private final Hashtable<String, TerritoryEffect> territoryEffectList = new Hashtable<>(); private final BattleRecordsList battleRecordsList = new BattleRecordsList(this); /** Creates new GameData. */ public GameData() { super(); delegateList = new DelegateList(this); properties.set(GAME_UUID, UUID.randomUUID().toString()); } private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); lockUtil = LockUtil.INSTANCE; } /** * Return the GameMap. The game map allows you to list the territories in the game, and * to see which territory is connected to which. * * @return the map for this game. */ public GameMap getMap() { return map; } /** * Print an exception report if we are testing the lock is held, and * do not currently hold the read or write lock. */ private void ensureLockHeld() { if (!testLockIsHeld) { return; } if (readWriteLockMissing()) { return; } if (!lockUtil.isLockHeld(readWriteLock.readLock()) && !lockUtil.isLockHeld(readWriteLock.writeLock())) { new Exception("Lock not held").printStackTrace(System.out); } } /** * @return a collection of all units in the game. */ public UnitsList getUnits() { // ensureLockHeld(); return unitsList; } /** * @return list of Players in the game. */ public PlayerList getPlayerList() { return playerList; } /** * @return list of resources available in the game. */ public ResourceList getResourceList() { ensureLockHeld(); return resourceList; } /** * @return list of production Frontiers for this game. */ public ProductionFrontierList getProductionFrontierList() { ensureLockHeld(); return productionFrontierList; } /** * @return list of Production Rules for the game. */ public ProductionRuleList getProductionRuleList() { ensureLockHeld(); return productionRuleList; } /** * @return The Technology Frontier for this game. */ public TechnologyFrontier getTechnologyFrontier() { return technologyFrontier; } /** * @return The list of production Frontiers for this game. */ public RepairFrontierList getRepairFrontierList() { ensureLockHeld(); return repairFrontierList; } /** * @return The list of Production Rules for the game. */ public RepairRuleList getRepairRuleList() { ensureLockHeld(); return repairRuleList; } /** * @return The Alliance Tracker for the game. */ public AllianceTracker getAllianceTracker() { ensureLockHeld(); return alliances; } /** * @return whether we should throw an error if changes to this game data are made outside of the swing * event thread. */ public boolean areChangesOnlyInSwingEventThread() { return forceInSwingEventThread; } /** * If set to true, then we will throw an error when the game data is changed outside * the swing event thread. */ public void forceChangesOnlyInSwingEventThread() { forceInSwingEventThread = true; } public GameSequence getSequence() { ensureLockHeld(); return sequence; } public UnitTypeList getUnitTypeList() { ensureLockHeld(); return unitTypeList; } public DelegateList getDelegateList() { ensureLockHeld(); return delegateList; } public UnitHolder getUnitHolder(final String name, final String type) { ensureLockHeld(); if (type.equals(UnitHolder.PLAYER)) { return playerList.getPlayerID(name); } else if (type.equals(UnitHolder.TERRITORY)) { return map.getTerritory(name); } else { throw new IllegalStateException("Invalid type:" + type); } } public GameProperties getProperties() { return properties; } public void addTerritoryListener(final TerritoryListener listener) { territoryListeners.add(listener); } public void removeTerritoryListener(final TerritoryListener listener) { territoryListeners.remove(listener); } public void addDataChangeListener(final GameDataChangeListener listener) { dataChangeListeners.add(listener); } public void removeDataChangeListener(final GameDataChangeListener listener) { dataChangeListeners.remove(listener); } public void addGameMapListener(final GameMapListener listener) { gameMapListeners.add(listener); } public void removeGameMapListener(final GameMapListener listener) { gameMapListeners.remove(listener); } void notifyTerritoryUnitsChanged(final Territory t) { territoryListeners.forEach(territoryListener -> territoryListener.unitsChanged(t)); } void notifyTerritoryAttachmentChanged(final Territory t) { territoryListeners.forEach(territoryListener -> territoryListener.attachmentChanged(t)); } void notifyTerritoryOwnerChanged(final Territory t) { territoryListeners.forEach(territoryListener -> territoryListener.ownerChanged(t)); } void notifyGameDataChanged(final Change aChange) { dataChangeListeners.forEach(dataChangelistener -> dataChangelistener.gameDataChanged(aChange)); } void notifyMapDataChanged() { gameMapListeners.forEach(gameMapListener -> gameMapListener.gameMapDataChanged()); } public IGameLoader getGameLoader() { return loader; } void setGameLoader(final IGameLoader loader) { this.loader = loader; } void setGameVersion(final Version gameVersion) { this.gameVersion = gameVersion; } public Version getGameVersion() { return gameVersion; } void setGameName(final String gameName) { this.gameName = gameName; } public String getGameName() { return gameName; } void setDiceSides(final int diceSides) { if (diceSides > 0 && diceSides <= 200) { this.diceSides = diceSides; } else { this.diceSides = 6; } } public int getDiceSides() { return diceSides; } public History getHistory() { // don't ensure the lock is held when getting the history // history operations often acquire the write lock // and we cant acquire the write lock if we have the read lock return gameHistory; } /** * Not to be called by mere mortals. */ public void postDeSerialize() { territoryListeners = new ListenerList<>(); dataChangeListeners = new ListenerList<>(); gameMapListeners = new ListenerList<>(); } /** * No changes to the game data should be made unless this lock is held. * calls to acquire lock will block if the lock is held, and will be held * until the release method is called */ public void acquireReadLock() { if (readWriteLockMissing()) { return; } lockUtil.acquireLock(readWriteLock.readLock()); } public void releaseReadLock() { if (readWriteLockMissing()) { return; } lockUtil.releaseLock(readWriteLock.readLock()); } /** * No changes to the game data should be made unless this lock is held. * calls to acquire lock will block if the lock is held, and will be held * until the release method is called */ public void acquireWriteLock() { if (readWriteLockMissing()) { return; } lockUtil.acquireLock(readWriteLock.writeLock()); } public void releaseWriteLock() { if (readWriteLockMissing()) { return; } lockUtil.releaseLock(readWriteLock.writeLock()); } /** * @return boolean, whether readWriteLock is missing * This can happen in very odd circumstances while deserializing. */ private boolean readWriteLockMissing() { return readWriteLock == null; } public void clearAllListeners() { dataChangeListeners.clear(); territoryListeners.clear(); gameMapListeners.clear(); if (resourceLoader != null) { resourceLoader.close(); resourceLoader = null; } } /** * On reads of the game data components, make sure that the * read or write lock is held. */ public void testLocksOnRead() { testLockIsHeld = true; } public void addToAttachmentOrderAndValues( final Tuple<IAttachment, ArrayList<Tuple<String, String>>> attachmentAndValues) { attachmentOrderAndValues.add(attachmentAndValues); } public List<Tuple<IAttachment, ArrayList<Tuple<String, String>>>> getAttachmentOrderAndValues() { return attachmentOrderAndValues; } /** * @return all relationshipTypes that are valid in this game, default there is the NullRelation (relation with the * Nullplayer / Neutral) * and the SelfRelation (Relation with yourself) all other relations are mapdesigner defined. */ public RelationshipTypeList getRelationshipTypeList() { ensureLockHeld(); return relationshipTypeList; } /** * @return a tracker which tracks all current relationships that exist between all players. */ public RelationshipTracker getRelationshipTracker() { ensureLockHeld(); return relationships; } public Hashtable<String, TerritoryEffect> getTerritoryEffectList() { return territoryEffectList; } public BattleRecordsList getBattleRecordsList() { return battleRecordsList; } /** * Call this before starting the game and before the game data has been sent to the clients in order to make any * final modifications to the game data. * For example, this method will remove player delegates for players who have been disabled. */ public void doPreGameStartDataModifications(final PlayerListing playerListing) { final Set<PlayerID> playersWhoShouldBeRemoved = new HashSet<>(); final Map<String, Boolean> playersEnabledListing = playerListing.getPlayersEnabledListing(); playerList.getPlayers().stream() .filter(p -> (p.getCanBeDisabled() && !playersEnabledListing.get(p.getName()))) .forEach(p -> { p.setIsDisabled(true); playersWhoShouldBeRemoved.add(p); }); if (!playersWhoShouldBeRemoved.isEmpty()) { removePlayerStepsFromSequence(playersWhoShouldBeRemoved); } } private void removePlayerStepsFromSequence(final Set<PlayerID> playersWhoShouldBeRemoved) { final int currentIndex = sequence.getStepIndex(); int index = 0; int toSubtract = 0; final Iterator<GameStep> stepIter = sequence.iterator(); while (stepIter.hasNext()) { final GameStep step = stepIter.next(); if (playersWhoShouldBeRemoved.contains(step.getPlayerID())) { stepIter.remove(); if (index < currentIndex) { toSubtract++; } } index++; } sequence.setStepIndex(Math.max(0, Math.min(sequence.size() - 1, currentIndex - toSubtract))); } public void performChange(final Change change) { if (areChangesOnlyInSwingEventThread() && !SwingUtilities.isEventDispatchThread()) { throw new IllegalStateException("Wrong thread"); } try { acquireWriteLock(); change.perform(this); } finally { releaseWriteLock(); } notifyGameDataChanged(change); } }