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:
parent
9ac60514c3
commit
fee875066f
@ -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
|
||||
|
@ -0,0 +1,16 @@
|
||||
GameSettings.prototype.Attributes.Savegame = class extends GameSetting
|
||||
{
|
||||
value = null;
|
||||
|
||||
toInitAttributes(attribs)
|
||||
{
|
||||
if (this.value !== null)
|
||||
attribs.settings.Savegame = this.value;
|
||||
}
|
||||
|
||||
fromInitAttributes(attribs)
|
||||
{
|
||||
const newValue = this.getLegacySetting(attribs, "Savegame");
|
||||
this.value = newValue ?? null;
|
||||
}
|
||||
};
|
@ -189,6 +189,7 @@
|
||||
{ "nick": "MattDoerksen", "name": "Matt Doerksen" },
|
||||
{ "nick": "mattlott", "name": "Matt Lott" },
|
||||
{ "nick": "maveric", "name": "Anton Protko" },
|
||||
{ "nick": "mbusy", "name": "Maxime Busy" },
|
||||
{ "nick": "Micnasty", "name": "Travis Gorkin" },
|
||||
{ "name": "Mikołaj \"Bajter\" Korcz" },
|
||||
{ "nick": "mimo" },
|
||||
|
@ -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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@ -95,8 +95,11 @@ class PlayerAssignmentsController
|
||||
*/
|
||||
onClientJoin(newGUID, newAssignments)
|
||||
{
|
||||
if (!g_IsController || newAssignments[newGUID].player != -1)
|
||||
if (!g_IsController || newAssignments[newGUID].player !== -1 ||
|
||||
g_GameSettings.savegame.value !== null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Assign the client (or only buddies if prefered) to a free slot
|
||||
if (newGUID != Engine.GetPlayerGUID())
|
||||
|
@ -38,12 +38,12 @@ SetupWindowPages.AIConfigPage = class
|
||||
return this.row++;
|
||||
}
|
||||
|
||||
openPage(playerIndex)
|
||||
openPage(playerIndex, enabled)
|
||||
{
|
||||
this.playerIndex = playerIndex;
|
||||
|
||||
for (let handler of this.openPageHandlers)
|
||||
handler(playerIndex);
|
||||
handler(playerIndex, enabled);
|
||||
|
||||
this.aiConfigPage.hidden = false;
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
class AIGameSettingControlDropdown extends GameSettingControlDropdown
|
||||
{
|
||||
onOpenPage(playerIndex)
|
||||
onOpenPage(playerIndex, enabled)
|
||||
{
|
||||
this.setEnabled(true);
|
||||
this.setEnabled(enabled);
|
||||
this.playerIndex = playerIndex;
|
||||
this.render();
|
||||
}
|
||||
|
@ -55,6 +55,11 @@ class GameSettingControl /* extends Profilable /* Uncomment to profile controls
|
||||
|
||||
if (this.onPlayerAssignmentsChange)
|
||||
this.playerAssignmentsController.registerPlayerAssignmentsChangeHandler(this.onPlayerAssignmentsChange.bind(this));
|
||||
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
const isSavegame = g_GameSettings.savegame.value !== null;
|
||||
this.setEnabled(this.onSavegameChanged?.(isSavegame) ?? !isSavegame);
|
||||
}, ["value"]);
|
||||
}
|
||||
|
||||
setTitle(titleCaption)
|
||||
|
@ -5,6 +5,9 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC
|
||||
super(...args);
|
||||
|
||||
this.aiConfigButton = Engine.GetGUIObjectByName("aiConfigButton[" + this.playerIndex + "]");
|
||||
this.aiConfigButton.onPress = () => {
|
||||
this.setupWindow.pages.AIConfigPage.openPage(this.playerIndex, this.enabled);
|
||||
};
|
||||
|
||||
g_GameSettings.playerAI.watch(() => this.render(), ["values"]);
|
||||
// Save little performance by not reallocating every call
|
||||
@ -12,12 +15,6 @@ PlayerSettingControls.AIConfigButton = class AIConfigButton extends GameSettingC
|
||||
this.render();
|
||||
}
|
||||
|
||||
onLoad()
|
||||
{
|
||||
let aiConfigPage = this.setupWindow.pages.AIConfigPage;
|
||||
this.aiConfigButton.onPress = aiConfigPage.openPage.bind(aiConfigPage, this.playerIndex);
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
this.aiConfigButton.hidden = !g_GameSettings.playerAI.get(this.playerIndex);
|
||||
|
@ -34,6 +34,19 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
|
||||
g_GameSettings.playerCount.watch((_, oldNb) => this.OnPlayerNbChange(oldNb), ["nbPlayers"]);
|
||||
}
|
||||
|
||||
onSavegameChanged(isSavegame)
|
||||
{
|
||||
const savedAI = isSavegame && g_GameSettings.playerAI.get(this.playerIndex);
|
||||
|
||||
if (savedAI)
|
||||
this.setSelectedValue(savedAI.bot);
|
||||
else
|
||||
this.rebuildList();
|
||||
|
||||
// If loading a savegame, AIs have to stay.
|
||||
return !savedAI;
|
||||
}
|
||||
|
||||
setControl()
|
||||
{
|
||||
this.dropdown = Engine.GetGUIObjectByName("playerAssignment[" + this.playerIndex + "]");
|
||||
@ -103,9 +116,14 @@ PlayerSettingControls.PlayerAssignment = class PlayerAssignment extends GameSett
|
||||
// TODO: this particular bit is done for each row, which is unnecessarily inefficient.
|
||||
this.playerItems = sortGUIDsByPlayerID().map(
|
||||
this.clientItemFactory.createItem.bind(this.clientItemFactory));
|
||||
|
||||
// If loading a savegame clients and unassigned players can't be replaced by a AI. Don't show
|
||||
// the AIs in the dropdown.
|
||||
const AIsSelectable = g_GameSettings.savegame.value === null ||
|
||||
g_GameSettings.playerAI.get(this.playerIndex);
|
||||
this.values = prepareForDropdown([
|
||||
...this.playerItems,
|
||||
...this.aiItems,
|
||||
...AIsSelectable ? this.aiItems : [],
|
||||
this.unassignedItem
|
||||
]);
|
||||
|
||||
@ -161,7 +179,8 @@ PlayerSettingControls.PlayerAssignment.prototype.AutocompleteOrder = 100;
|
||||
if (ai)
|
||||
g_GameSettings.playerAI.swap(sourcePlayer, playerIndex);
|
||||
// Swap color + civ as well - this allows easy reorganizing of player order.
|
||||
if (g_GameSettings.map.type !== "scenario")
|
||||
if (g_GameSettings.savegame.value === null &&
|
||||
g_GameSettings.map.type !== "scenario")
|
||||
{
|
||||
g_GameSettings.playerCiv.swap(sourcePlayer, playerIndex);
|
||||
g_GameSettings.playerColor.swap(sourcePlayer, playerIndex);
|
||||
|
@ -14,6 +14,11 @@ PlayerSettingControls.PlayerCiv = class PlayerCiv extends GameSettingControlDrop
|
||||
this.rebuild();
|
||||
}
|
||||
|
||||
onSavegameChanged()
|
||||
{
|
||||
return !g_GameSettings.playerCiv.locked[this.playerIndex] && null;
|
||||
}
|
||||
|
||||
setControl()
|
||||
{
|
||||
this.label = Engine.GetGUIObjectByName("playerCivText[" + this.playerIndex + "]");
|
||||
|
@ -8,6 +8,11 @@ PlayerSettingControls.PlayerColor = class PlayerColor extends GameSettingControl
|
||||
this.render();
|
||||
}
|
||||
|
||||
onSavegameChanged()
|
||||
{
|
||||
return g_GameSettings.map.type !== "scenario" && null;
|
||||
}
|
||||
|
||||
setControl()
|
||||
{
|
||||
this.dropdown = Engine.GetGUIObjectByName("playerColor[" + this.playerIndex + "]");
|
||||
|
@ -24,6 +24,11 @@ PlayerSettingControls.PlayerTeam = class PlayerTeam extends GameSettingControlDr
|
||||
this.render();
|
||||
}
|
||||
|
||||
onSavegameChanged()
|
||||
{
|
||||
return g_GameSettings.map.type !== "scenario" && null;
|
||||
}
|
||||
|
||||
setControl()
|
||||
{
|
||||
this.label = Engine.GetGUIObjectByName("playerTeamText[" + this.playerIndex + "]");
|
||||
|
@ -13,6 +13,11 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl
|
||||
this.render();
|
||||
}
|
||||
|
||||
onSavegameChanged()
|
||||
{
|
||||
this.onPlayerAssignmentsChange();
|
||||
}
|
||||
|
||||
onPlayerAssignmentsChange()
|
||||
{
|
||||
this.guid = undefined;
|
||||
|
@ -25,7 +25,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt
|
||||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(this.enabled);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -15,9 +15,14 @@ SetupWindowPages.GameSetupPage = class
|
||||
let startGameButton = new StartGameButton(setupWindow);
|
||||
let readyButton = new ReadyButton(setupWindow);
|
||||
this.panelButtons = {
|
||||
"cancelButton": new CancelButton(setupWindow, startGameButton, readyButton),
|
||||
"civInfoButton": new CivInfoButton(),
|
||||
"lobbyButton": new LobbyButton(),
|
||||
"clearSavegameButton": new ClearSavegameButton(
|
||||
setupWindow.controls.gameSettingsController),
|
||||
"loadSavegameButton": new LoadSavegameButton(
|
||||
setupWindow.controls.gameSettingsController),
|
||||
"savegameLabel": new SavegameLabel(),
|
||||
"cancelButton": new CancelButton(setupWindow, startGameButton, readyButton),
|
||||
"readyButton": readyButton,
|
||||
"startGameButton": startGameButton
|
||||
};
|
||||
@ -31,7 +36,7 @@ SetupWindowPages.GameSetupPage = class
|
||||
|
||||
this.panels = {
|
||||
"chatPanel": new ChatPanel(setupWindow, this.gameSettingControlManager, gameSettingsPanel),
|
||||
"gameSettingWarning": new GameSettingWarning(setupWindow, this.panelButtons.cancelButton),
|
||||
"gameSettingWarning": new GameSettingWarning(setupWindow),
|
||||
"gameDescription": new GameDescription(setupWindow, gameSettingTabs),
|
||||
"gameSettingsPanel": gameSettingsPanel,
|
||||
"gameSettingsTabs": gameSettingTabs,
|
||||
|
@ -67,23 +67,29 @@
|
||||
|
||||
<object name="bottomLeftPanel">
|
||||
|
||||
<object size="20 100%-32 100%-430 100%">
|
||||
<object size="20 100%-32 100%-698 100%">
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Tooltip.xml"/>
|
||||
</object>
|
||||
|
||||
<object size="0 100%-28 100%-320 100%">
|
||||
<object size="0 100%-28 100%-470 100%">
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/GameSettingWarning.xml"/>
|
||||
</object>
|
||||
|
||||
</object>
|
||||
|
||||
<object name="bottomRightPanel" size="100%-314 100%-28 100% 100%">
|
||||
<object name="bottomRightPanel" size="100%-464 100%-28 100% 100%">
|
||||
|
||||
<object size="0 0 140 100%">
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml"/>
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml"/>
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml"/>
|
||||
</object>
|
||||
|
||||
<object size="150 0 290 100%">
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/CancelButton.xml"/>
|
||||
</object>
|
||||
|
||||
<object size="150 0 290 100%">
|
||||
<object size="300 0 440 100%">
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ReadyButton.xml"/>
|
||||
<include file="gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/StartGameButton.xml"/>
|
||||
</object>
|
||||
|
@ -0,0 +1,17 @@
|
||||
class ClearSavegameButton
|
||||
{
|
||||
constructor(gameSettingsController)
|
||||
{
|
||||
const clearSavegameButton = Engine.GetGUIObjectByName("clearSavegameButton");
|
||||
clearSavegameButton.onPress = () => {
|
||||
g_GameSettings.savegame.value = null;
|
||||
gameSettingsController.setNetworkInitAttributes();
|
||||
};
|
||||
if (g_IsNetworked && g_IsController)
|
||||
{
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
clearSavegameButton.hidden = g_GameSettings.savegame.value === null;
|
||||
}, ["value"]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object
|
||||
name="clearSavegameButton"
|
||||
type="button"
|
||||
style="StoneButton"
|
||||
tooltip_style="onscreenToolTip"
|
||||
hidden="true">
|
||||
|
||||
<translatableAttribute id="caption">Clear Save</translatableAttribute>
|
||||
|
||||
<translatableAttribute id="tooltip">
|
||||
Discard the Savegame data and start a new match instead.
|
||||
</translatableAttribute>
|
||||
</object>
|
@ -0,0 +1,38 @@
|
||||
class LoadSavegameButton
|
||||
{
|
||||
constructor(gameSettingsController)
|
||||
{
|
||||
this.gameSettingsController = gameSettingsController;
|
||||
|
||||
const loadSavegameButton = Engine.GetGUIObjectByName("loadSavegameButton");
|
||||
loadSavegameButton.onPress = this.loadSavegame.bind(this);
|
||||
|
||||
if (g_IsNetworked && g_IsController)
|
||||
{
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
loadSavegameButton.hidden = g_GameSettings.savegame.value !== null;
|
||||
}, ["value"]);
|
||||
}
|
||||
else
|
||||
loadSavegameButton.hidden = true;
|
||||
}
|
||||
|
||||
async loadSavegame()
|
||||
{
|
||||
const gameId = await Engine.PushGuiPage("page_loadgame.xml");
|
||||
|
||||
// If no data is being provided, for instance if the cancel button is
|
||||
// pressed
|
||||
if (gameId === undefined)
|
||||
return;
|
||||
|
||||
const metadata = Engine.LoadSavedGameMetadata(gameId);
|
||||
|
||||
// Remove the gaia entry.
|
||||
metadata.initAttributes.settings.PlayerData.splice(0, 1);
|
||||
|
||||
g_GameSettings.fromInitAttributes(metadata.initAttributes);
|
||||
g_GameSettings.savegame.value = gameId;
|
||||
this.gameSettingsController.setNetworkInitAttributes();
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object
|
||||
name="loadSavegameButton"
|
||||
type="button"
|
||||
style="StoneButton"
|
||||
tooltip_style="onscreenToolTip">
|
||||
|
||||
<translatableAttribute id="caption">Load Save</translatableAttribute>
|
||||
|
||||
<translatableAttribute id="tooltip">
|
||||
Load a previously created Savegame. You will still have to press start after having loaded the game data.
|
||||
</translatableAttribute>
|
||||
</object>
|
@ -9,11 +9,25 @@ class ResetCivsButton
|
||||
this.civResetButton.onPress = this.onPress.bind(this);
|
||||
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
const isSavegame = g_GameSettings.savegame.value !== null;
|
||||
this.setEnabled(this.shouldBeEnabled() && !isSavegame);
|
||||
}, ["value"]);
|
||||
}
|
||||
|
||||
shouldBeEnabled()
|
||||
{
|
||||
return g_GameSettings.map.type !== "scenario" && g_IsController;
|
||||
}
|
||||
|
||||
setEnabled(enabled)
|
||||
{
|
||||
this.civResetButton.hidden = !enabled;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
this.civResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController;
|
||||
this.setEnabled(this.shouldBeEnabled());
|
||||
}
|
||||
|
||||
onPress()
|
||||
|
@ -9,11 +9,25 @@ class ResetTeamsButton
|
||||
this.teamResetButton.onPress = this.onPress.bind(this);
|
||||
|
||||
g_GameSettings.map.watch(() => this.render(), ["type"]);
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
const isSavegame = g_GameSettings.savegame.value !== null;
|
||||
this.setEnabled(this.shouldBeEnabled() && !isSavegame);
|
||||
}, ["value"]);
|
||||
}
|
||||
|
||||
shouldBeEnabled()
|
||||
{
|
||||
return g_GameSettings.map.type !== "scenario" && g_IsController;
|
||||
}
|
||||
|
||||
setEnabled(enabled)
|
||||
{
|
||||
this.teamResetButton.hidden = !enabled;
|
||||
}
|
||||
|
||||
render()
|
||||
{
|
||||
this.teamResetButton.hidden = g_GameSettings.map.type == "scenario" || !g_IsController;
|
||||
this.setEnabled(this.shouldBeEnabled());
|
||||
}
|
||||
|
||||
onPress()
|
||||
|
@ -0,0 +1,13 @@
|
||||
class SavegameLabel
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
const savegameLabel = Engine.GetGUIObjectByName("savegameLabel");
|
||||
if (g_IsNetworked && !g_IsController)
|
||||
{
|
||||
g_GameSettings.savegame.watch(() => {
|
||||
savegameLabel.hidden = g_GameSettings.savegame.value === null;
|
||||
}, ["value"]);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<object
|
||||
name="savegameLabel"
|
||||
type="text"
|
||||
style="ModernLabelText"
|
||||
tooltip_style="onscreenToolTip"
|
||||
hidden="true">
|
||||
|
||||
<translatableAttribute id="caption">Savegame</translatableAttribute>
|
||||
|
||||
<translatableAttribute id="tooltip">
|
||||
The controler loaded a savegame.
|
||||
</translatableAttribute>
|
||||
</object>
|
@ -17,7 +17,7 @@ class MapPreview
|
||||
|
||||
onPress()
|
||||
{
|
||||
this.setupWindow.pages.MapBrowserPage.openPage();
|
||||
this.setupWindow.pages.MapBrowserPage.openPage(g_GameSettings.savegame.value === null);
|
||||
}
|
||||
|
||||
renderName()
|
||||
|
@ -25,9 +25,9 @@ SetupWindowPages.MapBrowserPage = class extends MapBrowser
|
||||
this.gameSettingsController.setNetworkInitAttributes();
|
||||
}
|
||||
|
||||
openPage()
|
||||
openPage(enabled)
|
||||
{
|
||||
super.openPage(g_IsController);
|
||||
super.openPage(g_IsController && enabled);
|
||||
|
||||
this.controls.MapFiltering.select(
|
||||
this.gameSettingsController.guiData.mapFilter.filter,
|
||||
|
@ -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 : {},
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2022 Wildfire Games.
|
||||
/* Copyright (C) 2024 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -33,6 +33,7 @@
|
||||
#include "ps/GUID.h"
|
||||
#include "ps/Hashing.h"
|
||||
#include "ps/Pyrogenesis.h"
|
||||
#include "ps/SavedGame.h"
|
||||
#include "ps/Util.h"
|
||||
#include "scriptinterface/FunctionWrapper.h"
|
||||
#include "scriptinterface/StructuredClone.h"
|
||||
@ -260,10 +261,33 @@ void StartNetworkGame(const ScriptInterface& scriptInterface, JS::HandleValue at
|
||||
{
|
||||
ENSURE(g_NetClient);
|
||||
|
||||
// TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason).
|
||||
ScriptRequest rq(scriptInterface);
|
||||
|
||||
JS::RootedValue settings{rq.cx};
|
||||
if (!Script::GetProperty(rq, attribs1, "settings", &settings))
|
||||
{
|
||||
ScriptException::Raise(rq, "The initAttributes didn't contain a \"settings\" property.");
|
||||
return;
|
||||
}
|
||||
|
||||
JS::RootedValue attribs(rq.cx, attribs1);
|
||||
g_NetClient->SendStartGameMessage(Script::StringifyJSON(rq, &attribs));
|
||||
std::string attributesAsString{Script::StringifyJSON(rq, &attribs)};
|
||||
|
||||
std::wstring savegameID;
|
||||
if (Script::HasProperty(rq, settings, "Savegame") &&
|
||||
Script::GetProperty(rq, settings, "Savegame", savegameID))
|
||||
{
|
||||
const std::optional<SavedGames::LoadResult> loadResult{
|
||||
SavedGames::Load(scriptInterface, savegameID)};
|
||||
if (!loadResult)
|
||||
{
|
||||
ScriptException::Raise(rq, "Failed to load the saved game: \"%ls\"", savegameID.c_str());
|
||||
return;
|
||||
}
|
||||
g_NetClient->SendStartSavedGameMessage(attributesAsString, loadResult->savedState);
|
||||
return;
|
||||
}
|
||||
g_NetClient->SendStartGameMessage(attributesAsString);
|
||||
}
|
||||
|
||||
void SetTurnLength(int length)
|
||||
|
@ -204,7 +204,8 @@ private:
|
||||
std::string* m_SavedState;
|
||||
};
|
||||
|
||||
Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState)
|
||||
std::optional<SavedGames::LoadResult> SavedGames::Load(const ScriptInterface& scriptInterface,
|
||||
const std::wstring& name)
|
||||
{
|
||||
// Determine the filename to load
|
||||
const VfsPath basename(L"saves/" + name);
|
||||
@ -212,20 +213,41 @@ Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptI
|
||||
|
||||
// Don't crash just because file isn't found, this can happen if the file is deleted from the OS
|
||||
if (!VfsFileExists(filename))
|
||||
return ERR::FILE_NOT_FOUND;
|
||||
return std::nullopt;
|
||||
|
||||
OsPath realPath;
|
||||
WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath));
|
||||
{
|
||||
const Status status{g_VFS->GetRealPath(filename, realPath)};
|
||||
if (status < 0)
|
||||
{
|
||||
DEBUG_WARN_ERR(status);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath);
|
||||
if (!archiveReader)
|
||||
WARN_RETURN(ERR::FAIL);
|
||||
{
|
||||
DEBUG_WARN_ERR(ERR::FAIL);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string savedState;
|
||||
CGameLoader loader(scriptInterface, &savedState);
|
||||
WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader));
|
||||
metadata.set(loader.GetMetadata());
|
||||
{
|
||||
const Status status{archiveReader->ReadEntries(CGameLoader::ReadEntryCallback,
|
||||
reinterpret_cast<uintptr_t>(&loader))};
|
||||
if (status < 0)
|
||||
{
|
||||
DEBUG_WARN_ERR(status);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
const ScriptRequest rq{scriptInterface};
|
||||
JS::RootedValue metadata{rq.cx, loader.GetMetadata()};
|
||||
|
||||
return INFO::OK;
|
||||
// `std::make_optional` can't be used since `LoadResult` doesn't have a constructor.
|
||||
return {{metadata, std::move(savedState)}};
|
||||
}
|
||||
|
||||
JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface)
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2024 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -19,8 +19,11 @@
|
||||
#define INCLUDED_SAVEDGAME
|
||||
|
||||
#include "ps/CStr.h"
|
||||
#include "scriptinterface/ScriptTypes.h"
|
||||
#include "scriptinterface/StructuredClone.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
class CSimulation2;
|
||||
|
||||
/**
|
||||
@ -59,18 +62,24 @@ namespace SavedGames
|
||||
*/
|
||||
Status SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const Script::StructuredClone& guiMetadataClone);
|
||||
|
||||
struct LoadResult
|
||||
{
|
||||
// Object containing metadata associated with saved game,
|
||||
// parsed from metadata.json inside the archive.
|
||||
JS::Value metadata;
|
||||
// Serialized simulation state stored as string of bytes,
|
||||
// loaded from simulation.dat inside the archive.
|
||||
std::string savedState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load saved game archive with the given name
|
||||
*
|
||||
* @param name filename of saved game (without path or extension)
|
||||
* @param scriptInterface
|
||||
* @param[out] metadata object containing metadata associated with saved game,
|
||||
* parsed from metadata.json inside the archive.
|
||||
* @param[out] savedState serialized simulation state stored as string of bytes,
|
||||
* loaded from simulation.dat inside the archive.
|
||||
* @return INFO::OK if successfully loaded, else an error Status
|
||||
* @return An empty `std::optional` if an error ocoured.
|
||||
*/
|
||||
Status Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState);
|
||||
std::optional<LoadResult> Load(const ScriptInterface& scriptInterface, const std::wstring& name);
|
||||
|
||||
/**
|
||||
* Get list of saved games for GUI script usage
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
/* Copyright (C) 2024 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -29,6 +29,8 @@
|
||||
#include "simulation2/Simulation2.h"
|
||||
#include "simulation2/system/TurnManager.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace JSI_SavedGame
|
||||
{
|
||||
JS::Value GetSavedGames(const ScriptInterface& scriptInterface)
|
||||
@ -75,6 +77,13 @@ void QuickLoad()
|
||||
LOGERROR("Can't load quicksave if game is not running!");
|
||||
}
|
||||
|
||||
JS::Value LoadSavedGameMetadata(const ScriptInterface& scriptInterface, const std::wstring& name)
|
||||
{
|
||||
std::optional<SavedGames::LoadResult> data{SavedGames::Load(scriptInterface, name)};
|
||||
|
||||
return !data ? JS::UndefinedValue() : data->metadata;
|
||||
}
|
||||
|
||||
JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstring& name)
|
||||
{
|
||||
// We need to be careful with different compartments and contexts.
|
||||
@ -88,13 +97,12 @@ JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstr
|
||||
|
||||
ENSURE(!g_Game);
|
||||
|
||||
// Load the saved game data from disk
|
||||
JS::RootedValue guiContextMetadata(rqGui.cx);
|
||||
std::string savedState;
|
||||
Status err = SavedGames::Load(name, scriptInterface, &guiContextMetadata, savedState);
|
||||
if (err < 0)
|
||||
std::optional<SavedGames::LoadResult> data{SavedGames::Load(scriptInterface, name)};
|
||||
if (!data)
|
||||
return JS::UndefinedValue();
|
||||
|
||||
JS::RootedValue guiContextMetadata{rqGui.cx, data->metadata};
|
||||
|
||||
g_Game = new CGame(true);
|
||||
|
||||
{
|
||||
@ -109,7 +117,7 @@ JS::Value StartSavedGame(const ScriptInterface& scriptInterface, const std::wstr
|
||||
Script::GetProperty(rqGame, gameContextMetadata, "playerID", playerID);
|
||||
|
||||
g_Game->SetPlayerID(playerID);
|
||||
g_Game->StartGame(&gameInitAttributes, savedState);
|
||||
g_Game->StartGame(&gameInitAttributes, data->savedState);
|
||||
}
|
||||
|
||||
return guiContextMetadata;
|
||||
@ -131,6 +139,7 @@ void RegisterScriptFunctions(const ScriptRequest& rq)
|
||||
ScriptFunction::Register<&QuickSave>(rq, "QuickSave");
|
||||
ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad");
|
||||
ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest");
|
||||
ScriptFunction::Register<&LoadSavedGameMetadata>(rq, "LoadSavedGameMetadata");
|
||||
ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame");
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user