diff --git a/binaries/data/mods/public/gamesettings/attributes/Savegame.js b/binaries/data/mods/public/gamesettings/attributes/Savegame.js new file mode 100644 index 0000000000..8f91c1c3f5 --- /dev/null +++ b/binaries/data/mods/public/gamesettings/attributes/Savegame.js @@ -0,0 +1,16 @@ +GameSettings.prototype.Attributes.Savegame = class extends GameSetting +{ + value = null; + + toInitAttributes(attribs) + { + if (this.value !== null) + attribs.settings.Savegame = this.value; + } + + fromInitAttributes(attribs) + { + const newValue = this.getLegacySetting(attribs, "Savegame"); + this.value = newValue ?? null; + } +}; diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json b/binaries/data/mods/public/gui/credits/texts/programming.json index 91941ab72d..95ab09ea60 100644 --- a/binaries/data/mods/public/gui/credits/texts/programming.json +++ b/binaries/data/mods/public/gui/credits/texts/programming.json @@ -189,6 +189,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" }, diff --git a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js index 771fab92e5..4096bbe238 100644 --- a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js +++ b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js @@ -95,8 +95,11 @@ class PlayerAssignmentsController */ onClientJoin(newGUID, newAssignments) { - if (!g_IsController || newAssignments[newGUID].player != -1) + if (!g_IsController || newAssignments[newGUID].player !== -1 || + g_GameSettings.savegame.value !== null) + { return; + } // Assign the client (or only buddies if prefered) to a free slot if (newGUID != Engine.GetPlayerGUID()) diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js index c3d1ab862e..9f558ca7f0 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js @@ -38,12 +38,12 @@ SetupWindowPages.AIConfigPage = class 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; } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js index 6a9bb19cbf..4c64ff316e 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js +++ b/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(); } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js index f87111114b..c30f1c1f23 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js @@ -55,6 +55,11 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls if (this.onPlayerAssignmentsChange) this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); + + g_GameSettings.savegame.watch(() => { + const isSavegame = g_GameSettings.savegame.value !== null; + this.setEnabled(this.onSavegameChanged?.(isSavegame) ?? !isSavegame); + }, ["value"]); } setTitle(titleCaption) diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js index b7f7841982..3f611190cc 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js @@ -5,6 +5,9 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC super(...args); this.aiConfigButton = Engine.GetGUIObjectByName("aiConfigButton[" + this.playerIndex + "]"); + this.aiConfigButton.onPress = () => { + this.setupWindow.pages.AIConfigPage.openPage(this.playerIndex, this.enabled); + }; g_GameSettings.playerAI.watch(() => this.render(), ["values"]); // Save little performance by not reallocating every call @@ -12,12 +15,6 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC this.render(); } - onLoad() - { - let aiConfigPage = this.setupWindow.pages.AIConfigPage; - this.aiConfigButton.onPress = aiConfigPage.openPage.bind(aiConfigPage, this.playerIndex); - } - render() { this.aiConfigButton.hidden = !g_GameSettings.playerAI.get(this.playerIndex); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js index 4afe2ad703..191b64fa4a 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js @@ -34,6 +34,19 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); } + onSavegameChanged(isSavegame) + { + const savedAI = 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 +116,14 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett // 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 = g_GameSettings.savegame.value === null || + g_GameSettings.playerAI.get(this.playerIndex); this.values = prepareForDropdown([ ...this.playerItems, - ...this.aiItems, + ...AIsSelectable ? this.aiItems : [], this.unassignedItem ]); @@ -161,7 +179,8 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100; if (ai) g_GameSettings.playerAI.swap(sourcePlayer, playerIndex); // Swap color + civ as well - this allows easy reorganizing of player order. - if (g_GameSettings.map.type !== "scenario") + if (g_GameSettings.savegame.value === null && + g_GameSettings.map.type !== "scenario") { g_GameSettings.playerCiv.swap(sourcePlayer, playerIndex); g_GameSettings.playerColor.swap(sourcePlayer, playerIndex); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js index 561c692117..3bbfd45fc6 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js @@ -14,6 +14,11 @@ PlayerSettingControls.PlayerCiv = class PlayerCiv extends GameSettingControlDrop this.rebuild(); } + onSavegameChanged() + { + return !g_GameSettings.playerCiv.locked[this.playerIndex] && null; + } + setControl() { this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js index a0b4e8fd5f..26286b79fd 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js @@ -8,6 +8,11 @@ PlayerSettingControls.PlayerColor = class PlayerColor extends GameSettingControl this.render(); } + onSavegameChanged() + { + return g_GameSettings.map.type !== "scenario" && null; + } + setControl() { this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js index e7e865cd37..b09e6abc6f 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js @@ -24,6 +24,11 @@ PlayerSettingControls.PlayerTeam = class PlayerTeam extends GameSettingControlDr this.render(); } + onSavegameChanged() + { + return g_GameSettings.map.type !== "scenario" && null; + } + setControl() { this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js index fc584e655a..728b1ec52c 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js @@ -13,6 +13,13 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl this.render(); } + onSavegameChanged() + { + this.onPlayerAssignmentsChange(); + g_GameSettings.playerName.maybeUpdate(); + this.render(); + } + onPlayerAssignmentsChange() { this.guid = undefined; diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js index ddc90c5176..52cde43016 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js @@ -25,7 +25,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(this.enabled); } }; diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js index 5c4560161e..308d00afdc 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js @@ -15,9 +15,14 @@ SetupWindowPages.GameSetupPage = class 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.controls.gameSettingsController), + "loadSavegameButton": new LoadSavegameButton( + setupWindow.controls.gameSettingsController), + "savegameLabel": new SavegameLabel(), + "cancelButton": new CancelButton(setupWindow, startGameButton, readyButton), "readyButton": readyButton, "startGameButton": startGameButton }; @@ -31,7 +36,7 @@ SetupWindowPages.GameSetupPage = class this.panels = { "chatPanel": new ChatPanel(setupWindow, this.gameSettingControlManager, gameSettingsPanel), - "gameSettingWarning": new GameSettingWarning(setupWindow, this.panelButtons.cancelButton), + "gameSettingWarning": new GameSettingWarning(setupWindow), "gameDescription": new GameDescription(setupWindow, gameSettingTabs), "gameSettingsPanel": gameSettingsPanel, "gameSettingsTabs": gameSettingTabs, diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml index 7b525684da..43d2a2f29d 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml @@ -67,23 +67,29 @@ - + - + - + + + + + + + - + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js new file mode 100644 index 0000000000..8b9676318d --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js @@ -0,0 +1,17 @@ +class ClearSavegameButton +{ + constructor(gameSettingsController) + { + const clearSavegameButton = Engine.GetGUIObjectByName("clearSavegameButton"); + clearSavegameButton.onPress = () => { + g_GameSettings.savegame.value = null; + gameSettingsController.setNetworkInitAttributes(); + }; + if (g_IsNetworked && g_IsController) + { + g_GameSettings.savegame.watch(() => { + clearSavegameButton.hidden = g_GameSettings.savegame.value === null; + }, ["value"]); + } + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml new file mode 100644 index 0000000000..33a42001b3 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml @@ -0,0 +1,14 @@ + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js new file mode 100644 index 0000000000..60a9c637ad --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js @@ -0,0 +1,38 @@ +class LoadSavegameButton +{ + constructor(gameSettingsController) + { + this.gameSettingsController = gameSettingsController; + + const loadSavegameButton = Engine.GetGUIObjectByName("loadSavegameButton"); + loadSavegameButton.onPress = this.loadSavegame.bind(this); + + if (g_IsNetworked && g_IsController) + { + g_GameSettings.savegame.watch(() => { + loadSavegameButton.hidden = g_GameSettings.savegame.value !== null; + }, ["value"]); + } + else + loadSavegameButton.hidden = true; + } + + async loadSavegame() + { + const gameId = await Engine.PushGuiPage("page_loadgame.xml"); + + // If no data is being provided, for instance if the cancel button is + // pressed + if (gameId === undefined) + return; + + const metadata = Engine.LoadSavedGameMetadata(gameId); + + // Remove the gaia entry. + metadata.initAttributes.settings.PlayerData.splice(0, 1); + + g_GameSettings.fromInitAttributes(metadata.initAttributes); + g_GameSettings.savegame.value = gameId; + this.gameSettingsController.setNetworkInitAttributes(); + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml new file mode 100644 index 0000000000..595358eb5b --- /dev/null +++ b/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. + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js index 996bd36ab8..cb253ac5b5 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js @@ -9,11 +9,25 @@ class ResetCivsButton this.civResetButton.onPress = this.onPress.bind(this); g_GameSettings.map.watch(() => this.render(), ["type"]); + g_GameSettings.savegame.watch(() => { + const isSavegame = g_GameSettings.savegame.value !== null; + this.setEnabled(this.shouldBeEnabled() && !isSavegame); + }, ["value"]); + } + + shouldBeEnabled() + { + return g_GameSettings.map.type !== "scenario" && g_IsController; + } + + setEnabled(enabled) + { + this.civResetButton.hidden = !enabled; } render() { - this.civResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; + this.setEnabled(this.shouldBeEnabled()); } onPress() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js index 77086a5272..2669be6189 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js @@ -9,11 +9,25 @@ class ResetTeamsButton this.teamResetButton.onPress = this.onPress.bind(this); g_GameSettings.map.watch(() => this.render(), ["type"]); + g_GameSettings.savegame.watch(() => { + const isSavegame = g_GameSettings.savegame.value !== null; + this.setEnabled(this.shouldBeEnabled() && !isSavegame); + }, ["value"]); + } + + shouldBeEnabled() + { + return g_GameSettings.map.type !== "scenario" && g_IsController; + } + + setEnabled(enabled) + { + this.teamResetButton.hidden = !enabled; } render() { - this.teamResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; + this.setEnabled(this.shouldBeEnabled()); } onPress() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js new file mode 100644 index 0000000000..3460b3c97e --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js @@ -0,0 +1,13 @@ +class SavegameLabel +{ + constructor() + { + const savegameLabel = Engine.GetGUIObjectByName("savegameLabel"); + if (g_IsNetworked && !g_IsController) + { + g_GameSettings.savegame.watch(() => { + savegameLabel.hidden = g_GameSettings.savegame.value === null; + }, ["value"]); + } + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml new file mode 100644 index 0000000000..2cc167e3c4 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml @@ -0,0 +1,14 @@ + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js index 169bc5fef6..3438e9891c 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js @@ -17,7 +17,7 @@ class MapPreview onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(g_GameSettings.savegame.value === null); } renderName() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js index dd34f5c0bf..3347c729e0 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js @@ -25,9 +25,9 @@ SetupWindowPages.MapBrowserPage = class extends MapBrowser this.gameSettingsController.setNetworkInitAttributes(); } - openPage() + openPage(enabled) { - super.openPage(g_IsController); + super.openPage(g_IsController && enabled); this.controls.MapFiltering.select( this.gameSettingsController.guiData.mapFilter.filter, diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 878f098593..5850f067e1 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -84,6 +84,7 @@ CNetClient::CNetClient(CGame* game) : AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, &OnClientTimeout, this); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, &OnClientPerformance, this); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, &OnGameStart, this); + AddTransition(NCS_PREGAME, (uint)NMT_SAVED_GAME_START, NCS_LOADING, &OnSavedGameStart, this); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, &OnJoinSyncStart, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, &OnChat, this); @@ -93,6 +94,7 @@ CNetClient::CNetClient(CGame* game) : AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, &OnClientTimeout, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, &OnClientPerformance, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, &OnGameStart, this); + AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SAVED_GAME_START, NCS_LOADING, &OnSavedGameStart, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, &OnInGame, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, &OnJoinSyncEndCommandBatch, this); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, &OnLoadedGame, this); @@ -489,6 +491,15 @@ void CNetClient::SendStartGameMessage(const CStr& initAttribs) SendMessage(&gameStart); } +void CNetClient::SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState) +{ + m_SavedState = savedState; + + CGameSavedStartMessage gameSavedStart; + gameSavedStart.m_InitAttributes = initAttribs; + SendMessage(&gameSavedStart); +} + void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; @@ -524,25 +535,29 @@ bool CNetClient::HandleMessage(CNetMessage* message) { CFileTransferRequestMessage* reqMessage = static_cast(message); - ENSURE(static_cast(reqMessage->m_RequestType) == - CNetFileTransferer::RequestType::REJOIN); + std::string toCompress{[&] + { + if (static_cast(reqMessage->m_RequestType) == + CNetFileTransferer::RequestType::LOADGAME) + { + return std::exchange(m_SavedState, {}); + } - // TODO: we should support different transfer request types, instead of assuming - // it's always requesting the simulation state + std::stringstream stream; - std::stringstream stream; + LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); + u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); + stream.write((char*)&turn, sizeof(turn)); - LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); - u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); - stream.write((char*)&turn, sizeof(turn)); - - bool ok = m_Game->GetSimulation2()->SerializeState(stream); - ENSURE(ok); + bool ok = m_Game->GetSimulation2()->SerializeState(stream); + ENSURE(ok); + return stream.str(); + }()}; // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; - CompressZLib(stream.str(), compressed, true); + CompressZLib(toCompress, compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); @@ -605,6 +620,18 @@ void CNetClient::SendAuthenticateMessage() SendMessage(&authenticate); } +void CNetClient::StartGame(const JS::MutableHandleValue 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()}; + + m_Game->SetPlayerID(player); + m_Game->StartGame(initAttributes, savedState); +} + bool CNetClient::OnConnect(CNetClient* client, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); @@ -759,29 +786,40 @@ bool CNetClient::OnGameStart(CNetClient* client, CFsmEvent* event) 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); + 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); - + client->PushGuiMessage("type", "start", "initAttributes", initAttribs); + client->StartGame(&initAttribs, ""); return true; } +bool CNetClient::OnSavedGameStart(CNetClient* client, CFsmEvent* event) +{ + ENSURE(event->GetType() == static_cast(NMT_SAVED_GAME_START)); + CGameSavedStartMessage* message{static_cast(event->GetParamRef())}; + + const ScriptInterface& scriptInterface{client->m_Game->GetSimulation2()->GetScriptInterface()}; + ScriptRequest rq{scriptInterface}; + const std::shared_ptr initAttribs{std::make_shared(rq.cx)}; + Script::ParseJSON(rq, message->m_InitAttributes, &*initAttribs); + + client->PushGuiMessage("type", "start", "initAttributes", *initAttribs); + + client->m_Session->GetFileTransferer().StartTask(CNetFileTransferer::RequestType::LOADGAME, + [client, initAttribs](std::string buffer) + { + std::string state; + DecompressZLib(buffer, state, true); + + client->StartGame(&*initAttribs, state); + }); + return true; +} + + bool CNetClient::OnJoinSyncStart(CNetClient* client, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); diff --git a/source/network/NetClient.h b/source/network/NetClient.h index 41671d9b24..87b8081939 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -236,6 +236,8 @@ public: 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 @@ private: static bool OnPlayerAssignment(CNetClient* client, CFsmEvent* event); static bool OnInGame(CNetClient* client, CFsmEvent* event); static bool OnGameStart(CNetClient* client, CFsmEvent* event); + static bool OnSavedGameStart(CNetClient* client, CFsmEvent* event); static bool OnJoinSyncStart(CNetClient* client, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(CNetClient* client, CFsmEvent* event); static bool OnRejoined(CNetClient* client, CFsmEvent* event); @@ -292,6 +295,12 @@ private: */ 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 JS::MutableHandleValue initAttributes, const std::string& savedState); + /** * Push a message onto the GUI queue listing the current player assignments. */ @@ -342,6 +351,8 @@ private: /// Serialized game state received when joining an in-progress game std::string m_JoinSyncBuffer; + std::string m_SavedState; + /// Time when the server was last checked for timeouts and bad latency std::time_t m_LastConnectionCheck; }; diff --git a/source/network/NetMessage.cpp b/source/network/NetMessage.cpp index 9515d939de..3a38f1cd94 100644 --- a/source/network/NetMessage.cpp +++ b/source/network/NetMessage.cpp @@ -183,6 +183,10 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData, pNewMessage = new CGameStartMessage; break; + case NMT_SAVED_GAME_START: + pNewMessage = new CGameSavedStartMessage; + break; + case NMT_END_COMMAND_BATCH: pNewMessage = new CEndCommandBatchMessage; break; diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index c6bdcb4f9d..7809eb70b2 100644 --- a/source/network/NetMessages.h +++ b/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 @@ enum NetMessageType NMT_LOADED_GAME, NMT_GAME_START, + NMT_SAVED_GAME_START, NMT_END_COMMAND_BATCH, NMT_SYNC_CHECK, // OOS-detection hash checking @@ -218,6 +219,10 @@ START_NMT_CLASS_(GameStart, NMT_GAME_START) NMT_FIELD(CStr, m_InitAttributes) END_NMT_CLASS() +START_NMT_CLASS_(GameSavedStart, NMT_SAVED_GAME_START) + NMT_FIELD(CStr, m_InitAttributes) +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) diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 9ad3a98e6e..6e7dbf7930 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -623,14 +623,15 @@ void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServ if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; - ENSURE(static_cast(reqMessage->m_RequestType) == - CNetFileTransferer::RequestType::REJOIN); - // Rejoining client got our JoinSyncStart after we received the state from - // another client, and has now requested that we forward it to them + // A client requested the gamestate. Clients only request the gamestate when we sent them a + // JoinSyncStart or a GameSavedStart message. We only send thous messages after we received the + // gamestate. + // For joins and loads the gamestate is in a different format. Send the respective one. - ENSURE(!m_JoinSyncFile.empty()); - session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); + session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, + static_cast(reqMessage->m_RequestType) == + CNetFileTransferer::RequestType::LOADGAME ? m_SavedState : m_JoinSyncFile); return; } @@ -663,6 +664,7 @@ void CNetServerWorker::SetupSession(CNetServerSession* session) session->AddTransition(NSS_PREGAME, (uint)NMT_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, session); session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, session); session->AddTransition(NSS_PREGAME, (uint)NMT_GAME_START, NSS_PREGAME, &OnGameStart, session); + session->AddTransition(NSS_PREGAME, (uint)NMT_SAVED_GAME_START, NSS_PREGAME, &OnSavedGameStart, session); session->AddTransition(NSS_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, session); session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, session); @@ -1318,6 +1320,25 @@ bool CNetServerWorker::OnGameStart(CNetServerSession* session, CFsmEvent* event) return true; } +bool CNetServerWorker::OnSavedGameStart(CNetServerSession* session, CFsmEvent* event) +{ + ENSURE(event->GetType() == static_cast(NMT_SAVED_GAME_START)); + CNetServerWorker& server{session->GetServer()}; + + if (session->GetGUID() != server.m_ControllerGUID) + return true; + + CGameSavedStartMessage* message = static_cast(event->GetParamRef()); + session->GetFileTransferer().StartTask(CNetFileTransferer::RequestType::LOADGAME, + [&server, initAttributes = std::move(message->m_InitAttributes)](std::string buffer) + { + server.m_SavedState = std::move(buffer); + + server.StartSavedGame(initAttributes); + }); + return true; +} + bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); @@ -1341,6 +1362,10 @@ bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent* message.m_Clients.push_back(client); } + // If no other player is loading the server can clear the savestate + if (message.m_Clients.empty()) + server.m_SavedState.clear(); + // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet loadedSession->SendMessage(&message); server.Broadcast(&message, { NSS_INGAME }); @@ -1502,7 +1527,7 @@ bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession) 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 +1558,26 @@ void CNetServerWorker::StartGame(const CStr& initAttribs) // 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) +{ + PreStartGame(initAttribs); + + CGameSavedStartMessage gameSavedStart; + gameSavedStart.m_InitAttributes = initAttribs; + Broadcast(&gameSavedStart, { NSS_PREGAME }); +} + CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original) { const size_t MAX_LENGTH = 32; diff --git a/source/network/NetServer.h b/source/network/NetServer.h index d571ab8444..c76669585a 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -255,11 +255,22 @@ private: */ 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); + /** * Make a player name 'nicer' by limiting the length and removing forbidden characters etc. */ @@ -305,6 +316,7 @@ private: static bool OnGameSetup(CNetServerSession* session, CFsmEvent* event); static bool OnAssignPlayer(CNetServerSession* session, CFsmEvent* event); static bool OnGameStart(CNetServerSession* session, CFsmEvent* event); + static bool OnSavedGameStart(CNetServerSession* session, CFsmEvent* event); static bool OnLoadedGame(CNetServerSession* session, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmEvent* event); static bool OnRejoined(CNetServerSession* session, CFsmEvent* event); @@ -399,6 +411,11 @@ private: */ std::string m_JoinSyncFile; + /** + * The loaded game data when a game is loaded. + */ + std::string m_SavedState; + /** * Time when the clients connections were last checked for timeouts and latency. */ diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index f3ee514cdb..13b27cb615 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/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" @@ -260,10 +261,33 @@ void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue at { ENSURE(g_NetClient); - // TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason). ScriptRequest rq(scriptInterface); + + JS::RootedValue settings{rq.cx}; + if (!Script::GetProperty(rq, attribs1, "settings", &settings)) + { + ScriptException::Raise(rq, "The initAttributes didn't contain a \"settings\" property."); + return; + } + JS::RootedValue attribs(rq.cx, attribs1); - g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs)); + std::string attributesAsString{Script::StringifyJSON(rq, &attribs)}; + + std::wstring savegameID; + if (Script::HasProperty(rq, settings, "Savegame") && + Script::GetProperty(rq, settings, "Savegame", savegameID)) + { + const std::optional loadResult{ + SavedGames::Load(scriptInterface, savegameID)}; + if (!loadResult) + { + ScriptException::Raise(rq, "Failed to load the saved game: \"%ls\"", savegameID.c_str()); + return; + } + g_NetClient->SendStartSavedGameMessage(attributesAsString, loadResult->savedState); + return; + } + g_NetClient->SendStartGameMessage(attributesAsString); } void SetTurnLength(int length) diff --git a/source/ps/SavedGame.cpp b/source/ps/SavedGame.cpp index 5e3135200e..2a1cda1d1c 100644 --- a/source/ps/SavedGame.cpp +++ b/source/ps/SavedGame.cpp @@ -204,7 +204,8 @@ private: 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 @@ Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptI // 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; + } + } + const 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) diff --git a/source/ps/SavedGame.h b/source/ps/SavedGame.h index 63360ccc37..8ce0f2783c 100644 --- a/source/ps/SavedGame.h +++ b/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 @@ namespace SavedGames */ 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 diff --git a/source/ps/scripting/JSInterface_SavedGame.cpp b/source/ps/scripting/JSInterface_SavedGame.cpp index e02a1998f7..b0f209f6e1 100644 --- a/source/ps/scripting/JSInterface_SavedGame.cpp +++ b/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 @@ void QuickLoad() 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 @@ JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstr 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 @@ JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstr 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 @@ void RegisterScriptFunctions(const ScriptRequest& rq) 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"); } }