/******************************************************************************* * Copyright (c) 2015, 2016 * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. *******************************************************************************/ package jsettlers.main; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Date; import jsettlers.ai.highlevel.AiExecutor; import jsettlers.common.CommonConstants; import jsettlers.common.map.IGraphicsGrid; import jsettlers.common.map.MapLoadException; import jsettlers.common.menu.EGameError; import jsettlers.common.menu.EProgressState; import jsettlers.common.menu.IGameExitListener; import jsettlers.common.menu.IMapInterfaceConnector; import jsettlers.common.menu.IStartedGame; import jsettlers.common.menu.IStartingGame; import jsettlers.common.menu.IStartingGameListener; import jsettlers.common.player.IInGamePlayer; import jsettlers.common.resources.ResourceManager; import jsettlers.common.statistics.IGameTimeProvider; import jsettlers.graphics.map.draw.ImageProvider; import jsettlers.input.GuiInterface; import jsettlers.input.IGameStoppable; import jsettlers.input.PlayerState; import jsettlers.logic.buildings.Building; import jsettlers.logic.buildings.trading.MarketBuilding; import jsettlers.logic.constants.MatchConstants; import jsettlers.logic.map.grid.MainGrid; import jsettlers.logic.map.grid.partition.PartitionsGrid; import jsettlers.logic.map.loading.IGameCreator; import jsettlers.logic.map.loading.IGameCreator.MainGridWithUiSettings; import jsettlers.logic.map.loading.MapLoader; import jsettlers.logic.movable.Movable; import jsettlers.logic.player.Player; import jsettlers.logic.player.PlayerSetting; import jsettlers.logic.timer.RescheduleTimer; import jsettlers.main.replay.ReplayUtils; import jsettlers.network.client.OfflineNetworkConnector; import jsettlers.network.client.interfaces.INetworkConnector; /** * This class can start a Thread that loads and sets up a game and wait's for its termination. * * @author Andreas Eberle */ public class JSettlersGame { private final Object stopMutex = new Object(); private final IGameCreator mapCreator; private final long randomSeed; private final byte playerId; private final PlayerSetting[] playerSettings; private final INetworkConnector networkConnector; private final boolean multiplayer; private final DataInputStream replayFileInputStream; private final GameRunner gameRunner; private boolean started = false; private boolean stopped = false; private boolean shutdownFinished; private PrintStream systemErrorStream; private PrintStream systemOutStream; private JSettlersGame(IGameCreator mapCreator, long randomSeed, INetworkConnector networkConnector, byte playerId, PlayerSetting[] playerSettings, boolean controlAll, boolean multiplayer, DataInputStream replayFileInputStream) { configureLogging(mapCreator); System.out.println("OS version: " + System.getProperty("os.name") + " " + System.getProperty("os.arch") + " " + System.getProperty("os.version")); System.out.println("Java version: " + System.getProperty("java.vendor") + " " + System.getProperty("java.version")); System.out.println("JsettlersGame(): seed: " + randomSeed + " playerId: " + playerId + " availablePlayers: " + Arrays.toString(playerSettings) + " multiplayer: " + multiplayer + " mapCreator: " + mapCreator); if (mapCreator == null) { throw new NullPointerException("mapCreator"); } this.mapCreator = mapCreator; this.randomSeed = randomSeed; this.networkConnector = networkConnector; this.playerId = playerId; this.playerSettings = playerSettings; this.multiplayer = multiplayer; this.replayFileInputStream = replayFileInputStream; MatchConstants.ENABLE_ALL_PLAYER_FOG_OF_WAR = controlAll; MatchConstants.ENABLE_ALL_PLAYER_SELECTION = controlAll; MatchConstants.ENABLE_FOG_OF_WAR_DISABLING = controlAll; MatchConstants.ENABLE_DEBUG_COLORS = controlAll; this.gameRunner = new GameRunner(); } /** * @param mapCreator * @param randomSeed * @param networkConnector * @param playerId */ public JSettlersGame(IGameCreator mapCreator, long randomSeed, INetworkConnector networkConnector, byte playerId, PlayerSetting[] playerSettings) { this(mapCreator, randomSeed, networkConnector, playerId, playerSettings, CommonConstants.CONTROL_ALL, true, null); } /** * Creates a new {@link JSettlersGame} object with an {@link OfflineNetworkConnector}. * * @param mapCreator * @param randomSeed * @param playerId */ public JSettlersGame(IGameCreator mapCreator, long randomSeed, byte playerId, PlayerSetting[] playerSettings) { this(mapCreator, randomSeed, new OfflineNetworkConnector(), playerId, playerSettings, CommonConstants.CONTROL_ALL, false, null); } public static JSettlersGame loadFromReplayFile(ReplayUtils.IReplayStreamProvider loadableReplayFile, INetworkConnector networkConnector, ReplayStartInformation replayStartInformation) throws MapLoadException { try { DataInputStream replayFileInputStream = new DataInputStream(loadableReplayFile.openStream()); replayStartInformation.deserialize(replayFileInputStream); MapLoader mapCreator = loadableReplayFile.getMap(replayStartInformation); return new JSettlersGame(mapCreator, replayStartInformation.getRandomSeed(), networkConnector, (byte) replayStartInformation.getPlayerId(), replayStartInformation.getReplayablePlayerSettings(), true, false, replayFileInputStream); } catch (IOException e) { throw new MapLoadException("Could not deserialize " + loadableReplayFile, e); } } /** * Starts the game in a new thread. Returns immediately. * * @return */ public synchronized IStartingGame start() { if (!started) { started = true; new Thread(null, gameRunner, "GameThread", 128 * 1024).start(); } return gameRunner; } public void stop() { synchronized (stopMutex) { printEndgameStatistic(); stopped = true; stopMutex.notifyAll(); } } // TODO remove me when an EndgameStatistic screen exists. private void printEndgameStatistic() { PartitionsGrid partitionsGrid = gameRunner.getMainGrid().getPartitionsGrid(); System.out.println("Endgame statistic:"); for (byte playerId = 0; playerId < partitionsGrid.getNumberOfPlayers(); playerId++) { Player player = partitionsGrid.getPlayer(playerId); if (player != null) { System.out.println("Player " + playerId + ": " + player.getEndgameStatistic()); } } } protected OutputStream createReplayWriteStream() throws IOException { final String replayFilename = getLogFile(mapCreator, "_replay.log"); return ResourceManager.writeUserFile(replayFilename); } public class GameRunner implements Runnable, IStartingGame, IStartedGame, IGameStoppable { private IStartingGameListener startingGameListener; private MainGrid mainGrid; private GameTimeProvider gameTimeProvider; private EProgressState progressState; private float progress; private IGameExitListener exitListener; private boolean gameRunning; private AiExecutor aiExecutor; @Override public void run() { try { updateProgressListener(EProgressState.LOADING, 0.1f); clearState(); MatchConstants.init(networkConnector.getGameClock(), randomSeed); try { MatchConstants.clock().setReplayLogStream(createReplayFileStream()); } catch (IOException e) { // TODO: log that we do not have write access to resources. System.out.println("Cannot write jsettlers.integration.replay file."); } updateProgressListener(EProgressState.LOADING_MAP, 0.3f); Thread imagePreloader = ImageProvider.getInstance().startPreloading(); MainGridWithUiSettings gridWithUiState = mapCreator.loadMainGrid(playerSettings); mainGrid = gridWithUiState.getMainGrid(); PlayerState playerState = gridWithUiState.getPlayerState(playerId); RescheduleTimer.schedule(MatchConstants.clock()); // schedule timer updateProgressListener(EProgressState.LOADING_IMAGES, 0.7f); gameTimeProvider = new GameTimeProvider(MatchConstants.clock()); mainGrid.initForPlayer(playerId, playerState.getFogOfWar()); mainGrid.startThreads(); if (imagePreloader != null) imagePreloader.join(); // Wait for ImageProvider to finish loading the images waitForStartingGameListener(); updateProgressListener(EProgressState.WAITING_FOR_OTHER_PLAYERS, 0.98f); if (replayFileInputStream != null) { MatchConstants.clock().loadReplayLogFromStream(replayFileInputStream); } networkConnector.setStartFinished(true); waitForAllPlayersStartFinished(networkConnector); final IMapInterfaceConnector connector = startingGameListener.preLoadFinished(this); GuiInterface guiInterface = new GuiInterface(connector, MatchConstants.clock(), networkConnector.getTaskScheduler(), mainGrid.getGuiInputGrid(), this, playerId, multiplayer); connector.loadUIState(playerState.getUiState()); // This is required after the GuiInterface instantiation so that // ConstructionMarksThread has it's mapArea variable initialized via the EActionType.SCREEN_CHANGE event. aiExecutor = new AiExecutor(playerSettings, mainGrid, networkConnector.getTaskScheduler()); networkConnector.getGameClock().schedule(aiExecutor, (short) 10000); MatchConstants.clock().startExecution(); // WARNING: GAME CLOCK IS STARTED! // NO CONFIGURATION AFTER THIS POINT! ================================= gameRunning = true; startingGameListener.startFinished(); synchronized (stopMutex) { while (!stopped) { try { stopMutex.wait(); } catch (InterruptedException e) { } } } networkConnector.shutdown(); mainGrid.stopThreads(); connector.shutdown(); guiInterface.stop(); clearState(); System.setErr(systemErrorStream); System.setOut(systemOutStream); } catch (MapLoadException e) { e.printStackTrace(); reportFail(EGameError.MAPLOADING_ERROR, e); } catch (Exception e) { e.printStackTrace(); reportFail(EGameError.UNKNOWN_ERROR, e); } finally { shutdownFinished = true; if (exitListener != null) { exitListener.gameExited(this); } } } public AiExecutor getAiExecutor() { return aiExecutor; } private DataOutputStream createReplayFileStream() throws IOException { DataOutputStream replayFileStream = new DataOutputStream(createReplayWriteStream()); ReplayStartInformation replayInfo = new ReplayStartInformation(randomSeed, mapCreator.getMapName(), mapCreator.getMapId(), playerId, playerSettings); replayInfo.serialize(replayFileStream); replayFileStream.flush(); return replayFileStream; } /** * Waits until the {@link #startingGameListener} has been set. */ private void waitForStartingGameListener() { while (startingGameListener == null) { try { Thread.sleep(5); } catch (InterruptedException e) { } } } private void waitForAllPlayersStartFinished(INetworkConnector networkConnector) { while (!networkConnector.haveAllPlayersStartFinished()) { try { Thread.sleep(5); } catch (InterruptedException e) { } } } private void updateProgressListener(EProgressState progressState, float progress) { this.progressState = progressState; this.progress = progress; if (startingGameListener != null) startingGameListener.startProgressChanged(progressState, progress); } private void reportFail(EGameError gameError, Exception e) { if (startingGameListener != null) startingGameListener.startFailed(gameError, e); } // METHODS of IStartingGame // ==================================================== @Override public void setListener(IStartingGameListener startingGameListener) { this.startingGameListener = startingGameListener; if (startingGameListener != null) startingGameListener.startProgressChanged(progressState, progress); } // METHODS of IStartedGame // ====================================================== @Override public IGraphicsGrid getMap() { return mainGrid.getGraphicsGrid(); } @Override public IGameTimeProvider getGameTimeProvider() { return gameTimeProvider; } @Override public IInGamePlayer getInGamePlayer() { return mainGrid.getPartitionsGrid().getPlayer(playerId); } @Override public boolean isShutdownFinished() { return shutdownFinished; } @Override public void stopGame() { stop(); } @Override public void setGameExitListener(IGameExitListener exitListener) { this.exitListener = exitListener; } @Override public boolean isStartupFinished() { return gameRunning; } public MainGrid getMainGrid() { return mainGrid; } } private void configureLogging(final IGameCreator mapcreator) { try { systemErrorStream = System.err; systemOutStream = System.out; if (!CommonConstants.ENABLE_CONSOLE_LOGGING) { PrintStream outLogStream = new PrintStream(ResourceManager.writeUserFile(getLogFile(mapcreator, "_out.log"))); System.setOut(outLogStream); System.setErr(outLogStream); } } catch (IOException ex) { throw new RuntimeException("Error setting up logging.", ex); } } private static String getLogFile(IGameCreator mapcreator, String suffix) { final String dateAndMap = getLogDateFormatter().format(new Date()) + "_" + mapcreator.getMapName().replace(" ", "_"); final String logFolder = "logs/" + dateAndMap + "/"; return logFolder + dateAndMap + suffix; } private static DateFormat getLogDateFormatter() { return new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss"); } public static void clearState() { RescheduleTimer.stopAndClear(); Movable.resetState(); Building.clearState(); MarketBuilding.clearState(); MatchConstants.clearState(); } }