1
0
forked from 0ad/0ad

Multiplayer saved games

Enables to save multiplayer games.
When the savegame is loaded, the settings are frozen (except the non-AI-player assignment settings).
This commit is contained in:
phosit 2024-08-31 10:54:56 +02:00
parent 3cfcaf7be6
commit 86d360a887
35 changed files with 476 additions and 87 deletions

View File

@ -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;
}
};

View File

@ -189,6 +189,7 @@
{ "nick": "MattDoerksen", "name": "Matt Doerksen" }, { "nick": "MattDoerksen", "name": "Matt Doerksen" },
{ "nick": "mattlott", "name": "Matt Lott" }, { "nick": "mattlott", "name": "Matt Lott" },
{ "nick": "maveric", "name": "Anton Protko" }, { "nick": "maveric", "name": "Anton Protko" },
{ "nick": "mbusy", "name": "Maxime Busy" },
{ "nick": "Micnasty", "name": "Travis Gorkin" }, { "nick": "Micnasty", "name": "Travis Gorkin" },
{ "name": "Mikołaj \"Bajter\" Korcz" }, { "name": "Mikołaj \"Bajter\" Korcz" },
{ "nick": "mimo" }, { "nick": "mimo" },

View File

@ -95,8 +95,11 @@ class PlayerAssignmentsController
*/ */
onClientJoin(newGUID, newAssignments) onClientJoin(newGUID, newAssignments)
{ {
if (!g_IsController || newAssignments[newGUID].player != -1) if (!g_IsController || newAssignments[newGUID].player !== -1 ||
g_GameSettings.savegame.value !== null)
{
return; return;
}
// Assign the client (or only buddies if prefered) to a free slot // Assign the client (or only buddies if prefered) to a free slot
if (newGUID != Engine.GetPlayerGUID()) if (newGUID != Engine.GetPlayerGUID())

View File

@ -38,12 +38,12 @@ SetupWindowPages.AIConfigPage = class
return this.row++; return this.row++;
} }
openPage(playerIndex) openPage(playerIndex, enabled)
{ {
this.playerIndex = playerIndex; this.playerIndex = playerIndex;
for (let handler of this.openPageHandlers) for (let handler of this.openPageHandlers)
handler(playerIndex); handler(playerIndex, enabled);
this.aiConfigPage.hidden = false; this.aiConfigPage.hidden = false;
} }

View File

@ -1,8 +1,8 @@
class AIGameSettingControlDropdown extends GameSettingControlDropdown class AIGameSettingControlDropdown extends GameSettingControlDropdown
{ {
onOpenPage(playerIndex) onOpenPage(playerIndex, enabled)
{ {
this.setEnabled(true); this.setEnabled(enabled);
this.playerIndex = playerIndex; this.playerIndex = playerIndex;
this.render(); this.render();
} }

View File

@ -55,6 +55,11 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls
if (this.onPlayerAssignmentsChange) if (this.onPlayerAssignmentsChange)
this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this)); 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) setTitle(titleCaption)

View File

@ -5,6 +5,9 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC
super(...args); super(...args);
this.aiConfigButton = Engine.GetGUIObjectByName("aiConfigButton[" + this.playerIndex + "]"); 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"]); g_GameSettings.playerAI.watch(() => this.render(), ["values"]);
// Save little performance by not reallocating every call // Save little performance by not reallocating every call
@ -12,12 +15,6 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC
this.render(); this.render();
} }
onLoad()
{
let aiConfigPage = this.setupWindow.pages.AIConfigPage;
this.aiConfigButton.onPress = aiConfigPage.openPage.bind(aiConfigPage, this.playerIndex);
}
render() render()
{ {
this.aiConfigButton.hidden = !g_GameSettings.playerAI.get(this.playerIndex); this.aiConfigButton.hidden = !g_GameSettings.playerAI.get(this.playerIndex);

View File

@ -34,6 +34,19 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]); 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() setControl()
{ {
this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]"); 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. // TODO: this particular bit is done for each row, which is unnecessarily inefficient.
this.playerItems = sortGUIDsByPlayerID().map( this.playerItems = sortGUIDsByPlayerID().map(
this.clientItemFactory.createItem.bind(this.clientItemFactory)); 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.values = prepareForDropdown([
...this.playerItems, ...this.playerItems,
...this.aiItems, ...AIsSelectable ? this.aiItems : [],
this.unassignedItem this.unassignedItem
]); ]);
@ -161,7 +179,8 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
if (ai) if (ai)
g_GameSettings.playerAI.swap(sourcePlayer, playerIndex); g_GameSettings.playerAI.swap(sourcePlayer, playerIndex);
// Swap color + civ as well - this allows easy reorganizing of player order. // 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.playerCiv.swap(sourcePlayer, playerIndex);
g_GameSettings.playerColor.swap(sourcePlayer, playerIndex); g_GameSettings.playerColor.swap(sourcePlayer, playerIndex);

View File

@ -14,6 +14,11 @@ PlayerSettingControls.PlayerCiv = class PlayerCiv extends GameSettingControlDrop
this.rebuild(); this.rebuild();
} }
onSavegameChanged()
{
return !g_GameSettings.playerCiv.locked[this.playerIndex] && null;
}
setControl() setControl()
{ {
this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]"); this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]");

View File

@ -8,6 +8,11 @@ PlayerSettingControls.PlayerColor = class PlayerColor extends GameSettingControl
this.render(); this.render();
} }
onSavegameChanged()
{
return g_GameSettings.map.type !== "scenario" && null;
}
setControl() setControl()
{ {
this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]"); this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]");

View File

@ -24,6 +24,11 @@ PlayerSettingControls.PlayerTeam = class PlayerTeam extends GameSettingControlDr
this.render(); this.render();
} }
onSavegameChanged()
{
return g_GameSettings.map.type !== "scenario" && null;
}
setControl() setControl()
{ {
this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]"); this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]");

View File

@ -13,6 +13,13 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl
this.render(); this.render();
} }
onSavegameChanged()
{
this.onPlayerAssignmentsChange();
g_GameSettings.playerName.maybeUpdate();
this.render();
}
onPlayerAssignmentsChange() onPlayerAssignmentsChange()
{ {
this.guid = undefined; this.guid = undefined;

View File

@ -25,7 +25,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt
onPress() onPress()
{ {
this.setupWindow.pages.MapBrowserPage.openPage(); this.setupWindow.pages.MapBrowserPage.openPage(this.enabled);
} }
}; };

View File

@ -15,9 +15,14 @@ SetupWindowPages.GameSetupPage = class
let startGameButton = new StartGameButton(setupWindow); let startGameButton = new StartGameButton(setupWindow);
let readyButton = new ReadyButton(setupWindow); let readyButton = new ReadyButton(setupWindow);
this.panelButtons = { this.panelButtons = {
"cancelButton": new CancelButton(setupWindow, startGameButton, readyButton),
"civInfoButton": new CivInfoButton(), "civInfoButton": new CivInfoButton(),
"lobbyButton": new LobbyButton(), "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, "readyButton": readyButton,
"startGameButton": startGameButton "startGameButton": startGameButton
}; };
@ -31,7 +36,7 @@ SetupWindowPages.GameSetupPage = class
this.panels = { this.panels = {
"chatPanel": new ChatPanel(setupWindow, this.gameSettingControlManager, gameSettingsPanel), "chatPanel": new ChatPanel(setupWindow, this.gameSettingControlManager, gameSettingsPanel),
"gameSettingWarning": new GameSettingWarning(setupWindow, this.panelButtons.cancelButton), "gameSettingWarning": new GameSettingWarning(setupWindow),
"gameDescription": new GameDescription(setupWindow, gameSettingTabs), "gameDescription": new GameDescription(setupWindow, gameSettingTabs),
"gameSettingsPanel": gameSettingsPanel, "gameSettingsPanel": gameSettingsPanel,
"gameSettingsTabs": gameSettingTabs, "gameSettingsTabs": gameSettingTabs,

View File

@ -67,23 +67,29 @@
<object name="bottomLeftPanel"> <object name="bottomLeftPanel">
<object size="20 100%-32 100%-430 100%"> <object size="20 100%-32 100%-698 100%">
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Tooltip.xml"/> <include file="gui/gamesetup/Pages/GameSetupPage/Panels/Tooltip.xml"/>
</object> </object>
<object size="0 100%-28 100%-320 100%"> <object size="0 100%-28 100%-470 100%">
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/GameSettingWarning.xml"/> <include file="gui/gamesetup/Pages/GameSetupPage/Panels/GameSettingWarning.xml"/>
</object> </object>
</object> </object>
<object name="bottomRightPanel" size="100%-314 100%-28 100% 100%"> <object name="bottomRightPanel" size="100%-464 100%-28 100% 100%">
<object size="0 0 140 100%"> <object size="0 0 140 100%">
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml"/>
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml"/>
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml"/>
</object>
<object size="150 0 290 100%">
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.xml"/> <include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.xml"/>
</object> </object>
<object size="150 0 290 100%"> <object size="300 0 440 100%">
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.xml"/> <include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.xml"/>
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.xml"/> <include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.xml"/>
</object> </object>

View File

@ -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"]);
}
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="clearSavegameButton"
type="button"
style="StoneButton"
tooltip_style="onscreenToolTip"
hidden="true">
<translatableAttribute id="caption">Clear Save</translatableAttribute>
<translatableAttribute id="tooltip">
Discard the Savegame data and start a new match instead.
</translatableAttribute>
</object>

View File

@ -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();
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="loadSavegameButton"
type="button"
style="StoneButton"
tooltip_style="onscreenToolTip">
<translatableAttribute id="caption">Load Save</translatableAttribute>
<translatableAttribute id="tooltip">
Load a previously created Savegame. You will still have to press start after having loaded the game data.
</translatableAttribute>
</object>

View File

@ -9,11 +9,25 @@ class ResetCivsButton
this.civResetButton.onPress = this.onPress.bind(this); this.civResetButton.onPress = this.onPress.bind(this);
g_GameSettings.map.watch(() => this.render(), ["type"]); 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() render()
{ {
this.civResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; this.setEnabled(this.shouldBeEnabled());
} }
onPress() onPress()

View File

@ -9,11 +9,25 @@ class ResetTeamsButton
this.teamResetButton.onPress = this.onPress.bind(this); this.teamResetButton.onPress = this.onPress.bind(this);
g_GameSettings.map.watch(() => this.render(), ["type"]); 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() render()
{ {
this.teamResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController; this.setEnabled(this.shouldBeEnabled());
} }
onPress() onPress()

View File

@ -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"]);
}
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<object
name="savegameLabel"
type="text"
style="ModernLabelText"
tooltip_style="onscreenToolTip"
hidden="true">
<translatableAttribute id="caption">Savegame</translatableAttribute>
<translatableAttribute id="tooltip">
The controler loaded a savegame.
</translatableAttribute>
</object>

View File

@ -17,7 +17,7 @@ class MapPreview
onPress() onPress()
{ {
this.setupWindow.pages.MapBrowserPage.openPage(); this.setupWindow.pages.MapBrowserPage.openPage(g_GameSettings.savegame.value === null);
} }
renderName() renderName()

View File

@ -25,9 +25,9 @@ SetupWindowPages.MapBrowserPage = class extends MapBrowser
this.gameSettingsController.setNetworkInitAttributes(); this.gameSettingsController.setNetworkInitAttributes();
} }
openPage() openPage(enabled)
{ {
super.openPage(g_IsController); super.openPage(g_IsController && enabled);
this.controls.MapFiltering.select( this.controls.MapFiltering.select(
this.gameSettingsController.guiData.mapFilter.filter, this.gameSettingsController.guiData.mapFilter.filter,

View File

@ -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_TIMEOUT, NCS_PREGAME, &OnClientTimeout, this);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, &OnClientPerformance, 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_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_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, &OnJoinSyncStart, this);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, &OnChat, 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_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_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_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_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_END_COMMAND_BATCH, NCS_JOIN_SYNCING, &OnJoinSyncEndCommandBatch, this);
AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, &OnLoadedGame, 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); 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() void CNetClient::SendRejoinedMessage()
{ {
CRejoinedMessage rejoinedMessage; CRejoinedMessage rejoinedMessage;
@ -524,25 +535,29 @@ bool CNetClient::HandleMessage(CNetMessage* message)
{ {
CFileTransferRequestMessage* reqMessage = static_cast<CFileTransferRequestMessage*>(message); CFileTransferRequestMessage* reqMessage = static_cast<CFileTransferRequestMessage*>(message);
ENSURE(static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) == std::string toCompress{[&]
CNetFileTransferer::RequestType::REJOIN); {
if (static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) ==
CNetFileTransferer::RequestType::LOADGAME)
{
return std::exchange(m_SavedState, {});
}
// TODO: we should support different transfer request types, instead of assuming std::stringstream stream;
// it's always requesting the simulation state
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()); bool ok = m_Game->GetSimulation2()->SerializeState(stream);
u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); ENSURE(ok);
stream.write((char*)&turn, sizeof(turn)); return stream.str();
}()};
bool ok = m_Game->GetSimulation2()->SerializeState(stream);
ENSURE(ok);
// Compress the content with zlib to save bandwidth // Compress the content with zlib to save bandwidth
// (TODO: if this is still too large, compressing with e.g. LZMA works much better) // (TODO: if this is still too large, compressing with e.g. LZMA works much better)
std::string compressed; std::string compressed;
CompressZLib(stream.str(), compressed, true); CompressZLib(toCompress, compressed, true);
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
@ -605,6 +620,18 @@ void CNetClient::SendAuthenticateMessage()
SendMessage(&authenticate); 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<int>(m_HostID), m_Game->GetReplayLogger()};
m_Game->SetPlayerID(player);
m_Game->StartGame(initAttributes, savedState);
}
bool CNetClient::OnConnect(CNetClient* client, CFsmEvent* event) bool CNetClient::OnConnect(CNetClient* client, CFsmEvent* event)
{ {
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
@ -759,29 +786,40 @@ bool CNetClient::OnGameStart(CNetClient* client, CFsmEvent* event)
CGameStartMessage* message = static_cast<CGameStartMessage*>(event->GetParamRef()); CGameStartMessage* message = static_cast<CGameStartMessage*>(event->GetParamRef());
// Find the player assigned to our GUID const ScriptInterface& scriptInterface{client->m_Game->GetSimulation2()->GetScriptInterface()};
int player = -1; ScriptRequest rq{scriptInterface};
if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) JS::RootedValue initAttribs{rq.cx};
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); Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs);
client->m_Game->SetPlayerID(player); client->PushGuiMessage("type", "start", "initAttributes", initAttribs);
client->m_Game->StartGame(&initAttribs, ""); client->StartGame(&initAttribs, "");
client->PushGuiMessage("type", "start",
"initAttributes", initAttribs);
return true; return true;
} }
bool CNetClient::OnSavedGameStart(CNetClient* client, CFsmEvent* event)
{
ENSURE(event->GetType() == static_cast<uint>(NMT_SAVED_GAME_START));
CGameSavedStartMessage* message{static_cast<CGameSavedStartMessage*>(event->GetParamRef())};
const ScriptInterface& scriptInterface{client->m_Game->GetSimulation2()->GetScriptInterface()};
ScriptRequest rq{scriptInterface};
const std::shared_ptr<JS::RootedValue> initAttribs{std::make_shared<JS::RootedValue>(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) bool CNetClient::OnJoinSyncStart(CNetClient* client, CFsmEvent* event)
{ {
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);

View File

@ -236,6 +236,8 @@ public:
void SendStartGameMessage(const CStr& initAttribs); void SendStartGameMessage(const CStr& initAttribs);
void SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState);
/** /**
* Call when the client has rejoined a running match and finished * Call when the client has rejoined a running match and finished
* the loading screen. * the loading screen.
@ -277,6 +279,7 @@ private:
static bool OnPlayerAssignment(CNetClient* client, CFsmEvent* event); static bool OnPlayerAssignment(CNetClient* client, CFsmEvent* event);
static bool OnInGame(CNetClient* client, CFsmEvent* event); static bool OnInGame(CNetClient* client, CFsmEvent* event);
static bool OnGameStart(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 OnJoinSyncStart(CNetClient* client, CFsmEvent* event);
static bool OnJoinSyncEndCommandBatch(CNetClient* client, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(CNetClient* client, CFsmEvent* event);
static bool OnRejoined(CNetClient* client, CFsmEvent* event); static bool OnRejoined(CNetClient* client, CFsmEvent* event);
@ -292,6 +295,12 @@ private:
*/ */
void SetAndOwnSession(CNetClientSession* session); 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. * 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 /// Serialized game state received when joining an in-progress game
std::string m_JoinSyncBuffer; std::string m_JoinSyncBuffer;
std::string m_SavedState;
/// Time when the server was last checked for timeouts and bad latency /// Time when the server was last checked for timeouts and bad latency
std::time_t m_LastConnectionCheck; std::time_t m_LastConnectionCheck;
}; };

View File

@ -183,6 +183,10 @@ CNetMessage* CNetMessageFactory::CreateMessage(const void* pData,
pNewMessage = new CGameStartMessage; pNewMessage = new CGameStartMessage;
break; break;
case NMT_SAVED_GAME_START:
pNewMessage = new CGameSavedStartMessage;
break;
case NMT_END_COMMAND_BATCH: case NMT_END_COMMAND_BATCH:
pNewMessage = new CEndCommandBatchMessage; pNewMessage = new CEndCommandBatchMessage;
break; break;

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games. /* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@ -75,6 +75,7 @@ enum NetMessageType
NMT_LOADED_GAME, NMT_LOADED_GAME,
NMT_GAME_START, NMT_GAME_START,
NMT_SAVED_GAME_START,
NMT_END_COMMAND_BATCH, NMT_END_COMMAND_BATCH,
NMT_SYNC_CHECK, // OOS-detection hash checking NMT_SYNC_CHECK, // OOS-detection hash checking
@ -218,6 +219,10 @@ START_NMT_CLASS_(GameStart, NMT_GAME_START)
NMT_FIELD(CStr, m_InitAttributes) NMT_FIELD(CStr, m_InitAttributes)
END_NMT_CLASS() 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) START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
NMT_FIELD_INT(m_Turn, u32, 4) NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_TurnLength, u32, 2) NMT_FIELD_INT(m_TurnLength, u32, 2)

View File

@ -623,14 +623,15 @@ void CNetServerWorker::HandleMessageReceive(const CNetMessage* message, CNetServ
if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) if (message->GetType() == NMT_FILE_TRANSFER_REQUEST)
{ {
CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message;
ENSURE(static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) ==
CNetFileTransferer::RequestType::REJOIN);
// Rejoining client got our JoinSyncStart after we received the state from // A client requested the gamestate. Clients only request the gamestate when we sent them a
// another client, and has now requested that we forward it to them // 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,
session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, m_JoinSyncFile); static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) ==
CNetFileTransferer::RequestType::LOADGAME ? m_SavedState : m_JoinSyncFile);
return; 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_ASSIGN_PLAYER, NSS_PREGAME, &OnAssignPlayer, session);
session->AddTransition(NSS_PREGAME, (uint)NMT_KICKED, NSS_PREGAME, &OnKickPlayer, 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_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_PREGAME, (uint)NMT_LOADED_GAME, NSS_INGAME, &OnLoadedGame, session);
session->AddTransition(NSS_JOIN_SYNCING, (uint)NMT_KICKED, NSS_JOIN_SYNCING, &OnKickPlayer, 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; return true;
} }
bool CNetServerWorker::OnSavedGameStart(CNetServerSession* session, CFsmEvent* event)
{
ENSURE(event->GetType() == static_cast<uint>(NMT_SAVED_GAME_START));
CNetServerWorker& server{session->GetServer()};
if (session->GetGUID() != server.m_ControllerGUID)
return true;
CGameSavedStartMessage* message = static_cast<CGameSavedStartMessage*>(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) bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent* event)
{ {
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
@ -1341,6 +1362,10 @@ bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent*
message.m_Clients.push_back(client); 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 // Send to the client who has loaded the game but did not reach the NSS_INGAME state yet
loadedSession->SendMessage(&message); loadedSession->SendMessage(&message);
server.Broadcast(&message, { NSS_INGAME }); server.Broadcast(&message, { NSS_INGAME });
@ -1502,7 +1527,7 @@ bool CNetServerWorker::CheckGameLoadStatus(CNetServerSession* changedSession)
return true; return true;
} }
void CNetServerWorker::StartGame(const CStr& initAttribs) void CNetServerWorker::PreStartGame(const CStr& initAttribs)
{ {
for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments) for (std::pair<const CStr, PlayerAssignment>& player : m_PlayerAssignments)
if (player.second.m_Enabled && player.second.m_PlayerID != -1 && player.second.m_Status == 0) 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. // Update init attributes. They should no longer change.
Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes); Script::ParseJSON(ScriptRequest(m_ScriptInterface), initAttribs, &m_InitAttributes);
}
void CNetServerWorker::StartGame(const CStr& initAttribs)
{
PreStartGame(initAttribs);
CGameStartMessage gameStart; CGameStartMessage gameStart;
gameStart.m_InitAttributes = initAttribs; gameStart.m_InitAttributes = initAttribs;
Broadcast(&gameStart, { NSS_PREGAME }); 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) CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{ {
const size_t MAX_LENGTH = 32; const size_t MAX_LENGTH = 32;

View File

@ -255,11 +255,22 @@ private:
*/ */
void AssignPlayer(int playerID, const CStr& guid); 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. * Switch in game mode and notify all clients to start the game.
*/ */
void StartGame(const CStr& initAttribs); 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. * 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 OnGameSetup(CNetServerSession* session, CFsmEvent* event);
static bool OnAssignPlayer(CNetServerSession* session, CFsmEvent* event); static bool OnAssignPlayer(CNetServerSession* session, CFsmEvent* event);
static bool OnGameStart(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 OnLoadedGame(CNetServerSession* session, CFsmEvent* event);
static bool OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmEvent* event); static bool OnJoinSyncingLoadedGame(CNetServerSession* session, CFsmEvent* event);
static bool OnRejoined(CNetServerSession* session, CFsmEvent* event); static bool OnRejoined(CNetServerSession* session, CFsmEvent* event);
@ -399,6 +411,11 @@ private:
*/ */
std::string m_JoinSyncFile; 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. * Time when the clients connections were last checked for timeouts and latency.
*/ */

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2022 Wildfire Games. /* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@ -33,6 +33,7 @@
#include "ps/GUID.h" #include "ps/GUID.h"
#include "ps/Hashing.h" #include "ps/Hashing.h"
#include "ps/Pyrogenesis.h" #include "ps/Pyrogenesis.h"
#include "ps/SavedGame.h"
#include "ps/Util.h" #include "ps/Util.h"
#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/FunctionWrapper.h"
#include "scriptinterface/StructuredClone.h" #include "scriptinterface/StructuredClone.h"
@ -260,10 +261,33 @@ void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue at
{ {
ENSURE(g_NetClient); 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); 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); 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<SavedGames::LoadResult> 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) void SetTurnLength(int length)

View File

@ -204,7 +204,8 @@ private:
std::string* m_SavedState; std::string* m_SavedState;
}; };
Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) std::optional<SavedGames::LoadResult> SavedGames::Load(const ScriptInterface& scriptInterface,
const std::wstring& name)
{ {
// Determine the filename to load // Determine the filename to load
const VfsPath basename(L"saves/" + name); 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 // Don't crash just because file isn't found, this can happen if the file is deleted from the OS
if (!VfsFileExists(filename)) if (!VfsFileExists(filename))
return ERR::FILE_NOT_FOUND; return std::nullopt;
OsPath realPath; 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); PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
if (!archiveReader) if (!archiveReader)
WARN_RETURN(ERR::FAIL); {
DEBUG_WARN_ERR(ERR::FAIL);
return std::nullopt;
}
std::string savedState;
CGameLoader loader(scriptInterface, &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<uintptr_t>(&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) JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface)

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games. /* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@ -19,8 +19,11 @@
#define INCLUDED_SAVEDGAME #define INCLUDED_SAVEDGAME
#include "ps/CStr.h" #include "ps/CStr.h"
#include "scriptinterface/ScriptTypes.h"
#include "scriptinterface/StructuredClone.h" #include "scriptinterface/StructuredClone.h"
#include <optional>
class CSimulation2; class CSimulation2;
/** /**
@ -59,18 +62,24 @@ namespace SavedGames
*/ */
Status SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const Script::StructuredClone& guiMetadataClone); 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 * Load saved game archive with the given name
* *
* @param name filename of saved game (without path or extension) * @param name filename of saved game (without path or extension)
* @param scriptInterface * @param scriptInterface
* @param[out] metadata object containing metadata associated with saved game, * @return An empty `std::optional` if an error ocoured.
* 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
*/ */
Status Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState); std::optional<LoadResult> Load(const ScriptInterface& scriptInterface, const std::wstring& name);
/** /**
* Get list of saved games for GUI script usage * Get list of saved games for GUI script usage

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Wildfire Games. /* Copyright (C) 2024 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@ -29,6 +29,8 @@
#include "simulation2/Simulation2.h" #include "simulation2/Simulation2.h"
#include "simulation2/system/TurnManager.h" #include "simulation2/system/TurnManager.h"
#include <optional>
namespace JSI_SavedGame namespace JSI_SavedGame
{ {
JS::Value GetSavedGames(const ScriptInterface& scriptInterface) JS::Value GetSavedGames(const ScriptInterface& scriptInterface)
@ -75,6 +77,13 @@ void QuickLoad()
LOGERROR("Can't load quicksave if game is not running!"); LOGERROR("Can't load quicksave if game is not running!");
} }
JS::Value LoadSavedGameMetadata(const ScriptInterface& scriptInterface, const std::wstring& name)
{
std::optional<SavedGames::LoadResult> data{SavedGames::Load(scriptInterface, name)};
return !data ? JS::UndefinedValue() : data->metadata;
}
JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name) JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name)
{ {
// We need to be careful with different compartments and contexts. // 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); ENSURE(!g_Game);
// Load the saved game data from disk std::optional<SavedGames::LoadResult> data{SavedGames::Load(scriptInterface, name)};
JS::RootedValue guiContextMetadata(rqGui.cx); if (!data)
std::string savedState;
Status err = SavedGames::Load(name, scriptInterface, &guiContextMetadata, savedState);
if (err < 0)
return JS::UndefinedValue(); return JS::UndefinedValue();
JS::RootedValue guiContextMetadata{rqGui.cx, data->metadata};
g_Game = new CGame(true); g_Game = new CGame(true);
{ {
@ -109,7 +117,7 @@ JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstr
Script::GetProperty(rqGame, gameContextMetadata, "playerID", playerID); Script::GetProperty(rqGame, gameContextMetadata, "playerID", playerID);
g_Game->SetPlayerID(playerID); g_Game->SetPlayerID(playerID);
g_Game->StartGame(&gameInitAttributes, savedState); g_Game->StartGame(&gameInitAttributes, data->savedState);
} }
return guiContextMetadata; return guiContextMetadata;
@ -131,6 +139,7 @@ void RegisterScriptFunctions(const ScriptRequest& rq)
ScriptFunction::Register<&QuickSave>(rq, "QuickSave"); ScriptFunction::Register<&QuickSave>(rq, "QuickSave");
ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad"); ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad");
ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest"); ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest");
ScriptFunction::Register<&LoadSavedGameMetadata>(rq, "LoadSavedGameMetadata");
ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame"); ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame");
} }
} }