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,10 @@ // NB: for multiplayer support, the clients must be listening to "start" net messages. if (this.isNetworked) - Engine.StartNetworkGame(this.finalizedAttributes, storeReplay); + if (savegameId !== undefined) + 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 @@ -236,10 +236,12 @@ * This is run on a timer to avoid flooding the network with messages, * e.g. when modifying a slider. */ - setNetworkInitAttributes() + setNetworkInitAttributes(savegameId) { + this.savegameId = savegameId; + for (let handler of this.settingsChangeHandlers) - handler(); + handler(!!savegameId); if (g_IsNetworked && this.timer === undefined) this.timer = setTimeout(this.setNetworkInitAttributesImmediately.bind(this), this.Timeout); @@ -282,7 +284,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/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,7 @@ { onOpenPage(playerIndex) { - this.setEnabled(true); + this.setEnabled(!g_isSaveLoaded); 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 @@ -53,6 +53,8 @@ if (this.onSettingsLoaded) this.gameSettingsController.registerSettingsLoadedHandler(this.onSettingsLoaded.bind(this)); + this.gameSettingsController.registerSettingsChangeHandler(this.onSettingsChanged.bind(this)); + if (this.onPlayerAssignmentsChange) this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); } @@ -77,6 +79,11 @@ this.setControlTooltip(tooltip); } + onSettingsChanged(isSavegame) + { + this.setEnabled(!isSavegame); + } + setEnabled(enabled) { this.enabled = enabled; 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,13 @@ g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); } + onSettingsChanged(isSavegame) + { + this.isSavegame = isSavegame; + // If loading a savegame, AIs have to stay. + this.setEnabled(!(isSavegame && g_GameSettings.playerAI.get(this.playerIndex))); + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]"); @@ -100,16 +107,26 @@ rebuildList() { Engine.ProfileStart("updatePlayerAssignmentsList"); - // TODO: this particular bit is done for each row, which is unnecessarily inefficient. this.playerItems = sortGUIDsByPlayerID().map( this.clientItemFactory.createItem.bind(this.clientItemFactory)); - this.values = prepareForDropdown([ - ...this.playerItems, - ...this.aiItems, - this.unassignedItem - ]); - let selected = this.dropdown.list_data?.[this.dropdown.selected]; + // If loading a savegame human and unassigned players can't be replaced by a AI. Don't show + // the AIs in the dropdown. + this.values = (this.isSavegame && !g_GameSettings.playerAI.get(this.playerIndex)) ? + prepareForDropdown([ + ...this.playerItems, + this.unassignedItem + ]) : + prepareForDropdown([ + ...this.playerItems, + ...this.aiItems, + this.unassignedItem + ]); + + // If loading a savegame and in the savegame this player is a bot, set the bot. Otherwise set + // the previously assigned player. + const selected = (this.isSavegame && g_GameSettings.playerAI.get(this.playerIndex)?.bot) || + this.dropdown.list_data?.[this.dropdown.selected]; this.dropdown.list = this.values.Caption; this.dropdown.list_data = this.values.Value.map(x => x || "undefined"); this.setSelectedValue(selected); 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(); } + onSettingsChanged(isSavegame) + { + this.setEnabled(!g_GameSettings.playerCiv.locked[this.playerIndex] && !isSavegame); + } + 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(); } + onSettingsChanged(isSavegame) + { + this.setEnabled(g_GameSettings.map.type !== "scenario" && !isSavegame); + } + 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(); } + onSettingsChanged(isSavegame) + { + this.setEnabled(g_GameSettings.map.type != "scenario" && !isSavegame); + } + 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 @@ -15,9 +15,10 @@ let startGameButton = new StartGameButton(setupWindow); let readyButton = new ReadyButton(setupWindow); this.panelButtons = { - "cancelButton": new CancelButton(setupWindow, startGameButton, readyButton), "civInfoButton": new CivInfoButton(), "lobbyButton": new LobbyButton(), + "loadGameButton": new LoadGameButton(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,27 @@ - + - + - + + + + + - + 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/LoadGameButton.js =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadGameButton.js @@ -0,0 +1,94 @@ +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 (const handler of this.buttonHiddenChangeHandlers) + handler(); + } + + updateCaption() + { + // Update the caption and tooltip of the button + const status = this.isSavegame ? "clear" : "load"; + + this.loadGameButton.caption = this.Caption[status]; + this.loadGameButton.tooltip = this.Tooltip[status]; + } + + onPress() + { + // Load or clear a previously loaded save + if (this.isSavegame) + this.onPressClearSave(); + else + this.onPressLoadSave(); + } + + onPressClearSave() + { + this.isSavegame = false; + + g_GameSettings.pickRandomItems(); + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(undefined); + 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; + + this.isSavegame = true; + + // 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 + data.metadata.initAttributes.settings.PlayerData.splice(0, 1); + + g_GameSettings.fromInitAttributes(data.metadata.initAttributes); + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(data.gameId); + 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/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 + + const 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,13 +12,25 @@ 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; + + setupWindow?.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + + onSettingsChanged(isSavegame) + { + this.isSavegame = isSavegame; } submitMapSelection() { + // Only submit the map selection if no save is loaded + if (this.isSavegame) + return; + let file = this.gridBrowser.getSelected(); let type = this.controls.MapFiltering.getSelectedMapType(); let filter = this.controls.MapFiltering.getSelectedMapFilter(); 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); + const 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,6 +1,6 @@ class MapBrowserPageControls { - constructor(mapBrowserPage, gridBrowser) + constructor(mapBrowserPage, gridBrowser, setupWindow = undefined) { for (let name in this) this[name] = new this[name](mapBrowserPage, gridBrowser); @@ -9,6 +9,15 @@ this.gridBrowser = gridBrowser; this.setupButtons(); + + setupWindow?.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + + onSettingsChanged(isSavegame) + { + // If the player isn't the controller or if save data has been loaded, hide the select button. + this.select.hidden = !g_IsController || isSavegame; } setupButtons() @@ -39,8 +48,6 @@ this.close.onPress = () => this.mapBrowserPage.closePage(); this.select.hidden = !g_IsController; - if (!g_IsController) - this.close.size = this.select.size; this.gridBrowser.registerSelectionChangeHandler(() => this.onSelectionChange()); } Index: source/network/NetClient.h =================================================================== --- source/network/NetClient.h +++ source/network/NetClient.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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. + */ + 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 @@ -117,6 +117,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); @@ -126,6 +127,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); @@ -522,6 +524,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; @@ -635,6 +649,27 @@ SendMessage(&authenticate); } +void CNetClient::StartGame(const std::string& initAttributes, const std::string& savedState) +{ + // Find the player assigned to our GUID + 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); @@ -796,31 +831,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, ""); + static_cast(context)->StartGame(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); + static_cast(context)->StartGame(message->m_InitAttributes, state); return true; } Index: source/network/NetMessage.cpp =================================================================== --- source/network/NetMessage.cpp +++ source/network/NetMessage.cpp @@ -45,14 +45,20 @@ { size_t size = GetSerializedLength(); Serialize_int_1(pBuffer, m_Type); - Serialize_int_2(pBuffer, size); + + if (m_Type == NMT_SAVED_GAME_START) + Serialize_int_3(pBuffer, size); + else + Serialize_int_2(pBuffer, size); return pBuffer; } const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd) { - if (pStart + 3 > pEnd) + const ptrdiff_t header_size{m_Type == NMT_SAVED_GAME_START ? 4 : 3}; + + if (pStart + header_size > pEnd) { LOGERROR("CNetMessage: Corrupt packet (smaller than header)"); return NULL; @@ -63,9 +69,14 @@ int type; size_t size; Deserialize_int_1(pBuffer, type); - Deserialize_int_2(pBuffer, size); + m_Type = (NetMessageType)type; + if (m_Type == NMT_SAVED_GAME_START) + Deserialize_int_3(pBuffer, size); + else + Deserialize_int_2(pBuffer, size); + if (pStart + size != pEnd) { LOGERROR("CNetMessage: Corrupt packet (incorrect size)"); @@ -77,8 +88,9 @@ size_t CNetMessage::GetSerializedLength() const { - // By default, return header size - return 3; + // By default, return header size. In case the type of the message is + // NMT_SAVED_GAME_START, the size of its header will be 4 + return m_Type == NMT_SAVED_GAME_START ? 4 : 3; } CStr CNetMessage::ToString() const @@ -183,6 +195,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) 2023 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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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 @@ -699,6 +699,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); @@ -1337,6 +1338,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 = (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); @@ -1527,7 +1542,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) @@ -1558,12 +1573,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) 2023 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,25 @@ 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); + + 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 +313,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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2023 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -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 @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2023 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,19 @@ 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); + + return err != INFO::OK ? JS::UndefinedValue() : guiContextMetadata; +} + JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name) { // We need to be careful with different compartments and contexts. @@ -131,6 +144,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"); } }