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:
parent
3cfcaf7be6
commit
86d360a887
@ -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;
|
||||
}
|
||||
};
|
@ -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" },
|
||||
|
@ -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())
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
class AIGameSettingControlDropdown extends GameSettingControlDropdown
|
||||
{
|
||||
onOpenPage(playerIndex)
|
||||
onOpenPage(playerIndex, enabled)
|
||||
{
|
||||
this.setEnabled(true);
|
||||
this.setEnabled(enabled);
|
||||
this.playerIndex = playerIndex;
|
||||
this.render();
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 + "]");
|
||||
|
@ -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 + "]");
|
||||
|
@ -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 + "]");
|
||||
|
@ -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;
|
||||
|
@ -25,7 +25,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt
|
||||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(this.enabled);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -67,23 +67,29 @@
|
||||
|
||||
<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"/>
|
||||
</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"/>
|
||||
</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%">
|
||||
<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"/>
|
||||
</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/StartGameButton.xml"/>
|
||||
</object>
|
||||
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -17,7 +17,7 @@ class MapPreview
|
||||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(g_GameSettings.savegame.value === null);
|
||||
}
|
||||
|
||||
renderName()
|
||||
|
@ -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,
|
||||
|
@ -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<CFileTransferRequestMessage*>(message);
|
||||
|
||||
ENSURE(static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) ==
|
||||
CNetFileTransferer::RequestType::REJOIN);
|
||||
std::string toCompress{[&]
|
||||
{
|
||||
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
|
||||
// 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<int>(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<CGameStartMessage*>(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<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)
|
||||
{
|
||||
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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<CNetFileTransferer::RequestType>(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<CNetFileTransferer::RequestType>(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<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)
|
||||
{
|
||||
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<const CStr, PlayerAssignment>& 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;
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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<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)
|
||||
|
@ -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::LoadResult> 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<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)
|
||||
|
@ -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 <optional>
|
||||
|
||||
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<LoadResult> Load(const ScriptInterface& scriptInterface, const std::wstring& name);
|
||||
|
||||
/**
|
||||
* Get list of saved games for GUI script usage
|
||||
|
@ -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 <optional>
|
||||
|
||||
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<SavedGames::LoadResult> 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<SavedGames::LoadResult> 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");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user