mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
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
20b4937ffd
commit
b90280855f
42 changed files with 542 additions and 199 deletions
|
|
@ -11,7 +11,7 @@
|
|||
*/
|
||||
class GameSettings
|
||||
{
|
||||
init(mapCache)
|
||||
init(mapCache, savegameID)
|
||||
{
|
||||
if (!mapCache)
|
||||
mapCache = new MapCache();
|
||||
|
|
@ -19,6 +19,10 @@ class GameSettings
|
|||
"value": mapCache,
|
||||
});
|
||||
|
||||
Object.defineProperty(this, "savegameID", {
|
||||
"value": savegameID,
|
||||
});
|
||||
|
||||
// Load all possible civ data - don't presume that some will be available.
|
||||
Object.defineProperty(this, "civData", {
|
||||
"value": loadCivData(false, false),
|
||||
|
|
@ -40,6 +44,15 @@ class GameSettings
|
|||
if (this[comp].init)
|
||||
this[comp].init();
|
||||
|
||||
if (!savegameID)
|
||||
return this;
|
||||
|
||||
const initAttributes = Engine.LoadSavedGameMetadata(savegameID).initAttributes;
|
||||
|
||||
// Remove the gaia entry.
|
||||
initAttributes.settings.PlayerData.splice(0, 1);
|
||||
this.fromInitAttributes(initAttributes);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +155,7 @@ class GameSettings
|
|||
|
||||
// NB: for multiplayer support, the clients must be listening to "start" net messages.
|
||||
if (this.isNetworked)
|
||||
Engine.StartNetworkGame(this.finalizedAttributes, storeReplay);
|
||||
Engine.StartNetworkGame(this.savegameID, this.finalizedAttributes, storeReplay);
|
||||
else
|
||||
Engine.StartGame(this.finalizedAttributes, playerAssignments.local.player, storeReplay);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSetting
|
||||
{
|
||||
randomPicked = false;
|
||||
|
||||
init()
|
||||
{
|
||||
// NB: watchers aren't auto-triggered when modifying array elements.
|
||||
|
|
@ -21,6 +23,9 @@ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSett
|
|||
attribs.settings.PlayerData = [];
|
||||
while (attribs.settings.PlayerData.length < this.values.length)
|
||||
attribs.settings.PlayerData.push({});
|
||||
if (this.isSavedGame && !this.randomPicked)
|
||||
return;
|
||||
|
||||
for (let i in this.values)
|
||||
if (this.values[i])
|
||||
attribs.settings.PlayerData[i].Name = this.values[i];
|
||||
|
|
@ -87,7 +92,7 @@ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSett
|
|||
const names = this.settings.civData[civ].AINames;
|
||||
const remainingNames = names.filter(name => !AIPlayerNamesList.includes(name));
|
||||
const chosenName = pickRandom(remainingNames.length ? remainingNames : names);
|
||||
|
||||
|
||||
// Avoid translating AI names if the game is networked, so all players see and refer to
|
||||
// English names instead of names in the language of the host.
|
||||
const translatedCountLabel = this.settings.isNetworked ? this.CountLabel : translate(this.CountLabel);
|
||||
|
|
@ -98,7 +103,7 @@ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSett
|
|||
count++;
|
||||
return count;
|
||||
}, 0);
|
||||
|
||||
|
||||
AIPlayerNamesList.push(chosenName);
|
||||
|
||||
this.values[i] = !duplicateNameCount ? translatedChosenName :
|
||||
|
|
@ -108,7 +113,10 @@ GameSettings.prototype.Attributes.PlayerName = class PlayerName extends GameSett
|
|||
});
|
||||
}
|
||||
if (picked)
|
||||
{
|
||||
this.randomPicked = true;
|
||||
this.trigger("values");
|
||||
}
|
||||
return picked;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,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" },
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
class GameSettingsController
|
||||
{
|
||||
constructor(setupWindow, netMessages, playerAssignmentsController, mapCache)
|
||||
constructor(setupWindow, netMessages, playerAssignmentsController, mapCache, isSavedGame)
|
||||
{
|
||||
this.setupWindow = setupWindow;
|
||||
this.mapCache = mapCache;
|
||||
|
|
@ -25,7 +25,7 @@ class GameSettingsController
|
|||
this.loadingChangeHandlers = new Set();
|
||||
this.settingsLoadedHandlers = new Set();
|
||||
|
||||
setupWindow.registerLoadHandler(this.onLoad.bind(this));
|
||||
setupWindow.registerLoadHandler(this.onLoad.bind(this, isSavedGame));
|
||||
setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
|
||||
|
||||
setupWindow.registerClosePageHandler(this.onClose.bind(this));
|
||||
|
|
@ -75,29 +75,32 @@ class GameSettingsController
|
|||
this.settingsLoadedHandlers.add(handler);
|
||||
}
|
||||
|
||||
onLoad(initData, hotloadData)
|
||||
onLoad(isSavedGame, initData, hotloadData)
|
||||
{
|
||||
// This initial settings parsing in wrapped in a try-catch because it can fail unexpectedly,
|
||||
// and particularly could fail with mods that change persistent settings, so this is
|
||||
// difficult to fully fix from the gameSettings code.
|
||||
// Also include hotloaded data because that can also fail and having to restart isn't very useful.
|
||||
try {
|
||||
if (hotloadData)
|
||||
this.parseSettings(hotloadData.initAttributes);
|
||||
else if (g_IsController && (initData?.gameSettings || this.persistentMatchSettings.enabled))
|
||||
{
|
||||
// Allow opting-in to persistence when sending initial data (though default off)
|
||||
if (initData?.gameSettings)
|
||||
this.persistentMatchSettings.enabled = !!initData.gameSettings?.usePersistence;
|
||||
const settings = initData?.gameSettings || this.persistentMatchSettings.loadFile();
|
||||
if (settings)
|
||||
this.parseSettings(settings);
|
||||
if (!isSavedGame)
|
||||
{
|
||||
// This initial settings parsing in wrapped in a try-catch because it can fail unexpectedly,
|
||||
// and particularly could fail with mods that change persistent settings, so this is
|
||||
// difficult to fully fix from the gameSettings code.
|
||||
// Also include hotloaded data because that can also fail and having to restart isn't very useful.
|
||||
try {
|
||||
if (hotloadData)
|
||||
this.parseSettings(hotloadData.initAttributes);
|
||||
else if (g_IsController && (initData?.gameSettings || this.persistentMatchSettings.enabled))
|
||||
{
|
||||
// Allow opting-in to persistence when sending initial data (though default off)
|
||||
if (initData?.gameSettings)
|
||||
this.persistentMatchSettings.enabled = !!initData.gameSettings?.usePersistence;
|
||||
const settings = initData?.gameSettings || this.persistentMatchSettings.loadFile();
|
||||
if (settings)
|
||||
this.parseSettings(settings);
|
||||
}
|
||||
} catch(err) {
|
||||
error("There was an error loading game settings. You may need to disable persistent match settings.");
|
||||
warn(err?.toString() ?? uneval(err));
|
||||
if (err.stack)
|
||||
warn(err.stack)
|
||||
}
|
||||
} catch(err) {
|
||||
error("There was an error loading game settings. You may need to disable persistent match settings.");
|
||||
warn(err?.toString() ?? uneval(err));
|
||||
if (err.stack)
|
||||
warn(err.stack)
|
||||
}
|
||||
|
||||
// If the new settings led to AI & players conflict, remove the AI.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
class PlayerAssignmentsController
|
||||
{
|
||||
constructor(setupWindow, netMessages)
|
||||
constructor(setupWindow, netMessages, isSavedGame)
|
||||
{
|
||||
this.clientJoinHandlers = new Set();
|
||||
this.clientLeaveHandlers = new Set();
|
||||
|
|
@ -35,7 +35,7 @@ class PlayerAssignmentsController
|
|||
setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
|
||||
netMessages.registerNetMessageHandler("players", this.onPlayerAssignmentMessage.bind(this));
|
||||
|
||||
this.registerClientJoinHandler(this.onClientJoin.bind(this));
|
||||
this.registerClientJoinHandler(this.onClientJoin.bind(this, isSavedGame));
|
||||
}
|
||||
|
||||
registerPlayerAssignmentsChangeHandler(handler)
|
||||
|
|
@ -93,10 +93,12 @@ class PlayerAssignmentsController
|
|||
* On client join, try to assign them to a free slot.
|
||||
* (This is called before g_PlayerAssignments is updated).
|
||||
*/
|
||||
onClientJoin(newGUID, newAssignments)
|
||||
onClientJoin(isSavedGame, newGUID, newAssignments)
|
||||
{
|
||||
if (!g_IsController || newAssignments[newGUID].player != -1)
|
||||
if (!g_IsController || newAssignments[newGUID].player !== -1 || isSavedGame)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign the client (or only buddies if prefered) to a free slot
|
||||
if (newGUID != Engine.GetPlayerGUID())
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class AIGameSettingControls
|
|||
|
||||
SetupWindowPages.AIConfigPage = class
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
this.gameSettingsController = setupWindow.controls.gameSettingsController;
|
||||
|
||||
|
|
@ -18,7 +18,8 @@ SetupWindowPages.AIConfigPage = class
|
|||
|
||||
for (let name of this.AIGameSettingControlOrder)
|
||||
this.AIGameSettingControls[name] =
|
||||
new AIGameSettingControls[name](this, undefined, undefined, setupWindow);
|
||||
new AIGameSettingControls[name](this, undefined, undefined, setupWindow,
|
||||
isSavedGame);
|
||||
|
||||
this.aiDescription = new AIDescription(this, setupWindow);
|
||||
|
||||
|
|
@ -38,12 +39,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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,13 @@
|
|||
*/
|
||||
class GameSettingControl /* extends Profilable /* Uncomment to profile controls without hassle. */
|
||||
{
|
||||
constructor(gameSettingControlManager, category, playerIndex, setupWindow)
|
||||
constructor(gameSettingControlManager, category, playerIndex, setupWindow, isSavedGame)
|
||||
{
|
||||
// Store arguments
|
||||
{
|
||||
this.category = category;
|
||||
this.playerIndex = playerIndex;
|
||||
this.isSavedGame = isSavedGame;
|
||||
|
||||
this.setupWindow = setupWindow;
|
||||
this.gameSettingsController = setupWindow.controls.gameSettingsController;
|
||||
|
|
@ -55,6 +56,9 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls
|
|||
|
||||
if (this.onPlayerAssignmentsChange)
|
||||
this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this));
|
||||
|
||||
if (isSavedGame)
|
||||
this.setEnabled(this.EnabledWhenSavedGame);
|
||||
}
|
||||
|
||||
setTitle(titleCaption)
|
||||
|
|
@ -79,7 +83,7 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls
|
|||
|
||||
setEnabled(enabled)
|
||||
{
|
||||
this.enabled = enabled;
|
||||
this.enabled = enabled && (!this.isSavedGame || this.EnabledWhenSavedGame);
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
|
|
@ -138,6 +142,8 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls
|
|||
}
|
||||
}
|
||||
|
||||
GameSettingControl.prototype.EnabledWhenSavedGame = false;
|
||||
|
||||
GameSettingControl.prototype.TitleCaptionFormat =
|
||||
translateWithContext("Title for specific setting", "%(setting)s:");
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class GameSettingControls
|
|||
*/
|
||||
class GameSettingControlManager
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
this.setupWindow = setupWindow;
|
||||
|
||||
|
|
@ -24,17 +24,18 @@ class GameSettingControlManager
|
|||
for (let name in GameSettingControls)
|
||||
this.gameSettingControls[name] =
|
||||
new GameSettingControls[name](
|
||||
this, getCategory(name), undefined, setupWindow);
|
||||
this, getCategory(name), undefined, setupWindow, isSavedGame);
|
||||
|
||||
for (let victoryCondition of g_VictoryConditions)
|
||||
this.gameSettingControls[victoryCondition.Name] =
|
||||
new VictoryConditionCheckbox(
|
||||
victoryCondition, this, getCategory(victoryCondition.Name), undefined, setupWindow);
|
||||
victoryCondition, this, getCategory(victoryCondition.Name), undefined,
|
||||
setupWindow, isSavedGame);
|
||||
|
||||
this.playerSettingControlManagers = Array.from(
|
||||
new Array(g_MaxPlayers),
|
||||
(value, playerIndex) =>
|
||||
new PlayerSettingControlManager(playerIndex, setupWindow));
|
||||
new PlayerSettingControlManager(playerIndex, setupWindow, isSavedGame));
|
||||
}
|
||||
|
||||
getNextRow(name)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,16 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
|
|||
// Build the initial list of values with undefined & AI clients.
|
||||
this.rebuildList();
|
||||
|
||||
const savedAI = this.isSavedGame && g_GameSettings.playerAI.get(this.playerIndex);
|
||||
|
||||
if (savedAI)
|
||||
{
|
||||
this.setSelectedValue(savedAI.bot);
|
||||
this.setEnabled(false);
|
||||
}
|
||||
else
|
||||
this.rebuildList();
|
||||
|
||||
g_GameSettings.playerAI.watch(() => this.render(), ["values"]);
|
||||
g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]);
|
||||
}
|
||||
|
|
@ -103,9 +113,13 @@ 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 saved game clients and unassigned players can't be replaced by a AI. Don't show
|
||||
// the AIs in the dropdown.
|
||||
const disableAI = this.isSavedGame && !g_GameSettings.playerAI.get(this.playerIndex);
|
||||
this.values = prepareForDropdown([
|
||||
...this.playerItems,
|
||||
...this.aiItems,
|
||||
...disableAI ? [] : this.aiItems,
|
||||
this.unassignedItem
|
||||
]);
|
||||
|
||||
|
|
@ -122,7 +136,8 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
|
|||
this.gameSettingsController,
|
||||
this.playerAssignmentsController,
|
||||
this.playerIndex,
|
||||
this.values.Value[itemIdx]);
|
||||
this.values.Value[itemIdx],
|
||||
this.isSavedGame);
|
||||
}
|
||||
|
||||
getAutocompleteEntries()
|
||||
|
|
@ -131,6 +146,8 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
|
|||
}
|
||||
};
|
||||
|
||||
PlayerSettingControls.PlayerAssignment.prototype.EnabledWhenSavedGame = true;
|
||||
|
||||
PlayerSettingControls.PlayerAssignment.prototype.Tooltip =
|
||||
translate("Select player.");
|
||||
|
||||
|
|
@ -151,7 +168,8 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
|
|||
};
|
||||
}
|
||||
|
||||
onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex, guidToAssign)
|
||||
onSelectionChange(gameSettingsController, playerAssignmentsController, playerIndex,
|
||||
guidToAssign, isSavedGame)
|
||||
{
|
||||
let sourcePlayer = g_PlayerAssignments[guidToAssign].player - 1;
|
||||
if (sourcePlayer >= 0)
|
||||
|
|
@ -161,7 +179,7 @@ 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.map.type !== "scenario" && !isSavedGame)
|
||||
{
|
||||
g_GameSettings.playerCiv.swap(sourcePlayer, playerIndex);
|
||||
g_GameSettings.playerColor.swap(sourcePlayer, playerIndex);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ PlayerSettingControls.PlayerCiv = class PlayerCiv extends GameSettingControlDrop
|
|||
|
||||
rebuild()
|
||||
{
|
||||
const isLocked = g_GameSettings.playerCiv.locked[this.playerIndex];
|
||||
const isLocked = g_GameSettings.playerCiv.locked[this.playerIndex] || this.isSavedGame;
|
||||
if (this.wasLocked !== isLocked)
|
||||
{
|
||||
this.wasLocked = isLocked;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl
|
|||
constructor(...args)
|
||||
{
|
||||
super(...args);
|
||||
g_GameSettings.playerName.isSavedGame = this.isSavedGame;
|
||||
|
||||
this.playerName = Engine.GetGUIObjectByName("playerName[" + this.playerIndex + "]");
|
||||
g_GameSettings.playerCount.watch(() => this.render(), ["nbPlayers"]);
|
||||
|
|
@ -29,8 +30,8 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl
|
|||
|
||||
render()
|
||||
{
|
||||
let name = this.guid ? g_PlayerAssignments[this.guid].name :
|
||||
g_GameSettings.playerName.values[this.playerIndex];
|
||||
let name = this.guid && this.isSavedGame ?
|
||||
g_PlayerAssignments[this.guid].name : g_GameSettings.playerName.values[this.playerIndex];
|
||||
|
||||
if (g_IsNetworked)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt
|
|||
{
|
||||
super(...args);
|
||||
|
||||
if (this.isSavedGame)
|
||||
{
|
||||
this.setHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.button.tooltip = colorizeHotkey(this.HotkeyTooltip, this.HotkeyConfig);
|
||||
Engine.SetGlobalHotkey(this.HotkeyConfig, "Press", this.onPress.bind(this));
|
||||
}
|
||||
|
|
@ -25,7 +31,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt
|
|||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(this.enabled);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ GameSettingControls.LastManStanding = class LastManStanding extends GameSettingC
|
|||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
}
|
||||
|
||||
onSettingsLoaded()
|
||||
{
|
||||
this.render();
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
// Always display this, so that players are aware that there is this gamemode
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ GameSettingControls.Rating = class Rating extends GameSettingControlCheckbox
|
|||
|
||||
// The availability of rated games is not a GUI concern, unlike most other
|
||||
// potentially available settings.
|
||||
g_GameSettings.rating.watch(() => this.render(), ["enabled", "available"]);
|
||||
if (this.isSavedGame)
|
||||
g_GameSettings.rating.enabled = false;
|
||||
else
|
||||
g_GameSettings.rating.watch(() => this.render(), ["enabled", "available"]);
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,21 +3,22 @@
|
|||
*/
|
||||
SetupWindowPages.GameSetupPage = class
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
Engine.ProfileStart("GameSetupPage");
|
||||
|
||||
// This class instance owns all game setting GUI controls such as dropdowns and checkboxes visible in this page.
|
||||
this.gameSettingControlManager = new GameSettingControlManager(setupWindow);
|
||||
this.gameSettingControlManager = new GameSettingControlManager(setupWindow, isSavedGame);
|
||||
|
||||
// These classes manage GUI buttons.
|
||||
{
|
||||
let startGameButton = new StartGameButton(setupWindow);
|
||||
let readyButton = new ReadyButton(setupWindow);
|
||||
this.panelButtons = {
|
||||
"cancelButton": new CancelButton(setupWindow, startGameButton, readyButton),
|
||||
"civInfoButton": new CivInfoButton(),
|
||||
"lobbyButton": new LobbyButton(),
|
||||
"savedGameLabel": new SavedGameLabel(isSavedGame),
|
||||
"cancelButton": new CancelButton(setupWindow, startGameButton, readyButton),
|
||||
"readyButton": readyButton,
|
||||
"startGameButton": startGameButton
|
||||
};
|
||||
|
|
@ -31,13 +32,13 @@ 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,
|
||||
"mapPreview": new MapPreview(setupWindow),
|
||||
"resetCivsButton": new ResetCivsButton(setupWindow),
|
||||
"resetTeamsButton": new ResetTeamsButton(setupWindow),
|
||||
"mapPreview": new MapPreview(setupWindow, isSavedGame),
|
||||
"resetCivsButton": new ResetCivsButton(setupWindow, isSavedGame),
|
||||
"resetTeamsButton": new ResetTeamsButton(setupWindow, isSavedGame),
|
||||
"soundNotification": new SoundNotification(setupWindow),
|
||||
"tipsPanel": new TipsPanel(gameSettingsPanel)
|
||||
};
|
||||
|
|
|
|||
|
|
@ -67,23 +67,27 @@
|
|||
|
||||
<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/SavedGameLabel.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>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
class ResetCivsButton
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
this.gameSettingsController = setupWindow.controls.gameSettingsController;
|
||||
|
||||
|
|
@ -8,12 +8,15 @@ class ResetCivsButton
|
|||
this.civResetButton.tooltip = this.Tooltip;
|
||||
this.civResetButton.onPress = this.onPress.bind(this);
|
||||
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
if (isSavedGame)
|
||||
this.civResetButton.hidden = true;
|
||||
else
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
this.civResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController;
|
||||
this.civResetButton.hidden = g_GameSettings.map.type === "scenario" || !g_IsController;
|
||||
}
|
||||
|
||||
onPress()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
class ResetTeamsButton
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
this.gameSettingsController = setupWindow.controls.gameSettingsController;
|
||||
|
||||
|
|
@ -8,12 +8,15 @@ class ResetTeamsButton
|
|||
this.teamResetButton.tooltip = this.Tooltip;
|
||||
this.teamResetButton.onPress = this.onPress.bind(this);
|
||||
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
if (isSavedGame)
|
||||
this.teamResetButton.hidden = true;
|
||||
else
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
this.teamResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController;
|
||||
this.teamResetButton.hidden = g_GameSettings.map.type === "scenario" || !g_IsController;
|
||||
}
|
||||
|
||||
onPress()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
class SavedGameLabel
|
||||
{
|
||||
constructor(isSavedGame)
|
||||
{
|
||||
if (isSavedGame)
|
||||
Engine.GetGUIObjectByName("savedGameLabel").hidden = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object
|
||||
name="savedGameLabel"
|
||||
type="text"
|
||||
style="ModernLabelText"
|
||||
tooltip_style="onscreenToolTip"
|
||||
hidden="true">
|
||||
|
||||
<translatableAttribute id="caption">Saved game</translatableAttribute>
|
||||
|
||||
<translatableAttribute id="tooltip">
|
||||
The controller loaded a saved game.
|
||||
</translatableAttribute>
|
||||
</object>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
class MapPreview
|
||||
{
|
||||
constructor(setupWindow)
|
||||
constructor(setupWindow, isSavedGame)
|
||||
{
|
||||
this.setupWindow = setupWindow;
|
||||
this.gameSettingsController = setupWindow.controls.gameSettingsController;
|
||||
|
|
@ -8,16 +8,27 @@ class MapPreview
|
|||
|
||||
this.mapInfoName = Engine.GetGUIObjectByName("mapInfoName");
|
||||
this.mapPreview = Engine.GetGUIObjectByName("mapPreview");
|
||||
this.mapPreview.onMouseLeftPress = this.onPress.bind(this); // TODO: Why does onPress not work? CGUI.cpp seems to support it
|
||||
this.mapPreview.tooltip = this.Tooltip;
|
||||
|
||||
if (isSavedGame)
|
||||
{
|
||||
// Delay the settings registration handler until we have the map cache.
|
||||
setupWindow.controls.gameSettingsController.registerSettingsLoadedHandler(() => {
|
||||
this.renderName();
|
||||
this.renderPreview();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Why does onPress not work? CGUI.cpp seems to support it.
|
||||
this.mapPreview.onMouseLeftPress = this.onPress.bind(this, isSavedGame);
|
||||
this.mapPreview.tooltip = this.Tooltip;
|
||||
g_GameSettings.map.watch(() => this.renderName(), ["map"]);
|
||||
g_GameSettings.mapPreview.watch(() => this.renderPreview(), ["value"]);
|
||||
}
|
||||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(true);
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -25,13 +25,20 @@ class SetupWindow
|
|||
if (initData?.backPage)
|
||||
this.backPage = initData.backPage;
|
||||
|
||||
const savedGame = initData?.savedGame;
|
||||
const isSavedGame = !!savedGame;
|
||||
|
||||
const mapCache = new MapCache();
|
||||
g_GameSettings = new GameSettings().init(mapCache);
|
||||
g_GameSettings = new GameSettings();
|
||||
g_GameSettings.init(mapCache, g_IsController ? savedGame : null);
|
||||
|
||||
|
||||
let netMessages = new NetMessages();
|
||||
let mapFilters = new MapFilters(mapCache);
|
||||
let playerAssignmentsController = new PlayerAssignmentsController(this, netMessages);
|
||||
let gameSettingsController = new GameSettingsController(this, netMessages, playerAssignmentsController, mapCache);
|
||||
let playerAssignmentsController =
|
||||
new PlayerAssignmentsController(this, netMessages, isSavedGame);
|
||||
let gameSettingsController = new GameSettingsController(this, netMessages,
|
||||
playerAssignmentsController, mapCache, isSavedGame);
|
||||
let readyController = new ReadyController(netMessages, gameSettingsController, playerAssignmentsController);
|
||||
const lobbyGameRegistrationController = g_IsController && Engine.HasXmppClient() &&
|
||||
new LobbyGameRegistrationController(initData, this, netMessages, mapCache, playerAssignmentsController);
|
||||
|
|
@ -50,7 +57,7 @@ class SetupWindow
|
|||
// These are the pages within the setup window that may use the controls defined above
|
||||
this.pages = {};
|
||||
for (let name in SetupWindowPages)
|
||||
this.pages[name] = new SetupWindowPages[name](this);
|
||||
this.pages[name] = new SetupWindowPages[name](this, isSavedGame);
|
||||
|
||||
netMessages.registerNetMessageHandler("netwarn", addNetworkWarning);
|
||||
setTimeout(displayGamestateNotifications, 1000);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ function init(attribs)
|
|||
error("Unrecognised multiplayer game type: " + attribs.multiplayerGameType);
|
||||
break;
|
||||
}
|
||||
|
||||
Engine.GetGUIObjectByName("multiplayerPages").onTick = onTick.bind(null, attribs.loadSavedGame);
|
||||
Engine.GetGUIObjectByName("continueButton").onPress = confirmSetup.bind(null, attribs.loadSavedGame);
|
||||
}
|
||||
|
||||
function cancelSetup()
|
||||
|
|
@ -104,7 +107,7 @@ function confirmPassword()
|
|||
switchSetupPage("pageConnecting");
|
||||
}
|
||||
|
||||
function confirmSetup()
|
||||
function confirmSetup(loadSavedGame)
|
||||
{
|
||||
if (!Engine.GetGUIObjectByName("pageJoin").hidden)
|
||||
{
|
||||
|
|
@ -137,8 +140,11 @@ function confirmSetup()
|
|||
|
||||
let hostPlayerName = Engine.GetGUIObjectByName("hostPlayerName").caption;
|
||||
let hostPassword = Engine.GetGUIObjectByName("hostPassword").caption;
|
||||
if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword))
|
||||
if (startHost(hostPlayerName, hostServerName, getValidPort(hostPort), hostPassword,
|
||||
loadSavedGame))
|
||||
{
|
||||
switchSetupPage("pageConnecting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -150,12 +156,12 @@ function startConnectionStatus(type)
|
|||
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Connecting to server...");
|
||||
}
|
||||
|
||||
function onTick()
|
||||
function onTick(loadSavedGame)
|
||||
{
|
||||
if (!g_IsConnecting)
|
||||
return;
|
||||
|
||||
pollAndHandleNetworkClient();
|
||||
pollAndHandleNetworkClient(loadSavedGame);
|
||||
}
|
||||
|
||||
function getConnectionFailReason(reason)
|
||||
|
|
@ -182,7 +188,7 @@ function reportConnectionFail(reason)
|
|||
);
|
||||
}
|
||||
|
||||
function pollAndHandleNetworkClient()
|
||||
function pollAndHandleNetworkClient(loadSavedGame)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
|
|
@ -277,17 +283,8 @@ function pollAndHandleNetworkClient()
|
|||
break;
|
||||
|
||||
case "authenticated":
|
||||
if (message.rejoining)
|
||||
{
|
||||
Engine.GetGUIObjectByName("connectionStatus").caption = translate("Game has already started, rejoining...");
|
||||
g_IsRejoining = true;
|
||||
return; // we'll process the game setup messages in the next tick
|
||||
}
|
||||
Engine.SwitchGuiPage("page_gamesetup.xml", {
|
||||
"serverName": g_ServerName,
|
||||
"hasPassword": g_ServerHasPassword
|
||||
});
|
||||
return; // don't process any more messages - leave them for the game GUI loop
|
||||
handleAuthenticated(message, loadSavedGame);
|
||||
return;
|
||||
|
||||
case "disconnected":
|
||||
cancelSetup();
|
||||
|
|
@ -311,6 +308,34 @@ function pollAndHandleNetworkClient()
|
|||
}
|
||||
}
|
||||
|
||||
async function handleAuthenticated(message, loadSavedGame)
|
||||
{
|
||||
if (message.rejoining)
|
||||
{
|
||||
Engine.GetGUIObjectByName("connectionStatus").caption =
|
||||
translate("Game has already started, rejoining...");
|
||||
g_IsRejoining = true;
|
||||
return; // we'll process the game setup messages in the next tick
|
||||
}
|
||||
g_IsConnecting = false;
|
||||
|
||||
const savegameID = loadSavedGame ? await Engine.PushGuiPage("page_loadgame.xml") : null;
|
||||
|
||||
if (loadSavedGame && !savegameID)
|
||||
{
|
||||
Engine.DisconnectNetworkGame();
|
||||
cancelSetup();
|
||||
return;
|
||||
}
|
||||
|
||||
Engine.SwitchGuiPage("page_gamesetup.xml", {
|
||||
"savedGame": savegameID ?? message.savedGame,
|
||||
"serverName": g_ServerName,
|
||||
"hasPassword": g_ServerHasPassword
|
||||
});
|
||||
return; // don't process any more messages - leave them for the game GUI loop
|
||||
}
|
||||
|
||||
function switchSetupPage(newPage)
|
||||
{
|
||||
let multiplayerPages = Engine.GetGUIObjectByName("multiplayerPages");
|
||||
|
|
@ -343,14 +368,14 @@ function switchSetupPage(newPage)
|
|||
Engine.GetGUIObjectByName("continueButton").hidden = newPage == "pageConnecting" || newPage == "pagePassword";
|
||||
}
|
||||
|
||||
function startHost(playername, servername, port, password)
|
||||
function startHost(playername, servername, port, password, loadSavedGame)
|
||||
{
|
||||
startConnectionStatus("server");
|
||||
|
||||
Engine.ConfigDB_CreateValue("user", "playername.multiplayer", playername);
|
||||
Engine.ConfigDB_CreateValue("user", "multiplayerhosting.port", port);
|
||||
Engine.ConfigDB_SaveChanges("user");
|
||||
|
||||
|
||||
let hostFeedback = Engine.GetGUIObjectByName("hostFeedback");
|
||||
|
||||
// Disallow identically named games in the multiplayer lobby
|
||||
|
|
@ -366,7 +391,8 @@ function startHost(playername, servername, port, password)
|
|||
|
||||
try
|
||||
{
|
||||
Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port, useSTUN, password, true);
|
||||
Engine.StartNetworkHost(playername + (g_UserRating ? " (" + g_UserRating + ")" : ""), port,
|
||||
useSTUN, password, loadSavedGame, true);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@
|
|||
|
||||
<object name="multiplayerPages" type="image" style="ModernDialog" size="50%-230 50%-120 50%+230 50%+120">
|
||||
|
||||
<action on="Tick">
|
||||
onTick();
|
||||
</action>
|
||||
|
||||
<object style="ModernLabelText" type="text" size="50%-128 0%-16 50%+128 16">
|
||||
<translatableAttribute id="caption">Multiplayer</translatableAttribute>
|
||||
</object>
|
||||
|
|
@ -134,7 +130,6 @@
|
|||
|
||||
<object name="continueButton" hotkey="confirm" type="button" size="50%+5 100%-45 100%-18 100%-17" style="ModernButtonRed">
|
||||
<translatableAttribute id="caption">Continue</translatableAttribute>
|
||||
<action on="Press">confirmSetup();</action>
|
||||
</object>
|
||||
|
||||
<object type="button" style="ModernButtonRed" size="18 100%-45 50%-5 100%-17" hotkey="cancel">
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@
|
|||
*/
|
||||
class HostButton
|
||||
{
|
||||
constructor(dialog, xmppMessages)
|
||||
constructor(dialog, xmppMessages, button, loadSavedGame)
|
||||
{
|
||||
this.hostButton = Engine.GetGUIObjectByName("hostButton");
|
||||
this.hostButton.onPress = this.onPress.bind(this);
|
||||
this.hostButton.caption = translate("Host Game");
|
||||
this.hostButton = button;
|
||||
this.hostButton.onPress = this.onPress.bind(this, loadSavedGame);
|
||||
this.hostButton.hidden = dialog;
|
||||
|
||||
let onConnectionStatusChange = this.onConnectionStatusChange.bind(this);
|
||||
|
|
@ -21,9 +20,10 @@ class HostButton
|
|||
this.hostButton.enabled = Engine.IsXmppClientConnected();
|
||||
}
|
||||
|
||||
onPress()
|
||||
onPress(loadSavedGame)
|
||||
{
|
||||
Engine.PushGuiPage("page_gamesetup_mp.xml", {
|
||||
"loadSavedGame": loadSavedGame,
|
||||
"multiplayerGameType": "host",
|
||||
"name": g_Nickname,
|
||||
"rating": Engine.LobbyGetPlayerRating(g_Nickname)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object>
|
||||
<object name="joinButton" size="0 100%-104 100% 100%-81" type="button" style="ModernButtonRed"/>
|
||||
|
||||
<object name="hostButton" size="0 100%-79 100% 100%-54" type="button" style="ModernButtonRed">
|
||||
<translatableAttribute id="caption">Host New Game</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="hostSavedGameButton" size="0 100%-52 100% 100%-27" type="button" style="ModernButtonRed">
|
||||
<translatableAttribute id="caption">Host Saved Game</translatableAttribute>
|
||||
</object>
|
||||
|
||||
<object name="leaveButton" size="0 100%-25 100% 100%" type="button" style="ModernButtonRed"/>
|
||||
</object>
|
||||
|
|
@ -18,8 +18,11 @@ class LobbyPage
|
|||
"buttons": {
|
||||
"buddyButton": buddyButton,
|
||||
"accountSettingsButton": accountSettingsButton,
|
||||
"hostButton": new HostButton(dialog, xmppMessages),
|
||||
"joinButton": new JoinButton(dialog, gameList),
|
||||
"hostButton": new HostButton(dialog, xmppMessages,
|
||||
Engine.GetGUIObjectByName("hostButton"), false),
|
||||
"hostSavedGameButton": new HostButton(dialog, xmppMessages,
|
||||
Engine.GetGUIObjectByName("hostSavedGameButton"), true),
|
||||
"leaderboardButton": new LeaderboardButton(xmppMessages, leaderboardPage),
|
||||
"profileButton": new ProfileButton(xmppMessages, profilePage),
|
||||
"quitButton": new QuitButton(dialog, leaderboardPage, profilePage)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,9 @@
|
|||
<object name="subjectPanel">
|
||||
<include file="gui/lobby/LobbyPage/Subject.xml"/>
|
||||
</object>
|
||||
<object name="joinButton" size="0 100%-79 100% 100%-54" type="button" style="ModernButtonRed"/>
|
||||
<object name="hostButton" size="0 100%-52 100% 100%-27" type="button" style="ModernButtonRed"/>
|
||||
<object name="leaveButton" size="0 100%-25 100% 100%" type="button" style="ModernButtonRed"/>
|
||||
<object name="gameButtonsPanel">
|
||||
<include file="gui/lobby/LobbyPage/GameButtons.xml"/>
|
||||
</object>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
|
|
|
|||
|
|
@ -187,13 +187,20 @@ var g_MainMenuItems = [
|
|||
}
|
||||
},
|
||||
{
|
||||
"caption": translate("Host Game"),
|
||||
"tooltip": translate("Host a multiplayer game."),
|
||||
"onPress": () => {
|
||||
Engine.PushGuiPage("page_gamesetup_mp.xml", {
|
||||
"multiplayerGameType": "host"
|
||||
});
|
||||
}
|
||||
"caption": translate("Host New Game"),
|
||||
"tooltip": translate("Host a new multiplayer game."),
|
||||
"onPress": Engine.PushGuiPage.bind(null, "page_gamesetup_mp.xml", {
|
||||
"multiplayerGameType": "host",
|
||||
"loadSavedGame": false
|
||||
})
|
||||
},
|
||||
{
|
||||
"caption": translate("Host Saved Game"),
|
||||
"tooltip": translate("Continue playing a game from a savegame."),
|
||||
"onPress": Engine.PushGuiPage.bind(null, "page_gamesetup_mp.xml", {
|
||||
"multiplayerGameType": "host",
|
||||
"loadSavedGame": true
|
||||
})
|
||||
},
|
||||
{
|
||||
"caption": translate("Game Lobby"),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -499,6 +501,15 @@ void CNetClient::SendFlareMessage(const CStr& positionX, const CStr& positionY,
|
|||
SendMessage(&flare);
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -534,27 +545,32 @@ bool CNetClient::HandleMessage(CNetMessage* message)
|
|||
{
|
||||
CFileTransferRequestMessage* reqMessage = static_cast<CFileTransferRequestMessage*>(message);
|
||||
|
||||
ENSURE(static_cast<CNetFileTransferer::RequestType>(reqMessage->m_RequestType) ==
|
||||
CNetFileTransferer::RequestType::REJOIN);
|
||||
std::string uncompressedGameState{[&]
|
||||
{
|
||||
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);
|
||||
std::string compressedGameState;
|
||||
CompressZLib(std::move(uncompressedGameState), compressedGameState, true);
|
||||
|
||||
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed);
|
||||
m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID,
|
||||
std::move(compressedGameState));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -615,6 +631,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);
|
||||
|
|
@ -690,7 +718,8 @@ bool CNetClient::OnAuthenticate(CNetClient* client, CFsmEvent* event)
|
|||
client->PushGuiMessage(
|
||||
"type", "netstatus",
|
||||
"status", "authenticated",
|
||||
"rejoining", client->m_Rejoin);
|
||||
"rejoining", client->m_Rejoin,
|
||||
"savedGame", message->m_Code == ARC_OK_SAVED_GAME);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -769,29 +798,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);
|
||||
|
|
|
|||
|
|
@ -237,6 +237,8 @@ public:
|
|||
|
||||
void SendStartGameMessage(const CStr& initAttribs);
|
||||
|
||||
void SendStartSavedGameMessage(const CStr& initAttribs, const CStr& savedState);
|
||||
|
||||
/**
|
||||
* Call when the client (player or observer) has sent a flare.
|
||||
*/
|
||||
|
|
@ -283,6 +285,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 OnFlare(CNetClient* client, CFsmEvent* event);
|
||||
|
|
@ -299,6 +302,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.
|
||||
*/
|
||||
|
|
@ -349,6 +358,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
|
||||
|
|
@ -88,6 +89,7 @@ enum NetMessageType
|
|||
enum AuthenticateResultCode
|
||||
{
|
||||
ARC_OK,
|
||||
ARC_OK_SAVED_GAME,
|
||||
ARC_OK_REJOINING,
|
||||
ARC_PASSWORD_INVALID,
|
||||
};
|
||||
|
|
@ -226,6 +228,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)
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ static CStr DebugName(CNetServerSession* session)
|
|||
* See https://gitea.wildfiregames.com/0ad/0ad/issues/654
|
||||
*/
|
||||
|
||||
CNetServerWorker::CNetServerWorker(bool useLobbyAuth) :
|
||||
CNetServerWorker::CNetServerWorker(const bool continueSavedGame, const bool useLobbyAuth) :
|
||||
m_ContinuesSavedGame{continueSavedGame},
|
||||
m_LobbyAuth(useLobbyAuth),
|
||||
m_Shutdown(false),
|
||||
m_ScriptInterface(NULL),
|
||||
|
|
@ -623,14 +624,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 those 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 +665,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);
|
||||
|
|
@ -1094,7 +1097,8 @@ bool CNetServerWorker::OnAuthenticate(CNetServerSession* session, CFsmEvent* eve
|
|||
session->SetHostID(newHostID);
|
||||
|
||||
CAuthenticateResultMessage authenticateResult;
|
||||
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING : ARC_OK;
|
||||
authenticateResult.m_Code = isRejoining ? ARC_OK_REJOINING :
|
||||
server.m_ContinuesSavedGame ? ARC_OK_SAVED_GAME : ARC_OK;
|
||||
authenticateResult.m_HostID = newHostID;
|
||||
authenticateResult.m_Message = L"Logged in";
|
||||
authenticateResult.m_IsController = 0;
|
||||
|
|
@ -1331,6 +1335,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);
|
||||
|
|
@ -1354,6 +1377,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 });
|
||||
|
|
@ -1515,7 +1542,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)
|
||||
|
|
@ -1546,12 +1573,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;
|
||||
|
|
@ -1608,8 +1649,8 @@ void CNetServerWorker::SendHolePunchingMessage(const CStr& ipStr, u16 port)
|
|||
|
||||
|
||||
|
||||
CNetServer::CNetServer(bool useLobbyAuth) :
|
||||
m_Worker(new CNetServerWorker(useLobbyAuth)),
|
||||
CNetServer::CNetServer(const bool continueSavedGame, const bool useLobbyAuth) :
|
||||
m_Worker{new CNetServerWorker{continueSavedGame, useLobbyAuth}},
|
||||
m_LobbyAuth(useLobbyAuth), m_UseSTUN(false), m_PublicIp(""), m_PublicPort(20595), m_Password()
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public:
|
|||
* Construct a new network server.
|
||||
* once this many players are connected (intended for the command-line testing mode).
|
||||
*/
|
||||
CNetServer(bool useLobbyAuth = false);
|
||||
CNetServer(const bool isSavedGame, const bool useLobbyAuth = false);
|
||||
|
||||
~CNetServer();
|
||||
|
||||
|
|
@ -234,7 +234,7 @@ public:
|
|||
private:
|
||||
friend class CNetServer;
|
||||
|
||||
CNetServerWorker(bool useLobbyAuth);
|
||||
CNetServerWorker(const bool continuesSavedGame, const bool useLobbyAuth);
|
||||
~CNetServerWorker();
|
||||
|
||||
bool CheckPassword(const std::string& password, const std::string& salt) const;
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
@ -306,6 +317,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);
|
||||
|
|
@ -349,6 +361,11 @@ private:
|
|||
*/
|
||||
JS::PersistentRootedValue m_InitAttributes;
|
||||
|
||||
/**
|
||||
* Whether this match continues a saved game.
|
||||
*/
|
||||
const bool m_ContinuesSavedGame;
|
||||
|
||||
/**
|
||||
* Whether this match requires lobby authentication.
|
||||
*/
|
||||
|
|
@ -400,6 +417,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"
|
||||
|
|
@ -62,7 +63,8 @@ bool HasNetClient()
|
|||
return !!g_NetClient;
|
||||
}
|
||||
|
||||
void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, bool useSTUN, const CStr& password, bool storeReplay)
|
||||
void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, bool useSTUN,
|
||||
const CStr& password, const bool continueSavedGame, bool storeReplay)
|
||||
{
|
||||
ENSURE(!g_NetClient);
|
||||
ENSURE(!g_NetServer);
|
||||
|
|
@ -70,7 +72,7 @@ void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u1
|
|||
|
||||
// Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames.
|
||||
bool hasLobby = !!g_XmppClient;
|
||||
g_NetServer = new CNetServer(hasLobby);
|
||||
g_NetServer = new CNetServer(continueSavedGame, hasLobby);
|
||||
|
||||
if (!g_NetServer->SetupConnection(serverPort))
|
||||
{
|
||||
|
|
@ -256,14 +258,29 @@ void ClearAllPlayerReady ()
|
|||
g_NetClient->SendClearAllReadyMessage();
|
||||
}
|
||||
|
||||
void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue attribs1)
|
||||
void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue savegame, JS::HandleValue attribs1)
|
||||
{
|
||||
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 attribs(rq.cx, attribs1);
|
||||
g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs));
|
||||
std::string attributesAsString{Script::StringifyJSON(rq, &attribs)};
|
||||
|
||||
if (savegame.isFalse())
|
||||
{
|
||||
g_NetClient->SendStartGameMessage(attributesAsString);
|
||||
return;
|
||||
}
|
||||
|
||||
std::wstring savegameID;
|
||||
Script::FromJSVal(rq, savegame, savegameID);
|
||||
|
||||
const std::optional<SavedGames::LoadResult> loadResult{SavedGames::Load(scriptInterface, savegameID)};
|
||||
if (loadResult)
|
||||
g_NetClient->SendStartSavedGameMessage(attributesAsString, loadResult->savedState);
|
||||
else
|
||||
ScriptException::Raise(rq, "Failed to load the saved game: \"%ls\"", savegameID.c_str());
|
||||
}
|
||||
|
||||
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 ? data->metadata : JS::UndefinedValue();
|
||||
}
|
||||
|
||||
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 a new issue