Index: ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 18202) +++ ps/trunk/binaries/data/mods/public/gui/credits/texts/programming.json (revision 18203) @@ -1,206 +1,207 @@ { "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": "Ykkrosh", "name": "Philip Taylor"}] }, { "List": [ {"nick": "01d55"}, {"nick": "Acumen", "name": "Stuart Walpole"}, {"name": "Adrian Fatol"}, {"nick": "AI-Amsterdam"}, {"nick": "Alan", "name": "Alan Kemp"}, {"nick": "aBothe", "name": "Alexander Bothe"}, {"nick": "alpha123", "name": "Peter P. Cannici"}, {"nick": "Aurium", "name": "Aurélio Heckert"}, {"nick": "badmadblacksad", "name": "Martin F"}, {"name": "Mikołaj \"Bajter\" Korcz"}, {"nick": "bb", "name": "Bouke Jansen"}, {"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": "Caius", "name": "Lars Kemmann"}, {"nick": "Calefaction", "name": "Matt Holmes"}, {"nick": "Calvinh", "name": "Carl-Johan Höiby"}, {"name": "Cédric Houbart"}, {"nick": "Chakakhan", "name": "Kenny Long"}, {"nick": "Clockwork-Muse", "name": "Stephen A. Imhoff"}, {"nick": "Cracker78", "name": "Chad Heim"}, {"nick": "Crynux", "name": "Stephen J. Fewer"}, {"nick": "cwprogger"}, {"nick": "Dak Lozar", "name": "Dave Loeser"}, {"nick": "dalerank", "name": "Sergey Kushnirenko"}, {"nick": "dan", "name": "Dan Strandberg"}, {"name": "Daniel Trevitz"}, {"nick": "DanCar", "name": "Daniel Cardenas"}, {"nick": "Dave", "name": "David Protasowski"}, {"nick": "dax", "name": "Dacian Fiordean"}, {"nick": "deebee", "name": "Deepak Anthony"}, {"nick": "Deiz"}, {"nick": "Dietger", "name": "Dietger van Antwerpen"}, {"nick": "dpiquet", "name": "Damien Piquet"}, {"nick": "dumbo"}, {"nick": "dvangennip", "name": "Doménique"}, {"nick": "Echelon9", "name": "Rhys Kidd"}, + {"nick": "echotangoecho"}, {"nick": "eihrul", "name": "Lee Salzman"}, {"nick": "elexis", "name": "Alexander Heinsius"}, {"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": "fcxSanya", "name": "Alexander Olkhovskiy"}, {"nick": "FeXoR", "name": "Florian Finke"}, {"nick": "Fire Giant", "name": "Malte Schwarzkopf"}, {"nick": "fpre", "name": "Frederick Stallmeyer"}, {"nick": "freenity", "name": "Anton Galitch"}, {"nick": "gbish (aka Iny)", "name": "Grant Bishop"}, {"nick": "Gee", "name": "Gustav Larsson"}, {"nick": "gerbilOFdoom"}, {"nick": "godlikeldh"}, {"nick": "greybeard", "name": "Joe Cocovich"}, {"nick": "grillaz"}, {"nick": "gudo"}, {"nick": "Guuts", "name": "Matthew Guttag"}, {"name": "Samuel Guarnieri"}, {"nick": "Haommin"}, {"nick": "h20", "name": "Daniel Wilhelm"}, {"nick": "historic_bruno", "name": "Ben Brian"}, {"nick": "idanwin"}, {"nick": "Imarok", "name": "J. S."}, {"nick": "infyquest", "name": "Vijay Kiran Kamuju"}, {"nick": "IronNerd", "name": "Matthew McMullan"}, {"nick": "Itms", "name": "Nicolas Auvray"}, {"nick": "Jaison", "name": "Marco tom Suden"}, {"nick": "jammus", "name": "James Scott"}, {"nick": "Jgwman"}, {"nick": "JonBaer", "name": "Jon Baer"}, {"nick": "Josh", "name": "Joshua J. Bakita"}, {"nick": "jP_wanN", "name": "Jonas Platte"}, {"nick": "Jubalbarca", "name": "James Baillie"}, {"nick": "JubJub", "name": "Sebastian Vetter"}, {"nick": "kabzerek", "name": "Grzegorz Kabza"}, {"nick": "Kai", "name": "Kai Chen"}, {"name": "Kareem Ergawy"}, {"nick": "kevmo", "name": "Kevin Caffrey"}, {"nick": "kezz", "name": "Graeme Kerry"}, {"nick": "kingadami", "name": "Adam Winsor"}, {"nick": "kingbasil", "name": "Giannis Fafalios"}, {"nick": "lafferjm", "name": "Justin Lafferty"}, {"nick": "leper", "name": "Georg Kilzer"}, {"nick": "LittleDev"}, {"nick": "livingaftermidnight", "name": "Will Dull"}, {"nick": "Louhike"}, {"nick": "lsdh"}, {"nick": "madmax", "name": "Abhijit Nandy"}, {"nick": "m0l0t0ph", "name": "Christoph Gielisch"}, {"nick": "markcho"}, {"nick": "MarkT", "name": "Mark Thompson"}, {"nick": "Markus"}, {"nick": "Matei", "name": "Matei Zaharia"}, {"nick": "MattDoerksen", "name": "Matt Doerksen"}, {"nick": "mattlott", "name": "Matt Lott"}, {"nick": "maveric", "name": "Anton Protko"}, {"nick": "Micnasty", "name": "Travis Gorkin"}, {"nick": "mimo"}, {"nick": "mk12", "name": "Mitchell Kember"}, {"nick": "Molotov", "name": "Dario Alvarez"}, {"nick": "mpmoreti", "name": "Marcos Paulo Moreti"}, {"nick": "mreiland", "name": "Michael Reiland"}, {"nick": "myconid"}, {"nick": "nd3c3nt", "name": "Gavin Fowler"}, {"nick": "niektb", "name": "Niek ten Brinke"}, {"nick": "njm"}, {"nick": "NoMonkey", "name": "John Mena"}, {"nick": "notpete", "name": "Rich Cross"}, {"nick": "Ols", "name": "Oliver Whiteman"}, {"nick": "olsner", "name": "Simon Brenner"}, {"nick": "otero"}, {"name": "Nick Owens"}, {"nick": "Palaxin", "name": "David A. Freitag"}, {"name": "Paul Withers"}, {"nick": "pcpa", "name": "Paulo Andrade"}, {"nick": "Pendingchaos"}, {"nick": "PeteVasi", "name": "Pete Vasiliauskas"}, {"nick": "Polakrity"}, {"nick": "Poya", "name": "Poya Manouchehri"}, {"name": "Quentin Pradet"}, {"nick": "prefect", "name": "Nicolai Hähnle"}, {"nick": "pstumpf", "name": "Pascal Stumpf"}, {"name": "André Puel"}, {"nick": "Prodigal Son"}, {"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": "RedFox", "name": "Jorma Rebane"}, {"nick": "RefinedCode"}, {"nick": "Riemer"}, {"name": "Rolf Sievers"}, {"nick": "s0600204", "name": "Matthew Norwood"}, {"nick": "SafaAlfulaij"}, {"nick": "sanderd17", "name": "Sander Deryckere"}, {"nick": "sathyam", "name": "Sathyam Vellal"}, {"nick": "sbte", "name": "Sven Baars"}, {"nick": "scroogie", "name": "André Gemünd"}, {"nick": "scythetwirler", "name": "Casey X."}, {"nick": "serveurix"}, {"nick": "Shane", "name": "Shane Grant"}, {"nick": "Silk", "name": "Josh Godsiff"}, {"nick": "silure"}, {"nick": "Simikolon", "name": "Yannick & Simon"}, {"nick": "Spahbod", "name": "Omid Davoodi"}, {"nick": "stanislas69", "name": "Stanislas Dolcini"}, {"nick": "Stefan"}, {"nick": "stilz", "name": "Sławomir Zborowski"}, {"nick": "stwf", "name": "Steven Fuchs"}, {"nick": "svott", "name": "Sven Ott"}, {"nick": "t4nk004"}, {"nick": "tbm", "name": "Martin Michlmayr"}, {"nick": "tau"}, {"nick": "Teiresias"}, {"nick": "texane"}, {"nick": "thamlett", "name": "Timothy Hamlett"}, {"nick": "thedrunkyak", "name": "Dan Fuhr"}, {"nick": "TrinityDeath", "name": "Jethro Lu"}, {"nick": "triumvir", "name": "Corin Schedler"}, {"nick": "trompetin17", "name": "Juan Guillermo"}, {"nick": "vladislavbelov", "name": "Vladislav Belov"}, {"nick": "vts", "name": "Jeroen DR"}, {"nick": "WhiteTreePaladin", "name": "Brian Ashley"}, {"nick": "wraitii", "name": "Lancelot de Ferrière le Vayer"}, {"nick": "Xentelian", "name": "Mark Strawson"}, {"nick": "Xienen", "name": "Dayle Flowers"}, {"nick": "xtizer", "name": "Matt Green"}, {"nick": "yashi", "name": "Yasushi Shoji"}, {"nick": "Ykkrosh", "name": "Philip Taylor"}, {"nick": "Yves"}, {"nick": "Zeusthor", "name": "Jeffrey Tavares"}, {"nick": "zoot"}, {"nick": "zsol", "name": "Zsolt Dollenstein"}, {"nick": "Zyi", "name": "Charles De Meulenaer"} ] } ] } Index: ps/trunk/binaries/data/mods/public/gui/loading/loading.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/loading/loading.js (revision 18202) +++ ps/trunk/binaries/data/mods/public/gui/loading/loading.js (revision 18203) @@ -1,112 +1,108 @@ let g_Data; const g_EndPieceWidth = 16; function init(data) { g_Data = data; // Set to "hourglass" cursor. Engine.SetCursor("cursor-wait"); // Get tip image and corresponding tip text let tipTextLoadingArray = Engine.BuildDirEntList("gui/text/tips/", "*.txt", false); if (tipTextLoadingArray.length > 0) { // Set tip text let tipTextFilePath = tipTextLoadingArray[getRandom(0, tipTextLoadingArray.length-1)]; let tipText = Engine.TranslateLines(Engine.ReadFile(tipTextFilePath)); if (tipText) { let index = tipText.indexOf("\n"); let tipTextTitle = tipText.substring(0, index); let tipTextMessage = tipText.substring(index); Engine.GetGUIObjectByName("tipTitle").caption = tipTextTitle? tipTextTitle : ""; Engine.GetGUIObjectByName("tipText").caption = tipTextMessage? tipTextMessage : ""; } // Set tip image let fileName = tipTextFilePath.substring(tipTextFilePath.lastIndexOf("/")+1).replace(".txt", ".png"); let tipImageFilePath = "loading/tips/" + fileName; let sprite = "stretched:" + tipImageFilePath; Engine.GetGUIObjectByName("tipImage").sprite = sprite? sprite : ""; } else error("Failed to find any matching tips for the loading screen."); // janwas: main loop now sets progress / description, but that won't // happen until the first timeslice completes, so set initial values. let loadingMapName = Engine.GetGUIObjectByName("loadingMapName"); if (data) { let mapName = translate(data.attribs.settings.Name); switch (data.attribs.mapType) { case "skirmish": case "scenario": loadingMapName.caption = sprintf(translate("Loading “%(map)s”"), { "map": mapName }); break; case "random": loadingMapName.caption = sprintf(translate("Generating “%(map)s”"), { "map": mapName }); break; default: error("Unknown map type: " + data.attribs.mapType); } } Engine.GetGUIObjectByName("progressText").caption = ""; Engine.GetGUIObjectByName("progressbar").caption = 0; // Pick a random quote of the day (each line is a separate tip). let quoteArray = Engine.ReadFileLines("gui/text/quotes.txt"); Engine.GetGUIObjectByName("quoteText").caption = translate(quoteArray[getRandom(0, quoteArray.length-1)]); } function displayProgress() { // Make the progessbar finish a little early so that the user can actually see it finish if (g_Progress >= 100) return; // Show 100 when it is really 99 let progress = g_Progress + 1; Engine.GetGUIObjectByName("progressbar").caption = progress; // display current progress Engine.GetGUIObjectByName("progressText").caption = progress + "%"; // Displays detailed loading info rather than a percent // Engine.GetGUIObjectByName("progressText").caption = g_LoadDescription; // display current progess details // Keep curved right edge of progress bar in sync with the rest of the progress bar let middle = Engine.GetGUIObjectByName("progressbar"); let rightSide = Engine.GetGUIObjectByName("progressbar_right"); let middleLength = (middle.size.right - middle.size.left) - (g_EndPieceWidth / 2); let increment = Math.round(progress * middleLength / 100); let size = rightSide.size; size.left = increment; size.right = increment + g_EndPieceWidth; rightSide.size = size; } /** * This is a reserved function name that is executed by the engine when it is ready * to start the game (i.e. loading progress has reached 100%). */ function reallyStartGame() { // Switch GUI from loading screen to game session. Engine.SwitchGuiPage("page_session.xml", g_Data); // Restore default cursor. Engine.SetCursor("arrow-default"); - - // Notify the other clients that we have finished the loading screen - if (g_Data.isNetworked && g_Data.isRejoining) - Engine.SendNetworkRejoined(); } Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp =================================================================== --- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 18202) +++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 18203) @@ -1,1125 +1,1117 @@ /* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/Camera.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "graphics/scripting/JSInterface_GameView.h" #include "gui/GUI.h" #include "gui/GUIManager.h" #include "gui/IGUIObject.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "i18n/L10n.h" #include "i18n/scripting/JSInterface_L10n.h" #include "lib/svn_revision.h" #include "lib/sysdep/sysdep.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lobby/scripting/JSInterface_Lobby.h" #include "maths/FixedVector3D.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetTurnManager.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Errors.h" #include "ps/GUID.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/Globals.h" // g_frequencyFilter #include "ps/Hotkey.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/SavedGame.h" #include "ps/UserReport.h" #include "ps/World.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_Mod.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/scripting/JSInterface_VisualReplay.h" #include "renderer/scripting/JSInterface_Renderer.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpGuiInterface.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/helpers/Selection.h" #include "soundmanager/SoundManager.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "tools/atlas/GameInterface/GameLoop.h" /* * This file defines a set of functions that are available to GUI scripts, to allow * interaction with the rest of the engine. * Functions are exposed to scripts within the global object 'Engine', so * scripts should call "Engine.FunctionName(...)" etc. */ extern void restart_mainloop_in_atlas(); // from main.cpp extern void EndGame(); extern void kill_mainloop(); namespace { // Note that the initData argument may only contain clonable data. // Functions aren't supported for example! // TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning. void PushGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData) { g_GUI->PushPage(name, pCxPrivate->pScriptInterface->WriteStructuredClone(initData)); } void SwitchGuiPage(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue initData) { g_GUI->SwitchPage(name, pCxPrivate->pScriptInterface, initData); } void PopGuiPage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_GUI->PopPage(); } // Note that the args argument may only contain clonable data. // Functions aren't supported for example! // TODO: Use LOGERROR to print a friendly error message when the requirements aren't met instead of failing with debug_warn when cloning. void PopGuiPageCB(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue args) { g_GUI->PopPageCB(pCxPrivate->pScriptInterface->WriteStructuredClone(args)); } JS::Value GuiInterfaceCall(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name, JS::HandleValue data) { if (!g_Game) return JS::UndefinedValue(); CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY); if (!cmpGuiInterface) return JS::UndefinedValue(); int player = g_Game->GetPlayerID(); JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue arg(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), data)); JS::RootedValue ret(cxSim); cmpGuiInterface->ScriptCall(player, name, arg, &ret); return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(sim->GetScriptInterface(), ret); } void PostNetworkCommand(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue cmd) { if (!g_Game) return; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY); if (!cmpCommandQueue) return; JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue cmd2(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), cmd)); cmpCommandQueue->PostNetworkCommand(cmd2); } entity_id_t PickEntityAtPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y) { return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID(), false); } std::vector PickPlayerEntitiesInRect(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x0, int y0, int x1, int y1, int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false); } std::vector PickPlayerEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate, int player) { return PickPlayerEntitiesInRect(pCxPrivate, 0, 0, g_xres, g_yres, player); } std::vector PickNonGaiaEntitiesOnScreen(ScriptInterface::CxPrivate* pCxPrivate) { std::vector entities; CmpPtr cmpPlayerManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpPlayerManager) return entities; i32 numPlayers = cmpPlayerManager->GetNumPlayers(); for (i32 player = 1; player < numPlayers; ++player) { std::vector ents = PickPlayerEntitiesOnScreen(pCxPrivate, player); entities.insert(entities.end(), ents.begin(), ents.end()); } return entities; } std::vector PickSimilarPlayerEntities(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations) { return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank, false, allowFoundations); } CFixedVector3D GetTerrainAtScreenPoint(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int x, int y) { CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true); return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z)); } std::wstring SetCursor(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name) { std::wstring old = g_CursorName; g_CursorName = name; return old; } bool IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return false; return g_Game->IsVisualReplay(); } std::wstring GetCurrentReplayDirectory(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return std::wstring(); if (g_Game->IsVisualReplay()) return OsPath(g_Game->GetReplayPath()).Parent().Filename().string(); return g_Game->GetReplayLogger().GetDirectory().Filename().string(); } int GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (g_Game) return g_Game->GetPlayerID(); return -1; } void SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (g_Game) g_Game->SetPlayerID(id); } void SetViewedPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (g_Game) g_Game->SetViewedPlayerID(id); } JS::Value GetEngineInfo(ScriptInterface::CxPrivate* pCxPrivate) { return SavedGames::GetEngineInfo(*(pCxPrivate->pScriptInterface)); } void StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_NetServer); g_NetServer->StartGame(); } void StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); JSContext* cxSim = sim->GetScriptInterface().GetContext(); JSAutoRequest rqSim(cxSim); JS::RootedValue gameAttribs(cxSim, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs)); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameAttribs, ""); } JS::Value StartSavedGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& name) { // We need to be careful with different compartments and contexts. // The GUI calls this function from the GUI context and expects the return value in the same context. // The game we start from here creates another context and expects data in this context. JSContext* cxGui = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cxGui); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); // Load the saved game data from disk JS::RootedValue guiContextMetadata(cxGui); std::string savedState; Status err = SavedGames::Load(name, *(pCxPrivate->pScriptInterface), &guiContextMetadata, savedState); if (err < 0) return JS::UndefinedValue(); g_Game = new CGame(); { CSimulation2* sim = g_Game->GetSimulation2(); JSContext* cxGame = sim->GetScriptInterface().GetContext(); JSAutoRequest rq(cxGame); JS::RootedValue gameContextMetadata(cxGame, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata)); JS::RootedValue gameInitAttributes(cxGame); sim->GetScriptInterface().GetProperty(gameContextMetadata, "initAttributes", &gameInitAttributes); int playerID; sim->GetScriptInterface().GetProperty(gameContextMetadata, "player", playerID); // Start the game g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameInitAttributes, savedState); } return guiContextMetadata; } void SaveGame(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata) { shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata); if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0) LOGERROR("Failed to save game"); } void SaveGamePrefix(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata) { shared_ptr GUIMetadataClone = pCxPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata); if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone, g_Game->GetPlayerID()) < 0) LOGERROR("Failed to save game"); } void SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1) { ENSURE(g_NetServer); //TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere // (with no obvious reason). JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue attribs(cx, attribs1); g_NetServer->UpdateGameAttributes(&attribs, *(pCxPrivate->pScriptInterface)); } void StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& playerName) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_NetServer = new CNetServer(); if (!g_NetServer->SetupConnection()) { pCxPrivate->pScriptInterface->ReportError("Failed to start server"); SAFE_DELETE(g_NetServer); return; } g_Game = new CGame(); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection("127.0.0.1")) { pCxPrivate->pScriptInterface->ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& playerName, const std::string& serverAddress) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_Game = new CGame(); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection(serverAddress)) { pCxPrivate->pScriptInterface->ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void DisconnectNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { // TODO: we ought to do async reliable disconnections SAFE_DELETE(g_NetServer); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } std::string GetPlayerGUID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_NetClient) return std::string(); return g_NetClient->GetGUID(); } bool KickPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& playerName, bool ban) { if (!g_NetServer) return false; return g_NetServer->KickPlayer(playerName, ban); } JS::Value PollNetworkClient(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_NetClient) return JS::UndefinedValue(); // Convert from net client context to GUI script context JSContext* cxNet = g_NetClient->GetScriptInterface().GetContext(); JSAutoRequest rqNet(cxNet); JS::RootedValue pollNet(cxNet); g_NetClient->GuiPoll(&pollNet); return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), pollNet); } void AssignNetworkPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int playerID, const std::string& guid) { ENSURE(g_NetServer); g_NetServer->AssignPlayer(playerID, guid); } void SetNetworkPlayerStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& guid, int ready) { ENSURE(g_NetServer); g_NetServer->SetPlayerReady(guid, ready); } void ClearAllPlayerReady (ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_NetServer); g_NetServer->ClearAllPlayerReady(); } void SendNetworkChat(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } void SendNetworkReady(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int message) { ENSURE(g_NetClient); g_NetClient->SendReadyMessage(message); } -void SendNetworkRejoined(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) -{ - ENSURE(g_NetClient); - - g_NetClient->SendRejoinedMessage(); -} - JS::Value GetAIs(ScriptInterface::CxPrivate* pCxPrivate) { return ICmpAIManager::GetAIs(*(pCxPrivate->pScriptInterface)); } JS::Value GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate) { return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface)); } bool DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name) { return SavedGames::DeleteSavedGame(name); } void OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url) { sys_open_url(url); } std::wstring GetMatchID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return ps_generate_guid().FromUTF8(); } void RestartInAtlas(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { restart_mainloop_in_atlas(); } bool AtlasIsAvailable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return ATLAS_IsAvailable(); } bool IsAtlasRunning(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return (g_AtlasGameLoop && g_AtlasGameLoop->running); } JS::Value LoadMapSettings(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); CMapSummaryReader reader; if (reader.LoadMap(pathname) != PSRETURN_OK) return JS::UndefinedValue(); JS::RootedValue settings(cx); reader.GetMapSettings(*(pCxPrivate->pScriptInterface), &settings); return settings; } JS::Value GetMapSettings(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_Game) return JS::UndefinedValue(); JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); JS::RootedValue mapSettings(cx); g_Game->GetSimulation2()->GetMapSettings(&mapSettings); return pCxPrivate->pScriptInterface->CloneValueFromOtherContext( g_Game->GetSimulation2()->GetScriptInterface(), mapSettings); } /** * Get the current X coordinate of the camera. */ float CameraGetX(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (g_Game && g_Game->GetView()) return g_Game->GetView()->GetCameraX(); return -1; } /** * Get the current Z coordinate of the camera. */ float CameraGetZ(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (g_Game && g_Game->GetView()) return g_Game->GetView()->GetCameraZ(); return -1; } /** * Start / stop camera following mode * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollow(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, false); } /** * Start / stop first-person camera following mode * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollowFPS(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, true); } /** * Set the data (position, orientation and zoom) of the camera */ void SetCameraData(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom) { // called from JS; must not fail if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain())) return; CVector3D Pos = CVector3D(x.ToFloat(), y.ToFloat(), z.ToFloat()); float RotX = rotx.ToFloat(); float RotY = roty.ToFloat(); float Zoom = zoom.ToFloat(); g_Game->GetView()->SetCamera(Pos, RotX, RotY, Zoom); } /// Move camera to a 2D location void CameraMoveTo(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), entity_pos_t x, entity_pos_t z) { // called from JS; must not fail if(!(g_Game && g_Game->GetWorld() && g_Game->GetView() && g_Game->GetWorld()->GetTerrain())) return; CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; target.X = x.ToFloat(); target.Z = z.ToFloat(); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } entity_id_t GetFollowedEntity(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (g_Game && g_Game->GetView()) return g_Game->GetView()->GetFollowedEntity(); return INVALID_ENTITY; } bool HotkeyIsPressed_(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& hotkeyName) { return HotkeyIsPressed(hotkeyName); } void DisplayErrorDialog(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& msg) { debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL); } JS::Value GetProfilerState(ScriptInterface::CxPrivate* pCxPrivate) { return g_ProfileViewer.SaveToJS(*(pCxPrivate->pScriptInterface)); } bool IsUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_UserReporter.IsReportingEnabled(); } void SetUserReportEnabled(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled) { g_UserReporter.SetReportingEnabled(enabled); } std::string GetUserReportStatus(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_UserReporter.GetStatus(); } void SubmitUserReport(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& type, int version, const std::wstring& data) { g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data)); } void SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate) { g_Game->SetSimRate(rate); } float GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_Game->GetSimRate(); } void SetTurnLength(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int length) { if (g_NetServer) g_NetServer->SetTurnLength(length); else LOGERROR("Only network host can change turn length"); } // Focus the game camera on a given position. void SetCameraTarget(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float x, float y, float z) { g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z)); } // Deliberately cause the game to crash. // Currently implemented via access violation (read of address 0). // Useful for testing the crashlog/stack trace code. int Crash(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { debug_printf("Crashing at user's request.\n"); return *(volatile int*)0; } void DebugWarn(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { debug_warn(L"Warning at user's request."); } // Force a JS garbage collection cycle to take place immediately. // Writes an indication of how long this took to the console. void ForceGC(ScriptInterface::CxPrivate* pCxPrivate) { double time = timer_Time(); JS_GC(pCxPrivate->pScriptInterface->GetJSRuntime()); time = timer_Time() - time; g_Console->InsertMessage(fmt::sprintf("Garbage collection completed in: %f", time)); } void DumpSimState(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { OsPath path = psLogDir()/"sim_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); g_Game->GetSimulation2()->DumpDebugState(file); } void DumpTerrainMipmap(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { VfsPath filename(L"screenshots/terrainmipmap.png"); g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename); OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8()); } void EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_Game->GetTurnManager()->QuickSave(); } void QuickLoad(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_Game->GetTurnManager()->QuickLoad(); } void SetBoundingBoxDebugOverlay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool enabled) { ICmpSelectable::ms_EnableDebugOverlays = enabled; } void Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { EndGame(); } CStrW GetSystemUsername(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return sys_get_user_name(); } // Cause the game to exit gracefully. // params: // returns: // notes: // - Exit happens after the current main loop iteration ends // (since this only sets a flag telling it to end) void ExitProgram(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { kill_mainloop(); } // Is the game paused? bool IsPaused(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_Game) { JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started"); return false; } return g_Game->m_Paused; } // Pause/unpause the game void SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause) { if (!g_Game) { JS_ReportError(pCxPrivate->pScriptInterface->GetContext(), "Game is not started"); return; } g_Game->m_Paused = pause; #if CONFIG2_AUDIO if (g_SoundManager) g_SoundManager->Pause(pause); #endif } // Return the global frames-per-second value. // params: // returns: FPS [int] // notes: // - This value is recalculated once a frame. We take special care to // filter it, so it is both accurate and free of jitter. int GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { int freq = 0; if (g_frequencyFilter) freq = g_frequencyFilter->StableFrequency(); return freq; } JS::Value GetGUIObjectByName(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStr& name) { IGUIObject* guiObj = g_GUI->FindObjectByName(name); if (guiObj) return JS::ObjectValue(*guiObj->GetJSObject()); else return JS::UndefinedValue(); } // Return the date/time at which the current executable was compiled. // params: mode OR an integer specifying // what to display: -1 for "date time (svn revision)", 0 for date, 1 for time, 2 for svn revision // returns: string with the requested timestamp info // notes: // - Displayed on main menu screen; tells non-programmers which auto-build // they are running. Could also be determined via .EXE file properties, // but that's a bit more trouble. // - To be exact, the date/time returned is when scriptglue.cpp was // last compiled, but the auto-build does full rebuilds. // - svn revision is generated by calling svnversion and cached in // lib/svn_revision.cpp. it is useful to know when attempting to // reproduce bugs (the main EXE and PDB should be temporarily reverted to // that revision so that they match user-submitted crashdumps). std::wstring GetBuildTimestamp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int mode) { char buf[200]; if (mode == -1) // Date, time and revision. { UDate dateTime = g_L10n.ParseDateTime(__DATE__ " " __TIME__, "MMM d yyyy HH:mm:ss", Locale::getUS()); std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::DateTime, SimpleDateFormat::DATE_TIME); char svnRevision[32]; sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision); if (strcmp(svnRevision, "custom build") == 0) { // Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build. sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (custom build)").c_str(), dateTimeString.c_str()); } else { // Translation: First item is a date and time, item between parenthesis is the Subversion revision number of the current build. sprintf_s(buf, ARRAY_SIZE(buf), g_L10n.Translate("%s (%ls)").c_str(), dateTimeString.c_str(), svn_revision); } } else if (mode == 0) // Date. { UDate dateTime = g_L10n.ParseDateTime(__DATE__, "MMM d yyyy", Locale::getUS()); std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Date, SimpleDateFormat::MEDIUM); sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str()); } else if (mode == 1) // Time. { UDate dateTime = g_L10n.ParseDateTime(__TIME__, "HH:mm:ss", Locale::getUS()); std::string dateTimeString = g_L10n.LocalizeDateTime(dateTime, L10n::Time, SimpleDateFormat::MEDIUM); sprintf_s(buf, ARRAY_SIZE(buf), "%s", dateTimeString.c_str()); } else if (mode == 2) // Revision. { char svnRevision[32]; sprintf_s(svnRevision, ARRAY_SIZE(svnRevision), "%ls", svn_revision); if (strcmp(svnRevision, "custom build") == 0) { sprintf_s(buf, ARRAY_SIZE(buf), "%s", g_L10n.Translate("custom build").c_str()); } else { sprintf_s(buf, ARRAY_SIZE(buf), "%ls", svn_revision); } } return wstring_from_utf8(buf); } JS::Value ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath) { JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue out(cx); pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out); return out; } void WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1) { JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. JS::RootedValue val(cx, val1); std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false)); VfsPath path(filePath); WriteBuffer buf; buf.Append(str.c_str(), str.length()); g_VFS->CreateFile(path, buf.Data(), buf.Size()); } bool TemplateExists(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName) { return g_GUI->TemplateExists(templateName); } CParamNode GetTemplate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& templateName) { return g_GUI->GetTemplate(templateName); } int GetTextWidth(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStr& fontName, const CStrW& text) { int width = 0; int height = 0; CStrIntern _fontName(fontName); CFontMetrics fontMetrics(_fontName); fontMetrics.CalculateStringSize(text.c_str(), width, height); return width; } //----------------------------------------------------------------------------- // Timer //----------------------------------------------------------------------------- // Script profiling functions: Begin timing a piece of code with StartJsTimer(num) // and stop timing with StopJsTimer(num). The results will be printed to stdout // when the game exits. static const size_t MAX_JS_TIMERS = 20; static TimerUnit js_start_times[MAX_JS_TIMERS]; static TimerUnit js_timer_overhead; static TimerClient js_timer_clients[MAX_JS_TIMERS]; static wchar_t js_timer_descriptions_buf[MAX_JS_TIMERS * 12]; // depends on MAX_JS_TIMERS and format string below static void InitJsTimers(ScriptInterface& scriptInterface) { wchar_t* pos = js_timer_descriptions_buf; for(size_t i = 0; i < MAX_JS_TIMERS; i++) { const wchar_t* description = pos; pos += swprintf_s(pos, 12, L"js_timer %d", (int)i)+1; timer_AddClient(&js_timer_clients[i], description); } // call several times to get a good approximation of 'hot' performance. // note: don't use a separate timer slot to warm up and then judge // overhead from another: that causes worse results (probably some // caching effects inside JS, but I don't entirely understand why). std::wstring calibration_script = L"Engine.StartXTimer(0);\n" \ L"Engine.StopXTimer (0);\n" \ L"\n"; scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script); // slight hack: call LoadGlobalScript twice because we can't average several // TimerUnit values because there's no operator/. this way is better anyway // because it hopefully avoids the one-time JS init overhead. js_timer_clients[0].sum.SetToZero(); scriptInterface.LoadGlobalScript("timer_calibration_script", calibration_script); js_timer_clients[0].sum.SetToZero(); js_timer_clients[0].num_calls = 0; } void StartJsTimer(ScriptInterface::CxPrivate* pCxPrivate, unsigned int slot) { ONCE(InitJsTimers(*(pCxPrivate->pScriptInterface))); if (slot >= MAX_JS_TIMERS) { LOGERROR("Exceeded the maximum number of timer slots for scripts!"); return; } js_start_times[slot].SetFromTimer(); } void StopJsTimer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int slot) { if (slot >= MAX_JS_TIMERS) { LOGERROR("Exceeded the maximum number of timer slots for scripts!"); return; } TimerUnit now; now.SetFromTimer(); now.Subtract(js_timer_overhead); BillingPolicy_Default()(&js_timer_clients[slot], js_start_times[slot], now); js_start_times[slot].SetToZero(); } } // namespace void GuiScriptingInit(ScriptInterface& scriptInterface) { JSI_IGUIObject::init(scriptInterface); JSI_GUITypes::init(scriptInterface); JSI_GameView::RegisterScriptFunctions(scriptInterface); JSI_Renderer::RegisterScriptFunctions(scriptInterface); JSI_Console::RegisterScriptFunctions(scriptInterface); JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); JSI_Mod::RegisterScriptFunctions(scriptInterface); JSI_Sound::RegisterScriptFunctions(scriptInterface); JSI_L10n::RegisterScriptFunctions(scriptInterface); JSI_Lobby::RegisterScriptFunctions(scriptInterface); JSI_VisualReplay::RegisterScriptFunctions(scriptInterface); // VFS (external) scriptInterface.RegisterFunction("BuildDirEntList"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("GetFileMTime"); scriptInterface.RegisterFunction("GetFileSize"); scriptInterface.RegisterFunction("ReadFile"); scriptInterface.RegisterFunction("ReadFileLines"); // GUI manager functions: scriptInterface.RegisterFunction("PushGuiPage"); scriptInterface.RegisterFunction("SwitchGuiPage"); scriptInterface.RegisterFunction("PopGuiPage"); scriptInterface.RegisterFunction("PopGuiPageCB"); scriptInterface.RegisterFunction("GetGUIObjectByName"); // Simulation<->GUI interface functions: scriptInterface.RegisterFunction("GuiInterfaceCall"); scriptInterface.RegisterFunction("PostNetworkCommand"); // Entity picking scriptInterface.RegisterFunction("PickEntityAtPoint"); scriptInterface.RegisterFunction, int, int, int, int, int, &PickPlayerEntitiesInRect>("PickPlayerEntitiesInRect"); scriptInterface.RegisterFunction, int, &PickPlayerEntitiesOnScreen>("PickPlayerEntitiesOnScreen"); scriptInterface.RegisterFunction, &PickNonGaiaEntitiesOnScreen>("PickNonGaiaEntitiesOnScreen"); scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities"); scriptInterface.RegisterFunction("GetTerrainAtScreenPoint"); // Network / game setup functions scriptInterface.RegisterFunction("StartNetworkGame"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("EndGame"); scriptInterface.RegisterFunction("StartNetworkHost"); scriptInterface.RegisterFunction("StartNetworkJoin"); scriptInterface.RegisterFunction("DisconnectNetworkGame"); scriptInterface.RegisterFunction("GetPlayerGUID"); scriptInterface.RegisterFunction("KickPlayer"); scriptInterface.RegisterFunction("PollNetworkClient"); scriptInterface.RegisterFunction("SetNetworkGameAttributes"); scriptInterface.RegisterFunction("AssignNetworkPlayer"); scriptInterface.RegisterFunction("SetNetworkPlayerStatus"); scriptInterface.RegisterFunction("ClearAllPlayerReady"); scriptInterface.RegisterFunction("SendNetworkChat"); scriptInterface.RegisterFunction("SendNetworkReady"); - scriptInterface.RegisterFunction("SendNetworkRejoined"); scriptInterface.RegisterFunction("GetAIs"); scriptInterface.RegisterFunction("GetEngineInfo"); // Saved games scriptInterface.RegisterFunction("StartSavedGame"); scriptInterface.RegisterFunction("GetSavedGames"); scriptInterface.RegisterFunction("DeleteSavedGame"); scriptInterface.RegisterFunction("SaveGame"); scriptInterface.RegisterFunction("SaveGamePrefix"); scriptInterface.RegisterFunction("QuickSave"); scriptInterface.RegisterFunction("QuickLoad"); // Misc functions scriptInterface.RegisterFunction("SetCursor"); scriptInterface.RegisterFunction("IsVisualReplay"); scriptInterface.RegisterFunction("GetCurrentReplayDirectory"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("SetPlayerID"); scriptInterface.RegisterFunction("SetViewedPlayer"); scriptInterface.RegisterFunction("OpenURL"); scriptInterface.RegisterFunction("GetMatchID"); scriptInterface.RegisterFunction("RestartInAtlas"); scriptInterface.RegisterFunction("AtlasIsAvailable"); scriptInterface.RegisterFunction("IsAtlasRunning"); scriptInterface.RegisterFunction("LoadMapSettings"); scriptInterface.RegisterFunction("GetMapSettings"); scriptInterface.RegisterFunction("CameraGetX"); scriptInterface.RegisterFunction("CameraGetZ"); scriptInterface.RegisterFunction("CameraFollow"); scriptInterface.RegisterFunction("CameraFollowFPS"); scriptInterface.RegisterFunction("SetCameraData"); scriptInterface.RegisterFunction("CameraMoveTo"); scriptInterface.RegisterFunction("GetFollowedEntity"); scriptInterface.RegisterFunction("HotkeyIsPressed"); scriptInterface.RegisterFunction("DisplayErrorDialog"); scriptInterface.RegisterFunction("GetProfilerState"); scriptInterface.RegisterFunction("Exit"); scriptInterface.RegisterFunction("IsPaused"); scriptInterface.RegisterFunction("SetPaused"); scriptInterface.RegisterFunction("GetFPS"); scriptInterface.RegisterFunction("GetBuildTimestamp"); scriptInterface.RegisterFunction("ReadJSONFile"); scriptInterface.RegisterFunction("WriteJSONFile"); scriptInterface.RegisterFunction("TemplateExists"); scriptInterface.RegisterFunction("GetTemplate"); scriptInterface.RegisterFunction("GetTextWidth"); // User report functions scriptInterface.RegisterFunction("IsUserReportEnabled"); scriptInterface.RegisterFunction("SetUserReportEnabled"); scriptInterface.RegisterFunction("GetUserReportStatus"); scriptInterface.RegisterFunction("SubmitUserReport"); // Development/debugging functions scriptInterface.RegisterFunction("StartXTimer"); scriptInterface.RegisterFunction("StopXTimer"); scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("GetSimRate"); scriptInterface.RegisterFunction("SetTurnLength"); scriptInterface.RegisterFunction("SetCameraTarget"); scriptInterface.RegisterFunction("Crash"); scriptInterface.RegisterFunction("DebugWarn"); scriptInterface.RegisterFunction("ForceGC"); scriptInterface.RegisterFunction("DumpSimState"); scriptInterface.RegisterFunction("DumpTerrainMipmap"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); scriptInterface.RegisterFunction("GetSystemUsername"); } Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 18202) +++ ps/trunk/source/network/NetClient.cpp (revision 18203) @@ -1,776 +1,780 @@ /* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetTurnManager.h" #include "lib/byte_order.h" #include "lib/sysdep/sysdep.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/GUID.h" #include "ps/Loader.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client) : m_Client(client) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; }; CNetClient::CNetClient(CGame* game, bool isLocalClient) : m_Session(NULL), m_UserName(L"anonymous"), m_GUID(ps_generate_guid()), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game), m_GameAttributes(game->GetSimulation2()->GetScriptInterface().GetContext()), m_IsLocalClient(isLocalClient), - m_LastConnectionCheck(0) + m_LastConnectionCheck(0), + m_Rejoin(false) { m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; JS_AddExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this); // Set up transitions for session AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context); AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context); AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context); AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_READY, NCS_PREGAME, (void*)&OnReady, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_PREGAME, (uint)NMT_KICKED, NCS_PREGAME, (void*)&OnKicked, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_PREGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_PREGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_PREGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_TIMEOUT, NCS_JOIN_SYNCING, (void*)&OnClientTimeout, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CLIENT_PERFORMANCE, NCS_JOIN_SYNCING, (void*)&OnClientPerformance, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context); AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context); AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_TIMEOUT, NCS_LOADING, (void*)&OnClientTimeout, context); AddTransition(NCS_LOADING, (uint)NMT_CLIENT_PERFORMANCE, NCS_LOADING, (void*)&OnClientPerformance, context); AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_INGAME, (uint)NMT_REJOINED, NCS_INGAME, (void*)&OnRejoined, context); AddTransition(NCS_INGAME, (uint)NMT_KICKED, NCS_INGAME, (void*)&OnKicked, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_TIMEOUT, NCS_INGAME, (void*)&OnClientTimeout, context); AddTransition(NCS_INGAME, (uint)NMT_CLIENT_PERFORMANCE, NCS_INGAME, (void*)&OnClientPerformance, context); AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context); AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context); AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context); // Set first state SetFirstState(NCS_UNCONNECTED); } CNetClient::~CNetClient() { DestroyConnection(); JS_RemoveExtraGCRootsTracer(GetScriptInterface().GetJSRuntime(), CNetClient::Trace, this); } void CNetClient::TraceMember(JSTracer *trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS_CallHeapValueTracer(trc, &guiMessage, "m_GuiMessageQueue"); } void CNetClient::SetUserName(const CStrW& username) { ENSURE(!m_Session); // must be called before we start the connection m_UserName = username; } bool CNetClient::SetupConnection(const CStr& server) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(PS_DEFAULT_PORT, server, m_IsLocalClient); SetAndOwnSession(session); return ok; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { // Send network messages from the current frame before connection is destroyed. if (m_ClientTurnManager) { m_ClientTurnManager->OnDestroyConnection(); // End sending of commands for scheduled turn. Flush(); // Make sure the messages are sent. } SAFE_DELETE(m_Session); } void CNetClient::Poll() { if (!m_Session) return; CheckServerConnection(); m_Session->Poll(); } void CNetClient::CheckServerConnection() { // Trigger local warnings if the connection to the server is bad. // At most once per second. std::time_t now = std::time(nullptr); if (now <= m_LastConnectionCheck) return; m_LastConnectionCheck = now; JSContext* cx = GetScriptInterface().GetContext(); // Report if we are losing the connection to the server u32 lastReceived = m_Session->GetLastReceivedTime(); if (lastReceived > NETWORK_WARNING_TIMEOUT) { JS::RootedValue msg(cx); GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'server-timeout' })", &msg); GetScriptInterface().SetProperty(msg, "lastReceivedTime", lastReceived); PushGuiMessage(msg); return; } // Report if we have a bad ping to the server u32 meanRTT = m_Session->GetMeanRTT(); if (meanRTT > DEFAULT_TURN_LENGTH_MP) { JS::RootedValue msg(cx); GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'server-latency' })", &msg); GetScriptInterface().SetProperty(msg, "meanRTT", meanRTT); PushGuiMessage(msg); } } void CNetClient::Flush() { if (m_Session) m_Session->Flush(); } void CNetClient::GuiPoll(JS::MutableHandleValue ret) { if (m_GuiMessageQueue.empty()) { ret.setUndefined(); return; } ret.set(m_GuiMessageQueue.front()); m_GuiMessageQueue.pop_front(); } void CNetClient::PushGuiMessage(const JS::HandleValue message) { ENSURE(!message.isUndefined()); m_GuiMessageQueue.push_back(JS::Heap(message)); } std::string CNetClient::TestReadGuiMessages() { JSContext* cx = GetScriptInterface().GetContext(); JSAutoRequest rq(cx); std::string r; JS::RootedValue msg(cx); while (true) { GuiPoll(&msg); if (msg.isUndefined()) break; r += GetScriptInterface().ToString(&msg) + "\n"; } return r; } ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { JSContext* cx = GetScriptInterface().GetContext(); JSAutoRequest rq(cx); JS::RootedValue msg(cx); GetScriptInterface().Eval("({'type':'players', 'hosts':{}})", &msg); JS::RootedValue hosts(cx); GetScriptInterface().GetProperty(msg, "hosts", &hosts); for (const std::pair& p : m_PlayerAssignments) { JS::RootedValue host(cx); GetScriptInterface().Eval("({})", &host); GetScriptInterface().SetProperty(host, "name", std::wstring(p.second.m_Name), false); GetScriptInterface().SetProperty(host, "player", p.second.m_PlayerID, false); GetScriptInterface().SetProperty(host, "status", p.second.m_Status, false); GetScriptInterface().SetProperty(hosts, p.first.c_str(), host, false); } PushGuiMessage(msg); } bool CNetClient::SendMessage(const CNetMessage* message) { if (!m_Session) return false; return m_Session->SendMessage(message); } void CNetClient::HandleConnect() { Update((uint)NMT_CONNECT_COMPLETE, NULL); } void CNetClient::HandleDisconnect(u32 reason) { JSContext* cx = GetScriptInterface().GetContext(); JSAutoRequest rq(cx); JS::RootedValue msg(cx); GetScriptInterface().Eval("({'type':'netstatus','status':'disconnected'})", &msg); GetScriptInterface().SetProperty(msg, "reason", (int)reason, false); PushGuiMessage(msg); SAFE_DELETE(m_Session); // Update the state immediately to UNCONNECTED (don't bother with FSM transitions since // we'd need one for every single state, and we don't need to use per-state actions) SetCurrState(NCS_UNCONNECTED); } void CNetClient::SendChatMessage(const std::wstring& text) { CChatMessage chat; chat.m_Message = text; SendMessage(&chat); } void CNetClient::SendReadyMessage(const int status) { CReadyMessage readyStatus; readyStatus.m_Status = status; SendMessage(&readyStatus); } void CNetClient::SendRejoinedMessage() { CRejoinedMessage rejoinedMessage; SendMessage(&rejoinedMessage); } bool CNetClient::HandleMessage(CNetMessage* message) { // Handle non-FSM messages first Status status = m_Session->GetFileTransferer().HandleMessageReceive(message); if (status == INFO::OK) return true; if (status != INFO::SKIPPED) return false; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // TODO: we should support different transfer request types, instead of assuming // it's always requesting the simulation state std::stringstream stream; LOGMESSAGERENDER("Serializing game at turn %u for rejoining player", m_ClientTurnManager->GetCurrentTurn()); u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); stream.write((char*)&turn, sizeof(turn)); bool ok = m_Game->GetSimulation2()->SerializeState(stream); ENSURE(ok); // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; CompressZLib(stream.str(), compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); return true; } // Update FSM bool ok = Update(message->GetType(), message); if (!ok) LOGERROR("Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState()); return ok; } void CNetClient::LoadFinished() { JSContext* cx = GetScriptInterface().GetContext(); JSAutoRequest rq(cx); if (!m_JoinSyncBuffer.empty()) { // We're rejoining a game, and just finished loading the initial map, // so deserialize the saved game state now std::string state; DecompressZLib(m_JoinSyncBuffer, state, true); std::stringstream stream(state); u32 turn; stream.read((char*)&turn, sizeof(turn)); turn = to_le32(turn); LOGMESSAGE("Rejoining client deserializing state at turn %u\n", turn); bool ok = m_Game->GetSimulation2()->DeserializeState(stream); ENSURE(ok); m_ClientTurnManager->ResetState(turn, turn); JS::RootedValue msg(cx); GetScriptInterface().Eval("({'type':'netstatus','status':'join_syncing'})", &msg); PushGuiMessage(msg); } else { // Connecting at the start of a game, so we'll wait for other players to finish loading JS::RootedValue msg(cx); GetScriptInterface().Eval("({'type':'netstatus','status':'waiting_for_players'})", &msg); PushGuiMessage(msg); } CLoadedGameMessage loaded; loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); SendMessage(&loaded); } bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'netstatus','status':'connected'})", &msg); client->PushGuiMessage(msg); return true; } bool CNetClient::OnHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); CNetClient* client = (CNetClient*)context; CCliHandshakeMessage handshake; handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); return true; } bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE); CNetClient* client = (CNetClient*)context; CAuthenticateMessage authenticate; authenticate.m_GUID = client->m_GUID; authenticate.m_Name = client->m_UserName; authenticate.m_Password = L""; // TODO authenticate.m_IsLocalClient = client->m_IsLocalClient; client->SendMessage(&authenticate); return true; } bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); CAuthenticateResultMessage* message = (CAuthenticateResultMessage*)event->GetParamRef(); LOGMESSAGE("Net: Authentication result: host=%u, %s", message->m_HostID, utf8_from_wstring(message->m_Message)); - bool isRejoining = (message->m_Code == ARC_OK_REJOINING); - client->m_HostID = message->m_HostID; + client->m_Rejoin = message->m_Code == ARC_OK_REJOINING; JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'netstatus','status':'authenticated'})", &msg); - client->GetScriptInterface().SetProperty(msg, "rejoining", isRejoining); + client->GetScriptInterface().SetProperty(msg, "rejoining", client->m_Rejoin); client->PushGuiMessage(msg); return true; } bool CNetClient::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); CChatMessage* message = (CChatMessage*)event->GetParamRef(); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'chat'})", &msg); client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false); client->GetScriptInterface().SetProperty(msg, "text", std::wstring(message->m_Message), false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnReady(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_READY); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); CReadyMessage* message = (CReadyMessage*)event->GetParamRef(); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'ready'})", &msg); client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false); client->GetScriptInterface().SetProperty(msg, "status", int (message->m_Status), false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); client->m_GameAttributes = message->m_Data; JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'gamesetup'})", &msg); client->GetScriptInterface().SetProperty(msg, "data", message->m_Data, false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT); CNetClient* client = (CNetClient*)context; CPlayerAssignmentMessage* message = (CPlayerAssignmentMessage*)event->GetParamRef(); // Unpack the message PlayerAssignmentMap newPlayerAssignments; for (size_t i = 0; i < message->m_Hosts.size(); ++i) { PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = message->m_Hosts[i].m_Name; assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID; assignment.m_Status = message->m_Hosts[i].m_Status; newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment; } client->m_PlayerAssignments.swap(newPlayerAssignments); client->PostPlayerAssignmentsToScript(); return true; } bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // 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()); client->m_Game->SetPlayerID(player); client->m_Game->StartGame(&client->m_GameAttributes, ""); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'start'})", &msg); client->PushGuiMessage(msg); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = (CNetClient*)context; // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = (CNetClient*)context; CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef(); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); // Execute all the received commands for the latest turn client->m_ClientTurnManager->UpdateFastForward(); return true; } bool CNetClient::OnRejoined(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_REJOINED); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); CRejoinedMessage* message = (CRejoinedMessage*)event->GetParamRef(); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'rejoined'})", &msg); client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID), false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnKicked(void *context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_KICKED); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); CKickedMessage* message = (CKickedMessage*)event->GetParamRef(); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({})", &msg); client->GetScriptInterface().SetProperty(msg, "username", message->m_Name); client->GetScriptInterface().SetProperty(msg, "type", message->m_Ban ? std::string("banned") : std::string("kicked")); client->PushGuiMessage(msg); return true; } bool CNetClient::OnClientTimeout(void *context, CFsmEvent* event) { // Report the timeout of some other client ENSURE(event->GetType() == (uint)NMT_CLIENT_TIMEOUT); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); if (client->GetCurrState() == NCS_LOADING) return true; CClientTimeoutMessage* message = (CClientTimeoutMessage*)event->GetParamRef(); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'client-timeout' })", &msg); client->GetScriptInterface().SetProperty(msg, "guid", std::string(message->m_GUID)); client->GetScriptInterface().SetProperty(msg, "lastReceivedTime", message->m_LastReceivedTime); client->PushGuiMessage(msg); return true; } bool CNetClient::OnClientPerformance(void *context, CFsmEvent* event) { // Performance statistics for one or multiple clients ENSURE(event->GetType() == (uint)NMT_CLIENT_PERFORMANCE); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); if (client->GetCurrState() == NCS_LOADING) return true; CClientPerformanceMessage* message = (CClientPerformanceMessage*)event->GetParamRef(); // Display warnings for other clients with bad ping for (size_t i = 0; i < message->m_Clients.size(); ++i) { if (message->m_Clients[i].m_MeanRTT < DEFAULT_TURN_LENGTH_MP || message->m_Clients[i].m_GUID == client->m_GUID) continue; JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({ 'type':'netwarn', 'warntype': 'client-latency' })", &msg); client->GetScriptInterface().SetProperty(msg, "guid", message->m_Clients[i].m_GUID); client->GetScriptInterface().SetProperty(msg, "meanRTT", message->m_Clients[i].m_MeanRTT); client->PushGuiMessage(msg); } return true; } bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetClient* client = (CNetClient*)context; JSContext* cx = client->GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // All players have loaded the game - start running the turn manager // so that the game begins client->m_Game->SetTurnManager(client->m_ClientTurnManager); JS::RootedValue msg(cx); client->GetScriptInterface().Eval("({'type':'netstatus','status':'active'})", &msg); client->PushGuiMessage(msg); + // If we have rejoined an in progress game, send the rejoined message to the server. + if (client->m_Rejoin) + client->SendRejoinedMessage(); + return true; } bool CNetClient::OnInGame(void *context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetClient* client = (CNetClient*)context; CNetMessage* message = (CNetMessage*)event->GetParamRef(); if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); client->m_ClientTurnManager->OnSimulationMessage(simMessage); } else if (message->GetType() == NMT_SYNC_ERROR) { CSyncErrorMessage* syncMessage = static_cast (message); client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected, syncMessage->m_PlayerNames); } else if (message->GetType() == NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); } } return true; } Index: ps/trunk/source/network/NetClient.h =================================================================== --- ps/trunk/source/network/NetClient.h (revision 18202) +++ ps/trunk/source/network/NetClient.h (revision 18203) @@ -1,267 +1,270 @@ /* Copyright (C) 2016 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef NETCLIENT_H #define NETCLIENT_H #include "network/fsm.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" #include "scriptinterface/ScriptVal.h" #include "ps/CStr.h" #include class CGame; class CNetClientSession; class CNetClientTurnManager; class CNetServer; class ScriptInterface; // NetClient session FSM states enum { NCS_UNCONNECTED, NCS_CONNECT, NCS_HANDSHAKE, NCS_AUTHENTICATE, NCS_INITIAL_GAMESETUP, NCS_PREGAME, NCS_LOADING, NCS_JOIN_SYNCING, NCS_INGAME }; /** * Network client. * This code is run by every player (including the host, if they are not * a dedicated server). * It provides an interface between the GUI, the network (via CNetClientSession), * and the game (via CGame and CNetClientTurnManager). */ class CNetClient : public CFsm { NONCOPYABLE(CNetClient); friend class CNetFileReceiveTask_ClientRejoin; public: /** * Construct a client associated with the given game object. * The game must exist for the lifetime of this object. */ CNetClient(CGame* game, bool isLocalClient); virtual ~CNetClient(); /** * We assume that adding a tracing function that's only called * during GC is better for performance than using a * PersistentRooted where each value needs to be added to * the root set. */ static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc); /** * Set the user's name that will be displayed to all players. * This must not be called after the connection setup. */ void SetUserName(const CStrW& username); /** * Returns the GUID of the local client. * Used for distinguishing observers. */ CStr GetGUID() const { return m_GUID; } /** * Set up a connection to the remote networked server. * @param server IP address or host name to connect to * @return true on success, false on connection failure */ bool SetupConnection(const CStr& server); /** * Destroy the connection to the server. * This client probably cannot be used again. */ void DestroyConnection(); /** * Poll the connection for messages from the server and process them, and send * any queued messages. * This must be called frequently (i.e. once per frame). */ void Poll(); /** * Locally triggers a GUI message if the connection to the server is being lost or has bad latency. */ void CheckServerConnection(); /** * Flush any queued outgoing network messages. * This should be called soon after sending a group of messages that may be batched together. */ void Flush(); /** * Retrieves the next queued GUI message, and removes it from the queue. * The returned value is in the GetScriptInterface() JS context. * * This is the only mechanism for the networking code to send messages to * the GUI - it is pull-based (instead of push) so the engine code does not * need to know anything about the code structure of the GUI scripts. * * The structure of the messages is { "type": "...", ... }. * The exact types and associated data are not specified anywhere - the * implementation and GUI scripts must make the same assumptions. * * @return next message, or the value 'undefined' if the queue is empty */ void GuiPoll(JS::MutableHandleValue); /** * Add a message to the queue, to be read by GuiPoll. * The script value must be in the GetScriptInterface() JS context. */ void PushGuiMessage(const JS::HandleValue message); /** * Return a concatenation of all messages in the GUI queue, * for test cases to easily verify the queue contents. */ std::string TestReadGuiMessages(); /** * Get the script interface associated with this network client, * which is equivalent to the one used by the CGame in the constructor. */ ScriptInterface& GetScriptInterface(); /** * Send a message to the server. * @param message message to send * @return true on success */ bool SendMessage(const CNetMessage* message); /** * Call when the network connection has been successfully initiated. */ void HandleConnect(); /** * Call when the network connection has been lost. */ void HandleDisconnect(u32 reason); /** * Call when a message has been received from the network. */ bool HandleMessage(CNetMessage* message); /** * Call when the game has started and all data files have been loaded, * to signal to the server that we are ready to begin the game. */ void LoadFinished(); void SendChatMessage(const std::wstring& text); void SendReadyMessage(const int status); /** * Call when the client has rejoined a running match and finished * the loading screen. */ void SendRejoinedMessage(); private: // Net message / FSM transition handlers static bool OnConnect(void* context, CFsmEvent* event); static bool OnHandshake(void* context, CFsmEvent* event); static bool OnHandshakeResponse(void* context, CFsmEvent* event); static bool OnAuthenticate(void* context, CFsmEvent* event); static bool OnChat(void* context, CFsmEvent* event); static bool OnReady(void* context, CFsmEvent* event); static bool OnGameSetup(void* context, CFsmEvent* event); static bool OnPlayerAssignment(void* context, CFsmEvent* event); static bool OnInGame(void* context, CFsmEvent* event); static bool OnGameStart(void* context, CFsmEvent* event); static bool OnJoinSyncStart(void* context, CFsmEvent* event); static bool OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event); static bool OnRejoined(void* context, CFsmEvent* event); static bool OnKicked(void* context, CFsmEvent* event); static bool OnClientTimeout(void* context, CFsmEvent* event); static bool OnClientPerformance(void* context, CFsmEvent* event); static bool OnLoadedGame(void* context, CFsmEvent* event); /** * Take ownership of a session object, and use it for all network communication. */ void SetAndOwnSession(CNetClientSession* session); /** * Push a message onto the GUI queue listing the current player assignments. */ void PostPlayerAssignmentsToScript(); CGame *m_Game; CStrW m_UserName; /// Current network session (or NULL if not connected) CNetClientSession* m_Session; /// Turn manager associated with the current game (or NULL if we haven't started the game yet) CNetClientTurnManager* m_ClientTurnManager; /// Unique-per-game identifier of this client, used to identify the sender of simulation commands u32 m_HostID; + /// True if the player is currently rejoining or has already rejoined the game. + bool m_Rejoin; + /// Whether to prevent the client of the host from timing out bool m_IsLocalClient; /// Latest copy of game setup attributes heard from the server JS::PersistentRootedValue m_GameAttributes; /// Latest copy of player assignments heard from the server PlayerAssignmentMap m_PlayerAssignments; /// Globally unique identifier to distinguish users beyond the lifetime of a single network session CStr m_GUID; /// Queue of messages for GuiPoll std::deque> m_GuiMessageQueue; /// Serialized game state received when joining an in-progress game std::string m_JoinSyncBuffer; /// Time when the server was last checked for timeouts and bad latency std::time_t m_LastConnectionCheck; }; /// Global network client for the standard game extern CNetClient *g_NetClient; #endif // NETCLIENT_H