package games.strategy.engine.framework;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import games.strategy.debug.ClientLogger;
import games.strategy.debug.ErrorConsole;
import games.strategy.engine.ClientContext;
import games.strategy.engine.GameOverException;
import games.strategy.engine.data.Change;
import games.strategy.engine.data.CompositeChange;
import games.strategy.engine.data.GameData;
import games.strategy.engine.data.GameStep;
import games.strategy.engine.data.PlayerID;
import games.strategy.engine.data.PlayerManager;
import games.strategy.engine.data.changefactory.ChangeFactory;
import games.strategy.engine.delegate.AutoSave;
import games.strategy.engine.delegate.DefaultDelegateBridge;
import games.strategy.engine.delegate.DelegateExecutionManager;
import games.strategy.engine.delegate.IDelegate;
import games.strategy.engine.delegate.IDelegateBridge;
import games.strategy.engine.delegate.IPersistentDelegate;
import games.strategy.engine.framework.headlessGameServer.HeadlessGameServer;
import games.strategy.engine.framework.startup.mc.IObserverWaitingToJoin;
import games.strategy.engine.framework.startup.ui.InGameLobbyWatcherWrapper;
import games.strategy.engine.framework.ui.SaveGameFileChooser;
import games.strategy.engine.gamePlayer.IGamePlayer;
import games.strategy.engine.history.DelegateHistoryWriter;
import games.strategy.engine.history.Event;
import games.strategy.engine.history.EventChild;
import games.strategy.engine.history.HistoryNode;
import games.strategy.engine.history.Step;
import games.strategy.engine.message.ConnectionLostException;
import games.strategy.engine.message.IRemote;
import games.strategy.engine.message.MessageContext;
import games.strategy.engine.message.RemoteName;
import games.strategy.engine.random.IRandomSource;
import games.strategy.engine.random.IRemoteRandom;
import games.strategy.engine.random.PlainRandomSource;
import games.strategy.engine.random.RandomStats;
import games.strategy.net.INode;
import games.strategy.net.Messengers;
import games.strategy.triplea.TripleAPlayer;
/**
* Represents a running game.
* Lookups to get a GamePlayer from PlayerId and the current Delegate.
*/
public class ServerGame extends AbstractGame {
public static final RemoteName SERVER_REMOTE =
new RemoteName("games.strategy.engine.framework.ServerGame.SERVER_REMOTE", IServerRemote.class);
public static final String GAME_HAS_BEEN_SAVED_PROPERTY =
"games.strategy.engine.framework.ServerGame.GameHasBeenSaved";
// maps PlayerID->GamePlayer
private final RandomStats m_randomStats;
private IRandomSource m_randomSource = new PlainRandomSource();
private IRandomSource m_delegateRandomSource;
private final DelegateExecutionManager m_delegateExecutionManager = new DelegateExecutionManager();
private InGameLobbyWatcherWrapper m_inGameLobbyWatcher;
private boolean m_needToInitialize = true;
/**
* When the delegate execution is stopped, we countdown on this latch to prevent the startgame(...) method from
* returning.
*/
private final CountDownLatch m_delegateExecutionStoppedLatch = new CountDownLatch(1);
/**
* Has the delegate signaled that delegate execution should stop.
*/
private volatile boolean m_delegateExecutionStopped = false;
/**
* @param data
* game data.
* @param localPlayers
* Set - A set of GamePlayers
* @param remotePlayerMapping
* Map
* @param messengers
* IServerMessenger
*/
public ServerGame(final GameData data, final Set<IGamePlayer> localPlayers,
final Map<String, INode> remotePlayerMapping, final Messengers messengers) {
super(data, localPlayers, remotePlayerMapping, messengers);
m_gameModifiedChannel = new IGameModifiedChannel() {
@Override
public void gameDataChanged(final Change aChange) {
assertCorrectCaller();
m_data.performChange(aChange);
m_data.getHistory().getHistoryWriter().addChange(aChange);
}
private void assertCorrectCaller() {
if (!MessageContext.getSender().equals(getMessenger().getServerNode())) {
throw new IllegalStateException("Only server can change game data");
}
}
@Override
public void startHistoryEvent(final String event, final Object renderingData) {
startHistoryEvent(event);
if (renderingData != null) {
setRenderingData(renderingData);
}
}
@Override
public void startHistoryEvent(final String event) {
assertCorrectCaller();
m_data.getHistory().getHistoryWriter().startEvent(event);
}
@Override
public void addChildToEvent(final String text, final Object renderingData) {
assertCorrectCaller();
m_data.getHistory().getHistoryWriter().addChildToEvent(new EventChild(text, renderingData));
}
protected void setRenderingData(final Object renderingData) {
assertCorrectCaller();
m_data.getHistory().getHistoryWriter().setRenderingData(renderingData);
}
@Override
public void stepChanged(final String stepName, final String delegateName, final PlayerID player, final int round,
final String displayName, final boolean loadedFromSavedGame) {
assertCorrectCaller();
if (loadedFromSavedGame) {
return;
}
m_data.getHistory().getHistoryWriter().startNextStep(stepName, delegateName, player, displayName);
}
// nothing to do, we call this
@Override
public void shutDown() {}
};
m_channelMessenger.registerChannelSubscriber(m_gameModifiedChannel, IGame.GAME_MODIFICATION_CHANNEL);
setupDelegateMessaging(data);
m_randomStats = new RandomStats(m_remoteMessenger);
final IServerRemote m_serverRemote = () -> {
final ByteArrayOutputStream sink = new ByteArrayOutputStream(5000);
try {
saveGame(sink);
} catch (final IOException e) {
ClientLogger.logQuietly(e);
throw new IllegalStateException(e);
}
return sink.toByteArray();
};
m_remoteMessenger.registerRemote(m_serverRemote, SERVER_REMOTE);
}
public void addObserver(final IObserverWaitingToJoin blockingObserver,
final IObserverWaitingToJoin nonBlockingObserver, final INode newNode) {
try {
if (!m_delegateExecutionManager.blockDelegateExecution(2000)) {
nonBlockingObserver.cannotJoinGame("Could not block delegate execution");
return;
}
} catch (final InterruptedException e) {
nonBlockingObserver.cannotJoinGame(e.getMessage());
return;
}
try {
final CountDownLatch waitOnObserver = new CountDownLatch(1);
final ByteArrayOutputStream sink = new ByteArrayOutputStream(1000);
saveGame(sink);
(new Thread(() -> {
try {
blockingObserver.joinGame(sink.toByteArray(), m_playerManager.getPlayerMapping());
waitOnObserver.countDown();
} catch (final ConnectionLostException cle) {
System.out.println("Connection lost to observer while joining: " + newNode.getName());
} catch (final Exception e) {
ClientLogger.logQuietly(e);
}
}, "Waiting on observer to finish joining: " + newNode.getName())).start();
try {
if (!waitOnObserver.await(GameRunner.getServerObserverJoinWaitTime(), TimeUnit.SECONDS)) {
nonBlockingObserver.cannotJoinGame("Taking too long to join.");
}
} catch (final InterruptedException e) {
ClientLogger.logQuietly(e);
nonBlockingObserver.cannotJoinGame(e.getMessage());
}
} catch (final Exception e) {
ClientLogger.logQuietly(e);
nonBlockingObserver.cannotJoinGame(e.getMessage());
} finally {
m_delegateExecutionManager.resumeDelegateExecution();
}
}
private void setupDelegateMessaging(final GameData data) {
for (final IDelegate delegate : data.getDelegateList()) {
addDelegateMessenger(delegate);
}
}
public void addDelegateMessenger(final IDelegate delegate) {
final Class<? extends IRemote> remoteType = delegate.getRemoteType();
// if its null then it shouldn't be added as an IRemote
if (remoteType == null) {
return;
}
final Object wrappedDelegate =
m_delegateExecutionManager.createInboundImplementation(delegate, new Class<?>[] {delegate.getRemoteType()});
final RemoteName descriptor = getRemoteName(delegate);
m_remoteMessenger.registerRemote(wrappedDelegate, descriptor);
}
public static RemoteName getRemoteName(final IDelegate delegate) {
return new RemoteName("games.strategy.engine.framework.ServerGame.DELEGATE_REMOTE." + delegate.getName(),
delegate.getRemoteType());
}
public static RemoteName getRemoteName(final PlayerID id, final GameData data) {
return new RemoteName("games.strategy.engine.framework.ServerGame.PLAYER_REMOTE." + id.getName(),
data.getGameLoader().getRemotePlayerType());
}
public static RemoteName getRemoteRandomName(final PlayerID id) {
return new RemoteName("games.strategy.engine.framework.ServerGame.PLAYER_RANDOM_REMOTE" + id.getName(),
IRemoteRandom.class);
}
private GameStep getCurrentStep() {
return m_data.getSequence().getStep();
// m_data.getSequence().getStep(m_currentStepIndex);
}
/**
* And here we go.
* Starts the game in a new thread
*/
public void startGame() {
try {
// we dont want to notify that the step has been saved when reloading a saved game, since
// in fact the step hasnt changed, we are just resuming where we left off
final boolean gameHasBeenSaved = m_data.getProperties().get(GAME_HAS_BEEN_SAVED_PROPERTY, false);
if (!gameHasBeenSaved) {
m_data.getProperties().set(GAME_HAS_BEEN_SAVED_PROPERTY, Boolean.TRUE);
}
startPersistentDelegates();
if (gameHasBeenSaved) {
runStep(gameHasBeenSaved);
}
while (!m_isGameOver) {
if (m_delegateExecutionStopped) {
// the delegate has told us to stop stepping through game steps
try {
// dont let this method return, as this method returning signals
// that the game is over.
m_delegateExecutionStoppedLatch.await();
} catch (final InterruptedException e) {
// ignore
}
} else {
runStep(false);
}
}
} catch (final GameOverException e) {
if (!m_isGameOver) {
ClientLogger.logQuietly(e);
}
}
}
public void stopGame() {
// we have already shut down
if (m_isGameOver) {
System.out.println("Game previously stopped, cannot stop again.");
return;
} else if (HeadlessGameServer.headless()) {
System.out.println("Attempting to stop game.");
}
m_isGameOver = true;
m_delegateExecutionStoppedLatch.countDown();
// tell the players (especially the AI's) that the game is stopping, so stop doing stuff.
for (final IGamePlayer player : m_gamePlayers.values()) {
// not sure whether to put this before or after we delegate execution block, but definitely before the game loader
// shutdown
player.stopGame();
}
// block delegate execution to prevent outbound messages to the players while we shut down.
try {
if (!m_delegateExecutionManager.blockDelegateExecution(16000)) {
System.err.println("Could not stop delegate execution.");
if (HeadlessGameServer.getInstance() != null) {
HeadlessGameServer.getInstance().printThreadDumpsAndStatus();
} else {
ErrorConsole.getConsole().dumpStacks();
}
// Try one more time
if (!m_delegateExecutionManager.blockDelegateExecution(16000)) {
System.err.println("Exiting...");
System.exit(-1);
}
}
} catch (final InterruptedException e) {
ClientLogger.logQuietly(e);
}
// shutdown
try {
m_delegateExecutionManager.setGameOver();
getGameModifiedBroadcaster().shutDown();
m_randomStats.shutDown();
m_channelMessenger.unregisterChannelSubscriber(m_gameModifiedChannel, IGame.GAME_MODIFICATION_CHANNEL);
m_remoteMessenger.unregisterRemote(SERVER_REMOTE);
m_vault.shutDown();
final Iterator<IGamePlayer> localPlayersIter = m_gamePlayers.values().iterator();
while (localPlayersIter.hasNext()) {
final IGamePlayer gp = localPlayersIter.next();
m_remoteMessenger.unregisterRemote(getRemoteName(gp.getPlayerID(), m_data));
}
final Iterator<IDelegate> delegateIter = m_data.getDelegateList().iterator();
while (delegateIter.hasNext()) {
final IDelegate delegate = delegateIter.next();
final Class<? extends IRemote> remoteType = delegate.getRemoteType();
// if its null then it shouldnt be added as an IRemote
if (remoteType == null) {
continue;
}
m_remoteMessenger.unregisterRemote(getRemoteName(delegate));
}
} catch (final RuntimeException e) {
ClientLogger.logQuietly(e);
} finally {
m_delegateExecutionManager.resumeDelegateExecution();
}
m_data.getGameLoader().shutDown();
if (HeadlessGameServer.headless()) {
System.out.println("StopGame successful.");
}
}
private void autoSave() {
SaveGameFileChooser.ensureMapsFolderExists();
final File f1 =
new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveFileName());
final File f2 =
new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSave2FileName());
final File f;
if (f1.lastModified() > f2.lastModified()) {
f = f2;
} else {
f = f1;
}
try (FileOutputStream out = new FileOutputStream(f)) {
saveGame(out);
} catch (final Exception e) {
ClientLogger.logQuietly(e);
}
}
private void autoSaveRound() {
SaveGameFileChooser.ensureMapsFolderExists();
final File autosaveFile;
if (m_data.getSequence().getRound() % 2 == 0) {
autosaveFile =
new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveEvenFileName());
} else {
autosaveFile =
new File(ClientContext.folderSettings().getSaveGamePath(), SaveGameFileChooser.getAutoSaveOddFileName());
}
try (FileOutputStream out = new FileOutputStream(autosaveFile)) {
saveGame(out);
} catch (final Exception e) {
ClientLogger.logQuietly(e);
}
}
@Override
public void saveGame(final File f) {
try (FileOutputStream fout = new FileOutputStream(f)) {
saveGame(fout);
} catch (final IOException e) {
ClientLogger.logQuietly(e);
}
}
public void saveGame(final OutputStream out) throws IOException {
try {
if (!m_delegateExecutionManager.blockDelegateExecution(6000)) {
throw new IOException("Could not lock delegate execution");
}
} catch (final InterruptedException ie) {
throw new IOException(ie.getMessage());
}
try {
new GameDataManager().saveGame(out, m_data);
} finally {
m_delegateExecutionManager.resumeDelegateExecution();
}
}
private void runStep(final boolean stepIsRestoredFromSavedGame) {
if (getCurrentStep().hasReachedMaxRunCount()) {
m_data.getSequence().next();
return;
}
if (m_isGameOver) {
return;
}
startStep(stepIsRestoredFromSavedGame);
if (m_isGameOver) {
return;
}
waitForPlayerToFinishStep();
if (m_isGameOver) {
return;
}
final boolean autoSaveAfterDelegateDone = endStep();
if (m_isGameOver) {
return;
}
if (m_data.getSequence().next()) {
m_data.getHistory().getHistoryWriter().startNextRound(m_data.getSequence().getRound());
autoSaveRound();
}
// save after the step has advanced
// otherwise, the delegate will execute again.
if (autoSaveAfterDelegateDone) {
autoSave();
}
}
/**
* @return true if the step should autosave.
*/
private boolean endStep() {
m_delegateExecutionManager.enterDelegateExecution();
try {
getCurrentStep().getDelegate().end();
} finally {
m_delegateExecutionManager.leaveDelegateExecution();
}
getCurrentStep().incrementRunCount();
if (m_data.getSequence().getStep().getDelegate().getClass().isAnnotationPresent(AutoSave.class)) {
if (m_data.getSequence().getStep().getDelegate().getClass().getAnnotation(AutoSave.class).afterStepEnd()) {
return true;
}
}
return false;
}
private void startPersistentDelegates() {
final Iterator<IDelegate> delegateIter = m_data.getDelegateList().iterator();
while (delegateIter.hasNext()) {
final IDelegate delegate = delegateIter.next();
if (!(delegate instanceof IPersistentDelegate)) {
continue;
}
final DefaultDelegateBridge bridge = new DefaultDelegateBridge(m_data, this,
new DelegateHistoryWriter(m_channelMessenger), m_randomStats, m_delegateExecutionManager);
if (m_delegateRandomSource == null) {
m_delegateRandomSource = (IRandomSource) m_delegateExecutionManager.createOutboundImplementation(m_randomSource,
new Class<?>[] {IRandomSource.class});
}
bridge.setRandomSource(m_delegateRandomSource);
m_delegateExecutionManager.enterDelegateExecution();
try {
delegate.setDelegateBridgeAndPlayer(bridge);
delegate.start();
} finally {
m_delegateExecutionManager.leaveDelegateExecution();
}
}
}
private void startStep(final boolean stepIsRestoredFromSavedGame) {
// dont save if we just loaded
if (!stepIsRestoredFromSavedGame) {
if (m_data.getSequence().getStep().getDelegate().getClass().isAnnotationPresent(AutoSave.class)) {
if (m_data.getSequence().getStep().getDelegate().getClass().getAnnotation(AutoSave.class).beforeStepStart()) {
autoSave();
}
}
}
final DefaultDelegateBridge bridge = new DefaultDelegateBridge(m_data, this,
new DelegateHistoryWriter(m_channelMessenger), m_randomStats, m_delegateExecutionManager);
if (m_delegateRandomSource == null) {
m_delegateRandomSource = (IRandomSource) m_delegateExecutionManager.createOutboundImplementation(m_randomSource,
new Class<?>[] {IRandomSource.class});
}
bridge.setRandomSource(m_delegateRandomSource);
// do any initialization of game data for all players here (not based on a delegate, and should not be)
// we cannot do this the very first run through, because there are no history nodes yet. We should do after first
// node is created.
if (m_needToInitialize) {
addPlayerTypesToGameData(m_gamePlayers.values(), m_playerManager, bridge);
}
notifyGameStepChanged(stepIsRestoredFromSavedGame);
m_delegateExecutionManager.enterDelegateExecution();
try {
final IDelegate delegate = getCurrentStep().getDelegate();
delegate.setDelegateBridgeAndPlayer(bridge);
delegate.start();
} finally {
m_delegateExecutionManager.leaveDelegateExecution();
}
}
private void waitForPlayerToFinishStep() {
final PlayerID playerID = getCurrentStep().getPlayerID();
// no player specified for the given step
if (playerID == null) {
return;
}
if (!getCurrentStep().getDelegate().delegateCurrentlyRequiresUserInput()) {
return;
}
final IGamePlayer player = m_gamePlayers.get(playerID);
if (player != null) {
// a local player
player.start(getCurrentStep().getName());
} else {
// a remote player
final INode destination = m_playerManager.getNode(playerID.getName());
final IGameStepAdvancer advancer =
(IGameStepAdvancer) m_remoteMessenger.getRemote(ClientGame.getRemoteStepAdvancerName(destination));
advancer.startPlayerStep(getCurrentStep().getName(), playerID);
}
}
private void notifyGameStepChanged(final boolean loadedFromSavedGame) {
final GameStep currentStep = getCurrentStep();
final String stepName = currentStep.getName();
final String delegateName = currentStep.getDelegate().getName();
final String displayName = currentStep.getDisplayName();
final int round = m_data.getSequence().getRound();
final PlayerID id = currentStep.getPlayerID();
notifyGameStepListeners(stepName, delegateName, id, round, displayName);
getGameModifiedBroadcaster().stepChanged(stepName, delegateName, id, round, displayName, loadedFromSavedGame);
}
private void addPlayerTypesToGameData(final Collection<IGamePlayer> localPlayers, final PlayerManager allPlayers,
final IDelegateBridge aBridge) {
final GameData data = aBridge.getData();
// potential bugs with adding changes to a game that has not yet started and has no history nodes yet. So wait for
// the first delegate to
// start before making changes.
if (getCurrentStep() == null || getCurrentStep().getPlayerID() == null || (m_firstRun)) {
m_firstRun = false;
return;
}
// we can't add a new event or add new changes if we are not in a step.
final HistoryNode curNode = data.getHistory().getLastNode();
if (!(curNode instanceof Step) && !(curNode instanceof Event) && !(curNode instanceof EventChild)) {
return;
}
final CompositeChange change = new CompositeChange();
final Set<String> allPlayersString = allPlayers.getPlayers();
aBridge.getHistoryWriter().startEvent("Game Loaded");
for (final IGamePlayer player : localPlayers) {
allPlayersString.remove(player.getName());
final boolean isHuman = player instanceof TripleAPlayer;
aBridge.getHistoryWriter()
.addChildToEvent(
player.getName()
+ ((player.getName().endsWith("s") || player.getName().endsWith("ese")
|| player.getName().endsWith("ish")) ? " are" : " is")
+ " now being played by: " + player.getType());
final PlayerID p = data.getPlayerList().getPlayerID(player.getName());
final String newWhoAmI = ((isHuman ? "Human" : "AI") + ":" + player.getType());
if (!p.getWhoAmI().equals(newWhoAmI)) {
change.add(ChangeFactory.changePlayerWhoAmIChange(p, newWhoAmI));
}
}
final Iterator<String> playerIter = allPlayersString.iterator();
while (playerIter.hasNext()) {
final String player = playerIter.next();
playerIter.remove();
aBridge.getHistoryWriter().addChildToEvent(
player + ((player.endsWith("s") || player.endsWith("ese") || player.endsWith("ish")) ? " are" : " is")
+ " now being played by: Human:Client");
final PlayerID p = data.getPlayerList().getPlayerID(player);
final String newWhoAmI = "Human:Client";
if (!p.getWhoAmI().equals(newWhoAmI)) {
change.add(ChangeFactory.changePlayerWhoAmIChange(p, newWhoAmI));
}
}
if (!change.isEmpty()) {
aBridge.addChange(change);
}
m_needToInitialize = false;
if (!allPlayersString.isEmpty()) {
throw new IllegalStateException("Not all Player Types (ai/human/client) could be added to game data.");
}
}
private IGameModifiedChannel getGameModifiedBroadcaster() {
return (IGameModifiedChannel) m_channelMessenger.getChannelBroadcastor(IGame.GAME_MODIFICATION_CHANNEL);
}
@Override
public void addChange(final Change aChange) {
getGameModifiedBroadcaster().gameDataChanged(aChange);
// let our channel subscribor do the change,
// that way all changes will happen in the same thread
}
@Override
public boolean canSave() {
return true;
}
@Override
public IRandomSource getRandomSource() {
return m_randomSource;
}
public void setRandomSource(final IRandomSource randomSource) {
m_randomSource = randomSource;
m_delegateRandomSource = null;
}
public InGameLobbyWatcherWrapper getInGameLobbyWatcher() {
return m_inGameLobbyWatcher;
}
public void setInGameLobbyWatcher(final InGameLobbyWatcherWrapper inGameLobbyWatcher) {
m_inGameLobbyWatcher = inGameLobbyWatcher;
}
public void stopGameSequence() {
m_delegateExecutionStopped = true;
}
public boolean isGameSequenceRunning() {
return !m_delegateExecutionStopped;
}
}