1
0
forked from 0ad/0ad

Multiplayer saved games

Enables to save multiplayer games.
When the savegame is loaded, the settings are frozen (except the non-AI-player assignment settings).
We might unfreze more settings(victory condition, player color...) in the future.
This commit is contained in:
phosit 2024-08-31 10:54:56 +02:00
parent 9ac60514c3
commit fee875066f
38 changed files with 759 additions and 77 deletions

View File

@ -141,6 +141,7 @@ class GameSettings
});
// NB: for multiplayer support, the clients must be listening to "start" net messages.
warn(uneval(this.finalizedAttributes));
if (this.isNetworked)
Engine.StartNetworkGame(this.finalizedAttributes, storeReplay);
else

View File

@ -0,0 +1,16 @@
GameSettings.prototype.Attributes.Savegame = class extends GameSetting
{
value = null;
toInitAttributes(attribs)
{
if (this.value !== null)
attribs.settings.Savegame = this.value;
}
fromInitAttributes(attribs)
{
const newValue = this.getLegacySetting(attribs, "Savegame");
this.value = newValue ?? null;
}
};

View File

@ -189,6 +189,7 @@
{ "nick": "MattDoerksen", "name": "Matt Doerksen" },
{ "nick": "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" },

View File

@ -0,0 +1,322 @@
{
"Title": "Programming",
"Content": [
{
"Title": "Programming managers",
"List": [
{ "nick": "Acumen", "name": "Stuart Walpole" },
{ "nick": "Dak Lozar", "name": "Dave Loeser" },
{ "nick": "h20", "name": "Daniel Wilhelm" },
{ "nick": "Janwas", "name": "Jan Wassenberg" },
{ "nick": "Raj", "name": "Raj Sharma" }
]
},
{
"Subtitle": "Special thanks to",
"List": [
{ "nick": "leper", "name": "Georg Kilzer" },
{ "nick": "Ykkrosh", "name": "Philip Taylor" }
]
},
{
"List": [
{ "nick": "01d55" },
{ "nick": "abian", "name": "David Abián" },
{ "nick": "aBothe", "name": "Alexander Bothe" },
{ "nick": "animus", "name": "Itay Krishtal" },
{ "nick": "Acumen", "name": "Stuart Walpole" },
{ "nick": "adrian", "name": "Adrian Boguszewszki" },
{ "name": "Adrian Fatol" },
{ "nick": "AI-Amsterdam" },
{ "nick": "Alan", "name": "Alan Kemp" },
{ "nick": "Alex", "name": "Alexander Yakobovich" },
{ "nick": "alpha123", "name": "Peter P. Cannici" },
{ "nick": "alre" },
{ "nick": "Ampaex", "name": "Antonio Vazquez" },
{ "name": "André Puel" },
{ "nick": "andy5995", "name": "Andy Alt" },
{ "nick": "Angen" },
{ "nick": "Arfrever", "name": "Arfrever Frehtes Taifersar Arahesis" },
{ "nick": "ArnH", "name": "Arno Hemelhof" },
{ "nick": "Aurium", "name": "Aurélio Heckert" },
{ "nick": "azayrahmad", "name": "Aziz Rahmad" },
{ "nick": "baco", "name": "Dionisio E Alonso" },
{ "nick": "badmadblacksad", "name": "Martin F" },
{ "nick": "badosu", "name": "Amadeus Folego" },
{ "nick": "bb", "name": "Bouke Jansen" },
{ "nick": "Bellaz89", "name": "Andrea Bellandi" },
{ "nick": "Ben", "name": "Ben Vinegar" },
{ "nick": "Bird" },
{ "nick": "Blue", "name": "Richard Welsh" },
{ "nick": "bmwiedemann" },
{ "nick": "boeseRaupe", "name": "Michael Kluge" },
{ "nick": "bog_dan_ro", "name": "BogDan Vatra" },
{ "nick": "Bonk", "name": "Christopher Ebbert" },
{ "nick": "Boudica" },
{ "nick": "Caius", "name": "Lars Kemmann" },
{ "nick": "Calefaction", "name": "Matt Holmes" },
{ "nick": "Calvinh", "name": "Carl-Johan Höiby" },
{ "nick": "causative", "name": "Bart Parkis" },
{ "nick": "Cayleb-Ordo", "name": "Simon Fentzl" },
{ "name": "Cédric Houbart" },
{ "nick": "Ceres" },
{ "nick": "Chakakhan", "name": "Kenny Long" },
{ "nick": "Clockwork-Muse", "name": "Stephen A. Imhoff" },
{ "nick": "cpc", "name": "Clément Pit-Claudel" },
{ "nick": "Cracker78", "name": "Chad Heim" },
{ "nick": "Crynux", "name": "Stephen J. Fewer" },
{ "nick": "cwprogger" },
{ "nick": "cygal", "name": "Quentin Pradet" },
{ "nick": "Dak Lozar", "name": "Dave Loeser" },
{ "nick": "dalerank", "name": "Sergey Kushnirenko" },
{ "nick": "dan", "name": "Dan Strandberg" },
{ "nick": "DanCar", "name": "Daniel Cardenas" },
{ "nick": "danger89", "name": "Melroy van den Berg" },
{ "name": "Daniel Trevitz" },
{ "nick": "Dariost", "name": "Dario Ostuni" },
{ "nick": "Dave", "name": "David Protasowski" },
{ "name": "David Marshall" },
{ "nick": "dax", "name": "Dacian Fiordean" },
{ "nick": "deebee", "name": "Deepak Anthony" },
{ "nick": "Deiz" },
{ "nick": "Dietger", "name": "Dietger van Antwerpen" },
{ "nick": "DigitalSeraphim", "name": "Nick Owens" },
{ "nick": "dp304" },
{ "nick": "dpiquet", "name": "Damien Piquet" },
{ "nick": "dumbo" },
{ "nick": "Dunedan", "name": "Daniel Roschka" },
{ "nick": "dvangennip", "name": "Doménique" },
{ "nick": "DynamoFox" },
{ "nick": "Echelon9", "name": "Rhys Kidd" },
{ "nick": "echotangoecho" },
{ "nick": "edoput", "name": "Edoardo Putti"},
{ "nick": "eihrul", "name": "Lee Salzman" },
{ "nick": "elexis", "name": "Alexander Heinsius" },
{ "name": "Emily" },
{ "nick": "EmjeR", "name": "Matthijs de Rijk" },
{ "nick": "EMontana" },
{ "nick": "ericb" },
{ "nick": "evanssthomas", "name": "Evans Thomas" },
{ "nick": "Evulant", "name": "Alexander S." },
{ "nick": "fabio", "name": "Fabio Pedretti" },
{ "nick": "falsevision", "name": "Mahdi Khodadadifard" },
{ "nick": "fatherbushido", "name": "Nicolas Tisserand" },
{ "nick": "Fatton", "name": "Alexey Beloyarov" },
{ "nick": "fcxSanya", "name": "Alexander Olkhovskiy" },
{ "nick": "FeXoR", "name": "Florian Finke" },
{ "nick": "Fire Giant", "name": "Malte Schwarzkopf" },
{ "name": "Fork AD" },
{ "nick": "fpre", "name": "Frederick Stallmeyer" },
{ "nick": "Freagarach" },
{ "nick": "freenity", "name": "Anton Galitch" },
{ "nick": "froissant", "name": "Anthony Froissant" },
{ "nick": "Gallaecio", "name": "Adrián Chaves" },
{ "nick": "gbish (aka Iny)", "name": "Grant Bishop" },
{ "nick": "Gee", "name": "Gustav Larsson" },
{ "nick": "Gentz", "name": "Hal Gentz" },
{ "nick": "gerbilOFdoom" },
{ "nick": "godlikeldh" },
{ "nick": "Grapjas", "name": "Richie van Coesant" },
{ "nick": "greybeard", "name": "Joe Cocovich" },
{ "nick": "grillaz" },
{ "nick": "Grugnas", "name": "Giuseppe Tranchese" },
{ "nick": "gudo" },
{ "nick": "Guuts", "name": "Matthew Guttag" },
{ "nick": "h20", "name": "Daniel Wilhelm" },
{ "nick": "Hannibal_Barca", "name": "Clive Juhász S." },
{ "nick": "Haommin" },
{ "nick": "happyconcepts", "name": "Ben Bird" },
{ "nick": "historic_bruno", "name": "Ben Brian" },
{ "nick": "hyiltiz", "name": "Hormet Yiltiz" },
{ "nick": "idanwin" },
{ "nick": "Imarok", "name": "J. S." },
{ "nick": "Inari" },
{ "nick": "infyquest", "name": "Vijay Kiran Kamuju" },
{ "nick": "irishninja", "name": "Brian Broll" },
{ "nick": "IronNerd", "name": "Matthew McMullan" },
{ "nick": "Itms", "name": "Nicolas Auvray" },
{ "nick": "Jaison", "name": "Marco tom Suden" },
{ "nick": "jammus", "name": "James Scott" },
{ "nick": "Jammyjamjamman", "name": "James Sherratt" },
{ "nick": "Janwas", "name": "Jan Wassenberg" },
{ "nick": "javiergodas", "name": "Javier Godas Vieitez" },
{ "nick": "JCWasmx86" },
{ "nick": "Jgwman" },
{ "nick": "JonBaer", "name": "Jon Baer" },
{ "nick": "Josh", "name": "Joshua J. Bakita" },
{ "nick": "joskar", "name": "Johnny Oskarsson" },
{ "nick": "jP_wanN", "name": "Jonas Platte" },
{ "nick": "jprahman", "name": "Jason Rahman" },
{ "nick": "jpshack", "name": "John-Mason Shackelford" },
{ "nick": "Jubalbarca", "name": "James Baillie" },
{ "nick": "JubJub", "name": "Sebastian Vetter" },
{ "nick": "jurgemaister" },
{ "nick": "kabzerek", "name": "Grzegorz Kabza" },
{ "nick": "Kai", "name": "Kai Chen" },
{ "nick": "kalev", "name": "Kalev Lember" },
{ "name": "Kareem Ergawy" },
{ "nick": "karmux", "name": "Karmo Rosental" },
{ "nick": "kevmo", "name": "Kevin Caffrey" },
{ "nick": "kezz", "name": "Graeme Kerry" },
{ "nick": "kingadami", "name": "Adam Winsor" },
{ "nick": "kingbasil", "name": "Giannis Fafalios" },
{ "nick": "Krinkle", "name": "Timo Tijhof" },
{ "nick": "Kuba386", "name": "Jakub Kośmicki" },
{ "nick": "lafferjm", "name": "Justin Lafferty" },
{ "nick": "lairkers" },
{ "nick": "Langbart" },
{ "nick": "LeanderH", "name": "Leander Hemelhof" },
{ "nick": "leper", "name": "Georg Kilzer" },
{ "nick": "Link Mauve", "name": "Emmanuel Gil Peyrot" },
{ "nick": "LittleDev" },
{ "nick": "livingaftermidnight", "name": "Will Dull" },
{ "nick": "lonehawk", "name": "Vignesh Krishnan" },
{ "nick": "Louhike" },
{ "nick": "lsdh" },
{ "nick": "Ludovic", "name": "Ludovic Rousseau" },
{ "nick": "luiko", "name": "Luis Carlos Garcia Barajas" },
{ "nick": "m0l0t0ph", "name": "Christoph Gielisch" },
{ "nick": "madmax", "name": "Abhijit Nandy" },
{ "nick": "madpilot", "name": "Guido Falsi" },
{ "nick": "mammadori", "name": "Marco Amadori" },
{ "nick": "marder", "name": "Stefan R. F." },
{ "nick": "markcho" },
{ "nick": "MarkT", "name": "Mark Thompson" },
{ "nick": "Markus" },
{ "nick": "Mate-86", "name": "Mate Kovacs" },
{ "nick": "Matei", "name": "Matei Zaharia" },
{ "nick": "MatSharrow" },
{ "nick": "MattDoerksen", "name": "Matt Doerksen" },
{ "nick": "mattlott", "name": "Matt Lott" },
{ "nick": "maveric", "name": "Anton Protko" },
{ "nick": "Micnasty", "name": "Travis Gorkin" },
{ "name": "Mikołaj \"Bajter\" Korcz" },
{ "nick": "mimo" },
{ "nick": "mk12", "name": "Mitchell Kember" },
{ "nick": "mmayfield45", "name": "Michael Mayfield" },
{ "nick": "mmoanis", "name": "Mohamed Moanis" },
{ "nick": "Molotov", "name": "Dario Alvarez" },
{ "nick": "mpmoreti", "name": "Marcos Paulo Moreti" },
{ "nick": "mreiland", "name": "Michael Reiland" },
{ "nick": "myconid" },
{ "nick": "n1xc0d3r", "name": "Luis Guerrero" },
{ "nick": "nani", "name": "S. N." },
{ "nick": "nd3c3nt", "name": "Gavin Fowler" },
{ "nick": "nephele" },
{ "nick": "Nescio" },
{ "nick": "niektb", "name": "Niek ten Brinke" },
{ "nick": "nikagra", "name": "Mikita Hradovich" },
{ "nick": "njm" },
{ "nick": "NoMonkey", "name": "John Mena" },
{ "nick": "Norse_Harold" },
{ "nick": "norsnor" },
{ "nick": "notpete", "name": "Rich Cross" },
{ "nick": "Nullus" },
{ "nick": "nwtour" },
{ "nick": "odoaker", "name": "Ágoston Sipos" },
{ "nick": "Offensive ePeen", "name": "Jared Ryan Bills" },
{ "nick": "Ols", "name": "Oliver Whiteman" },
{ "nick": "olsner", "name": "Simon Brenner" },
{ "nick": "OptimusShepard", "name": "Pirmin Stanglmeier" },
{ "nick": "otero" },
{ "nick": "Palaxin", "name": "David A. Freitag" },
{ "name": "Paul Withers" },
{ "nick": "paulobezerr", "name": "Paulo George Gomes Bezerra" },
{ "nick": "pcpa", "name": "Paulo Andrade" },
{ "nick": "Pendingchaos" },
{ "nick": "PeteVasi", "name": "Pete Vasiliauskas" },
{ "nick": "phosit" },
{ "nick": "pilino1234" },
{ "nick": "PingvinBetyar", "name": "Schronk Tamás" },
{ "nick": "plugwash", "name": "Peter Michael Green" },
{ "nick": "Polakrity" },
{ "nick": "Poya", "name": "Poya Manouchehri" },
{ "nick": "prefect", "name": "Nicolai Hähnle" },
{ "nick": "Prodigal Son" },
{ "nick": "pstumpf", "name": "Pascal Stumpf" },
{ "nick": "pszemsza", "name": "Przemek Szałaj" },
{ "nick": "pyrolink", "name": "Andrew Decker" },
{ "nick": "quantumstate", "name": "Jonathan Waller" },
{ "nick": "QuickShot", "name": "Walter Krawec" },
{ "nick": "quonter" },
{ "nick": "qwertz" },
{ "nick": "Radagast" },
{ "nick": "Raj", "name": "Raj Sharma" },
{ "nick": "ramtzok1", "name": "Ram" },
{ "nick": "rapidelectron", "name": "Christian Weihsbach" },
{ "nick": "r-a-sattarov", "name": "Ramil Sattarov" },
{ "nick": "RedFox", "name": "Jorma Rebane" },
{ "nick": "RefinedCode" },
{ "nick": "Riemer" },
{ "nick": "Riesi" },
{ "name": "Rolf Sievers" },
{ "nick": "s0600204", "name": "Matthew Norwood" },
{ "nick": "sacha_vrand", "name": "Sacha Vrand" },
{ "nick": "SafaAlfulaij" },
{ "name": "Samuel Guarnieri" },
{ "nick": "Samulis", "name": "Sam Gossner" },
{ "nick": "Sandarac" },
{ "nick": "sanderd17", "name": "Sander Deryckere" },
{ "nick": "sathyam", "name": "Sathyam Vellal" },
{ "nick": "sbirmi", "name": "Sharad Birmiwal" },
{ "nick": "sbte", "name": "Sven Baars" },
{ "nick": "scroogie", "name": "André Gemünd" },
{ "nick": "scythetwirler", "name": "Casey X." },
{ "nick": "sera", "name": "Ralph Sennhauser" },
{ "nick": "serveurix" },
{ "nick": "Shane", "name": "Shane Grant" },
{ "nick": "shh" },
{ "nick": "Silk", "name": "Josh Godsiff" },
{ "nick": "silure" },
{ "nick": "Simikolon", "name": "Yannick & Simon" },
{ "nick": "smiley", "name": "M. L." },
{ "nick": "Spahbod", "name": "Omid Davoodi" },
{ "nick": "Stan", "name": "Stanislas Dolcini" },
{ "nick": "Stefan" },
{ "nick": "StefanBruens", "name": "Stefan Brüns" },
{ "nick": "stilz", "name": "Sławomir Zborowski" },
{ "nick": "stwf", "name": "Steven Fuchs" },
{ "nick": "svott", "name": "Sven Ott" },
{ "nick": "t4nk004" },
{ "nick": "tau" },
{ "nick": "tbm", "name": "Martin Michlmayr" },
{ "nick": "Teiresias" },
{ "nick": "temple" },
{ "nick": "texane" },
{ "nick": "thamlett", "name": "Timothy Hamlett" },
{ "nick": "thedrunkyak", "name": "Dan Fuhr" },
{ "nick": "Tobbi" },
{ "nick": "Toonijn", "name": "Toon Baeyens" },
{ "nick": "TrinityDeath", "name": "Jethro Lu" },
{ "nick": "triumvir", "name": "Corin Schedler" },
{ "nick": "trompetin17", "name": "Juan Guillermo" },
{ "nick": "tpearson", "name": "Timothy Pearson" },
{ "nick": "user1", "name": "A. C." },
{ "nick": "usey11" },
{ "nick": "Vantha"},
{ "nick": "vincent_c", "name": "Vincent Cheng" },
{ "nick": "vinhig", "name": "Vincent Higginson" },
{ "nick": "vladislavbelov", "name": "Vladislav Belov" },
{ "nick": "voroskoi" },
{ "nick": "vts", "name": "Jeroen DR" },
{ "nick": "wacko", "name": "Andrew Spiering" },
{ "nick": "WhiteTreePaladin", "name": "Brian Ashley" },
{ "nick": "wowgetoffyourcellphone", "name": "Justus Avramenko" },
{ "nick": "wraitii", "name": "Lancelot de Ferrière le Vayer" },
{ "nick": "Xentelian", "name": "Mark Strawson" },
{ "nick": "Xienen", "name": "Dayle Flowers" },
{ "nick": "xone47", "name": "Brent Johnson" },
{ "nick": "xtizer", "name": "Matt Green" },
{ "nick": "yashi", "name": "Yasushi Shoji" },
{ "nick": "Ykkrosh", "name": "Philip Taylor" },
{ "nick": "Yves" },
{ "nick": "z0rg", "name": "Sébastien Maire" },
{ "nick": "Zeusthor", "name": "Jeffrey Tavares" },
{ "nick": "zoot" },
{ "nick": "zsol", "name": "Zsolt Dollenstein" },
{ "nick": "ztamas", "name": "Tamas Zolnai" },
{ "nick": "Zyi", "name": "Charles De Meulenaer" }
]
}
]
}

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,14 @@ SetupWindowPages.GameSetupPage = class
let startGameButton = new StartGameButton(setupWindow);
let 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,

View File

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

View File

@ -0,0 +1,17 @@
class ClearSavegameButton
{
constructor(gameSettingsController)
{
const clearSavegameButton = Engine.GetGUIObjectByName("clearSavegameButton");
clearSavegameButton.onPress = () => {
g_GameSettings.savegame.value = null;
gameSettingsController.setNetworkInitAttributes();
};
if (g_IsNetworked && g_IsController)
{
g_GameSettings.savegame.watch(() => {
clearSavegameButton.hidden = g_GameSettings.savegame.value === null;
}, ["value"]);
}
}
}

View File

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

View File

@ -0,0 +1,38 @@
class LoadSavegameButton
{
constructor(gameSettingsController)
{
this.gameSettingsController = gameSettingsController;
const loadSavegameButton = Engine.GetGUIObjectByName("loadSavegameButton");
loadSavegameButton.onPress = this.loadSavegame.bind(this);
if (g_IsNetworked && g_IsController)
{
g_GameSettings.savegame.watch(() => {
loadSavegameButton.hidden = g_GameSettings.savegame.value !== null;
}, ["value"]);
}
else
loadSavegameButton.hidden = true;
}
async loadSavegame()
{
const gameId = await Engine.PushGuiPage("page_loadgame.xml");
// If no data is being provided, for instance if the cancel button is
// pressed
if (gameId === undefined)
return;
const metadata = Engine.LoadSavedGameMetadata(gameId);
// Remove the gaia entry.
metadata.initAttributes.settings.PlayerData.splice(0, 1);
g_GameSettings.fromInitAttributes(metadata.initAttributes);
g_GameSettings.savegame.value = gameId;
this.gameSettingsController.setNetworkInitAttributes();
}
}

View File

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

View File

@ -9,11 +9,25 @@ class ResetCivsButton
this.civResetButton.onPress = this.onPress.bind(this);
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()

View File

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

View File

@ -0,0 +1,13 @@
class SavegameLabel
{
constructor()
{
const savegameLabel = Engine.GetGUIObjectByName("savegameLabel");
if (g_IsNetworked && !g_IsController)
{
g_GameSettings.savegame.watch(() => {
savegameLabel.hidden = g_GameSettings.savegame.value === null;
}, ["value"]);
}
}
}

View File

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

View File

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

View File

@ -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,

View File

@ -38,6 +38,9 @@ class PersistentMatchSettings
*/
saveFile(settings)
{
for (const player of settings.settings.PlayerData)
delete player.Name;
Engine.ProfileStart("savePersistMatchSettingsFile");
Engine.WriteJSONFile(this.filename, {
"attributes": this.enabled ? settings : {},

View File

@ -84,6 +84,7 @@ CNetClient::CNetClient(CGame* game) :
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, &OnClientTimeout, this);
AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_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)
{
CGameSavedStartMessage gameSavedStart;
gameSavedStart.m_InitAttributes = initAttribs;
CompressZLib(savedState, gameSavedStart.m_SavedState, true);
SendMessage(&gameSavedStart);
}
void CNetClient::SendRejoinedMessage()
{
CRejoinedMessage rejoinedMessage;
@ -602,6 +613,26 @@ void CNetClient::SendAuthenticateMessage()
SendMessage(&authenticate);
}
void CNetClient::StartGame(const std::string& initAttributes, const std::string& savedState)
{
const auto foundPlayer = m_PlayerAssignments.find(m_GUID);
const i32 player{foundPlayer != m_PlayerAssignments.end() ? foundPlayer->second.m_PlayerID : -1};
m_ClientTurnManager = new CNetClientTurnManager{*m_Game->GetSimulation2(), *this,
static_cast<int>(m_HostID), m_Game->GetReplayLogger()};
// Parse init attributes.
const ScriptInterface& scriptInterface{m_Game->GetSimulation2()->GetScriptInterface()};
ScriptRequest rq{scriptInterface};
JS::RootedValue initAttribs{rq.cx};
Script::ParseJSON(rq, initAttributes, &initAttribs);
m_Game->SetPlayerID(player);
m_Game->StartGame(&initAttribs, savedState);
PushGuiMessage("type", "start", "initAttributes", initAttribs);
}
bool CNetClient::OnConnect(CNetClient* client, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE);
@ -755,30 +786,22 @@ bool CNetClient::OnGameStart(CNetClient* client, CFsmEvent* event)
ENSURE(event->GetType() == (uint)NMT_GAME_START);
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);
Script::ParseJSON(rq, message->m_InitAttributes, &initAttribs);
client->m_Game->SetPlayerID(player);
client->m_Game->StartGame(&initAttribs, "");
client->PushGuiMessage("type", "start",
"initAttributes", initAttribs);
client->StartGame(message->m_InitAttributes, "");
return true;
}
bool CNetClient::OnSavedGameStart(CNetClient* context, CFsmEvent* event)
{
ENSURE(event->GetType() == static_cast<uint>(NMT_SAVED_GAME_START));
std::string state;
CGameSavedStartMessage* message{static_cast<CGameSavedStartMessage*>(event->GetParamRef())};
DecompressZLib(message->m_SavedState, state, true);
context->StartGame(message->m_InitAttributes, state);
return true;
}
bool CNetClient::OnJoinSyncStart(CNetClient* client, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START);

View File

@ -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 std::string& initAttributes, const std::string& savedState);
/**
* Push a message onto the GUI queue listing the current player assignments.
*/

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2023 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
@ -45,14 +45,14 @@ u8* CNetMessage::Serialize(u8* pBuffer) const
{
size_t size = GetSerializedLength();
Serialize_int_1(pBuffer, m_Type);
Serialize_int_2(pBuffer, size);
Serialize_int_3(pBuffer, size);
return pBuffer;
}
const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
{
if (pStart + 3 > pEnd)
if (pStart + 4 > pEnd)
{
LOGERROR("CNetMessage: Corrupt packet (smaller than header)");
return NULL;
@ -63,7 +63,7 @@ const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
int type;
size_t size;
Deserialize_int_1(pBuffer, type);
Deserialize_int_2(pBuffer, size);
Deserialize_int_3(pBuffer, size);
m_Type = (NetMessageType)type;
if (pStart + size != pEnd)
@ -78,7 +78,7 @@ const u8* CNetMessage::Deserialize(const u8* pStart, const u8* pEnd)
size_t CNetMessage::GetSerializedLength() const
{
// By default, return header size
return 3;
return 4;
}
CStr CNetMessage::ToString() const
@ -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;

View File

@ -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
@ -217,6 +218,11 @@ 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)
NMT_FIELD(CStr, m_SavedState)
END_NMT_CLASS()
START_NMT_CLASS_(EndCommandBatch, NMT_END_COMMAND_BATCH)
NMT_FIELD_INT(m_Turn, u32, 4)
NMT_FIELD_INT(m_TurnLength, u32, 2)

View File

@ -661,6 +661,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);
@ -1315,6 +1316,19 @@ 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());
server.StartSavedGame(message->m_InitAttributes, message->m_SavedState);
return true;
}
bool CNetServerWorker::OnLoadedGame(CNetServerSession* loadedSession, CFsmEvent* event)
{
ENSURE(event->GetType() == (uint)NMT_LOADED_GAME);
@ -1499,7 +1513,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)
@ -1530,12 +1544,27 @@ 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, const CStr& savedState)
{
PreStartGame(initAttribs);
CGameSavedStartMessage gameSavedStart;
gameSavedStart.m_InitAttributes = initAttribs;
gameSavedStart.m_SavedState = savedState;
Broadcast(&gameSavedStart, { NSS_PREGAME });
}
CStrW CNetServerWorker::SanitisePlayerName(const CStrW& original)
{
const size_t MAX_LENGTH = 32;

View File

@ -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, const CStr& savedData);
/**
* 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);

View File

@ -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)

View File

@ -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)

View File

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

View File

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