Index: binaries/data/mods/public/gamesettings/GameSettings.js =================================================================== --- binaries/data/mods/public/gamesettings/GameSettings.js +++ binaries/data/mods/public/gamesettings/GameSettings.js @@ -142,7 +142,10 @@ // NB: for multiplayer support, the clients must be listening to "start" net messages. if (this.isNetworked) - Engine.StartNetworkGame(this.finalizedAttributes, storeReplay); + if (g_isSaveLoaded && g_savedGameId !== undefined) + Engine.StartNetworkSavedGame(g_savedGameId, this.finalizedAttributes, storeReplay); + else + Engine.StartNetworkGame(this.finalizedAttributes, storeReplay); else Engine.StartGame(this.finalizedAttributes, playerAssignments.local.player, storeReplay); } Index: binaries/data/mods/public/gui/credits/texts/programming.json =================================================================== --- binaries/data/mods/public/gui/credits/texts/programming.json +++ binaries/data/mods/public/gui/credits/texts/programming.json @@ -177,6 +177,7 @@ { "nick": "MattDoerksen", "name": "Matt Doerksen" }, { "nick": "mattlott", "name": "Matt Lott" }, { "nick": "maveric", "name": "Anton Protko" }, + { "nick": "mbusy", "name": "Maxime Busy" }, { "nick": "Micnasty", "name": "Travis Gorkin" }, { "name": "Mikołaj \"Bajter\" Korcz" }, { "nick": "mimo" }, Index: binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js +++ binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js @@ -2,7 +2,9 @@ { onOpenPage(playerIndex) { - this.setEnabled(true); + let enabled = g_IsController && !g_isSaveLoaded; + this.setEnabled(enabled); + this.playerIndex = playerIndex; this.render(); } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js @@ -52,6 +52,9 @@ if (this.onSettingsLoaded) this.gameSettingsController.registerSettingsLoadedHandler(this.onSettingsLoaded.bind(this)); + + if (this.onSettingsChanged) + this.gameSettingsController.registerSettingsChangeHandler(this.onSettingsChanged.bind(this)); if (this.onPlayerAssignmentsChange) this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlCheckbox.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlCheckbox.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlCheckbox.js @@ -11,6 +11,12 @@ this.previousSelectedValue = undefined; } + onSettingsChanged() + { + let enabled = g_IsController && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl(gameSettingControlManager) { let row = gameSettingControlManager.getNextRow("checkboxSettingFrame"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlDropdown.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlDropdown.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlDropdown.js @@ -13,6 +13,12 @@ this.dropdown.onHoverChange = this.onHoverChange.bind(this); } + onSettingsChanged() + { + let enabled = g_IsController && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl(gameSettingControlManager) { let row = gameSettingControlManager.getNextRow("dropdownSettingFrame"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlSlider.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlSlider.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControlSlider.js @@ -21,6 +21,12 @@ this.slider.max_value = this.MaxValue; } + onSettingsChanged() + { + let enabled = g_IsController && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl(gameSettingControlManager) { let row = gameSettingControlManager.getNextRow("sliderSettingFrame"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js @@ -34,6 +34,12 @@ g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); } + onSettingsChanged() + { + let enabled = g_IsController; + this.setEnabled(enabled); + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js @@ -14,6 +14,12 @@ this.rebuild(); } + onSettingsChanged() + { + let enabled = g_IsController && !g_GameSettings.playerCiv.locked[this.playerIndex] && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl() { this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js @@ -8,6 +8,12 @@ this.render(); } + onSettingsChanged() + { + let enabled = g_IsController && g_GameSettings.map.type !== "scenario" && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js @@ -24,6 +24,12 @@ this.render(); } + onSettingsChanged() + { + let enabled = g_IsController && g_GameSettings.map.type != "scenario" && !g_isSaveLoaded; + this.setEnabled(enabled); + } + setControl() { this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js @@ -6,20 +6,27 @@ constructor(setupWindow) { Engine.ProfileStart("GameSetupPage"); + + // No save data has been loaded for now + g_isSaveLoaded = false; + g_savedGameId = undefined; // This class instance owns all game setting GUI controls such as dropdowns and checkboxes visible in this page. this.gameSettingControlManager = new GameSettingControlManager(setupWindow); // These classes manage GUI buttons. { - let startGameButton = new StartGameButton(setupWindow); - let readyButton = new ReadyButton(setupWindow); + let loadGameButton = new LoadGameButton(setupWindow); + let startGameButton = new StartGameButton(setupWindow, loadGameButton); + let readyButton = new ReadyButton(setupWindow, loadGameButton); + let cancelButton = new CancelButton(setupWindow, startGameButton, readyButton) this.panelButtons = { - "cancelButton": new CancelButton(setupWindow, startGameButton, readyButton), "civInfoButton": new CivInfoButton(), "lobbyButton": new LobbyButton(), + "cancelButton": cancelButton, "readyButton": readyButton, - "startGameButton": startGameButton + "startGameButton": startGameButton, + "loadGameButton": loadGameButton }; } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml @@ -67,17 +67,20 @@ - + + - + + - + + @@ -88,6 +91,10 @@ + + + + Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.js @@ -24,8 +24,9 @@ onNeighborButtonHiddenChange() { + // Resizing the close button if the load button is hidden this.cancelButton.size = this.buttonPositions[ - this.buttonPositions[1].children.every(button => button.hidden) ? 1 : 0].size; + this.buttonPositions[2].children.every(button => button.hidden) ? 1 : 0].size; for (let handler of this.cancelButtonResizeHandlers) handler(this.cancelButton); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadGameButton.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadGameButton.js @@ -0,0 +1,129 @@ +class LoadGameButton +{ + constructor(setupWindow) + { + this.setupWindow = setupWindow; + this.buttonHiddenChangeHandlers = new Set(); + + this.loadGameButton = Engine.GetGUIObjectByName("loadGameButton"); + this.loadGameButton.onPress = this.onPress.bind(this); + this.updateCaption(); + + setupWindow.registerLoadHandler(this.onLoad.bind(this)); + } + + registerButtonHiddenChangeHandler(handler) + { + this.buttonHiddenChangeHandlers.add(handler); + } + + onLoad() + { + // Only give that option to the player hosting the game + this.loadGameButton.hidden = !g_IsController; + + for (let handler of this.buttonHiddenChangeHandlers) + handler(); + } + + updateCaption() + { + // Update the caption and tooltip of the button + let status = g_isSaveLoaded ? "clear" : "load"; + + this.loadGameButton.caption = this.Caption[status]; + this.loadGameButton.tooltip = this.Tooltip[status]; + } + + onPress() + { + // Load or clear a previously loaded save + if (g_isSaveLoaded) + this.onPressClearSave(); + else + this.onPressLoadSave(); + } + + onPressClearSave() + { + // Pass the global g_isSaveLoaded variable to false, clear the saved + // game ID and unlock the settings + g_isSaveLoaded = false; + g_savedGameId = undefined; + + g_GameSettings.pickRandomItems(); + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(); + this.updateCaption(); + } + + onPressLoadSave() + { + Engine.PushGuiPage( + "page_loadgame.xml", + {}, + this.parseGameData.bind(this)); + } + + parseGameData(data) + { + // If no data is being provided, for instance if the cancel button is + // pressed + if (typeof data === 'undefined') + return; + + + // WARNING: This line removes the null entry at index 0 of player data + // accounting for Gaïa (for display purposes). TODO: this can be + // handled more gracefully + data.metadata.initAttributes.settings.PlayerData.splice(0, 1); + + // Update the data depending on if extra human players are present. If + // the loaded data contains AI but more humans are present, the AIs are + // removed and replaced by the human players. + + //TODO: in practice removing the AI relative values in PlayerData is + // not enough, if the player was originally a petra bot, both the + // player and the petra bot will send commands + let minPlayerNumber = Math.min( + data.metadata.initAttributes.settings.PlayerData.length, + g_GameSettings.toInitAttributes().settings.PlayerData.length) + + for (let i = 0; i < minPlayerNumber; ++i) + { + let currentIndexAI = g_GameSettings.toInitAttributes().settings.PlayerData[i]["AI"]; + + if (data.metadata.initAttributes.settings.PlayerData[i]["AI"] !== currentIndexAI) + { + data.metadata.initAttributes.settings.PlayerData[i]["AI"] = currentIndexAI; + + if (currentIndexAI === false && data.metadata.initAttributes.settings.PlayerData[i].hasOwnProperty("AIBehavior")) + delete data.metadata.initAttributes.settings.PlayerData[i]["AIBehavior"] + if (currentIndexAI === false && data.metadata.initAttributes.settings.PlayerData[i].hasOwnProperty("AIDiff")) + delete data.metadata.initAttributes.settings.PlayerData[i]["AIDiff"] + } + } + + // Pass the global g_isSaveLoaded variable to true, set the + // g_savedGameId global variable and update the settings + g_isSaveLoaded = true; + g_savedGameId = data.gameId; + + g_GameSettings.fromInitAttributes(data.metadata.initAttributes); + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(); + this.updateCaption(); + } + + +} + +LoadGameButton.prototype.Caption = +{ + "load" : translate("Load Game"), + "clear" : translate("Clear Save") +}; + +LoadGameButton.prototype.Tooltip = +{ + "load" : translate("Load a previously created game. You will still have to press start after having loaded the game data"), + "clear" : translate("Clear the loaded saved data, allowing to update the setting once again and start a new game from scratch") +}; Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadGameButton.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadGameButton.xml @@ -0,0 +1,8 @@ + + Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.js @@ -1,6 +1,6 @@ class ReadyButton { - constructor(setupWindow) + constructor(setupWindow, loadGameButton) { this.readyController = setupWindow.controls.readyController; @@ -16,6 +16,9 @@ setupWindow.controls.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); setupWindow.controls.netMessages.registerNetMessageHandler("netstatus", this.onNetStatusMessage.bind(this)); + this.buttonPositions = Engine.GetGUIObjectByName("bottomRightPanel").children; + loadGameButton.registerButtonHiddenChangeHandler(this.onNeighborButtonHiddenChange.bind(this)); + if (g_IsController && g_IsNetworked) this.readyController.setReady(this.readyController.StayReady, true); } @@ -25,6 +28,16 @@ this.buttonHiddenChangeHandlers.add(handler); } + onNeighborButtonHiddenChange() + { + // Resizing the ready button if the load button is hidden + this.readyButton.size = this.buttonPositions[ + this.buttonPositions[2].children.every(button => button.hidden) ? 1 : 0].size; + + for (let handler of this.buttonHiddenChangeHandlers) + handler(); + } + onNetStatusMessage(message) { if (message.status == "disconnected") @@ -49,7 +62,7 @@ this.readyButton.hidden = hidden; for (let handler of this.buttonHiddenChangeHandlers) - handler(this.readyButton); + handler(); } registerReadyButtonPressHandler(handler) Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.js @@ -1,6 +1,6 @@ class StartGameButton { - constructor(setupWindow) + constructor(setupWindow, loadGameButton) { this.setupWindow = setupWindow; this.gameStarted = false; @@ -13,6 +13,9 @@ setupWindow.registerLoadHandler(this.onLoad.bind(this)); setupWindow.controls.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.update.bind(this)); + + this.buttonPositions = Engine.GetGUIObjectByName("bottomRightPanel").children; + loadGameButton.registerButtonHiddenChangeHandler(this.onNeighborButtonHiddenChange.bind(this)); } registerButtonHiddenChangeHandler(handler) @@ -20,9 +23,22 @@ this.buttonHiddenChangeHandlers.add(handler); } + onNeighborButtonHiddenChange() + { + // Resizing the start game button if the load button is hidden + // (although is shouldn't be displayed), because in theory the player + // isn't the controller + this.startGameButton.size = this.buttonPositions[ + this.buttonPositions[2].children.every(button => button.hidden) ? 1 : 0].size; + + for (let handler of this.buttonHiddenChangeHandlers) + handler(); + } + onLoad() { this.startGameButton.hidden = !g_IsController; + for (let handler of this.buttonHiddenChangeHandlers) handler(); } Index: binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/gamesetup.js +++ binaries/data/mods/public/gui/gamesetup/gamesetup.js @@ -24,6 +24,21 @@ */ var g_GameSettings; +/** + * If save data has been loaded from the singleplayer or multiplayer gamesetup, + * this variable will be set to true. If not, it'll be set to false. This + * variable will be used to prevent settings modifications once the game has + * been loaded, but not yet started. + */ +var g_isSaveLoaded; + +/** + * This variable will contain the gameID of a saved game, selected from the + * multiplayer gamesetup. If no save has been loaded, or if the loaded save is + * cleared, this variable will be undefined + */ + var g_savedGameId; + /** * Whether this is a single- or multiplayer match. */ Index: binaries/data/mods/public/gui/loadgame/SavegameLoader.js =================================================================== --- binaries/data/mods/public/gui/loadgame/SavegameLoader.js +++ binaries/data/mods/public/gui/loadgame/SavegameLoader.js @@ -27,7 +27,11 @@ if (sameEngineVersion && sameMods) { - this.reallyLoadGame(gameId); + if (!Engine.HasNetClient()) + this.reallyLoadGame(gameId); + else + this.reallyLoadMultiplayerGame(gameId); + return; } @@ -84,4 +88,15 @@ "savedGUIData": metadata.gui }); } + + reallyLoadMultiplayerGame(gameId) + { + // Parses the data from a multiplayer game and sends it back to the + // mutliplayer gamesetup while the page is popped. The game won't be + // started from here + + let metadata = Engine.ParseSavedGame(gameId); + Engine.PopGuiPage({"gameId": gameId, "metadata": metadata}); + return; + } } Index: binaries/data/mods/public/gui/maps/mapbrowser/MapBrowser.js =================================================================== --- binaries/data/mods/public/gui/maps/mapbrowser/MapBrowser.js +++ binaries/data/mods/public/gui/maps/mapbrowser/MapBrowser.js @@ -12,7 +12,7 @@ this.mapBrowserPageDialog = Engine.GetGUIObjectByName("mapBrowserPageDialog"); this.gridBrowser = new MapGridBrowser(this, setupWindow); - this.controls = new MapBrowserPageControls(this, this.gridBrowser); + this.controls = new MapBrowserPageControls(this, this.gridBrowser, setupWindow); this.open = false; } Index: binaries/data/mods/public/gui/maps/mapbrowser/MapBrowserPage.js =================================================================== --- binaries/data/mods/public/gui/maps/mapbrowser/MapBrowserPage.js +++ binaries/data/mods/public/gui/maps/mapbrowser/MapBrowserPage.js @@ -10,7 +10,7 @@ { let cache = new MapCache(); let filters = new MapFilters(cache); - let browser = new MapBrowser(cache, filters); + let browser = new MapBrowser(cache, filters, g_SetupWindow); browser.registerClosePageHandler(() => Engine.PopGuiPage()); browser.openPage(); browser.controls.MapFiltering.select("default", "skirmish"); Index: binaries/data/mods/public/gui/maps/mapbrowser/controls/MapBrowserControls.js =================================================================== --- binaries/data/mods/public/gui/maps/mapbrowser/controls/MapBrowserControls.js +++ binaries/data/mods/public/gui/maps/mapbrowser/controls/MapBrowserControls.js @@ -1,14 +1,39 @@ class MapBrowserPageControls { - constructor(mapBrowserPage, gridBrowser) + constructor(mapBrowserPage, gridBrowser, setupWindow = undefined) { for (let name in this) this[name] = new this[name](mapBrowserPage, gridBrowser); this.mapBrowserPage = mapBrowserPage; this.gridBrowser = gridBrowser; + this.setupWindow = setupWindow; + + this.originalCloseSize = undefined; this.setupButtons(); + + if (this.onSettingsChanged) + { + this.setupWindow.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + } + + onSettingsChanged() + { + // If the player isn't the controller or if save data has been loaded, + // hide the pickRandom and select buttons, update the size of the close + // button + let hidden = !g_IsController || g_isSaveLoaded; + + this.pickRandom.hidden = hidden; + this.select.hidden = hidden; + + if (hidden) + this.close.size = this.select.size; + else + this.close.size = this.originalCloseSize; } setupButtons() @@ -26,6 +51,8 @@ this.select.onPress = () => this.onSelect(); this.close = Engine.GetGUIObjectByName("mapBrowserPageClose"); + this.originalCloseSize = this.close.size; + if (g_SetupWindow) this.close.tooltip = colorizeHotkey( translate("%(hotkey)s: Close map browser and discard the selection."), "cancel"); Index: binaries/data/mods/public/gui/maps/mapbrowser/grid/MapGridBrowserItem.js =================================================================== --- binaries/data/mods/public/gui/maps/mapbrowser/grid/MapGridBrowserItem.js +++ binaries/data/mods/public/gui/maps/mapbrowser/grid/MapGridBrowserItem.js @@ -56,7 +56,9 @@ onMouseLeftDoubleClick() { - this.mapBrowserPage.submitMapSelection(); + // Only submit the map selection if no save is loaded + if (!g_isSaveLoaded) + this.mapBrowserPage.submitMapSelection(); } } Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -238,6 +238,8 @@ void SendStartGameMessage(const CStr& initAttribs); + void SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState); + /** * Call when the client has rejoined a running match and finished * the loading screen. @@ -279,6 +281,7 @@ static bool OnPlayerAssignment(void* context, CFsmEvent* event); static bool OnInGame(void* context, CFsmEvent* event); static bool OnGameStart(void* context, CFsmEvent* event); + static bool OnSavedGameStart(void* context, CFsmEvent* event); static bool OnJoinSyncStart(void* context, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); @@ -294,6 +297,12 @@ */ void SetAndOwnSession(CNetClientSession* session); + /** + * Starts a game with the specified init attributes and saved state. Called + * by the start game and start saved game callbacks. + */ + static void StartGame(void* context, const std::string& initAttributes, const std::string& savedState); + /** * Push a message onto the GUI queue listing the current player assignments. */ Index: source/network/NetClient.cpp =================================================================== --- source/network/NetClient.cpp +++ source/network/NetClient.cpp @@ -116,6 +116,7 @@ AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); + AddTransition(NCS_PREGAME, (uint)NMT_SAVED_GAME_START, NCS_LOADING, (void*)&OnSavedGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); @@ -125,6 +126,7 @@ AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SAVED_GAME_START, NCS_LOADING, (void*)&OnSavedGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); @@ -521,6 +523,18 @@ SendMessage(&gameStart); } +void CNetClient::SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState) +{ + CGameSavedStartMessage gameSavedStart; + gameSavedStart.m_InitAttributes = initAttribs; + + std::string compressed; + CompressZLib(savedState, compressed, true); + + gameSavedStart.m_SavedState = compressed; + SendMessage(&gameSavedStart); +} + void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; @@ -634,6 +648,30 @@ SendMessage(&authenticate); } +void CNetClient::StartGame(void* context, const std::string& initAttributes, const std::string& savedState) +{ + CNetClient* client = static_cast(context); + + // Find the player assigned to our GUID + int player = -1; + if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) + player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; + + client->m_ClientTurnManager = new CNetClientTurnManager( + *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); + + // Parse init attributes. + const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); + ScriptRequest rq(scriptInterface); + JS::RootedValue initAttribs(rq.cx); + Script::ParseJSON(rq, initAttributes, &initAttribs); + + client->m_Game->SetPlayerID(player); + client->m_Game->StartGame(&initAttribs, savedState); + + client->PushGuiMessage("type", "start", "initAttributes", initAttribs); +} + bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); @@ -795,31 +833,21 @@ // if this client rejoins and finishes the download of the simstate. bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { - ENSURE(event->GetType() == (uint)NMT_GAME_START); - - CNetClient* client = static_cast(context); + ENSURE(event->GetType() == static_cast(NMT_GAME_START)); CGameStartMessage* message = static_cast(event->GetParamRef()); - // Find the player assigned to our GUID - int player = -1; - if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) - player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; - - client->m_ClientTurnManager = new CNetClientTurnManager( - *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); - - // Parse init attributes. - const ScriptInterface& scriptInterface = client->m_Game->GetSimulation2()->GetScriptInterface(); - ScriptRequest rq(scriptInterface); - JS::RootedValue initAttribs(rq.cx); - Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs); - - client->m_Game->SetPlayerID(player); - client->m_Game->StartGame(&initAttribs, ""); + StartGame(context, message->m_InitAttributes, ""); + return true; +} - client->PushGuiMessage("type", "start", - "initAttributes", initAttribs); +bool CNetClient::OnSavedGameStart(void* context, CFsmEvent* event) +{ + ENSURE(event->GetType() == static_cast(NMT_SAVED_GAME_START)); + std::string state; + CGameSavedStartMessage* message = static_cast(event->GetParamRef()); + DecompressZLib(message->m_SavedState, state, true); + StartGame(context, message->m_InitAttributes, state); return true; } Index: source/network/NetMessage.cpp =================================================================== --- source/network/NetMessage.cpp +++ source/network/NetMessage.cpp @@ -66,10 +66,14 @@ Deserialize_int_2(pBuffer, size); m_Type = (NetMessageType)type; - if (pStart + size != pEnd) + // Don't check the size of the packet for a NMT_SAVED_GAME_START message. + // TODO: dirty, investigate why this test fails for a saved game start + // message. Note, even if the test fails the multiplayer game can be + // correctly loaded + if (m_Type != NMT_SAVED_GAME_START && pStart + size != pEnd) { LOGERROR("CNetMessage: Corrupt packet (incorrect size)"); - return NULL; + return nullptr; } return pBuffer; @@ -182,6 +186,10 @@ case NMT_GAME_START: pNewMessage = new CGameStartMessage; break; + + case NMT_SAVED_GAME_START: + pNewMessage = new CGameSavedStartMessage; + break; case NMT_END_COMMAND_BATCH: pNewMessage = new CEndCommandBatchMessage; Index: source/network/NetMessages.h =================================================================== --- source/network/NetMessages.h +++ source/network/NetMessages.h @@ -75,6 +75,7 @@ NMT_LOADED_GAME, NMT_GAME_START, + NMT_SAVED_GAME_START, NMT_END_COMMAND_BATCH, NMT_SYNC_CHECK, // OOS-detection hash checking @@ -217,6 +218,11 @@ NMT_FIELD(CStr, m_InitAttributes) END_NMT_CLASS() +START_NMT_CLASS_(GameSavedStart, NMT_SAVED_GAME_START) + NMT_FIELD(CStr, m_InitAttributes) + NMT_FIELD(CStr, m_SavedState) +END_NMT_CLASS() + START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH) NMT_FIELD_INT(m_Turn, u32, 4) NMT_FIELD_INT(m_TurnLength, u32, 2) Index: source/network/NetServer.h =================================================================== --- source/network/NetServer.h +++ source/network/NetServer.h @@ -256,11 +256,22 @@ */ void AssignPlayer(int playerID, const CStr& guid); + /** + * Switch in game mode. The clients will have to be notified to start the + * game. This method is called by StartGame and StartSavedGame + */ + void PreStartGame(const CStr& initAttribs); + /** * Switch in game mode and notify all clients to start the game. */ void StartGame(const CStr& initAttribs); + /** + * Switch in game mode and notify all clients to start the saved game. + */ + void StartSavedGame(const CStr& initAttribs, const CStr& savedData); + /** * Make a player name 'nicer' by limiting the length and removing forbidden characters etc. */ @@ -306,6 +317,7 @@ static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnAssignPlayer(void* context, CFsmEvent* event); static bool OnGameStart(void* context, CFsmEvent* event); + static bool OnSavedGameStart(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); Index: source/network/NetServer.cpp =================================================================== --- source/network/NetServer.cpp +++ source/network/NetServer.cpp @@ -698,6 +698,7 @@ session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, (void*)&OnAssignPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, (void*)&OnKickPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, (void*)&OnGameStart, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_SAVED_GAME_START, NSS_PREGAME, (void*)&OnSavedGameStart, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, (void*)&OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, (void*)&OnKickPlayer, context); @@ -1336,6 +1337,20 @@ return true; } +bool CNetServerWorker::OnSavedGameStart(void* context, CFsmEvent* event) +{ + ENSURE(event->GetType() == static_cast(NMT_SAVED_GAME_START)); + CNetServerSession* session = (CNetServerSession*)context; + CNetServerWorker& server = session->GetServer(); + + if (session->GetGUID() != server.m_ControllerGUID) + return true; + + CGameSavedStartMessage* message = (CGameSavedStartMessage*)event->GetParamRef(); + server.StartSavedGame(message->m_InitAttributes, message->m_SavedState); + return true; +} + bool CNetServerWorker::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); @@ -1526,7 +1541,7 @@ return true; } -void CNetServerWorker::StartGame(const CStr& initAttribs) +void CNetServerWorker::PreStartGame(const CStr& initAttribs) { for (std::pair& player : m_PlayerAssignments) if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) @@ -1557,12 +1572,27 @@ // Update init attributes. They should no longer change. Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes); +} + +void CNetServerWorker::StartGame(const CStr& initAttribs) +{ + PreStartGame(initAttribs); CGameStartMessage gameStart; gameStart.m_InitAttributes = initAttribs; Broadcast(&gameStart, { NSS_PREGAME }); } +void CNetServerWorker::StartSavedGame(const CStr& initAttribs, const CStr& savedState) +{ + PreStartGame(initAttribs); + + CGameSavedStartMessage gameSavedStart; + gameSavedStart.m_InitAttributes = initAttribs; + gameSavedStart.m_SavedState = savedState; + Broadcast(&gameSavedStart, { NSS_PREGAME }); +} + CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; Index: source/network/scripting/JSInterface_Network.cpp =================================================================== --- source/network/scripting/JSInterface_Network.cpp +++ source/network/scripting/JSInterface_Network.cpp @@ -33,6 +33,7 @@ #include "ps/GUID.h" #include "ps/Hashing.h" #include "ps/Pyrogenesis.h" +#include "ps/SavedGame.h" #include "ps/Util.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/StructuredClone.h" @@ -266,6 +267,24 @@ g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs)); } +void StartNetworkSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue attribs1) +{ + ENSURE(g_NetClient); + ENSURE(g_Game); + + ScriptRequest rq(scriptInterface); + JS::RootedValue attribs(rq.cx, attribs1); + + JS::RootedValue guiContextMetadata(rq.cx); + std::string savedState; + Status err = SavedGames::Load(name, scriptInterface, &guiContextMetadata, savedState); + + if (err != INFO::OK) + ScriptException::Raise(rq, "Failed to load the saved game"); + + g_NetClient->SendStartSavedGameMessage(Script::StringifyJSON(rq, &attribs), savedState); +} + void SetTurnLength(int length) { if (g_NetServer) @@ -293,6 +312,7 @@ ScriptFunction::Register<&SendNetworkReady>(rq, "SendNetworkReady"); ScriptFunction::Register<&ClearAllPlayerReady>(rq, "ClearAllPlayerReady"); ScriptFunction::Register<&StartNetworkGame>(rq, "StartNetworkGame"); + ScriptFunction::Register<&StartNetworkSavedGame>(rq, "StartNetworkSavedGame"); ScriptFunction::Register<&SetTurnLength>(rq, "SetTurnLength"); } } Index: source/ps/scripting/JSInterface_Game.cpp =================================================================== --- source/ps/scripting/JSInterface_Game.cpp +++ source/ps/scripting/JSInterface_Game.cpp @@ -38,7 +38,7 @@ { bool IsGameStarted() { - return g_Game; + return g_Game && g_Game->IsGameStarted(); } void StartGame(const ScriptInterface& guiInterface, JS::HandleValue attribs, int playerID, bool storeReplay) Index: source/ps/scripting/JSInterface_SavedGame.cpp =================================================================== --- source/ps/scripting/JSInterface_SavedGame.cpp +++ source/ps/scripting/JSInterface_SavedGame.cpp @@ -75,6 +75,22 @@ LOGERROR("Can't load quicksave if game is not running!"); } +JS::Value ParseSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name) +{ + // Parses the metadata of a saved game. + ScriptRequest rqGui(scriptInterface); + + // Load the saved game data from disk + JS::RootedValue guiContextMetadata(rqGui.cx); + std::string savedState; + Status err = SavedGames::Load(name, scriptInterface, &guiContextMetadata, savedState); + + if (err != INFO::OK) + return JS::UndefinedValue(); + + return guiContextMetadata; +} + JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name) { // We need to be careful with different compartments and contexts. @@ -131,6 +147,7 @@ ScriptFunction::Register<&QuickSave>(rq, "QuickSave"); ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad"); ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest"); + ScriptFunction::Register<&ParseSavedGame>(rq, "ParseSavedGame"); ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame"); } }