diff --git a/binaries/data/mods/public/gamesettings/GameSettings.js b/binaries/data/mods/public/gamesettings/GameSettings.js index c6f432a113..ff0113a9f4 100644 --- a/binaries/data/mods/public/gamesettings/GameSettings.js +++ b/binaries/data/mods/public/gamesettings/GameSettings.js @@ -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 diff --git a/binaries/data/mods/public/gamesettings/attributes/Savegame.js b/binaries/data/mods/public/gamesettings/attributes/Savegame.js new file mode 100644 index 0000000000..8f91c1c3f5 --- /dev/null +++ b/binaries/data/mods/public/gamesettings/attributes/Savegame.js @@ -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; + } +}; diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json b/binaries/data/mods/public/gui/credits/texts/programming.json index 91941ab72d..95ab09ea60 100644 --- a/binaries/data/mods/public/gui/credits/texts/programming.json +++ b/binaries/data/mods/public/gui/credits/texts/programming.json @@ -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" }, diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json.orig b/binaries/data/mods/public/gui/credits/texts/programming.json.orig new file mode 100644 index 0000000000..91941ab72d --- /dev/null +++ b/binaries/data/mods/public/gui/credits/texts/programming.json.orig @@ -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" } + ] + } + ] +} diff --git a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js index 771fab92e5..4096bbe238 100644 --- a/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js +++ b/binaries/data/mods/public/gui/gamesetup/Controllers/PlayerAssignmentsController.js @@ -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()) diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js index c3d1ab862e..9f558ca7f0 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIConfigPage.js @@ -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; } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js index 6a9bb19cbf..4c64ff316e 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/AIConfigPage/AIGameSettingControl.js @@ -1,8 +1,8 @@ class AIGameSettingControlDropdown extends GameSettingControlDropdown { - onOpenPage(playerIndex) + onOpenPage(playerIndex, enabled) { - this.setEnabled(true); + this.setEnabled(enabled); this.playerIndex = playerIndex; this.render(); } diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js index f87111114b..c30f1c1f23 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/GameSettingControl.js @@ -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) diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js index b7f7841982..3f611190cc 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/AIConfigButton.js @@ -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); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js index 4afe2ad703..191b64fa4a 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerAssignment.js @@ -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); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js index 561c692117..3bbfd45fc6 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerCiv.js @@ -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 + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js index a0b4e8fd5f..26286b79fd 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerColor.js @@ -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 + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js index e7e865cd37..b09e6abc6f 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/Dropdowns/PlayerTeam.js @@ -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 + "]"); diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js index fc584e655a..830e5f0439 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/PerPlayer/PlayerName.js @@ -13,6 +13,11 @@ PlayerSettingControls.PlayerName = class PlayerName extends GameSettingControl this.render(); } + onSavegameChanged() + { + this.onPlayerAssignmentsChange(); + } + onPlayerAssignmentsChange() { this.guid = undefined; diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js index ddc90c5176..52cde43016 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSettings/Single/Buttons/MapBrowser.js @@ -25,7 +25,7 @@ GameSettingControls.MapBrowser = class MapBrowser extends GameSettingControlButt onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(this.enabled); } }; diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js index 5c4560161e..308d00afdc 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.js @@ -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, diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml index 7b525684da..43d2a2f29d 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/GameSetupPage.xml @@ -67,23 +67,29 @@ - + - + - + + + + + + + - + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js new file mode 100644 index 0000000000..8b9676318d --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.js @@ -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"]); + } + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml new file mode 100644 index 0000000000..33a42001b3 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ClearSavegameButton.xml @@ -0,0 +1,14 @@ + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js new file mode 100644 index 0000000000..60a9c637ad --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.js @@ -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(); + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml new file mode 100644 index 0000000000..595358eb5b --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/LoadSavegameButton.xml @@ -0,0 +1,13 @@ + + + + Load Save + + + Load a previously created Savegame. You will still have to press start after having loaded the game data. + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js index 996bd36ab8..cb253ac5b5 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetCivsButton.js @@ -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() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js index 77086a5272..2669be6189 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/ResetTeamsButton.js @@ -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() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js new file mode 100644 index 0000000000..3460b3c97e --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.js @@ -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"]); + } + } +} diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml new file mode 100644 index 0000000000..2cc167e3c4 --- /dev/null +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/Buttons/SavegameLabel.xml @@ -0,0 +1,14 @@ + + diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js index 169bc5fef6..3438e9891c 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/GameSetupPage/Panels/MapPreview.js @@ -17,7 +17,7 @@ class MapPreview onPress() { - this.setupWindow.pages.MapBrowserPage.openPage(); + this.setupWindow.pages.MapBrowserPage.openPage(g_GameSettings.savegame.value === null); } renderName() diff --git a/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js b/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js index dd34f5c0bf..3347c729e0 100644 --- a/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js +++ b/binaries/data/mods/public/gui/gamesetup/Pages/MapBrowserPage/MapBrowserPage.js @@ -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, diff --git a/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js b/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js index 68f96b4112..262e1273c4 100644 --- a/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js +++ b/binaries/data/mods/public/gui/gamesetup/Persistence/PersistentMatchSettings.js @@ -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 : {}, diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 6c9da4adf8..eb68433e0d 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -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(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(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(NMT_SAVED_GAME_START)); + std::string state; + CGameSavedStartMessage* message{static_cast(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); diff --git a/source/network/NetClient.h b/source/network/NetClient.h index 41671d9b24..9bacd2603e 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -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. */ diff --git a/source/network/NetMessage.cpp b/source/network/NetMessage.cpp index 9515d939de..de31cb6cb1 100644 --- a/source/network/NetMessage.cpp +++ b/source/network/NetMessage.cpp @@ -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; diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index f2259d0a7e..370858cdbd 100644 --- a/source/network/NetMessages.h +++ b/source/network/NetMessages.h @@ -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) diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 7f22be1a1b..04ed264b4f 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -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(NMT_SAVED_GAME_START)); + CNetServerWorker& server{session->GetServer()}; + + if (session->GetGUID() != server.m_ControllerGUID) + return true; + + CGameSavedStartMessage* message = static_cast(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& 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; diff --git a/source/network/NetServer.h b/source/network/NetServer.h index d571ab8444..f23a5e7625 100644 --- a/source/network/NetServer.h +++ b/source/network/NetServer.h @@ -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); diff --git a/source/network/scripting/JSInterface_Network.cpp b/source/network/scripting/JSInterface_Network.cpp index f3ee514cdb..13b27cb615 100644 --- a/source/network/scripting/JSInterface_Network.cpp +++ b/source/network/scripting/JSInterface_Network.cpp @@ -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 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) diff --git a/source/ps/SavedGame.cpp b/source/ps/SavedGame.cpp index 5e3135200e..2a1cda1d1c 100644 --- a/source/ps/SavedGame.cpp +++ b/source/ps/SavedGame.cpp @@ -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::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(&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) diff --git a/source/ps/SavedGame.h b/source/ps/SavedGame.h index 63360ccc37..8ce0f2783c 100644 --- a/source/ps/SavedGame.h +++ b/source/ps/SavedGame.h @@ -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 + 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 Load(const ScriptInterface& scriptInterface, const std::wstring& name); /** * Get list of saved games for GUI script usage diff --git a/source/ps/scripting/JSInterface_SavedGame.cpp b/source/ps/scripting/JSInterface_SavedGame.cpp index e02a1998f7..b0f209f6e1 100644 --- a/source/ps/scripting/JSInterface_SavedGame.cpp +++ b/source/ps/scripting/JSInterface_SavedGame.cpp @@ -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 + 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 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 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"); } }