Index: binaries/data/mods/public/gamesettings/GameSettings.js =================================================================== --- binaries/data/mods/public/gamesettings/GameSettings.js +++ binaries/data/mods/public/gamesettings/GameSettings.js @@ -125,7 +125,7 @@ * since you'll need a GameSettings object anyways. * @param playerAssignments - A dict of 'local'/GUID per player and their name/slot. */ - launchGame(playerAssignments, storeReplay) + launchGame(playerAssignments, storeReplay, savegameId) { this.pickRandomItems(); @@ -142,7 +142,12 @@ // NB: for multiplayer support, the clients must be listening to "start" net messages. if (this.isNetworked) - Engine.StartNetworkGame(this.finalizedAttributes, storeReplay); + { + if (savegameId) + Engine.StartNetworkSavedGame(this.finalizedAttributes, savegameId); + else + Engine.StartNetworkGame(this.finalizedAttributes); + } 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 @@ -186,6 +186,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/Controllers/GameSettingsController.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js +++ binaries/data/mods/public/gui/gamesetup/Controllers/GameSettingsController.js @@ -245,6 +245,31 @@ this.timer = setTimeout(this.setNetworkInitAttributesImmediately.bind(this), this.Timeout); } + setSavegame(savegameId) + { + this.savegameId = savegameId; + + const gameSetupPage = this.setupWindow.pages.GameSetupPage; + const controlsManager = gameSetupPage.gameSettingControlManager; + + const mainControls = Object.values(controlsManager.gameSettingControls); + + const perPlayerControls = controlsManager.playerSettingControlManagers.flatMap( + ({playerSettingControls}) => Object.values(playerSettingControls)); + + const additionalControls = [ + gameSetupPage.panels.resetTeamsButton, + gameSetupPage.panels.resetCivsButton, + gameSetupPage.panels.mapPreview]; + + const isSavegame = savegameId != null; + + for (const control of [mainControls, perPlayerControls, additionalControls].flat()) + control.setEnabled(control.onSavegameChanged?.(isSavegame) ?? !isSavegame); + + this.setNetworkInitAttributes() + } + setNetworkInitAttributesImmediately() { if (this.timer) @@ -282,7 +307,7 @@ // This will resolve random settings & send game start messages. // TODO: this will trigger observers, which is somewhat wasteful. - g_GameSettings.launchGame(g_PlayerAssignments, true); + g_GameSettings.launchGame(g_PlayerAssignments, true, this.savegameId); // Switch to the loading page right away, // the GUI will otherwise show the unrandomised settings. Index: binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js +++ binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js @@ -5,6 +5,7 @@ { constructor(setupWindow, netMessages) { + this.setupWindow = setupWindow; this.clientJoinHandlers = new Set(); this.clientLeaveHandlers = new Set(); this.playerAssignmentsChangeHandlers = new Set(); @@ -95,8 +96,11 @@ */ onClientJoin(newGUID, newAssignments) { - if (!g_IsController || newAssignments[newGUID].player != -1) + if (!g_IsController || newAssignments[newGUID].player != -1 || + this.setupWindow.controls.gameSettingsController.savegameId == null) + { return; + } // Assign the client (or only buddies if prefered) to a free slot if (newGUID != Engine.GetPlayerGUID()) Index: binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js +++ binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js @@ -38,12 +38,12 @@ return this.row++; } - openPage(playerIndex) + openPage(playerIndex, enabled) { this.playerIndex = playerIndex; for (let handler of this.openPageHandlers) - handler(playerIndex); + handler(playerIndex, enabled); this.aiConfigPage.hidden = false; } 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 @@ -1,8 +1,8 @@ class AIGameSettingControlDropdown extends GameSettingControlDropdown { - onOpenPage(playerIndex) + onOpenPage(playerIndex, enabled) { - this.setEnabled(true); + this.setEnabled(enabled); this.playerIndex = playerIndex; this.render(); } Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js @@ -5,6 +5,7 @@ super(...args); this.aiConfigButton = Engine.GetGUIObjectByName("aiConfigButton[" + this.playerIndex + "]"); + this.aiConfigButton.onPress = this.onPress.bind(this); g_GameSettings.playerAI.watch(() => this.render(), ["values"]); // Save little performance by not reallocating every call @@ -14,8 +15,12 @@ onLoad() { - let aiConfigPage = this.setupWindow.pages.AIConfigPage; - this.aiConfigButton.onPress = aiConfigPage.openPage.bind(aiConfigPage, this.playerIndex); + this.aiConfigPage = this.setupWindow.pages.AIConfigPage; + } + + onPress() + { + this.aiConfigPage.openPage(this.playerIndex, this.enabled); } render() 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,21 @@ g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); } + onSavegameChanged(isSavegame) + { + this.isSavegame = isSavegame; + + const savedAI = this.isSavegame && g_GameSettings.playerAI.get(this.playerIndex); + + if (savedAI) + this.setSelectedValue(savedAI.bot); + else + this.rebuildList(); + + // If loading a savegame, AIs have to stay. + return !savedAI; + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]"); @@ -103,9 +118,13 @@ // TODO: this particular bit is done for each row, which is unnecessarily inefficient. this.playerItems = sortGUIDsByPlayerID().map( this.clientItemFactory.createItem.bind(this.clientItemFactory)); + + // If loading a savegame clients and unassigned players can't be replaced by a AI. Don't show + // the AIs in the dropdown. + const AIsSelectable = !this.isSavegame || g_GameSettings.playerAI.get(this.playerIndex); this.values = prepareForDropdown([ ...this.playerItems, - ...this.aiItems, + ...AIsSelectable ? this.aiItems : [], this.unassignedItem ]); 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,11 @@ this.rebuild(); } + onSavegameChanged() + { + return !g_GameSettings.playerCiv.locked[this.playerIndex] && null; + } + 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,11 @@ this.render(); } + onSavegameChanged() + { + return g_GameSettings.map.type !== "scenario" && null; + } + 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,11 @@ this.render(); } + onSavegameChanged() + { + return g_GameSettings.map.type !== "scenario" && null; + } + setControl() { this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]"); Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js @@ -13,6 +13,11 @@ this.render(); } + onSavegameChanged() + { + this.onPlayerAssignmentsChange(); + } + onPlayerAssignmentsChange() { this.guid = undefined; Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js @@ -25,7 +25,7 @@ onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(this.enabled); } }; 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 @@ -15,9 +15,11 @@ let startGameButton = new StartGameButton(setupWindow); let readyButton = new ReadyButton(setupWindow); this.panelButtons = { - "cancelButton": new CancelButton(setupWindow, startGameButton, readyButton), "civInfoButton": new CivInfoButton(), "lobbyButton": new LobbyButton(), + "clearSavegameButton": new ClearSavegameButton(setupWindow), + "loadSavegameButton": new LoadSavegameButton(setupWindow), + "cancelButton": new CancelButton(setupWindow, startGameButton, readyButton), "readyButton": readyButton, "startGameButton": startGameButton }; 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,23 +67,28 @@ - + - + - + + + + + + - + 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 @@ -25,7 +25,7 @@ onNeighborButtonHiddenChange() { 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/ClearSavegameButton.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js @@ -0,0 +1,18 @@ +class ClearSavegameButton +{ + constructor(setupWindow) + { + this.setupWindow = setupWindow; + + this.clearSavegameButton = Engine.GetGUIObjectByName("clearSavegameButton"); + this.clearSavegameButton.onPress = this.onPress.bind(this); + } + + onPress() + { + this.clearSavegameButton.hidden = true; + this.setupWindow.pages.GameSetupPage.panelButtons.loadSavegameButton.loadSavegameButton.hidden = + false; + this.setupWindow.controls.gameSettingsController.setSavegame(null); + } +} Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml @@ -0,0 +1,14 @@ + + Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js @@ -0,0 +1,47 @@ +class LoadSavegameButton +{ + constructor(setupWindow) + { + this.setupWindow = setupWindow; + + this.loadSavegameButton = Engine.GetGUIObjectByName("loadSavegameButton"); + this.loadSavegameButton.onPress = this.onPress.bind(this); + + setupWindow.registerLoadHandler(this.onLoad.bind(this)); + } + + onLoad() + { + this.loadSavegameButton.hidden = !g_IsController; + } + + onPress() + { + Engine.PushGuiPage( + "page_loadgame.xml", + {}, + this.parseGameData.bind(this)); + } + + parseGameData(gameId) + { + // If no data is being provided, for instance if the cancel button is + // pressed + if (gameId === undefined) + return; + + const metadata = Engine.LoadSavedGameMetadata(gameId); + + // WARNING: This line removes the null entry at index 0 of player data + // accounting for Gaia (for display purposes). TODO: this can be + // handled more gracefully + metadata.initAttributes.settings.PlayerData.splice(0, 1); + + g_GameSettings.fromInitAttributes(metadata.initAttributes); + + this.loadSavegameButton.hidden = true; + this.setupWindow.pages.GameSetupPage.panelButtons.clearSavegameButton.clearSavegameButton. + hidden = false; + this.setupWindow.controls.gameSettingsController.setSavegame(gameId); + } +} Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml @@ -0,0 +1,13 @@ + + + + Load Save + + + Load a previously created Savegame. You will still have to press start after having loaded the game data + + Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js @@ -11,9 +11,24 @@ g_GameSettings.map.watch(() => this.render(), ["type"]); } + shouldBeEnabled() + { + return g_GameSettings.map.type !== "scenario" && g_IsController; + } + + onSavegameChanged() + { + return this.shouldBeEnabled() && null; + } + + setEnabled(enabled) + { + this.civResetButton.hidden = !enabled; + } + render() { - this.civResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; + this.setEnabled(this.shouldBeEnabled()); } onPress() Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js @@ -11,9 +11,24 @@ g_GameSettings.map.watch(() => this.render(), ["type"]); } + shouldBeEnabled() + { + return g_GameSettings.map.type !== "scenario" && g_IsController; + } + + onSavegameChanged() + { + return this.shouldBeEnabled() && null; + } + + setEnabled(enabled) + { + this.teamResetButton.hidden = !enabled; + } + render() { - this.teamResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; + this.setEnabled(this.shouldBeEnabled()); } onPress() Index: binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js @@ -1,5 +1,7 @@ class MapPreview { + enabled = true; + constructor(setupWindow) { this.setupWindow = setupWindow; @@ -15,9 +17,14 @@ g_GameSettings.mapPreview.watch(() => this.renderPreview(), ["value"]); } + setEnabled(enabled) + { + this.enabled = enabled; + } + onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(this.enabled); } renderName() Index: binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js +++ binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js @@ -25,9 +25,9 @@ this.gameSettingsController.setNetworkInitAttributes(); } - openPage() + openPage(enabled) { - super.openPage(g_IsController); + super.openPage(g_IsController && enabled); this.controls.MapFiltering.select( this.gameSettingsController.guiData.mapFilter.filter, Index: binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js +++ binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js @@ -38,6 +38,9 @@ */ saveFile(settings) { + for (let player of settings.settings.PlayerData) + delete player.Name; + Engine.ProfileStart("savePersistMatchSettingsFile"); Engine.WriteJSONFile(this.filename, { "attributes": this.enabled ? settings : {}, Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -236,6 +236,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. @@ -277,6 +279,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); @@ -292,6 +295,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. + */ + void StartGame(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 @@ -86,6 +86,7 @@ AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, &OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, &OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, &OnGameStart, context); + AddTransition(NCS_PREGAME, (uint)NMT_SAVED_GAME_START, NCS_LOADING, &OnSavedGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, &OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, &OnChat, context); @@ -95,6 +96,7 @@ AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, &OnClientTimeout, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, &OnClientPerformance, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, &OnGameStart, context); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SAVED_GAME_START, NCS_LOADING, &OnSavedGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, &OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, &OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, &OnLoadedGame, context); @@ -491,6 +493,15 @@ SendMessage(&gameStart); } +void CNetClient::SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState) +{ + CGameSavedStartMessage gameSavedStart; + gameSavedStart.m_InitAttributes = initAttribs; + CompressZLib(savedState, gameSavedStart.m_SavedState, true); + + SendMessage(&gameSavedStart); +} + void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; @@ -604,6 +615,26 @@ SendMessage(&authenticate); } +void CNetClient::StartGame(const std::string& initAttributes, const std::string& savedState) +{ + const auto foundPlayer = m_PlayerAssignments.find(m_GUID); + const i32 player{foundPlayer != m_PlayerAssignments.end() ? foundPlayer->second.m_PlayerID : -1}; + + m_ClientTurnManager = new CNetClientTurnManager{*m_Game->GetSimulation2(), *this, + static_cast(m_HostID), m_Game->GetReplayLogger()}; + + // Parse init attributes. + const ScriptInterface& scriptInterface{m_Game->GetSimulation2()->GetScriptInterface()}; + ScriptRequest rq{scriptInterface}; + JS::RootedValue initAttribs{rq.cx}; + Script::ParseJSON(rq, initAttributes, &initAttribs); + + m_Game->SetPlayerID(player); + m_Game->StartGame(&initAttribs, savedState); + + PushGuiMessage("type", "start", "initAttributes", initAttribs); +} + bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); @@ -766,30 +797,19 @@ bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); - - CNetClient* client = static_cast(context); CGameStartMessage* message = static_cast(event->GetParamRef()); + static_cast(context)->StartGame(message->m_InitAttributes, ""); + return true; +} - // 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, ""); - - 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); + static_cast(context)->StartGame(message->m_InitAttributes, state); return true; } Index: source/network/NetMessage.cpp =================================================================== --- source/network/NetMessage.cpp +++ source/network/NetMessage.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2023 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -45,14 +45,14 @@ { size_t size = GetSerializedLength(); Serialize_int_1(pBuffer, m_Type); - Serialize_int_2(pBuffer, size); + Serialize_int_3(pBuffer, size); return pBuffer; } const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd) { - if (pStart + 3 > pEnd) + if (pStart + 4 > pEnd) { LOGERROR("CNetMessage: Corrupt packet (smaller than header)"); return NULL; @@ -63,7 +63,7 @@ int type; size_t size; Deserialize_int_1(pBuffer, type); - Deserialize_int_2(pBuffer, size); + Deserialize_int_3(pBuffer, size); m_Type = (NetMessageType)type; if (pStart + size != pEnd) @@ -78,7 +78,7 @@ size_t CNetMessage::GetSerializedLength() const { // By default, return header size - return 3; + return 4; } CStr CNetMessage::ToString() const @@ -183,6 +183,10 @@ pNewMessage = new CGameStartMessage; break; + case NMT_SAVED_GAME_START: + pNewMessage = new CGameSavedStartMessage; + break; + case NMT_END_COMMAND_BATCH: pNewMessage = new CEndCommandBatchMessage; break; Index: source/network/NetMessages.h =================================================================== --- source/network/NetMessages.h +++ source/network/NetMessages.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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 @@ -255,11 +255,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. */ @@ -305,6 +316,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 @@ -648,6 +648,7 @@ session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, context); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, context); + session->AddTransition(NSS_PREGAME, (uint)NMT_SAVED_GAME_START, NSS_PREGAME, &OnSavedGameStart, context); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, context); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, context); @@ -1312,6 +1313,20 @@ return true; } +bool CNetServerWorker::OnSavedGameStart(void* context, CFsmEvent* event) +{ + ENSURE(event->GetType() == static_cast(NMT_SAVED_GAME_START)); + CNetServerSession* session{static_cast(context)}; + CNetServerWorker& server{session->GetServer()}; + + if (session->GetGUID() != server.m_ControllerGUID) + return true; + + CGameSavedStartMessage* message = static_cast(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); @@ -1502,7 +1517,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) @@ -1533,12 +1548,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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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,26 @@ g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs)); } +void StartNetworkSavedGame(const ScriptInterface& scriptInterface, JS::HandleValue attribs1, + const std::wstring& name) +{ + ENSURE(g_NetClient); + ENSURE(g_Game); + + const ScriptRequest rq{scriptInterface}; + JS::RootedValue attribs{rq.cx, attribs1}; + + const std::optional loadResult{SavedGames::Load(scriptInterface, name)}; + + if (!loadResult) + { + ScriptException::Raise(rq, "Failed to load the saved game"); + return; + } + + g_NetClient->SendStartSavedGameMessage(Script::StringifyJSON(rq, &attribs), loadResult->savedState); +} + void SetTurnLength(int length) { if (g_NetServer) @@ -293,6 +314,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/SavedGame.h =================================================================== --- source/ps/SavedGame.h +++ source/ps/SavedGame.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -19,8 +19,11 @@ #define INCLUDED_SAVEDGAME #include "ps/CStr.h" +#include "scriptinterface/ScriptTypes.h" #include "scriptinterface/StructuredClone.h" +#include + class CSimulation2; /** @@ -59,18 +62,24 @@ */ Status SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const Script::StructuredClone& guiMetadataClone); + struct LoadResult + { + // Object containing metadata associated with saved game, + // parsed from metadata.json inside the archive. + JS::Value metadata; + // Serialized simulation state stored as string of bytes, + // loaded from simulation.dat inside the archive. + std::string savedState; + }; + /** * Load saved game archive with the given name * * @param name filename of saved game (without path or extension) * @param scriptInterface - * @param[out] metadata object containing metadata associated with saved game, - * parsed from metadata.json inside the archive. - * @param[out] savedState serialized simulation state stored as string of bytes, - * loaded from simulation.dat inside the archive. - * @return INFO::OK if successfully loaded, else an error Status + * @return An empty `std::optional` if an error ocoured. */ - Status Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState); + std::optional Load(const ScriptInterface& scriptInterface, const std::wstring& name); /** * Get list of saved games for GUI script usage Index: source/ps/SavedGame.cpp =================================================================== --- source/ps/SavedGame.cpp +++ source/ps/SavedGame.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -204,7 +204,8 @@ std::string* m_SavedState; }; -Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) +std::optional SavedGames::Load(const ScriptInterface& scriptInterface, + const std::wstring& name) { // Determine the filename to load const VfsPath basename(L"saves/" + name); @@ -212,20 +213,41 @@ // Don't crash just because file isn't found, this can happen if the file is deleted from the OS if (!VfsFileExists(filename)) - return ERR::FILE_NOT_FOUND; + return std::nullopt; OsPath realPath; - WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); + { + const Status status{g_VFS->GetRealPath(filename, realPath)}; + if (status < 0) + { + DEBUG_WARN_ERR(status); + return std::nullopt; + } + } PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) - WARN_RETURN(ERR::FAIL); + { + DEBUG_WARN_ERR(ERR::FAIL); + return std::nullopt; + } + std::string savedState; CGameLoader loader(scriptInterface, &savedState); - WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); - metadata.set(loader.GetMetadata()); + { + const Status status{archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, + reinterpret_cast(&loader))}; + if (status < 0) + { + DEBUG_WARN_ERR(status); + return std::nullopt; + } + } + ScriptRequest rq{scriptInterface}; + JS::RootedValue metadata{rq.cx, loader.GetMetadata()}; - return INFO::OK; + // `std::make_optional` can't be used since `LoadResult` doesn't have a constructor. + return {{metadata, std::move(savedState)}}; } JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface) Index: source/ps/scripting/JSInterface_SavedGame.cpp =================================================================== --- source/ps/scripting/JSInterface_SavedGame.cpp +++ source/ps/scripting/JSInterface_SavedGame.cpp @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -29,6 +29,8 @@ #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" +#include + namespace JSI_SavedGame { JS::Value GetSavedGames(const ScriptInterface& scriptInterface) @@ -75,6 +77,13 @@ LOGERROR("Can't load quicksave if game is not running!"); } +JS::Value LoadSavedGameMetadata(const ScriptInterface& scriptInterface, const std::wstring& name) +{ + std::optional data{SavedGames::Load(scriptInterface, name)}; + + return !data ? JS::UndefinedValue() : data->metadata; +} + JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name) { // We need to be careful with different compartments and contexts. @@ -88,13 +97,12 @@ ENSURE(!g_Game); - // 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 < 0) + std::optional data{SavedGames::Load(scriptInterface, name)}; + if (!data) return JS::UndefinedValue(); + JS::RootedValue guiContextMetadata{rqGui.cx, data->metadata}; + g_Game = new CGame(true); { @@ -109,7 +117,7 @@ Script::GetProperty(rqGame, gameContextMetadata, "playerID", playerID); g_Game->SetPlayerID(playerID); - g_Game->StartGame(&gameInitAttributes, savedState); + g_Game->StartGame(&gameInitAttributes, data->savedState); } return guiContextMetadata; @@ -131,6 +139,7 @@ ScriptFunction::Register<&QuickSave>(rq, "QuickSave"); ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad"); ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest"); + ScriptFunction::Register<&LoadSavedGameMetadata>(rq, "LoadSavedGameMetadata"); ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame"); } }