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 != null) + 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,13 @@ * This is run on a timer to avoid flooding the network with messages, * e.g. when modifying a slider. */ - setNetworkInitAttributes() + setNetworkInitAttributes(savegameId) { + if (savegameId !== undefined) + this.savegameId = savegameId; + for (let handler of this.settingsChangeHandlers) - handler(); + handler(this.savegameId != null); if (g_IsNetworked && this.timer === undefined) this.timer = setTimeout(this.setNetworkInitAttributesImmediately.bind(this), this.Timeout); @@ -282,7 +285,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/AIConfigPage.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js +++ binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js @@ -26,6 +26,8 @@ Engine.GetGUIObjectByName("aiConfigOkButton").onPress = this.closePage.bind(this); g_GameSettings.playerAI.watch(() => this.maybeClose(), ["values"]); + + this.gameSettingsController.registerSettingsChangeHandler(this.onSettingsChanged.bind(this)); } registerOpenPageHandler(handler) @@ -38,12 +40,17 @@ return this.row++; } + onSettingsChanged(isSavegame) + { + this.isSavegame = isSavegame; + } + openPage(playerIndex) { this.playerIndex = playerIndex; for (let handler of this.openPageHandlers) - handler(playerIndex); + handler(playerIndex, this.isSavegame); 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, isSavegame) { - this.setEnabled(true); + this.setEnabled(!isSavegame); 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,19 @@ g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); } + onSettingsChanged(isSavegame) + { + this.isSavegame = isSavegame; + + const savedAI = this.isSavegame && g_GameSettings.playerAI.get(this.playerIndex); + + if (savedAI) + this.setSelectedValue(savedAI.bot); + + // If loading a savegame, AIs have to stay. + this.setEnabled(!savedAI); + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]"); @@ -103,11 +116,14 @@ // 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, - this.unassignedItem - ]); + ...AIsSelectable ? this.aiItems : [], + this.unassignedItem]); let selected = this.dropdown.list_data?.[this.dropdown.selected]; this.dropdown.list = this.values.Caption; 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,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,23 @@ +class ClearSavegameButton +{ + constructor(setupWindow) + { + this.setupWindow = setupWindow; + + this.clearSavegameButton = Engine.GetGUIObjectByName("clearSavegameButton"); + this.clearSavegameButton.onPress = this.onPress.bind(this); + + setupWindow.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + + onSettingsChanged(isSavegame) + { + this.clearSavegameButton.hidden = !g_IsController ||!isSavegame; + } + + onPress() + { + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(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,52 @@ +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)); + setupWindow.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + + onLoad() + { + this.loadSavegameButton.hidden = !g_IsController; + } + + onSettingsChanged(isSavegame) + { + this.loadSavegameButton.hidden = !g_IsController || isSavegame; + } + + 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); + + this.isSave = 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 + metadata.initAttributes.settings.PlayerData.splice(0, 1); + + g_GameSettings.fromInitAttributes(metadata.initAttributes); + this.setupWindow.controls.gameSettingsController.setNetworkInitAttributes(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/MapBrowserPage/MapBrowserPage.js =================================================================== --- binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js +++ binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js @@ -6,6 +6,12 @@ this.mapBrowserPage.hidden = true; this.gameSettingsController = setupWindow.controls.gameSettingsController; + this.gameSettingsController.registerSettingsChangeHandler(this.onSettingsChanged.bind(this)); + } + + onSettingsChanged(isSavegame) + { + this.isSavegame = isSavegame; } onSubmitMapSelection(map, type, filter) @@ -27,7 +33,7 @@ openPage() { - super.openPage(g_IsController); + super.openPage(g_IsController && !this.isSavegame); this.controls.MapFiltering.select( this.gameSettingsController.guiData.mapFilter.filter, 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 @@ -8,7 +8,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(false); 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,14 @@ this.gridBrowser = gridBrowser; this.setupButtons(); + + setupWindow?.controls.gameSettingsController.registerSettingsChangeHandler( + this.onSettingsChanged.bind(this)); + } + + onSettingsChanged(isSavegame) + { + this.select.hidden = !g_IsController || isSavegame; } setupButtons() 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,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); @@ -796,31 +830,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,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) 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_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 @@ -29,6 +29,33 @@ #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" +#include + +namespace +{ +struct LoadSavedGameResult +{ + JS::Value metadata; + std::string savedState; +}; + +std::optional LoadSavedGame(const ScriptInterface& scriptInterface, + const std::wstring& name) +{ + ScriptRequest rq{scriptInterface}; + + // Load the saved game data from disk + JS::RootedValue metadata{rq.cx}; + std::string savedState; + Status err = SavedGames::Load(name, scriptInterface, &metadata, savedState); + + if (err != INFO::OK) + return std::nullopt; + + return {{metadata, std::move(savedState)}}; +} +} + namespace JSI_SavedGame { JS::Value GetSavedGames(const ScriptInterface& scriptInterface) @@ -75,6 +102,13 @@ LOGERROR("Can't load quicksave if game is not running!"); } +JS::Value LoadSavedGameMetadata(const ScriptInterface& scriptInterface, const std::wstring& name) +{ + std::optional data{LoadSavedGame(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 +122,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{LoadSavedGame(scriptInterface, name)}; + if (!data) return JS::UndefinedValue(); + JS::RootedValue guiContextMetadata{rqGui.cx, data->metadata}; + g_Game = new CGame(true); { @@ -109,7 +142,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 +164,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"); } }