Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp =================================================================== --- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24175) +++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24176) @@ -1,245 +1,243 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "JSInterface_Network.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/types.h" #include "lobby/IXmppClient.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetServer.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "scriptinterface/ScriptInterface.h" u16 JSI_Network::GetDefaultPort(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return PS_DEFAULT_PORT; } bool JSI_Network::HasNetServer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_NetServer; } bool JSI_Network::HasNetClient(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_NetClient; } JS::Value JSI_Network::FindStunEndpoint(ScriptInterface::CxPrivate* pCxPrivate, int port) { return StunClient::FindStunEndpointHost(*(pCxPrivate->pScriptInterface), port); } void JSI_Network::StartNetworkHost(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); // Always use lobby authentication for lobby matches to prevent impersonation and smurfing, in particular through mods that implemented an UI for arbitrary or other players nicknames. g_NetServer = new CNetServer(static_cast(g_XmppClient)); if (!g_NetServer->SetupConnection(serverPort)) { pCxPrivate->pScriptInterface->ReportError("Failed to start server"); SAFE_DELETE(g_NetServer); return; } g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostLobbyName); if (!g_NetClient->SetupConnection("127.0.0.1", serverPort, nullptr)) { pCxPrivate->pScriptInterface->ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void JSI_Network::StartNetworkJoin(ScriptInterface::CxPrivate* pCxPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); ENetHost* enetClient = nullptr; if (g_XmppClient && useSTUN) { // Find an unused port for (int i = 0; i < 5 && !enetClient; ++i) { // Ports below 1024 are privileged on unix u16 port = 1024 + rand() % (UINT16_MAX - 1024); ENetAddress hostAddr{ENET_HOST_ANY, port}; enetClient = enet_host_create(&hostAddr, 1, 1, 0, 0); ++hostAddr.port; } if (!enetClient) { pCxPrivate->pScriptInterface->ReportError("Could not find an unused port for the enet STUN client"); return; } StunClient::StunEndpoint stunEndpoint; if (!StunClient::FindStunEndpointJoin(*enetClient, stunEndpoint)) { pCxPrivate->pScriptInterface->ReportError("Could not find the STUN endpoint"); return; } g_XmppClient->SendStunEndpointToHost(stunEndpoint, hostJID); SDL_Delay(1000); } g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@"))); if (g_XmppClient && useSTUN) StunClient::SendHolePunchingMessages(*enetClient, serverAddress, serverPort); if (!g_NetClient->SetupConnection(serverAddress, serverPort, enetClient)) { pCxPrivate->pScriptInterface->ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void JSI_Network::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); } CStr JSI_Network::GetPlayerGUID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_NetClient) return "local"; return g_NetClient->GetGUID(); } JS::Value JSI_Network::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); + ScriptInterface::Request rqNet(g_NetClient->GetScriptInterface()); + JS::RootedValue pollNet(rqNet.cx); g_NetClient->GuiPoll(&pollNet); return pCxPrivate->pScriptInterface->CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), pollNet); } void JSI_Network::SetNetworkGameAttributes(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs1) { ENSURE(g_NetClient); // TODO: This is a workaround because we need to pass a MutableHandle to a JSAPI functions somewhere (with no obvious reason). - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue attribs(cx, attribs1); + ScriptInterface::Request rq(pCxPrivate); + JS::RootedValue attribs(rq.cx, attribs1); g_NetClient->SendGameSetupMessage(&attribs, *(pCxPrivate->pScriptInterface)); } void JSI_Network::AssignNetworkPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int playerID, const CStr& guid) { ENSURE(g_NetClient); g_NetClient->SendAssignPlayerMessage(playerID, guid); } void JSI_Network::KickPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& playerName, bool ban) { ENSURE(g_NetClient); g_NetClient->SendKickPlayerMessage(playerName, ban); } void JSI_Network::SendNetworkChat(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const CStrW& message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } void JSI_Network::SendNetworkReady(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int message) { ENSURE(g_NetClient); g_NetClient->SendReadyMessage(message); } void JSI_Network::ClearAllPlayerReady (ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_NetClient); g_NetClient->SendClearAllReadyMessage(); } void JSI_Network::StartNetworkGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ENSURE(g_NetClient); g_NetClient->SendStartGameMessage(); } void JSI_Network::SetTurnLength(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int length) { if (g_NetServer) g_NetServer->SetTurnLength(length); else LOGERROR("Only network host can change turn length"); } void JSI_Network::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetDefaultPort"); scriptInterface.RegisterFunction("HasNetServer"); scriptInterface.RegisterFunction("HasNetClient"); scriptInterface.RegisterFunction("FindStunEndpoint"); scriptInterface.RegisterFunction("StartNetworkHost"); scriptInterface.RegisterFunction("StartNetworkJoin"); scriptInterface.RegisterFunction("DisconnectNetworkGame"); scriptInterface.RegisterFunction("GetPlayerGUID"); scriptInterface.RegisterFunction("PollNetworkClient"); scriptInterface.RegisterFunction("SetNetworkGameAttributes"); scriptInterface.RegisterFunction("AssignNetworkPlayer"); scriptInterface.RegisterFunction("KickPlayer"); scriptInterface.RegisterFunction("SendNetworkChat"); scriptInterface.RegisterFunction("SendNetworkReady"); scriptInterface.RegisterFunction("ClearAllPlayerReady"); scriptInterface.RegisterFunction("StartNetworkGame"); scriptInterface.RegisterFunction("SetTurnLength"); } Index: ps/trunk/source/network/tests/test_NetMessage.h =================================================================== --- ps/trunk/source/network/tests/test_NetMessage.h (revision 24175) +++ ps/trunk/source/network/tests/test_NetMessage.h (revision 24176) @@ -1,52 +1,51 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "lib/self_test.h" #include "network/NetMessage.h" #include "scriptinterface/ScriptInterface.h" class TestNetMessage : public CxxTest::TestSuite { public: void test_sim() { ScriptInterface script("Test", "Test", g_ScriptRuntime); - JSContext* cx = script.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(script); - JS::RootedValue val(cx); - ScriptInterface::CreateArray(cx, &val); + JS::RootedValue val(rq.cx); + ScriptInterface::CreateArray(rq, &val); script.SetPropertyInt(val, 0, 4); CSimulationMessage msg(script, 1, 2, 3, val); TS_ASSERT_STR_EQUALS(msg.ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }"); size_t len = msg.GetSerializedLength(); u8* buf = new u8[len+1]; buf[len] = '!'; TS_ASSERT_EQUALS(msg.Serialize(buf) - (buf+len), 0); TS_ASSERT_EQUALS(buf[len], '!'); CNetMessage* msg2 = CNetMessageFactory::CreateMessage(buf, len, script); TS_ASSERT_STR_EQUALS(((CSimulationMessage*)msg2)->ToString(), "CSimulationMessage { m_Client: 1, m_Player: 2, m_Turn: 3, m_Data: [4] }"); delete msg2; delete[] buf; } }; Index: ps/trunk/source/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 24175) +++ ps/trunk/source/ps/Game.cpp (revision 24176) @@ -1,471 +1,469 @@ /* Copyright (C) 2020 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 "Game.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleManager.h" #include "graphics/UnitManager.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "lib/config2.h" #include "lib/timer.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Profile.h" #include "ps/Replay.h" #include "ps/Shapes.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/system/ReplayTurnManager.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" extern bool g_GameRestarted; extern GameLoopState* g_AtlasGameLoop; /** * Globally accessible pointer to the CGame object. **/ CGame *g_Game=NULL; const CStr CGame::EventNameSimulationUpdate = "SimulationUpdate"; /** * Constructor * **/ CGame::CGame(bool replayLog): m_World(new CWorld(this)), m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptRuntime, m_World->GetTerrain())), m_GameView(CRenderer::IsInitialised() ? new CGameView(this) : nullptr), m_GameStarted(false), m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1), m_ViewedPlayerID(-1), m_IsSavedGame(false), m_IsVisualReplay(false), m_ReplayStream(NULL) { // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? if (replayLog) m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); else m_ReplayLogger = new CDummyReplayLogger(); // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); } /** * Destructor * **/ CGame::~CGame() { // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); if (m_ReplayLogger && m_GameStarted) m_ReplayLogger->SaveMetadata(*m_Simulation2); delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_World; delete m_ReplayLogger; delete m_ReplayStream; } void CGame::SetTurnManager(CTurnManager* turnManager) { if (m_TurnManager) delete m_TurnManager; m_TurnManager = turnManager; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::LoadVisualReplayData() { ENSURE(m_IsVisualReplay); ENSURE(!m_ReplayPath.empty()); ENSURE(m_ReplayStream); CReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager()); u32 currentTurn = 0; std::string type; while ((*m_ReplayStream >> type).good()) { if (type == "turn") { u32 turn = 0; u32 turnLength = 0; *m_ReplayStream >> turn >> turnLength; ENSURE(turn == currentTurn && "You tried to replay a commands.txt file of a rejoined client. Please use the host's file."); replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); } else if (type == "cmd") { player_id_t player; *m_ReplayStream >> player; std::string line; std::getline(*m_ReplayStream, line); replayTurnMgr->StoreReplayCommand(currentTurn, player, line); } else if (type == "hash" || type == "hash-quick") { bool quick = (type == "hash-quick"); std::string replayHash; *m_ReplayStream >> replayHash; replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick); } else if (type == "end") ++currentTurn; else CancelLoad(L"Failed to load replay data (unrecognized content)"); } SAFE_DELETE(m_ReplayStream); m_FinalReplayTurn = currentTurn > 0 ? currentTurn - 1 : 0; replayTurnMgr->StoreFinalReplayTurn(m_FinalReplayTurn); return 0; } bool CGame::StartVisualReplay(const OsPath& replayPath) { debug_printf("Starting to replay %s\n", replayPath.string8().c_str()); m_IsVisualReplay = true; SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); m_ReplayPath = replayPath; m_ReplayStream = new std::ifstream(OsString(replayPath).c_str()); std::string type; ENSURE((*m_ReplayStream >> type).good() && type == "start"); std::string line; std::getline(*m_ReplayStream, line); const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - JS::RootedValue attribs(cx); + JS::RootedValue attribs(rq.cx); scriptInterface.ParseJSON(line, &attribs); StartGame(&attribs, ""); return true; } /** * Initializes the game with the set of attributes provided. * Makes calls to initialize the game view, world, and simulation objects. * Calls are made to facilitate progress reporting of the initialization. **/ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState) { const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); m_InitialSavedState = savedState; m_IsSavedGame = !savedState.empty(); m_Simulation2->SetInitAttributes(attribs); std::string mapType; scriptInterface.GetProperty(attribs, "mapType", mapType); float speed; if (scriptInterface.HasProperty(attribs, "gameSpeed") && scriptInterface.GetProperty(attribs, "gameSpeed", speed)) SetSimRate(speed); LDR_BeginRegistering(); RegMemFun(m_Simulation2, &CSimulation2::ProgressiveLoad, L"Simulation init", 1000); // RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization // overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default // values. At the minute, it's just lighting settings, but could be extended to store camera position. // Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at // some point to be stored in the world object? if (m_GameView) m_GameView->RegisterInit(); if (mapType == "random") { // Load random map attributes std::wstring scriptFile; - JS::RootedValue settings(cx); + JS::RootedValue settings(rq.cx); scriptInterface.GetProperty(attribs, "script", scriptFile); scriptInterface.GetProperty(attribs, "settings", &settings); m_World->RegisterInitRMS(scriptFile, scriptInterface.GetJSRuntime(), settings, m_PlayerID); } else { std::wstring mapFile; - JS::RootedValue settings(cx); + JS::RootedValue settings(rq.cx); scriptInterface.GetProperty(attribs, "map", mapFile); scriptInterface.GetProperty(attribs, "settings", &settings); m_World->RegisterInit(mapFile, scriptInterface.GetJSRuntime(), settings, m_PlayerID); } if (m_GameView) RegMemFun(g_Renderer.GetSingletonPtr()->GetWaterManager(), &WaterManager::LoadWaterTextures, L"LoadWaterTextures", 80); if (m_IsSavedGame) RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000); if (m_IsVisualReplay) RegMemFun(this, &CGame::LoadVisualReplayData, L"Loading visual replay data", 1000); LDR_EndRegistering(); } int CGame::LoadInitialState() { ENSURE(m_IsSavedGame); ENSURE(!m_InitialSavedState.empty()); std::string state; m_InitialSavedState.swap(state); // deletes the original to save a bit of memory std::stringstream stream(state); bool ok = m_Simulation2->DeserializeState(stream); if (!ok) { CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game."); return 0; } return 0; } /** * Game initialization has been completed. Set game started flag and start the session. * * @return PSRETURN 0 **/ PSRETURN CGame::ReallyStartGame() { // Call the script function InitGame only for new games, not saved games if (!m_IsSavedGame) { // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) // that needs to be done before setting up the AI and shouldn't be done in Atlas if (!g_AtlasGameLoop->running) m_Simulation2->PreInitGame(); m_Simulation2->InitGame(); } // We need to do an initial Interpolate call to set up all the models etc, // because Update might never interpolate (e.g. if the game starts paused) // and we could end up rendering before having set up any models (so they'd // all be invisible) Interpolate(0, 0); m_GameStarted=true; // Render a frame to begin loading assets if (CRenderer::IsInitialised()) Render(); if (g_NetClient) g_NetClient->LoadFinished(); // Call the reallyStartGame GUI function, but only if it exists if (g_GUI && g_GUI->GetPageCount()) { shared_ptr scriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); - JSContext* cx = scriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue global(cx, scriptInterface->GetGlobalObject()); + ScriptInterface::Request rq(scriptInterface); + + JS::RootedValue global(rq.cx, scriptInterface->GetGlobalObject()); if (scriptInterface->HasProperty(global, "reallyStartGame")) scriptInterface->CallFunctionVoid(global, "reallyStartGame"); } debug_printf("GAME STARTED, ALL INIT COMPLETE\n"); // The call tree we've built for pregame probably isn't useful in-game. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); g_GameRestarted = true; return 0; } int CGame::GetPlayerID() { return m_PlayerID; } void CGame::SetPlayerID(player_id_t playerID) { m_PlayerID = playerID; m_ViewedPlayerID = playerID; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::GetViewedPlayerID() { return m_ViewedPlayerID; } void CGame::SetViewedPlayerID(player_id_t playerID) { m_ViewedPlayerID = playerID; } void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { if (m_ReplayLogger) m_ReplayLogger->StartGame(attribs); RegisterInit(attribs, savedState); } // TODO: doInterpolate is optional because Atlas interpolates explicitly, // so that it has more control over the update rate. The game might want to // do the same, and then doInterpolate should be redundant and removed. void CGame::Update(const double deltaRealTime, bool doInterpolate) { if (m_Paused || !m_TurnManager) return; const double deltaSimTime = deltaRealTime * m_SimRate; if (deltaSimTime) { // To avoid confusing the profiler, we need to trigger the new turn // while we're not nested inside any PROFILE blocks if (m_TurnManager->WillUpdate(deltaSimTime)) g_Profiler.Turn(); // At the normal sim rate, we currently want to render at least one // frame per simulation turn, so let maxTurns be 1. But for fast-forward // sim rates we want to allow more, so it's not bounded by framerate, // so just use the sim rate itself as the number of turns per frame. size_t maxTurns = (size_t)m_SimRate; if (m_TurnManager->Update(deltaSimTime, maxTurns)) { { PROFILE3("gui sim update"); g_GUI->SendEventToAll(EventNameSimulationUpdate); } GetView()->GetLOSTexture().MakeDirty(); } if (CRenderer::IsInitialised()) g_Renderer.GetTimeManager().Update(deltaSimTime); } if (doInterpolate) m_TurnManager->Interpolate(deltaSimTime, deltaRealTime); } void CGame::Interpolate(float simFrameLength, float realFrameLength) { if (!m_TurnManager) return; m_TurnManager->Interpolate(simFrameLength, realFrameLength); } static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f); void CGame::CachePlayerColors() { m_PlayerColors.clear(); CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); if (!cmpPlayerManager) return; int numPlayers = cmpPlayerManager->GetNumPlayers(); m_PlayerColors.resize(numPlayers); for (int i = 0; i < numPlayers; ++i) { CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i)); if (!cmpPlayer) m_PlayerColors[i] = BrokenColor; else m_PlayerColors[i] = cmpPlayer->GetDisplayedColor(); } } CColor CGame::GetPlayerColor(player_id_t player) const { if (player < 0 || player >= (int)m_PlayerColors.size()) return BrokenColor; return m_PlayerColors[player]; } bool CGame::IsGameFinished() const { for (const std::pair& p : m_Simulation2->GetEntitiesWithInterface(IID_Player)) { CmpPtr cmpPlayer(*m_Simulation2, p.first); if (cmpPlayer && cmpPlayer->GetState() == "won") return true; } return false; } Index: ps/trunk/source/ps/GameSetup/HWDetect.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24175) +++ ps/trunk/source/ps/GameSetup/HWDetect.cpp (revision 24176) @@ -1,720 +1,718 @@ /* Copyright (C) 2020 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 "lib/ogl.h" #if CONFIG2_AUDIO #include "lib/snd.h" #endif #include "lib/svn_revision.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/external_libraries/libsdl.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/posix/posix_utsname.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/gfx.h" #include "lib/sysdep/numa.h" #include "lib/sysdep/os_cpu.h" #if ARCH_X86_X64 # include "lib/sysdep/arch/x86_x64/cache.h" # include "lib/sysdep/arch/x86_x64/topology.h" #endif #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Debug.h" #include "ps/UserReport.h" #include "ps/VideoMode.h" // TODO: Support OpenGL platforms which don’t use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #include #include // Define the GLX_MESA_query_renderer macros if built with // an old Mesa (<10.0) that doesn't provide them #ifndef GLX_MESA_query_renderer #define GLX_MESA_query_renderer 1 #define GLX_RENDERER_VENDOR_ID_MESA 0x8183 #define GLX_RENDERER_DEVICE_ID_MESA 0x8184 #define GLX_RENDERER_VERSION_MESA 0x8185 #define GLX_RENDERER_ACCELERATED_MESA 0x8186 #define GLX_RENDERER_VIDEO_MEMORY_MESA 0x8187 #define GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA 0x8188 #define GLX_RENDERER_PREFERRED_PROFILE_MESA 0x8189 #define GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA 0x818A #define GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA 0x818B #define GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA 0x818C #define GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA 0x818D #define GLX_RENDERER_ID_MESA 0x818E #endif /* GLX_MESA_query_renderer */ #endif static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings); static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings); #if ARCH_X86_X64 void ConvertCaches(const ScriptInterface& scriptInterface, x86_x64::IdxCache idxCache, JS::MutableHandleValue ret) { - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - ScriptInterface::CreateArray(cx, ret); + ScriptInterface::CreateArray(rq, ret); for (size_t idxLevel = 0; idxLevel < x86_x64::Cache::maxLevels; ++idxLevel) { const x86_x64::Cache* pcache = x86_x64::Caches(idxCache+idxLevel); if (pcache->m_Type == x86_x64::Cache::kNull || pcache->m_NumEntries == 0) continue; - JS::RootedValue cache(cx); + JS::RootedValue cache(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &cache, "type", static_cast(pcache->m_Type), "level", static_cast(pcache->m_Level), "associativity", static_cast(pcache->m_Associativity), "linesize", static_cast(pcache->m_EntrySize), "sharedby", static_cast(pcache->m_SharedBy), "totalsize", static_cast(pcache->TotalSize())); scriptInterface.SetPropertyInt(ret, idxLevel, cache); } } void ConvertTLBs(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - ScriptInterface::CreateArray(cx, ret); + ScriptInterface::CreateArray(rq, ret); for(size_t i = 0; ; i++) { const x86_x64::Cache* ptlb = x86_x64::Caches(x86_x64::TLB+i); if (!ptlb) break; - JS::RootedValue tlb(cx); + JS::RootedValue tlb(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &tlb, "type", static_cast(ptlb->m_Type), "level", static_cast(ptlb->m_Level), "associativity", static_cast(ptlb->m_Associativity), "pagesize", static_cast(ptlb->m_EntrySize), "entries", static_cast(ptlb->m_NumEntries)); scriptInterface.SetPropertyInt(ret, i, tlb); } } #endif void SetDisableAudio(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), bool disabled) { g_DisableAudio = disabled; } void RunHardwareDetection() { TIMER(L"RunHardwareDetection"); ScriptInterface scriptInterface("Engine", "HWDetect", g_ScriptRuntime); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + + ScriptInterface::Request rq(scriptInterface); JSI_Debug::RegisterScriptFunctions(scriptInterface); // Engine.DisplayErrorDialog JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); scriptInterface.RegisterFunction("SetDisableAudio"); // Load the detection script: const wchar_t* scriptName = L"hwdetect/hwdetect.js"; CVFSFile file; if (file.Load(g_VFS, scriptName) != PSRETURN_OK) { LOGERROR("Failed to load hardware detection script"); return; } std::string code = file.DecodeUTF8(); // assume it's UTF-8 scriptInterface.LoadScript(scriptName, code); // Collect all the settings we'll pass to the script: // (We'll use this same data for the opt-in online reporting system, so it // includes some fields that aren't directly useful for the hwdetect script) - JS::RootedValue settings(cx); - ScriptInterface::CreateObject(cx, &settings); + JS::RootedValue settings(rq.cx); + ScriptInterface::CreateObject(rq, &settings); scriptInterface.SetProperty(settings, "os_unix", OS_UNIX); scriptInterface.SetProperty(settings, "os_bsd", OS_BSD); scriptInterface.SetProperty(settings, "os_linux", OS_LINUX); scriptInterface.SetProperty(settings, "os_android", OS_ANDROID); scriptInterface.SetProperty(settings, "os_macosx", OS_MACOSX); scriptInterface.SetProperty(settings, "os_win", OS_WIN); scriptInterface.SetProperty(settings, "arch_ia32", ARCH_IA32); scriptInterface.SetProperty(settings, "arch_amd64", ARCH_AMD64); scriptInterface.SetProperty(settings, "arch_arm", ARCH_ARM); scriptInterface.SetProperty(settings, "arch_aarch64", ARCH_AARCH64); #ifdef NDEBUG scriptInterface.SetProperty(settings, "build_debug", 0); #else scriptInterface.SetProperty(settings, "build_debug", 1); #endif scriptInterface.SetProperty(settings, "build_opengles", CONFIG2_GLES); scriptInterface.SetProperty(settings, "build_datetime", std::string(__DATE__ " " __TIME__)); scriptInterface.SetProperty(settings, "build_revision", std::wstring(svn_revision)); scriptInterface.SetProperty(settings, "build_msc", (int)MSC_VERSION); scriptInterface.SetProperty(settings, "build_icc", (int)ICC_VERSION); scriptInterface.SetProperty(settings, "build_gcc", (int)GCC_VERSION); scriptInterface.SetProperty(settings, "build_clang", (int)CLANG_VERSION); scriptInterface.SetProperty(settings, "gfx_card", gfx::CardName()); scriptInterface.SetProperty(settings, "gfx_drv_ver", gfx::DriverInfo()); #if CONFIG2_AUDIO scriptInterface.SetProperty(settings, "snd_card", snd_card); scriptInterface.SetProperty(settings, "snd_drv_ver", snd_drv_ver); #endif ReportSDL(scriptInterface, settings); ReportGLLimits(scriptInterface, settings); scriptInterface.SetProperty(settings, "video_desktop_xres", g_VideoMode.GetDesktopXRes()); scriptInterface.SetProperty(settings, "video_desktop_yres", g_VideoMode.GetDesktopYRes()); scriptInterface.SetProperty(settings, "video_desktop_bpp", g_VideoMode.GetDesktopBPP()); scriptInterface.SetProperty(settings, "video_desktop_freq", g_VideoMode.GetDesktopFreq()); struct utsname un; uname(&un); scriptInterface.SetProperty(settings, "uname_sysname", std::string(un.sysname)); scriptInterface.SetProperty(settings, "uname_release", std::string(un.release)); scriptInterface.SetProperty(settings, "uname_version", std::string(un.version)); scriptInterface.SetProperty(settings, "uname_machine", std::string(un.machine)); #if OS_LINUX { std::ifstream ifs("/etc/lsb-release"); if (ifs.good()) { std::string str((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); scriptInterface.SetProperty(settings, "linux_release", str); } } #endif scriptInterface.SetProperty(settings, "cpu_identifier", std::string(cpu_IdentifierString())); scriptInterface.SetProperty(settings, "cpu_frequency", os_cpu_ClockFrequency()); scriptInterface.SetProperty(settings, "cpu_pagesize", (u32)os_cpu_PageSize()); scriptInterface.SetProperty(settings, "cpu_largepagesize", (u32)os_cpu_LargePageSize()); scriptInterface.SetProperty(settings, "cpu_numprocs", (u32)os_cpu_NumProcessors()); #if ARCH_X86_X64 scriptInterface.SetProperty(settings, "cpu_numpackages", (u32)topology::NumPackages()); scriptInterface.SetProperty(settings, "cpu_coresperpackage", (u32)topology::CoresPerPackage()); scriptInterface.SetProperty(settings, "cpu_logicalpercore", (u32)topology::LogicalPerCore()); scriptInterface.SetProperty(settings, "cpu_numcaches", (u32)topology::NumCaches()); #endif scriptInterface.SetProperty(settings, "numa_numnodes", (u32)numa_NumNodes()); scriptInterface.SetProperty(settings, "numa_factor", numa_Factor()); scriptInterface.SetProperty(settings, "numa_interleaved", numa_IsMemoryInterleaved()); scriptInterface.SetProperty(settings, "ram_total", (u32)os_cpu_MemorySize()); scriptInterface.SetProperty(settings, "ram_total_os", (u32)os_cpu_QueryMemorySize()); #if ARCH_X86_X64 scriptInterface.SetProperty(settings, "x86_vendor", (u32)x86_x64::Vendor()); scriptInterface.SetProperty(settings, "x86_model", (u32)x86_x64::Model()); scriptInterface.SetProperty(settings, "x86_family", (u32)x86_x64::Family()); u32 caps0, caps1, caps2, caps3; x86_x64::GetCapBits(&caps0, &caps1, &caps2, &caps3); scriptInterface.SetProperty(settings, "x86_caps[0]", caps0); scriptInterface.SetProperty(settings, "x86_caps[1]", caps1); scriptInterface.SetProperty(settings, "x86_caps[2]", caps2); scriptInterface.SetProperty(settings, "x86_caps[3]", caps3); - JS::RootedValue tmpVal(cx); + JS::RootedValue tmpVal(rq.cx); ConvertCaches(scriptInterface, x86_x64::L1I, &tmpVal); scriptInterface.SetProperty(settings, "x86_icaches", tmpVal); ConvertCaches(scriptInterface, x86_x64::L1D, &tmpVal); scriptInterface.SetProperty(settings, "x86_dcaches", tmpVal); ConvertTLBs(scriptInterface, &tmpVal); scriptInterface.SetProperty(settings, "x86_tlbs", tmpVal); #endif scriptInterface.SetProperty(settings, "timer_resolution", timer_Resolution()); // The version should be increased for every meaningful change. const int reportVersion = 13; // Send the same data to the reporting system g_UserReporter.SubmitReport( "hwdetect", reportVersion, scriptInterface.StringifyJSON(&settings, false), scriptInterface.StringifyJSON(&settings, true)); // Run the detection script: - JS::RootedValue global(cx, scriptInterface.GetGlobalObject()); + JS::RootedValue global(rq.cx, scriptInterface.GetGlobalObject()); scriptInterface.CallFunctionVoid(global, "RunHardwareDetection", settings); } static void ReportSDL(const ScriptInterface& scriptInterface, JS::HandleValue settings) { SDL_version build, runtime; SDL_VERSION(&build); char version[16]; snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", build.major, build.minor, build.patch); scriptInterface.SetProperty(settings, "sdl_build_version", version); SDL_GetVersion(&runtime); snprintf(version, ARRAY_SIZE(version), "%d.%d.%d", runtime.major, runtime.minor, runtime.patch); scriptInterface.SetProperty(settings, "sdl_runtime_version", version); // This is null in atlas (and further the call triggers an assertion). const char* backend = g_VideoMode.GetWindow() ? GetSDLSubsystem(g_VideoMode.GetWindow()) : "none"; scriptInterface.SetProperty(settings, "sdl_video_backend", backend ? backend : "unknown"); } static void ReportGLLimits(const ScriptInterface& scriptInterface, JS::HandleValue settings) { const char* errstr = "(error)"; #define INTEGER(id) do { \ GLint i = -1; \ glGetIntegerv(GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, i); \ } while (false) #define INTEGER2(id) do { \ GLint i[2] = { -1, -1 }; \ glGetIntegerv(GL_##id, i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", i[1]); \ } \ } while (false) #define FLOAT(id) do { \ GLfloat f = std::numeric_limits::quiet_NaN(); \ glGetFloatv(GL_##id, &f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #id, f); \ } while (false) #define FLOAT2(id) do { \ GLfloat f[2] = { std::numeric_limits::quiet_NaN(), std::numeric_limits::quiet_NaN() }; \ glGetFloatv(GL_##id, f); \ if (ogl_SquelchError(GL_INVALID_ENUM)) { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", errstr); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", errstr); \ } else { \ scriptInterface.SetProperty(settings, "GL_" #id "[0]", f[0]); \ scriptInterface.SetProperty(settings, "GL_" #id "[1]", f[1]); \ } \ } while (false) #define STRING(id) do { \ const char* c = (const char*)glGetString(GL_##id); \ if (!c) c = ""; \ if (ogl_SquelchError(GL_INVALID_ENUM)) c = errstr; \ scriptInterface.SetProperty(settings, "GL_" #id, std::string(c)); \ } while (false) #define QUERY(target, pname) do { \ GLint i = -1; \ pglGetQueryivARB(GL_##target, GL_##pname, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_" #target ".GL_" #pname, i); \ } while (false) #define VERTEXPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_VERTEX_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_VERTEX_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define FRAGMENTPROGRAM(id) do { \ GLint i = -1; \ pglGetProgramivARB(GL_FRAGMENT_PROGRAM_ARB, GL_##id, &i); \ if (ogl_SquelchError(GL_INVALID_ENUM)) \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, errstr); \ else \ scriptInterface.SetProperty(settings, "GL_FRAGMENT_PROGRAM_ARB.GL_" #id, i); \ } while (false) #define BOOL(id) INTEGER(id) ogl_WarnIfError(); // Core OpenGL 1.3: // (We don't bother checking extension strings for anything older than 1.3; // it'll just produce harmless warnings) STRING(VERSION); STRING(VENDOR); STRING(RENDERER); STRING(EXTENSIONS); #if !CONFIG2_GLES INTEGER(MAX_LIGHTS); INTEGER(MAX_CLIP_PLANES); // Skip MAX_COLOR_MATRIX_STACK_DEPTH (only in imaging subset) INTEGER(MAX_MODELVIEW_STACK_DEPTH); INTEGER(MAX_PROJECTION_STACK_DEPTH); INTEGER(MAX_TEXTURE_STACK_DEPTH); #endif INTEGER(SUBPIXEL_BITS); #if !CONFIG2_GLES INTEGER(MAX_3D_TEXTURE_SIZE); #endif INTEGER(MAX_TEXTURE_SIZE); INTEGER(MAX_CUBE_MAP_TEXTURE_SIZE); #if !CONFIG2_GLES INTEGER(MAX_PIXEL_MAP_TABLE); INTEGER(MAX_NAME_STACK_DEPTH); INTEGER(MAX_LIST_NESTING); INTEGER(MAX_EVAL_ORDER); #endif INTEGER2(MAX_VIEWPORT_DIMS); #if !CONFIG2_GLES INTEGER(MAX_ATTRIB_STACK_DEPTH); INTEGER(MAX_CLIENT_ATTRIB_STACK_DEPTH); INTEGER(AUX_BUFFERS); BOOL(RGBA_MODE); BOOL(INDEX_MODE); BOOL(DOUBLEBUFFER); BOOL(STEREO); #endif FLOAT2(ALIASED_POINT_SIZE_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_POINT_SIZE_RANGE); FLOAT(SMOOTH_POINT_SIZE_GRANULARITY); #endif FLOAT2(ALIASED_LINE_WIDTH_RANGE); #if !CONFIG2_GLES FLOAT2(SMOOTH_LINE_WIDTH_RANGE); FLOAT(SMOOTH_LINE_WIDTH_GRANULARITY); // Skip MAX_CONVOLUTION_WIDTH, MAX_CONVOLUTION_HEIGHT (only in imaging subset) INTEGER(MAX_ELEMENTS_INDICES); INTEGER(MAX_ELEMENTS_VERTICES); INTEGER(MAX_TEXTURE_UNITS); #endif INTEGER(SAMPLE_BUFFERS); INTEGER(SAMPLES); // TODO: compressed texture formats INTEGER(RED_BITS); INTEGER(GREEN_BITS); INTEGER(BLUE_BITS); INTEGER(ALPHA_BITS); #if !CONFIG2_GLES INTEGER(INDEX_BITS); #endif INTEGER(DEPTH_BITS); INTEGER(STENCIL_BITS); #if !CONFIG2_GLES INTEGER(ACCUM_RED_BITS); INTEGER(ACCUM_GREEN_BITS); INTEGER(ACCUM_BLUE_BITS); INTEGER(ACCUM_ALPHA_BITS); #endif #if !CONFIG2_GLES // Core OpenGL 2.0 (treated as extensions): if (ogl_HaveExtension("GL_EXT_texture_lod_bias")) { FLOAT(MAX_TEXTURE_LOD_BIAS_EXT); } if (ogl_HaveExtension("GL_ARB_occlusion_query")) { QUERY(SAMPLES_PASSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_shading_language_100")) { STRING(SHADING_LANGUAGE_VERSION_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader")) { INTEGER(MAX_VERTEX_ATTRIBS_ARB); INTEGER(MAX_VERTEX_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_VARYING_FLOATS_ARB); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS_ARB); } if (ogl_HaveExtension("GL_ARB_fragment_shader")) { INTEGER(MAX_FRAGMENT_UNIFORM_COMPONENTS_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_shader") || ogl_HaveExtension("GL_ARB_fragment_shader") || ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_TEXTURE_COORDS_ARB); } if (ogl_HaveExtension("GL_ARB_draw_buffers")) { INTEGER(MAX_DRAW_BUFFERS_ARB); } // Core OpenGL 3.0: if (ogl_HaveExtension("GL_EXT_gpu_shader4")) { INTEGER(MIN_PROGRAM_TEXEL_OFFSET); // no _EXT version of these in glext.h INTEGER(MAX_PROGRAM_TEXEL_OFFSET); } if (ogl_HaveExtension("GL_EXT_framebuffer_object")) { INTEGER(MAX_COLOR_ATTACHMENTS_EXT); INTEGER(MAX_RENDERBUFFER_SIZE_EXT); } if (ogl_HaveExtension("GL_EXT_framebuffer_multisample")) { INTEGER(MAX_SAMPLES_EXT); } if (ogl_HaveExtension("GL_EXT_texture_array")) { INTEGER(MAX_ARRAY_TEXTURE_LAYERS_EXT); } if (ogl_HaveExtension("GL_EXT_transform_feedback")) { INTEGER(MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS_EXT); INTEGER(MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS_EXT); } // Other interesting extensions: if (ogl_HaveExtension("GL_EXT_timer_query") || ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIME_ELAPSED, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_ARB_timer_query")) { QUERY(TIMESTAMP, QUERY_COUNTER_BITS); } if (ogl_HaveExtension("GL_EXT_texture_filter_anisotropic")) { FLOAT(MAX_TEXTURE_MAX_ANISOTROPY_EXT); } if (ogl_HaveExtension("GL_ARB_texture_rectangle")) { INTEGER(MAX_RECTANGLE_TEXTURE_SIZE_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program") || ogl_HaveExtension("GL_ARB_fragment_program")) { INTEGER(MAX_PROGRAM_MATRICES_ARB); INTEGER(MAX_PROGRAM_MATRIX_STACK_DEPTH_ARB); } if (ogl_HaveExtension("GL_ARB_vertex_program")) { VERTEXPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); if (ogl_HaveExtension("GL_ARB_fragment_program")) { // The spec seems to say these should be supported, but // Mesa complains about them so let's not bother /* VERTEXPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); VERTEXPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_fragment_program")) { FRAGMENTPROGRAM(MAX_PROGRAM_ENV_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_LOCAL_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_ATTRIBS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ALU_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INSTRUCTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEX_INDIRECTIONS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_TEMPORARIES_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_PARAMETERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ATTRIBS_ARB); if (ogl_HaveExtension("GL_ARB_vertex_program")) { // The spec seems to say these should be supported, but // Intel drivers on Windows complain about them so let's not bother /* FRAGMENTPROGRAM(MAX_PROGRAM_ADDRESS_REGISTERS_ARB); FRAGMENTPROGRAM(MAX_PROGRAM_NATIVE_ADDRESS_REGISTERS_ARB); */ } } if (ogl_HaveExtension("GL_ARB_geometry_shader4")) { INTEGER(MAX_GEOMETRY_TEXTURE_IMAGE_UNITS_ARB); INTEGER(MAX_GEOMETRY_OUTPUT_VERTICES_ARB); INTEGER(MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_UNIFORM_COMPONENTS_ARB); INTEGER(MAX_GEOMETRY_VARYING_COMPONENTS_ARB); INTEGER(MAX_VERTEX_VARYING_COMPONENTS_ARB); } #else // CONFIG2_GLES // Core OpenGL ES 2.0: STRING(SHADING_LANGUAGE_VERSION); INTEGER(MAX_VERTEX_ATTRIBS); INTEGER(MAX_VERTEX_UNIFORM_VECTORS); INTEGER(MAX_VARYING_VECTORS); INTEGER(MAX_COMBINED_TEXTURE_IMAGE_UNITS); INTEGER(MAX_VERTEX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_FRAGMENT_UNIFORM_VECTORS); INTEGER(MAX_TEXTURE_IMAGE_UNITS); INTEGER(MAX_RENDERBUFFER_SIZE); #endif // CONFIG2_GLES // TODO: Support OpenGL platforms which don’t use GLX as well. #if defined(SDL_VIDEO_DRIVER_X11) && !CONFIG2_GLES #define GLXQCR_INTEGER(id) do { \ unsigned int i = UINT_MAX; \ if (pglXQueryCurrentRendererIntegerMESA(id, &i)) \ scriptInterface.SetProperty(settings, #id, i); \ } while (false) #define GLXQCR_INTEGER2(id) do { \ unsigned int i[2] = { UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ } \ } while (false) #define GLXQCR_INTEGER3(id) do { \ unsigned int i[3] = { UINT_MAX, UINT_MAX, UINT_MAX }; \ if (pglXQueryCurrentRendererIntegerMESA(id, i)) { \ scriptInterface.SetProperty(settings, #id "[0]", i[0]); \ scriptInterface.SetProperty(settings, #id "[1]", i[1]); \ scriptInterface.SetProperty(settings, #id "[2]", i[2]); \ } \ } while (false) #define GLXQCR_STRING(id) do { \ const char* str = pglXQueryCurrentRendererStringMESA(id); \ if (str) \ scriptInterface.SetProperty(settings, #id ".string", str); \ } while (false) SDL_SysWMinfo wminfo; SDL_VERSION(&wminfo.version); const int ret = SDL_GetWindowWMInfo(g_VideoMode.GetWindow(), &wminfo); if (ret && wminfo.subsystem == SDL_SYSWM_X11) { Display* dpy = wminfo.info.x11.display; int scrnum = DefaultScreen(dpy); const char* glxexts = glXQueryExtensionsString(dpy, scrnum); scriptInterface.SetProperty(settings, "glx_extensions", glxexts); if (strstr(glxexts, "GLX_MESA_query_renderer") && pglXQueryCurrentRendererIntegerMESA && pglXQueryCurrentRendererStringMESA) { GLXQCR_INTEGER(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_INTEGER(GLX_RENDERER_DEVICE_ID_MESA); GLXQCR_INTEGER3(GLX_RENDERER_VERSION_MESA); GLXQCR_INTEGER(GLX_RENDERER_ACCELERATED_MESA); GLXQCR_INTEGER(GLX_RENDERER_VIDEO_MEMORY_MESA); GLXQCR_INTEGER(GLX_RENDERER_UNIFIED_MEMORY_ARCHITECTURE_MESA); GLXQCR_INTEGER(GLX_RENDERER_PREFERRED_PROFILE_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_CORE_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_COMPATIBILITY_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES_PROFILE_VERSION_MESA); GLXQCR_INTEGER2(GLX_RENDERER_OPENGL_ES2_PROFILE_VERSION_MESA); GLXQCR_STRING(GLX_RENDERER_VENDOR_ID_MESA); GLXQCR_STRING(GLX_RENDERER_DEVICE_ID_MESA); } } #endif // SDL_VIDEO_DRIVER_X11 } Index: ps/trunk/source/ps/ModInstaller.cpp =================================================================== --- ps/trunk/source/ps/ModInstaller.cpp (revision 24175) +++ ps/trunk/source/ps/ModInstaller.cpp (revision 24176) @@ -1,110 +1,113 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2020 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 "ModInstaller.h" #include "lib/file/vfs/vfs_util.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #include CModInstaller::CModInstaller(const OsPath& modsdir, const OsPath& tempdir) : m_ModsDir(modsdir), m_TempDir(tempdir / "_modscache"), m_CacheDir("cache/") { m_VFS = CreateVfs(); CreateDirectories(m_TempDir, 0700); } CModInstaller::~CModInstaller() { m_VFS.reset(); DeleteDirectory(m_TempDir); } CModInstaller::ModInstallationResult CModInstaller::Install( const OsPath& mod, const std::shared_ptr& scriptRuntime, bool keepFile) { const OsPath modTemp = m_TempDir / mod.Basename() / mod.Filename().ChangeExtension(L".zip"); CreateDirectories(modTemp.Parent(), 0700); if (keepFile) CopyFile(mod, modTemp, true); else wrename(mod, modTemp); // Load the mod to VFS if (m_VFS->Mount(m_CacheDir, m_TempDir / "") != INFO::OK) return FAIL_ON_VFS_MOUNT; CVFSFile modinfo; PSRETURN modinfo_status = modinfo.Load(m_VFS, m_CacheDir / modTemp.Basename() / "mod.json", false); m_VFS->Clear(); if (modinfo_status != PSRETURN_OK) return FAIL_ON_MOD_LOAD; // Extract the name of the mod - ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime); - JSContext* cx = scriptInterface.GetContext(); - JS::RootedValue json_val(cx); - if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val)) - return FAIL_ON_PARSE_JSON; - JS::RootedObject json_obj(cx, json_val.toObjectOrNull()); - JS::RootedValue name_val(cx); - if (!JS_GetProperty(cx, json_obj, "name", &name_val)) - return FAIL_ON_EXTRACT_NAME; CStr modName; - ScriptInterface::FromJSVal(cx, name_val, modName); - if (modName.empty()) - return FAIL_ON_EXTRACT_NAME; + { + ScriptInterface scriptInterface("Engine", "ModInstaller", scriptRuntime); + ScriptInterface::Request rq(scriptInterface); + + JS::RootedValue json_val(rq.cx); + if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json_val)) + return FAIL_ON_PARSE_JSON; + JS::RootedObject json_obj(rq.cx, json_val.toObjectOrNull()); + JS::RootedValue name_val(rq.cx); + if (!JS_GetProperty(rq.cx, json_obj, "name", &name_val)) + return FAIL_ON_EXTRACT_NAME; + ScriptInterface::FromJSVal(rq, name_val, modName); + if (modName.empty()) + return FAIL_ON_EXTRACT_NAME; + } const OsPath modDir = m_ModsDir / modName; const OsPath modPath = modDir / (modName + ".zip"); // Create a directory with the following structure: // mod-name/ // mod-name.zip CreateDirectories(modDir, 0700); if (wrename(modTemp, modPath) != 0) return FAIL_ON_MOD_MOVE; DeleteDirectory(modTemp.Parent()); #ifdef OS_WIN // On Windows, write the contents of mod.json to a separate file next to the archive: // mod-name/ // mod-name.zip // mod.json std::ofstream mod_json((modDir / "mod.json").string8()); if (mod_json.good()) { mod_json << modinfo.GetAsString(); mod_json.close(); } #endif // OS_WIN m_InstalledMods.emplace_back(modName); return SUCCESS; } const std::vector& CModInstaller::GetInstalledMods() const { return m_InstalledMods; } Index: ps/trunk/source/ps/Replay.cpp =================================================================== --- ps/trunk/source/ps/Replay.cpp (revision 24175) +++ ps/trunk/source/ps/Replay.cpp (revision 24176) @@ -1,343 +1,338 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "Replay.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/file/file_system.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/Game.h" #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" #include "ps/Mod.h" #include "ps/Util.h" #include "ps/VisualReplay.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptRuntime.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/components/ICmpGuiInterface.h" #include "simulation2/helpers/Player.h" #include "simulation2/helpers/SimulationCommand.h" #include "simulation2/Simulation2.h" #include "simulation2/system/CmpPtr.h" #include #include /** * Number of turns between two saved profiler snapshots. * Keep in sync with source/tools/replayprofile/graph.js */ static const int PROFILE_TURN_INTERVAL = 20; CReplayLogger::CReplayLogger(const ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface), m_Stream(NULL) { } CReplayLogger::~CReplayLogger() { delete m_Stream; } void CReplayLogger::StartGame(JS::MutableHandleValue attribs) { - JSContext* cx = m_ScriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(m_ScriptInterface); // Add timestamp, since the file-modification-date can change m_ScriptInterface.SetProperty(attribs, "timestamp", (double)std::time(nullptr)); // Add engine version and currently loaded mods for sanity checks when replaying m_ScriptInterface.SetProperty(attribs, "engine_version", engine_version); - JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface)); + JS::RootedValue mods(rq.cx, Mod::GetLoadedModsWithVersions(m_ScriptInterface)); m_ScriptInterface.SetProperty(attribs, "mods", mods); m_Directory = createDateIndexSubdirectory(VisualReplay::GetDirectoryPath()); debug_printf("Writing replay to %s\n", m_Directory.string8().c_str()); m_Stream = new std::ofstream(OsString(m_Directory / L"commands.txt").c_str(), std::ofstream::out | std::ofstream::trunc); *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs, false) << "\n"; } void CReplayLogger::Turn(u32 n, u32 turnLength, std::vector& commands) { - JSContext* cx = m_ScriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(m_ScriptInterface); *m_Stream << "turn " << n << " " << turnLength << "\n"; for (SimulationCommand& command : commands) *m_Stream << "cmd " << command.player << " " << m_ScriptInterface.StringifyJSON(&command.data, false) << "\n"; *m_Stream << "end\n"; m_Stream->flush(); } void CReplayLogger::Hash(const std::string& hash, bool quick) { if (quick) *m_Stream << "hash-quick " << Hexify(hash) << "\n"; else *m_Stream << "hash " << Hexify(hash) << "\n"; } void CReplayLogger::SaveMetadata(const CSimulation2& simulation) { CmpPtr cmpGuiInterface(simulation, SYSTEM_ENTITY); if (!cmpGuiInterface) { LOGERROR("Could not save replay metadata!"); return; } ScriptInterface& scriptInterface = simulation.GetScriptInterface(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - JS::RootedValue arg(cx); - JS::RootedValue metadata(cx); + JS::RootedValue arg(rq.cx); + JS::RootedValue metadata(rq.cx); cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetReplayMetadata", arg, &metadata); const OsPath fileName = g_Game->GetReplayLogger().GetDirectory() / L"metadata.json"; CreateDirectories(fileName.Parent(), 0700); std::ofstream stream (OsString(fileName).c_str(), std::ofstream::out | std::ofstream::trunc); stream << scriptInterface.StringifyJSON(&metadata, false); stream.close(); debug_printf("Saved replay metadata to %s\n", fileName.string8().c_str()); } OsPath CReplayLogger::GetDirectory() const { return m_Directory; } //////////////////////////////////////////////////////////////// CReplayPlayer::CReplayPlayer() : m_Stream(NULL) { } CReplayPlayer::~CReplayPlayer() { delete m_Stream; } void CReplayPlayer::Load(const OsPath& path) { ENSURE(!m_Stream); m_Stream = new std::ifstream(OsString(path).c_str()); ENSURE(m_Stream->good()); } CStr CReplayPlayer::ModListToString(const std::vector>& list) const { CStr text; for (const std::vector& mod : list) text += mod[0] + " (" + mod[1] + ")\n"; return text; } void CReplayPlayer::CheckReplayMods(const ScriptInterface& scriptInterface, JS::HandleValue attribs) const { - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); std::vector> replayMods; scriptInterface.GetProperty(attribs, "mods", replayMods); std::vector> enabledMods; - JS::RootedValue enabledModsJS(cx, Mod::GetLoadedModsWithVersions(scriptInterface)); - scriptInterface.FromJSVal(cx, enabledModsJS, enabledMods); + JS::RootedValue enabledModsJS(rq.cx, Mod::GetLoadedModsWithVersions(scriptInterface)); + scriptInterface.FromJSVal(rq, enabledModsJS, enabledMods); CStr warn; if (replayMods.size() != enabledMods.size()) warn = "The number of enabled mods does not match the mods of the replay."; else for (size_t i = 0; i < replayMods.size(); ++i) { if (replayMods[i][0] != enabledMods[i][0]) { warn = "The enabled mods don't match the mods of the replay."; break; } else if (replayMods[i][1] != enabledMods[i][1]) { warn = "The mod '" + replayMods[i][0] + "' with version '" + replayMods[i][1] + "' is required by the replay file, but version '" + enabledMods[i][1] + "' is present!"; break; } } if (!warn.empty()) LOGWARNING("%s\nThe mods of the replay are:\n%s\nThese mods are enabled:\n%s", warn, ModListToString(replayMods), ModListToString(enabledMods)); } void CReplayPlayer::Replay(const bool serializationtest, const int rejointestturn, const bool ooslog, const bool testHashFull, const bool testHashQuick) { ENSURE(m_Stream); new CProfileViewer; new CProfileManager; g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptRuntime::CreateRuntime(runtimeSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptRuntime); g_Game = new CGame(false); if (serializationtest) g_Game->GetSimulation2()->EnableSerializationTest(); if (rejointestturn >= 0) g_Game->GetSimulation2()->EnableRejoinTest(rejointestturn); if (ooslog) g_Game->GetSimulation2()->EnableOOSLog(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); // Initialise h_mgr so it doesn't crash when emitting sounds h_mgr_init(); std::vector commands; u32 turn = 0; u32 turnLength = 0; { - JSContext* cx = g_Game->GetSimulation2()->GetScriptInterface().GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(g_Game->GetSimulation2()->GetScriptInterface()); std::string type; while ((*m_Stream >> type).good()) { if (type == "start") { std::string line; std::getline(*m_Stream, line); - JS::RootedValue attribs(cx); + JS::RootedValue attribs(rq.cx); ENSURE(g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &attribs)); CheckReplayMods(g_Game->GetSimulation2()->GetScriptInterface(), attribs); g_Game->StartGame(&attribs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } else if (type == "turn") { *m_Stream >> turn >> turnLength; debug_printf("Turn %u (%u)...\n", turn, turnLength); } else if (type == "cmd") { player_id_t player; *m_Stream >> player; std::string line; std::getline(*m_Stream, line); - JS::RootedValue data(cx); + JS::RootedValue data(rq.cx); g_Game->GetSimulation2()->GetScriptInterface().ParseJSON(line, &data); g_Game->GetSimulation2()->GetScriptInterface().FreezeObject(data, true); - commands.emplace_back(SimulationCommand(player, cx, data)); + commands.emplace_back(SimulationCommand(player, rq.cx, data)); } else if (type == "hash" || type == "hash-quick") { std::string replayHash; *m_Stream >> replayHash; TestHash(type, replayHash, testHashFull, testHashQuick); } else if (type == "end") { { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); g_Game->GetSimulation2()->Update(turnLength, commands); commands.clear(); } g_Profiler.Frame(); if (turn % PROFILE_TURN_INTERVAL == 0) g_ProfileViewer.SaveToFile(); } else debug_printf("Unrecognised replay token %s\n", type.c_str()); } } SAFE_DELETE(m_Stream); g_Profiler2.SaveToFile(); std::string hash; bool ok = g_Game->GetSimulation2()->ComputeStateHash(hash, false); ENSURE(ok); debug_printf("# Final state: %s\n", Hexify(hash).c_str()); timer_DisplayClientTotals(); SAFE_DELETE(g_Game); // Must be explicitly destructed here to avoid callbacks from the JSAPI trying to use g_Profiler2 when // it's already destructed. g_ScriptRuntime.reset(); // Clean up delete &g_TexMan; delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); } void CReplayPlayer::TestHash(const std::string& hashType, const std::string& replayHash, const bool testHashFull, const bool testHashQuick) { bool quick = (hashType == "hash-quick"); if ((quick && !testHashQuick) || (!quick && !testHashFull)) return; std::string hash; ENSURE(g_Game->GetSimulation2()->ComputeStateHash(hash, quick)); std::string hexHash = Hexify(hash); if (hexHash == replayHash) debug_printf("%s ok (%s)\n", hashType.c_str(), hexHash.c_str()); else debug_printf("%s MISMATCH (%s != %s)\n", hashType.c_str(), hexHash.c_str(), replayHash.c_str()); } Index: ps/trunk/source/ps/scripting/JSInterface_Game.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24175) +++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24176) @@ -1,189 +1,186 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "JSInterface_Game.h" #include "graphics/Terrain.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Replay.h" #include "ps/World.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/system/TurnManager.h" #include "simulation2/Simulation2.h" #include "soundmanager/SoundManager.h" extern void EndGame(); bool JSI_Game::IsGameStarted(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_Game; } void JSI_Game::StartGame(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue attribs, int playerID) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(true); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); - JSContext* cxSim = sim->GetScriptInterface().GetContext(); - JSAutoRequest rqSim(cxSim); + ScriptInterface::Request rqSim(sim->GetScriptInterface()); - JS::RootedValue gameAttribs(cxSim, + JS::RootedValue gameAttribs(rqSim.cx, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), attribs)); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameAttribs, ""); } void JSI_Game::Script_EndGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { EndGame(); } int JSI_Game::GetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return -1; return g_Game->GetPlayerID(); } void JSI_Game::SetPlayerID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (!g_Game) return; g_Game->SetPlayerID(id); } void JSI_Game::SetViewedPlayer(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), int id) { if (!g_Game) return; g_Game->SetViewedPlayerID(id); } float JSI_Game::GetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_Game->GetSimRate(); } void JSI_Game::SetSimRate(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), float rate) { g_Game->SetSimRate(rate); } bool JSI_Game::IsPaused(ScriptInterface::CxPrivate* pCxPrivate) { if (!g_Game) { - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS_ReportError(cx, "Game is not started"); + ScriptInterface::Request rq(pCxPrivate); + JS_ReportError(rq.cx, "Game is not started"); return false; } return g_Game->m_Paused; } void JSI_Game::SetPaused(ScriptInterface::CxPrivate* pCxPrivate, bool pause, bool sendMessage) { if (!g_Game) { - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS_ReportError(cx, "Game is not started"); + ScriptInterface::Request rq(pCxPrivate); + JS_ReportError(rq.cx, "Game is not started"); return; } g_Game->m_Paused = pause; #if CONFIG2_AUDIO if (g_SoundManager) g_SoundManager->Pause(pause); #endif if (g_NetClient && sendMessage) g_NetClient->SendPausedMessage(pause); } bool JSI_Game::IsVisualReplay(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return false; return g_Game->IsVisualReplay(); } std::wstring JSI_Game::GetCurrentReplayDirectory(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_Game) return std::wstring(); if (g_Game->IsVisualReplay()) return g_Game->GetReplayPath().Parent().Filename().string(); return g_Game->GetReplayLogger().GetDirectory().Filename().string(); } void JSI_Game::EnableTimeWarpRecording(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void JSI_Game::RewindTimeWarp(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void JSI_Game::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 JSI_Game::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("IsGameStarted"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("EndGame"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("SetPlayerID"); scriptInterface.RegisterFunction("SetViewedPlayer"); scriptInterface.RegisterFunction("GetSimRate"); scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("IsPaused"); scriptInterface.RegisterFunction("SetPaused"); scriptInterface.RegisterFunction("IsVisualReplay"); scriptInterface.RegisterFunction("GetCurrentReplayDirectory"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("DumpTerrainMipmap"); } Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24175) +++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24176) @@ -1,127 +1,125 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "JSInterface_SavedGame.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/SavedGame.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" JS::Value JSI_SavedGame::GetSavedGames(ScriptInterface::CxPrivate* pCxPrivate) { return SavedGames::GetSavedGames(*(pCxPrivate->pScriptInterface)); } bool JSI_SavedGame::DeleteSavedGame(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& name) { return SavedGames::DeleteSavedGame(name); } void JSI_SavedGame::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) < 0) LOGERROR("Failed to save game"); } void JSI_SavedGame::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) < 0) LOGERROR("Failed to save game"); } void JSI_SavedGame::QuickSave(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), JS::HandleValue GUIMetadata) { if (g_NetServer || g_NetClient) LOGERROR("Can't store quicksave during multiplayer!"); else if (g_Game) g_Game->GetTurnManager()->QuickSave(GUIMetadata); else LOGERROR("Can't store quicksave if game is not running!"); } void JSI_SavedGame::QuickLoad(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (g_NetServer || g_NetClient) LOGERROR("Can't load quicksave during multiplayer!"); else if (g_Game) g_Game->GetTurnManager()->QuickLoad(); else LOGERROR("Can't load quicksave if game is not running!"); } JS::Value JSI_SavedGame::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); + ScriptInterface::Request rqGui(pCxPrivate); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); // Load the saved game data from disk - JS::RootedValue guiContextMetadata(cxGui); + JS::RootedValue guiContextMetadata(rqGui.cx); std::string savedState; Status err = SavedGames::Load(name, *(pCxPrivate->pScriptInterface), &guiContextMetadata, savedState); if (err < 0) return JS::UndefinedValue(); g_Game = new CGame(true); { CSimulation2* sim = g_Game->GetSimulation2(); - JSContext* cxGame = sim->GetScriptInterface().GetContext(); - JSAutoRequest rq(cxGame); + ScriptInterface::Request rqGame(sim->GetScriptInterface()); - JS::RootedValue gameContextMetadata(cxGame, + JS::RootedValue gameContextMetadata(rqGame.cx, sim->GetScriptInterface().CloneValueFromOtherContext(*(pCxPrivate->pScriptInterface), guiContextMetadata)); - JS::RootedValue gameInitAttributes(cxGame); + JS::RootedValue gameInitAttributes(rqGame.cx); sim->GetScriptInterface().GetProperty(gameContextMetadata, "initAttributes", &gameInitAttributes); int playerID; sim->GetScriptInterface().GetProperty(gameContextMetadata, "playerID", playerID); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameInitAttributes, savedState); } return guiContextMetadata; } void JSI_SavedGame::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("GetSavedGames"); scriptInterface.RegisterFunction("DeleteSavedGame"); scriptInterface.RegisterFunction("SaveGame"); scriptInterface.RegisterFunction("SaveGamePrefix"); scriptInterface.RegisterFunction("QuickSave"); scriptInterface.RegisterFunction("QuickLoad"); scriptInterface.RegisterFunction("StartSavedGame"); } Index: ps/trunk/source/scriptinterface/NativeWrapperDecls.h =================================================================== --- ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24175) +++ ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24176) @@ -1,112 +1,112 @@ /* Copyright (C) 2017 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 #include // MaybeRef should be private, but has to be public due to a compiler bug in clang. // TODO: Make this private when the bug is fixed in all supported versions of clang. template struct MaybeRef; // Define lots of useful macros: // Varieties of comma-separated list to fit on the head/tail/whole of another comma-separated list #define NUMBERED_LIST_HEAD(z, i, data) data##i, #define NUMBERED_LIST_TAIL(z, i, data) ,data##i #define NUMBERED_LIST_TAIL_MAYBE_REF(z, i, data) , typename MaybeRef::Type #define NUMBERED_LIST_BALANCED(z, i, data) BOOST_PP_COMMA_IF(i) data##i #define NUMBERED_LIST_BALANCED_MAYBE_REF(z, i, data) BOOST_PP_COMMA_IF(i) typename MaybeRef::Type // TODO: We allow optional parameters when the C++ type can be converted from JS::UndefinedValue. // FromJSVal is expected to either set a##i or return false (otherwise we could get undefined // behaviour because some types have undefined values when not being initialized). // This is not very clear and also a bit fragile. Another problem is that the error reporting lacks // a bit. SpiderMonkey will throw a JS exception and abort the execution of the current function when // we return false here (without printing a callstack or additional detail telling that an argument // conversion failed). So we have two TODOs here: // 1. On the conceptual side: How to consistently work with optional parameters (or drop them completely?) // 2. On the technical side: Improve error handling, find a better way to ensure parameters are initialized #define CONVERT_ARG(z, i, data) \ bool typeConvRet##i; \ T##i a##i = ScriptInterface::AssignOrFromJSVal( \ - cx, \ + rq, \ i < args.length() ? args[i] : JS::UndefinedHandleValue, \ typeConvRet##i); \ if (!typeConvRet##i) return false; // List-generating macros, named roughly after their first list item #define TYPENAME_T0_HEAD(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_HEAD, typename T) // "typename T0, typename T1, " #define T0(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_BALANCED, T) // "T0, T1" #define T0_MAYBE_REF(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_BALANCED_MAYBE_REF, T) // "const T0&, T1" #define T0_TAIL(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL, T) // ", T0, T1" #define T0_TAIL_MAYBE_REF(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL_MAYBE_REF, T) // ", const T0&, T1" #define A0_TAIL(z, i) BOOST_PP_REPEAT_##z (i, NUMBERED_LIST_TAIL, a) // ", a0, a1" // Define RegisterFunction #define OVERLOADS(z, i, data) \ template \ void RegisterFunction(const char* name) const \ { \ Register(name, call, nargs()); \ } BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // JSFastNative-compatible function that wraps the function identified in the template argument list // (Definition comes later, since it depends on some things we haven't defined yet) #define OVERLOADS(z, i, data) \ template \ static bool call(JSContext* cx, uint argc, JS::Value* vp); BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // Similar, for class methods #define OVERLOADS(z, i, data) \ template \ static bool callMethod(JSContext* cx, uint argc, JS::Value* vp); BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // const methods #define OVERLOADS(z, i, data) \ template \ static bool callMethodConst(JSContext* cx, uint argc, JS::Value* vp); BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // Argument-number counter template static size_t nargs() { return sizeof...(Ts); } // Call the named property on the given object template bool CallFunction(JS::HandleValue val, const char* name, R& ret, const Ts&... params) const; // Implicit conversion from JS::Rooted* to JS::MutableHandle does not work with template argument deduction // (only exact type matches allowed). We need this overload to allow passing Rooted* using the & operator // (as people would expect it to work based on the SpiderMonkey rooting guide). template bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const Ts&... params) const; // This overload is for the case when a JS::MutableHandle type gets passed into CallFunction directly and // without requiring implicit conversion. template bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const Ts&... params) const; // Call the named property on the given object, with void return type template \ bool CallFunctionVoid(JS::HandleValue val, const char* name, const Ts&... params) const; Index: ps/trunk/source/scriptinterface/ScriptConversions.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptConversions.h (revision 24175) +++ ps/trunk/source/scriptinterface/ScriptConversions.h (revision 24176) @@ -1,112 +1,109 @@ /* Copyright (C) 2020 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 INCLUDED_SCRIPTCONVERSIONS #define INCLUDED_SCRIPTCONVERSIONS #include "ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" // for typed arrays #include -template static void ToJSVal_vector(JSContext* cx, JS::MutableHandleValue ret, const std::vector& val) +template static void ToJSVal_vector(const ScriptInterface::Request& rq, JS::MutableHandleValue ret, const std::vector& val) { - JSAutoRequest rq(cx); - JS::RootedObject obj(cx, JS_NewArrayObject(cx, 0)); + JS::RootedObject obj(rq.cx, JS_NewArrayObject(rq.cx, 0)); if (!obj) { ret.setUndefined(); return; } ENSURE(val.size() <= std::numeric_limits::max()); for (u32 i = 0; i < val.size(); ++i) { - JS::RootedValue el(cx); - ScriptInterface::ToJSVal(cx, &el, val[i]); - JS_SetElement(cx, obj, i, el); + JS::RootedValue el(rq.cx); + ScriptInterface::ToJSVal(rq, &el, val[i]); + JS_SetElement(rq.cx, obj, i, el); } ret.setObject(*obj); } -#define FAIL(msg) STMT(JS_ReportError(cx, msg); return false) +#define FAIL(msg) STMT(JS_ReportError(rq.cx, msg); return false) -template static bool FromJSVal_vector(JSContext* cx, JS::HandleValue v, std::vector& out) +template static bool FromJSVal_vector(const ScriptInterface::Request& rq, JS::HandleValue v, std::vector& out) { - JSAutoRequest rq(cx); - JS::RootedObject obj(cx); + JS::RootedObject obj(rq.cx); if (!v.isObject()) FAIL("Argument must be an array"); bool isArray; obj = &v.toObject(); - if ((!JS_IsArrayObject(cx, obj, &isArray) || !isArray) && !JS_IsTypedArrayObject(obj)) + if ((!JS_IsArrayObject(rq.cx, obj, &isArray) || !isArray) && !JS_IsTypedArrayObject(obj)) FAIL("Argument must be an array"); u32 length; - if (!JS_GetArrayLength(cx, obj, &length)) + if (!JS_GetArrayLength(rq.cx, obj, &length)) FAIL("Failed to get array length"); out.reserve(length); for (u32 i = 0; i < length; ++i) { - JS::RootedValue el(cx); - if (!JS_GetElement(cx, obj, i, &el)) + JS::RootedValue el(rq.cx); + if (!JS_GetElement(rq.cx, obj, i, &el)) FAIL("Failed to read array element"); T el2; - if (!ScriptInterface::FromJSVal(cx, el, el2)) + if (!ScriptInterface::FromJSVal(rq, el, el2)) return false; out.push_back(el2); } return true; } #undef FAIL #define JSVAL_VECTOR(T) \ -template<> void ScriptInterface::ToJSVal >(JSContext* cx, JS::MutableHandleValue ret, const std::vector& val) \ +template<> void ScriptInterface::ToJSVal >(const ScriptInterface::Request& rq, JS::MutableHandleValue ret, const std::vector& val) \ { \ - ToJSVal_vector(cx, ret, val); \ + ToJSVal_vector(rq, ret, val); \ } \ -template<> bool ScriptInterface::FromJSVal >(JSContext* cx, JS::HandleValue v, std::vector& out) \ +template<> bool ScriptInterface::FromJSVal >(const ScriptInterface::Request& rq, JS::HandleValue v, std::vector& out) \ { \ - return FromJSVal_vector(cx, v, out); \ + return FromJSVal_vector(rq, v, out); \ } -template bool ScriptInterface::FromJSProperty(JSContext* cx, const JS::HandleValue val, const char* name, T& ret, bool strict) +template bool ScriptInterface::FromJSProperty(const ScriptInterface::Request& rq, const JS::HandleValue val, const char* name, T& ret, bool strict) { if (!val.isObject()) return false; - JSAutoRequest rq(cx); - JS::RootedObject obj(cx, &val.toObject()); + JS::RootedObject obj(rq.cx, &val.toObject()); bool hasProperty; - if (!JS_HasProperty(cx, obj, name, &hasProperty) || !hasProperty) + if (!JS_HasProperty(rq.cx, obj, name, &hasProperty) || !hasProperty) return false; - JS::RootedValue value(cx); - if (!JS_GetProperty(cx, obj, name, &value)) + JS::RootedValue value(rq.cx); + if (!JS_GetProperty(rq.cx, obj, name, &value)) return false; if (strict && value.isNull()) return false; - return FromJSVal(cx, value, ret); + return FromJSVal(rq, value, ret); } #endif //INCLUDED_SCRIPTCONVERSIONS Index: ps/trunk/source/ps/ModIo.cpp =================================================================== --- ps/trunk/source/ps/ModIo.cpp (revision 24175) +++ ps/trunk/source/ps/ModIo.cpp (revision 24176) @@ -1,857 +1,855 @@ /* Copyright (C) 2020 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "precompiled.h" #include "ModIo.h" #include "i18n/L10n.h" #include "lib/file/file_system.h" #include "lib/sysdep/filesystem.h" #include "lib/sysdep/sysdep.h" #include "maths/MD5.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/GameSetup/Paths.h" #include "ps/Mod.h" #include "ps/ModInstaller.h" #include "ps/Util.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include #include ModIo* g_ModIo = nullptr; struct DownloadCallbackData { DownloadCallbackData() : fp(nullptr), md5(), hash_state(nullptr) { } DownloadCallbackData(FILE* _fp) : fp(_fp), md5() { hash_state = static_cast( sodium_malloc(crypto_generichash_statebytes())); ENSURE(hash_state); crypto_generichash_init(hash_state, nullptr, 0U, crypto_generichash_BYTES_MAX); } ~DownloadCallbackData() { if (hash_state) sodium_free(hash_state); } FILE* fp; MD5 md5; crypto_generichash_state* hash_state; }; ModIo::ModIo() : m_GamesRequest("/games"), m_CallbackData(nullptr) { // Get config values from the default namespace. // This can be overridden on the command line. // // We do this so a malicious mod cannot change the base url and // get the user to make connections to someone else's endpoint. // If another user of the engine wants to provide different values // here, while still using the same engine version, they can just // provide some shortcut/script that sets these using command line // parameters. std::string pk_str; g_ConfigDB.GetValue(CFG_DEFAULT, "modio.public_key", pk_str); g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.baseurl", m_BaseUrl); { std::string api_key; g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.api_key", api_key); m_ApiKey = "api_key=" + api_key; } { std::string nameid; g_ConfigDB.GetValue(CFG_DEFAULT, "modio.v1.name_id", nameid); m_IdQuery = "name_id="+nameid; } m_CurlMulti = curl_multi_init(); ENSURE(m_CurlMulti); m_Curl = curl_easy_init(); ENSURE(m_Curl); // Capture error messages curl_easy_setopt(m_Curl, CURLOPT_ERRORBUFFER, m_ErrorBuffer); // Fail if the server did curl_easy_setopt(m_Curl, CURLOPT_FAILONERROR, 1L); // Disable signal handlers (required for multithreaded applications) curl_easy_setopt(m_Curl, CURLOPT_NOSIGNAL, 1L); // To minimise security risks, don't support redirects (except for file // downloads, for which this setting will be enabled). curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L); // For file downloads, one redirect seems plenty for a CDN serving the files. curl_easy_setopt(m_Curl, CURLOPT_MAXREDIRS, 1L); m_Headers = NULL; std::string ua = "User-Agent: pyrogenesis "; ua += curl_version(); ua += " (https://play0ad.com/)"; m_Headers = curl_slist_append(m_Headers, ua.c_str()); curl_easy_setopt(m_Curl, CURLOPT_HTTPHEADER, m_Headers); if (sodium_init() < 0) ENSURE(0 && "Failed to initialize libsodium."); size_t bin_len = 0; if (sodium_base642bin((unsigned char*)&m_pk, sizeof m_pk, pk_str.c_str(), pk_str.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof m_pk) ENSURE(0 && "Failed to decode base64 public key. Please fix your configuration or mod.io will be unusable."); } ModIo::~ModIo() { // Clean things up to avoid unpleasant surprises, // and delete the temporary file if any. TearDownRequest(); if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) DeleteDownloadedFile(); curl_slist_free_all(m_Headers); curl_easy_cleanup(m_Curl); curl_multi_cleanup(m_CurlMulti); delete m_CallbackData; } size_t ModIo::ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp) { ModIo* self = static_cast(userp); self->m_ResponseData += std::string((char*)buffer, (char*)buffer+size*nmemb); return size*nmemb; } size_t ModIo::DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp) { DownloadCallbackData* data = static_cast(userp); if (!data->fp) return 0; size_t len = fwrite(buffer, size, nmemb, data->fp); // Only update the hash with data we actually managed to write. // In case we did not write all of it we will fail the download, // but we do not want to have a possibly valid hash in that case. size_t written = len*size; data->md5.Update(static_cast(buffer), written); ENSURE(data->hash_state); crypto_generichash_update(data->hash_state, static_cast(buffer), written); return written; } int ModIo::DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t UNUSED(ultotal), curl_off_t UNUSED(ulnow)) { DownloadProgressData* data = static_cast(clientp); // If we got more data than curl expected, something is very wrong, abort. if (dltotal != 0 && dlnow > dltotal) return 1; data->progress = dltotal == 0 ? 0 : static_cast(dlnow) / static_cast(dltotal); return 0; } CURLMcode ModIo::SetupRequest(const std::string& url, bool fileDownload) { if (fileDownload) { // The download link will most likely redirect elsewhere, so allow that. // We verify the validity of the file later. curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 1L); // Enable the progress meter curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 0L); // Set IO callbacks curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, DownloadCallback); curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, static_cast(m_CallbackData)); curl_easy_setopt(m_Curl, CURLOPT_XFERINFOFUNCTION, DownloadProgressCallback); curl_easy_setopt(m_Curl, CURLOPT_XFERINFODATA, static_cast(&m_DownloadProgressData)); // Initialize the progress counter m_DownloadProgressData.progress = 0; } else { // To minimise security risks, don't support redirects curl_easy_setopt(m_Curl, CURLOPT_FOLLOWLOCATION, 0L); // Disable the progress meter curl_easy_setopt(m_Curl, CURLOPT_NOPROGRESS, 1L); // Set IO callbacks curl_easy_setopt(m_Curl, CURLOPT_WRITEFUNCTION, ReceiveCallback); curl_easy_setopt(m_Curl, CURLOPT_WRITEDATA, this); } m_ErrorBuffer[0] = '\0'; curl_easy_setopt(m_Curl, CURLOPT_URL, url.c_str()); return curl_multi_add_handle(m_CurlMulti, m_Curl); } void ModIo::TearDownRequest() { ENSURE(curl_multi_remove_handle(m_CurlMulti, m_Curl) == CURLM_OK); if (m_CallbackData) { if (m_CallbackData->fp) fclose(m_CallbackData->fp); m_CallbackData->fp = nullptr; } } void ModIo::StartGetGameId() { // Don't start such a request during active downloads. if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID || m_DownloadProgressData.status == DownloadProgressStatus::LISTING || m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) return; m_GameId.clear(); CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+"?"+m_ApiKey+"&"+m_IdQuery, false); if (err != CURLM_OK) { TearDownRequest(); m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID; m_DownloadProgressData.error = fmt::sprintf( g_L10n.Translate("Failure while starting querying for game id. Error: %s; %s."), curl_multi_strerror(err), m_ErrorBuffer); return; } m_DownloadProgressData.status = DownloadProgressStatus::GAMEID; } void ModIo::StartListMods() { // Don't start such a request during active downloads. if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID || m_DownloadProgressData.status == DownloadProgressStatus::LISTING || m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) return; m_ModData.clear(); if (m_GameId.empty()) { LOGERROR("Game ID not fetched from mod.io. Call StartGetGameId first and wait for it to finish."); return; } CURLMcode err = SetupRequest(m_BaseUrl+m_GamesRequest+m_GameId+"/mods?"+m_ApiKey, false); if (err != CURLM_OK) { TearDownRequest(); m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING; m_DownloadProgressData.error = fmt::sprintf( g_L10n.Translate("Failure while starting querying for mods. Error: %s; %s."), curl_multi_strerror(err), m_ErrorBuffer); return; } m_DownloadProgressData.status = DownloadProgressStatus::LISTING; } void ModIo::StartDownloadMod(size_t idx) { // Don't start such a request during active downloads. if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID || m_DownloadProgressData.status == DownloadProgressStatus::LISTING || m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) return; if (idx >= m_ModData.size()) return; const Paths paths(g_args); const OsPath modUserPath = paths.UserData()/"mods"; const OsPath modPath = modUserPath/m_ModData[idx].properties["name_id"]; if (!DirectoryExists(modPath) && INFO::OK != CreateDirectories(modPath, 0700, false)) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING; m_DownloadProgressData.error = fmt::sprintf( g_L10n.Translate("Could not create mod directory: %s."), modPath.string8()); return; } // Name the file after the name_id, since using the filename would mean that // we could end up with multiple zip files in the folder that might not work // as expected for a user (since a later version might remove some files // that aren't compatible anymore with the engine version). // So we ignore the filename provided by the API and assume that we do not // care about handling update.zip files. If that is the case we would need // a way to find out what files are required by the current one and which // should be removed for everything to work. This seems to be too complicated // so we just do not support that usage. // NOTE: We do save the file under a slightly different name from the final // one, to ensure that in case a download aborts and the file stays // around, the game will not attempt to open the file which has not // been verified. m_DownloadFilePath = modPath/(m_ModData[idx].properties["name_id"]+".zip.temp"); delete m_CallbackData; m_CallbackData = new DownloadCallbackData(sys_OpenFile(m_DownloadFilePath, "wb")); if (!m_CallbackData->fp) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING; m_DownloadProgressData.error = fmt::sprintf( g_L10n.Translate("Could not open temporary file for mod download: %s."), m_DownloadFilePath.string8()); return; } CURLMcode err = SetupRequest(m_ModData[idx].properties["binary_url"], true); if (err != CURLM_OK) { TearDownRequest(); m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING; m_DownloadProgressData.error = fmt::sprintf( g_L10n.Translate("Failed to start the download. Error: %s; %s."), curl_multi_strerror(err), m_ErrorBuffer); return; } m_DownloadModID = idx; m_DownloadProgressData.status = DownloadProgressStatus::DOWNLOADING; } void ModIo::CancelRequest() { TearDownRequest(); switch (m_DownloadProgressData.status) { case DownloadProgressStatus::GAMEID: case DownloadProgressStatus::FAILED_GAMEID: m_DownloadProgressData.status = DownloadProgressStatus::NONE; break; case DownloadProgressStatus::LISTING: case DownloadProgressStatus::FAILED_LISTING: m_DownloadProgressData.status = DownloadProgressStatus::READY; break; case DownloadProgressStatus::DOWNLOADING: case DownloadProgressStatus::FAILED_DOWNLOADING: m_DownloadProgressData.status = DownloadProgressStatus::LISTED; DeleteDownloadedFile(); break; default: break; } } bool ModIo::AdvanceRequest(const ScriptInterface& scriptInterface) { // If the request was cancelled, stop trying to advance it if (m_DownloadProgressData.status != DownloadProgressStatus::GAMEID && m_DownloadProgressData.status != DownloadProgressStatus::LISTING && m_DownloadProgressData.status != DownloadProgressStatus::DOWNLOADING) return true; int stillRunning; CURLMcode err = curl_multi_perform(m_CurlMulti, &stillRunning); if (err != CURLM_OK) { std::string error = fmt::sprintf( g_L10n.Translate("Asynchronous download failure: %s, %s."), curl_multi_strerror(err), m_ErrorBuffer); TearDownRequest(); if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID) m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID; else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING) m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING; else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING; DeleteDownloadedFile(); } m_DownloadProgressData.error = error; return true; } CURLMsg* message; do { int in_queue; message = curl_multi_info_read(m_CurlMulti, &in_queue); if (!message) continue; CURLcode err = message->data.result; if (err == CURLE_OK) continue; std::string error = fmt::sprintf( g_L10n.Translate("Download failure. Server response: %s; %s."), curl_easy_strerror(err), m_ErrorBuffer); TearDownRequest(); if (m_DownloadProgressData.status == DownloadProgressStatus::GAMEID) m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID; else if (m_DownloadProgressData.status == DownloadProgressStatus::LISTING) m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING; else if (m_DownloadProgressData.status == DownloadProgressStatus::DOWNLOADING) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_DOWNLOADING; DeleteDownloadedFile(); } m_DownloadProgressData.error = error; return true; } while (message); if (stillRunning) return false; // Download finished. TearDownRequest(); // Perform parsing and/or checks std::string error; switch (m_DownloadProgressData.status) { case DownloadProgressStatus::GAMEID: if (!ParseGameId(scriptInterface, error)) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_GAMEID; m_DownloadProgressData.error = error; break; } m_DownloadProgressData.status = DownloadProgressStatus::READY; break; case DownloadProgressStatus::LISTING: if (!ParseMods(scriptInterface, error)) { m_ModData.clear(); // Failed during parsing, make sure we don't provide partial data m_DownloadProgressData.status = DownloadProgressStatus::FAILED_LISTING; m_DownloadProgressData.error = error; break; } m_DownloadProgressData.status = DownloadProgressStatus::LISTED; break; case DownloadProgressStatus::DOWNLOADING: if (!VerifyDownloadedFile(error)) { m_DownloadProgressData.status = DownloadProgressStatus::FAILED_FILECHECK; m_DownloadProgressData.error = error; DeleteDownloadedFile(); break; } m_DownloadProgressData.status = DownloadProgressStatus::SUCCESS; { Paths paths(g_args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); installer.Install(m_DownloadFilePath, g_ScriptRuntime, false); } break; default: break; } return true; } bool ModIo::ParseGameId(const ScriptInterface& scriptInterface, std::string& err) { int id = -1; bool ret = ParseGameIdResponse(scriptInterface, m_ResponseData, id, err); m_ResponseData.clear(); if (!ret) return false; m_GameId = "/" + std::to_string(id); return true; } bool ModIo::ParseMods(const ScriptInterface& scriptInterface, std::string& err) { bool ret = ParseModsResponse(scriptInterface, m_ResponseData, m_ModData, m_pk, err); m_ResponseData.clear(); return ret; } void ModIo::DeleteDownloadedFile() { if (wunlink(m_DownloadFilePath) != 0) LOGERROR("Failed to delete temporary file."); m_DownloadFilePath = OsPath(); } bool ModIo::VerifyDownloadedFile(std::string& err) { // Verify filesize, as a first basic download check. { u64 filesize = std::stoull(m_ModData[m_DownloadModID].properties.at("filesize")); if (filesize != FileSize(m_DownloadFilePath)) { err = g_L10n.Translate("Mismatched filesize."); return false; } } ENSURE(m_CallbackData); // MD5 (because upstream provides it) // Just used to make sure there was no obvious corruption during transfer. { u8 digest[MD5::DIGESTSIZE]; m_CallbackData->md5.Final(digest); std::string md5digest = Hexify(digest, MD5::DIGESTSIZE); if (m_ModData[m_DownloadModID].properties.at("filehash_md5") != md5digest) { err = fmt::sprintf( g_L10n.Translate("Invalid file. Expected md5 %s, got %s."), m_ModData[m_DownloadModID].properties.at("filehash_md5").c_str(), md5digest); return false; } } // Verify file signature. // Used to make sure that the downloaded file was actually checked and signed // by Wildfire Games. And has not been tampered with by the API provider, or the CDN. unsigned char hash_fin[crypto_generichash_BYTES_MAX] = {}; ENSURE(m_CallbackData->hash_state); if (crypto_generichash_final(m_CallbackData->hash_state, hash_fin, sizeof hash_fin) != 0) { err = g_L10n.Translate("Failed to compute final hash."); return false; } if (crypto_sign_verify_detached(m_ModData[m_DownloadModID].sig.sig, hash_fin, sizeof hash_fin, m_pk.pk) != 0) { err = g_L10n.Translate("Failed to verify signature."); return false; } return true; } #define FAIL(...) STMT(err = fmt::sprintf(__VA_ARGS__); CLEANUP(); return false;) /** * Parses the current content of m_ResponseData to extract m_GameId. * * The JSON data is expected to look like * { "data": [{"id": 42, ...}, ...], ... } * where we are only interested in the value of the id property. * * @returns true iff it successfully parsed the id. */ bool ModIo::ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err) { #define CLEANUP() id = -1; - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - JS::RootedValue gameResponse(cx); + JS::RootedValue gameResponse(rq.cx); if (!scriptInterface.ParseJSON(responseData, &gameResponse)) FAIL("Failed to parse response as JSON."); if (!gameResponse.isObject()) FAIL("response not an object."); - JS::RootedObject gameResponseObj(cx, gameResponse.toObjectOrNull()); - JS::RootedValue dataVal(cx); - if (!JS_GetProperty(cx, gameResponseObj, "data", &dataVal)) + JS::RootedObject gameResponseObj(rq.cx, gameResponse.toObjectOrNull()); + JS::RootedValue dataVal(rq.cx); + if (!JS_GetProperty(rq.cx, gameResponseObj, "data", &dataVal)) FAIL("data property not in response."); // [{"id": 42, ...}, ...] if (!dataVal.isObject()) FAIL("data property not an object."); - JS::RootedObject data(cx, dataVal.toObjectOrNull()); + JS::RootedObject data(rq.cx, dataVal.toObjectOrNull()); u32 length; bool isArray; - if (!JS_IsArrayObject(cx, data, &isArray) || !isArray || !JS_GetArrayLength(cx, data, &length) || !length) + if (!JS_IsArrayObject(rq.cx, data, &isArray) || !isArray || !JS_GetArrayLength(rq.cx, data, &length) || !length) FAIL("data property not an array with at least one element."); // {"id": 42, ...} - JS::RootedValue first(cx); - if (!JS_GetElement(cx, data, 0, &first)) + JS::RootedValue first(rq.cx); + if (!JS_GetElement(rq.cx, data, 0, &first)) FAIL("Couldn't get first element."); if (!first.isObject()) FAIL("First element not an object."); - JS::RootedObject firstObj(cx, &first.toObject()); + JS::RootedObject firstObj(rq.cx, &first.toObject()); bool hasIdProperty; - if (!JS_HasProperty(cx, firstObj, "id", &hasIdProperty) || !hasIdProperty) + if (!JS_HasProperty(rq.cx, firstObj, "id", &hasIdProperty) || !hasIdProperty) FAIL("No id property in first element."); - JS::RootedValue idProperty(cx); - ENSURE(JS_GetProperty(cx, firstObj, "id", &idProperty)); + JS::RootedValue idProperty(rq.cx); + ENSURE(JS_GetProperty(rq.cx, firstObj, "id", &idProperty)); // Make sure the property is not set to something that could be converted to a bogus value // TODO: We should be able to convert JS::Values to C++ variables in a way that actually // fails when types do not match (see https://trac.wildfiregames.com/ticket/5128). if (!idProperty.isNumber()) FAIL("id property not a number."); id = -1; - if (!ScriptInterface::FromJSVal(cx, idProperty, id) || id <= 0) + if (!ScriptInterface::FromJSVal(rq, idProperty, id) || id <= 0) FAIL("Invalid id."); return true; #undef CLEANUP } /** * Parses the current content of m_ResponseData into m_ModData. * * The JSON data is expected to look like * { data: [modobj1, modobj2, ...], ... (including result_count) } * where modobjN has the following structure * { homepage_url: "url", name: "displayname", nameid: "short-non-whitespace-name", * summary: "short desc.", modfile: { version: "1.2.4", filename: "asdf.zip", * filehash: { md5: "deadbeef" }, filesize: 1234, download: { binary_url: "someurl", ... } }, ... }. * Only the listed properties are of interest to consumers, and we flatten * the modfile structure as that simplifies handling and there are no conflicts. */ bool ModIo::ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err) { // Make sure we don't end up passing partial results back #define CLEANUP() modData.clear(); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - JS::RootedValue modResponse(cx); + JS::RootedValue modResponse(rq.cx); if (!scriptInterface.ParseJSON(responseData, &modResponse)) FAIL("Failed to parse response as JSON."); if (!modResponse.isObject()) FAIL("response not an object."); - JS::RootedObject modResponseObj(cx, modResponse.toObjectOrNull()); - JS::RootedValue dataVal(cx); - if (!JS_GetProperty(cx, modResponseObj, "data", &dataVal)) + JS::RootedObject modResponseObj(rq.cx, modResponse.toObjectOrNull()); + JS::RootedValue dataVal(rq.cx); + if (!JS_GetProperty(rq.cx, modResponseObj, "data", &dataVal)) FAIL("data property not in response."); // [modobj1, modobj2, ... ] if (!dataVal.isObject()) FAIL("data property not an object."); - JS::RootedObject rData(cx, dataVal.toObjectOrNull()); + JS::RootedObject rData(rq.cx, dataVal.toObjectOrNull()); u32 length; bool isArray; - if (!JS_IsArrayObject(cx, rData, &isArray) || !isArray || !JS_GetArrayLength(cx, rData, &length) || !length) + if (!JS_IsArrayObject(rq.cx, rData, &isArray) || !isArray || !JS_GetArrayLength(rq.cx, rData, &length) || !length) FAIL("data property not an array with at least one element."); modData.clear(); modData.reserve(length); #define INVALIDATE_DATA_AND_CONTINUE(...) \ {\ data.properties.emplace("invalid", "true");\ data.properties.emplace("error", __VA_ARGS__);\ continue;\ } for (u32 i = 0; i < length; ++i) { modData.emplace_back(); ModIoModData& data = modData.back(); - JS::RootedValue el(cx); - if (!JS_GetElement(cx, rData, i, &el) || !el.isObject()) + JS::RootedValue el(rq.cx); + if (!JS_GetElement(rq.cx, rData, i, &el) || !el.isObject()) INVALIDATE_DATA_AND_CONTINUE("Failed to get array element object.") bool ok = true; std::string copyStringError; #define COPY_STRINGS_ELSE_CONTINUE(prefix, obj, ...) \ for (const std::string& prop : { __VA_ARGS__ }) \ { \ std::string val; \ - if (!ScriptInterface::FromJSProperty(cx, obj, prop.c_str(), val, true)) \ + if (!ScriptInterface::FromJSProperty(rq, obj, prop.c_str(), val, true)) \ { \ ok = false; \ copyStringError = "Failed to get " + prop + " from " + #obj + "."; \ break; \ }\ data.properties.emplace(prefix+prop, val); \ } \ if (!ok) \ INVALIDATE_DATA_AND_CONTINUE(copyStringError); // TODO: Currently the homepage_url field does not contain a non-null value for any entry. COPY_STRINGS_ELSE_CONTINUE("", el, "name", "name_id", "summary") // Now copy over the modfile part, but without the pointless substructure - JS::RootedObject elObj(cx, el.toObjectOrNull()); - JS::RootedValue modFile(cx); - if (!JS_GetProperty(cx, elObj, "modfile", &modFile)) + JS::RootedObject elObj(rq.cx, el.toObjectOrNull()); + JS::RootedValue modFile(rq.cx); + if (!JS_GetProperty(rq.cx, elObj, "modfile", &modFile)) INVALIDATE_DATA_AND_CONTINUE("Failed to get modfile data."); if (!modFile.isObject()) INVALIDATE_DATA_AND_CONTINUE("modfile not an object."); COPY_STRINGS_ELSE_CONTINUE("", modFile, "version", "filesize"); - JS::RootedObject modFileObj(cx, modFile.toObjectOrNull()); - JS::RootedValue filehash(cx); - if (!JS_GetProperty(cx, modFileObj, "filehash", &filehash)) + JS::RootedObject modFileObj(rq.cx, modFile.toObjectOrNull()); + JS::RootedValue filehash(rq.cx); + if (!JS_GetProperty(rq.cx, modFileObj, "filehash", &filehash)) INVALIDATE_DATA_AND_CONTINUE("Failed to get filehash data."); COPY_STRINGS_ELSE_CONTINUE("filehash_", filehash, "md5"); - JS::RootedValue download(cx); - if (!JS_GetProperty(cx, modFileObj, "download", &download)) + JS::RootedValue download(rq.cx); + if (!JS_GetProperty(rq.cx, modFileObj, "download", &download)) INVALIDATE_DATA_AND_CONTINUE("Failed to get download data."); COPY_STRINGS_ELSE_CONTINUE("", download, "binary_url"); // Parse metadata_blob (sig+deps) std::string metadata_blob; - if (!ScriptInterface::FromJSProperty(cx, modFile, "metadata_blob", metadata_blob, true)) + if (!ScriptInterface::FromJSProperty(rq, modFile, "metadata_blob", metadata_blob, true)) INVALIDATE_DATA_AND_CONTINUE("Failed to get metadata_blob from modFile."); - JS::RootedValue metadata(cx); + JS::RootedValue metadata(rq.cx); if (!scriptInterface.ParseJSON(metadata_blob, &metadata)) INVALIDATE_DATA_AND_CONTINUE("Failed to parse metadata_blob as JSON."); if (!metadata.isObject()) INVALIDATE_DATA_AND_CONTINUE("metadata_blob is not decoded as an object."); - if (!ScriptInterface::FromJSProperty(cx, metadata, "dependencies", data.dependencies, true)) + if (!ScriptInterface::FromJSProperty(rq, metadata, "dependencies", data.dependencies, true)) INVALIDATE_DATA_AND_CONTINUE("Failed to get dependencies from metadata_blob."); std::vector minisigs; - if (!ScriptInterface::FromJSProperty(cx, metadata, "minisigs", minisigs, true)) + if (!ScriptInterface::FromJSProperty(rq, metadata, "minisigs", minisigs, true)) INVALIDATE_DATA_AND_CONTINUE("Failed to get minisigs from metadata_blob."); // Check we did find a valid matching signature. std::string signatureParsingErr; if (!ParseSignature(minisigs, data.sig, pk, signatureParsingErr)) INVALIDATE_DATA_AND_CONTINUE(signatureParsingErr); #undef COPY_STRINGS_ELSE_CONTINUE #undef INVALIDATE_DATA_AND_CONTINUE } return true; #undef CLEANUP } /** * Parse signatures to find one that matches the public key, and has a valid global signature. * Returns true and sets @param sig to the valid matching signature. */ bool ModIo::ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err) { #define CLEANUP() sig = {}; for (const std::string& file_sig : minisigs) { // Format of a .minisig file (created using minisign(1) with -SHm file.zip) // untrusted comment: .*\nb64sign_of_file\ntrusted comment: .*\nb64sign_of_sign_of_file_and_trusted_comment std::vector sig_lines; boost::split(sig_lines, file_sig, boost::is_any_of("\n")); if (sig_lines.size() < 4) FAIL("Invalid (too short) sig."); // Verify that both the untrusted comment and the trusted comment start with the correct prefix // because that is easy. const std::string untrusted_comment_prefix = "untrusted comment: "; const std::string trusted_comment_prefix = "trusted comment: "; if (!boost::algorithm::starts_with(sig_lines[0], untrusted_comment_prefix)) FAIL("Malformed untrusted comment."); if (!boost::algorithm::starts_with(sig_lines[2], trusted_comment_prefix)) FAIL("Malformed trusted comment."); // We only _really_ care about the second line which is the signature of the file (b64-encoded) // Also handling the other signature is nice, but not really required. const std::string& msg_sig = sig_lines[1]; size_t bin_len = 0; if (sodium_base642bin((unsigned char*)&sig, sizeof sig, msg_sig.c_str(), msg_sig.size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof sig) FAIL("Failed to decode base64 sig."); cassert(sizeof pk.keynum == sizeof sig.keynum); if (memcmp(&pk.keynum, &sig.keynum, sizeof sig.keynum) != 0) continue; // mismatched key, try another one if (memcmp(&sig.sig_alg, "ED", 2) != 0) FAIL("Only hashed minisign signatures are supported."); // Signature matches our public key // Now verify the global signature (sig || trusted_comment) unsigned char global_sig[crypto_sign_BYTES]; if (sodium_base642bin(global_sig, sizeof global_sig, sig_lines[3].c_str(), sig_lines[3].size(), NULL, &bin_len, NULL, sodium_base64_VARIANT_ORIGINAL) != 0 || bin_len != sizeof global_sig) FAIL("Failed to decode base64 global_sig."); const std::string trusted_comment = sig_lines[2].substr(trusted_comment_prefix.size()); unsigned char* sig_and_trusted_comment = (unsigned char*)sodium_malloc((sizeof sig.sig) + trusted_comment.size()); if (!sig_and_trusted_comment) FAIL("sodium_malloc failed."); memcpy(sig_and_trusted_comment, sig.sig, sizeof sig.sig); memcpy(sig_and_trusted_comment + sizeof sig.sig, trusted_comment.data(), trusted_comment.size()); if (crypto_sign_verify_detached(global_sig, sig_and_trusted_comment, (sizeof sig.sig) + trusted_comment.size(), pk.pk) != 0) { err = "Failed to verify global signature."; sodium_free(sig_and_trusted_comment); return false; } sodium_free(sig_and_trusted_comment); // Valid global sig, and the keynum matches the real one return true; } return false; #undef CLEANUP } #undef FAIL Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp (revision 24175) +++ ps/trunk/source/ps/SavedGame.cpp (revision 24176) @@ -1,305 +1,300 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "SavedGame.h" #include "graphics/GameView.h" #include "i18n/L10n.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/archive/archive_zip.h" #include "lib/utf8.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" // TODO: we ought to check version numbers when loading files Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d"); const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave"); VfsPath filename; // Don't make this a static global like NextNumberedFilename expects, because // that wouldn't work when 'prefix' changes, and because it's not thread-safe size_t nextSaveNumber = 0; vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename); return Save(filename.Filename().string(), description, simulation, guiMetadataClone); } Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const shared_ptr& guiMetadataClone) { - JSContext* cx = simulation.GetScriptInterface().GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(simulation.GetScriptInterface()); // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + name); const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave"); // ArchiveWriter_Zip can only write to OsPaths, not VfsPaths, // but we'd like to handle saved games via VFS. // To avoid potential confusion from writing with non-VFS then // reading the same file with VFS, we'll just write to a temporary // non-VFS path and then load and save again via VFS, // which is kind of a hack. OsPath tempSaveFileRealPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath)); tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave"; time_t now = time(NULL); // Construct the serialized state to be saved std::stringstream simStateStream; if (!simulation.SerializeState(simStateStream)) WARN_RETURN(ERR::FAIL); - JS::RootedValue initAttributes(cx, simulation.GetInitAttributes()); - JS::RootedValue mods(cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface())); + JS::RootedValue initAttributes(rq.cx, simulation.GetInitAttributes()); + JS::RootedValue mods(rq.cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface())); - JS::RootedValue metadata(cx); + JS::RootedValue metadata(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &metadata, "engine_version", engine_version, "time", static_cast(now), "playerID", g_Game->GetPlayerID(), "mods", mods, "initAttributes", initAttributes); - JS::RootedValue guiMetadata(cx); + JS::RootedValue guiMetadata(rq.cx); simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata); // get some camera data const CVector3D cameraPosition = g_Game->GetView()->GetCameraPosition(); const CVector3D cameraRotation = g_Game->GetView()->GetCameraRotation(); - JS::RootedValue cameraMetadata(cx); + JS::RootedValue cameraMetadata(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &cameraMetadata, "PosX", cameraPosition.X, "PosY", cameraPosition.Y, "PosZ", cameraPosition.Z, "RotX", cameraRotation.X, "RotY", cameraRotation.Y, "Zoom", g_Game->GetView()->GetCameraZoom()); simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata); simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata); simulation.GetScriptInterface().SetProperty(metadata, "description", description); std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true); // Write the saved game as zip file containing the various components PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false); if (!archiveWriter) WARN_RETURN(ERR::FAIL); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json")); WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat")); archiveWriter.reset(); // close the file WriteBuffer buffer; CFileInfo tempSaveFile; WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile)); buffer.Reserve(tempSaveFile.Size()); WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size())); WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size())); OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); LOGMESSAGERENDER(g_L10n.Translate("Saved game to '%s'"), realPath.string8()); debug_printf("Saved game to '%s'\n", realPath.string8().c_str()); return INFO::OK; } /** * Helper class for retrieving data from saved game archives */ class CGameLoader { NONCOPYABLE(CGameLoader); public: /** * @param scriptInterface the ScriptInterface used for loading metadata. * @param[out] savedState serialized simulation state stored as string of bytes, * loaded from simulation.dat inside the archive. * * Note: We use a different approach for returning the string and the metadata JS::Value. * We use a pointer for the string to avoid copies (efficiency). We don't use this approach * for the metadata because it would be error prone with rooting and the stack-based rooting * types and confusing (a chain of pointers pointing to other pointers). */ CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) : m_ScriptInterface(scriptInterface), m_Metadata(scriptInterface.GetJSRuntime()), m_SavedState(savedState) { } static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile); } void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile) { - JSContext* cx = m_ScriptInterface.GetContext(); - JSAutoRequest rq(cx); - if (pathname == L"metadata.json") { std::string buffer; buffer.resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size())); m_ScriptInterface.ParseJSON(buffer, &m_Metadata); } else if (pathname == L"simulation.dat" && m_SavedState) { m_SavedState->resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size())); } } JS::Value GetMetadata() { return m_Metadata.get(); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Metadata; std::string* m_SavedState; }; Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) { // Determine the filename to load const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); // 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; OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) WARN_RETURN(ERR::FAIL); CGameLoader loader(scriptInterface, &savedState); WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); metadata.set(loader.GetMetadata()); return INFO::OK; } JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface) { TIMER(L"GetSavedGames"); - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - JS::RootedValue games(cx); - ScriptInterface::CreateArray(cx, &games); + JS::RootedValue games(rq.cx); + ScriptInterface::CreateArray(rq, &games); Status err; VfsPaths pathnames; err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames); WARN_IF_ERR(err); for (size_t i = 0; i < pathnames.size(); ++i) { OsPath realPath; err = g_VFS->GetRealPath(pathnames[i], realPath); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) { // Triggered by e.g. the file being open in another program LOGWARNING("Failed to read saved game '%s'", realPath.string8()); continue; // skip this file } CGameLoader loader(scriptInterface, NULL); err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } - JS::RootedValue metadata(cx, loader.GetMetadata()); + JS::RootedValue metadata(rq.cx, loader.GetMetadata()); - JS::RootedValue game(cx); + JS::RootedValue game(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &game, "id", pathnames[i].Basename(), "metadata", metadata); scriptInterface.SetPropertyInt(games, i, game); } return games; } bool SavedGames::DeleteSavedGame(const std::wstring& name) { const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); OsPath realpath; // Make sure it exists in VFS and find its real path if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK) return false; // Error // Remove from VFS if (g_VFS->RemoveFile(filename) != INFO::OK) return false; // Error // Delete actual file if (wunlink(realpath) != 0) return false; // Error // Successfully deleted file return true; } Index: ps/trunk/source/ps/scripting/JSInterface_Main.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24175) +++ ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24176) @@ -1,139 +1,138 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2020 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 "JSInterface_Main.h" #include "graphics/FontMetrics.h" #include "graphics/MapReader.h" #include "lib/sysdep/sysdep.h" #include "lib/utf8.h" #include "maths/MD5.h" #include "ps/CStrIntern.h" #include "ps/GUID.h" #include "ps/GameSetup/Atlas.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Util.h" #include "scriptinterface/ScriptInterface.h" #include "tools/atlas/GameInterface/GameLoop.h" extern void QuitEngine(); extern void StartAtlas(); void JSI_Main::QuitEngine(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ::QuitEngine(); } void JSI_Main::StartAtlas(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { ::StartAtlas(); } bool JSI_Main::AtlasIsAvailable(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return ATLAS_IsAvailable(); } bool JSI_Main::IsAtlasRunning(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return g_AtlasGameLoop && g_AtlasGameLoop->running; } void JSI_Main::OpenURL(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& url) { sys_open_url(url); } std::wstring JSI_Main::GetSystemUsername(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return sys_get_user_name(); } std::wstring JSI_Main::GetMatchID(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return ps_generate_guid().FromUTF8(); } JS::Value JSI_Main::LoadMapSettings(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(pCxPrivate); CMapSummaryReader reader; if (reader.LoadMap(pathname) != PSRETURN_OK) return JS::UndefinedValue(); - JS::RootedValue settings(cx); + JS::RootedValue settings(rq.cx); reader.GetMapSettings(*(pCxPrivate->pScriptInterface), &settings); return settings; } bool JSI_Main::HotkeyIsPressed_(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& hotkeyName) { return HotkeyIsPressed(hotkeyName); } // This value is recalculated once a frame. We take special care to // filter it, so it is both accurate and free of jitter. int JSI_Main::GetFps(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { if (!g_frequencyFilter) return 0; return g_frequencyFilter->StableFrequency(); } int JSI_Main::GetTextWidth(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& fontName, const std::wstring& text) { int width = 0; int height = 0; CStrIntern _fontName(fontName); CFontMetrics fontMetrics(_fontName); fontMetrics.CalculateStringSize(text.c_str(), width, height); return width; } std::string JSI_Main::CalculateMD5(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::string& input) { u8 digest[MD5::DIGESTSIZE]; MD5 m; m.Update((const u8*)input.c_str(), input.length()); m.Final(digest); return Hexify(digest, MD5::DIGESTSIZE); } void JSI_Main::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("Exit"); scriptInterface.RegisterFunction("RestartInAtlas"); scriptInterface.RegisterFunction("AtlasIsAvailable"); scriptInterface.RegisterFunction("IsAtlasRunning"); scriptInterface.RegisterFunction("OpenURL"); scriptInterface.RegisterFunction("GetSystemUsername"); scriptInterface.RegisterFunction("GetMatchID"); scriptInterface.RegisterFunction("LoadMapSettings"); scriptInterface.RegisterFunction("HotkeyIsPressed"); scriptInterface.RegisterFunction("GetFPS"); scriptInterface.RegisterFunction("GetTextWidth"); scriptInterface.RegisterFunction("CalculateMD5"); } Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24175) +++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24176) @@ -1,280 +1,275 @@ /* Copyright (C) 2020 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 "JSInterface_VFS.h" #include "lib/file/vfs/vfs_util.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Filesystem.h" #include "scriptinterface/ScriptInterface.h" #include // Only allow engine compartments to read files they may be concerned about. #define PathRestriction_GUI {L""} #define PathRestriction_Simulation {L"simulation/"} #define PathRestriction_Maps {L"simulation/", L"maps/"} // shared error handling code #define JS_CHECK_FILE_ERR(err)\ /* this is liable to happen often, so don't complain */\ if (err == ERR::VFS_FILE_NOT_FOUND)\ {\ return 0; \ }\ /* unknown failure. We output an error message. */\ else if (err < 0)\ LOGERROR("Unknown failure in VFS %i", err ); /* else: success */ // state held across multiple BuildDirEntListCB calls; init by BuildDirEntList. struct BuildDirEntListState { - JSContext* cx; + ScriptInterface* pScriptInterface; JS::PersistentRootedObject filename_array; int cur_idx; - BuildDirEntListState(JSContext* cx_) - : cx(cx_), - filename_array(cx, JS_NewArrayObject(cx, JS::HandleValueArray::empty())), + BuildDirEntListState(ScriptInterface* scriptInterface) + : pScriptInterface(scriptInterface), + filename_array(scriptInterface->GetJSRuntime()), cur_idx(0) { + ScriptInterface::Request rq(pScriptInterface); + filename_array = JS_NewArrayObject(rq.cx, JS::HandleValueArray::empty()); } }; // called for each matching directory entry; add its full pathname to array. static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData) { BuildDirEntListState* s = (BuildDirEntListState*)cbData; - JSAutoRequest rq(s->cx); + ScriptInterface::Request rq(s->pScriptInterface); - JS::RootedObject filenameArrayObj(s->cx, s->filename_array); - JS::RootedValue val(s->cx); - ScriptInterface::ToJSVal( s->cx, &val, CStrW(pathname.string()) ); - JS_SetElement(s->cx, filenameArrayObj, s->cur_idx++, val); + JS::RootedObject filenameArrayObj(rq.cx, s->filename_array); + JS::RootedValue val(rq.cx); + ScriptInterface::ToJSVal(rq, &val, CStrW(pathname.string()) ); + JS_SetElement(rq.cx, filenameArrayObj, s->cur_idx++, val); return INFO::OK; } // Return an array of pathname strings, one for each matching entry in the // specified directory. // filter_string: default "" matches everything; otherwise, see vfs_next_dirent. // recurse: should subdirectories be included in the search? default false. JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse) { if (!PathRestrictionMet(pCxPrivate, validPaths, path)) return JS::NullValue(); // convert to const wchar_t*; if there's no filter, pass 0 for speed // (interpreted as: "accept all files without comparing"). const wchar_t* filter = 0; if (!filterStr.empty()) filter = filterStr.c_str(); int flags = recurse ? vfs::DIR_RECURSIVE : 0; - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - // build array in the callback function - BuildDirEntListState state(cx); + BuildDirEntListState state(pCxPrivate->pScriptInterface); vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags); return JS::ObjectValue(*state.filename_array); } // Return true iff the file exits bool JSI_VFS::FileExists(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filename) { return PathRestrictionMet(pCxPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK; } // Return time [seconds since 1970] of the last modification to the specified file. double JSI_VFS::GetFileMTime(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename) { CFileInfo fileInfo; Status err = g_VFS->GetFileInfo(filename, &fileInfo); JS_CHECK_FILE_ERR(err); return (double)fileInfo.MTime(); } // Return current size of file. unsigned int JSI_VFS::GetFileSize(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), const std::wstring& filename) { CFileInfo fileInfo; Status err = g_VFS->GetFileInfo(filename, &fileInfo); JS_CHECK_FILE_ERR(err); return (unsigned int)fileInfo.Size(); } // Return file contents in a string. Assume file is UTF-8 encoded text. JS::Value JSI_VFS::ReadFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename) { - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - CVFSFile file; if (file.Load(g_VFS, filename) != PSRETURN_OK) return JS::NullValue(); CStr contents = file.DecodeUTF8(); // assume it's UTF-8 // Fix CRLF line endings. (This function will only ever be used on text files.) contents.Replace("\r\n", "\n"); // Decode as UTF-8 - JS::RootedValue ret(cx); - ScriptInterface::ToJSVal(cx, &ret, contents.FromUTF8()); + ScriptInterface::Request rq(pCxPrivate); + JS::RootedValue ret(rq.cx); + ScriptInterface::ToJSVal(rq, &ret, contents.FromUTF8()); return ret; } // Return file contents as an array of lines. Assume file is UTF-8 encoded text. JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filename) { - const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface; - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); - CVFSFile file; if (file.Load(g_VFS, filename) != PSRETURN_OK) return JS::NullValue(); CStr contents = file.DecodeUTF8(); // assume it's UTF-8 // Fix CRLF line endings. (This function will only ever be used on text files.) contents.Replace("\r\n", "\n"); // split into array of strings (one per line) std::stringstream ss(contents); - JS::RootedValue line_array(cx); - ScriptInterface::CreateArray(cx, &line_array); + const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface; + ScriptInterface::Request rq(scriptInterface); + + JS::RootedValue line_array(rq.cx); + ScriptInterface::CreateArray(rq, &line_array); std::string line; int cur_line = 0; while (std::getline(ss, line)) { // Decode each line as UTF-8 - JS::RootedValue val(cx); - ScriptInterface::ToJSVal(cx, &val, CStr(line).FromUTF8()); + JS::RootedValue val(rq.cx); + ScriptInterface::ToJSVal(rq, &val, CStr(line).FromUTF8()); scriptInterface.SetPropertyInt(line_array, cur_line++, val); } return line_array; } JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath) { if (!PathRestrictionMet(pCxPrivate, validPaths, filePath)) return JS::NullValue(); - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue out(cx); - pCxPrivate->pScriptInterface->ReadJSONFile(filePath, &out); + const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface; + ScriptInterface::Request rq(scriptInterface); + JS::RootedValue out(rq.cx); + scriptInterface.ReadJSONFile(filePath, &out); return out; } void JSI_VFS::WriteJSONFile(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath, JS::HandleValue val1) { - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); + const ScriptInterface& scriptInterface = *pCxPrivate->pScriptInterface; + ScriptInterface::Request rq(scriptInterface); // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. - JS::RootedValue val(cx, val1); + JS::RootedValue val(rq.cx, val1); - std::string str(pCxPrivate->pScriptInterface->StringifyJSON(&val, false)); + std::string str(scriptInterface.StringifyJSON(&val, false)); VfsPath path(filePath); WriteBuffer buf; buf.Append(str.c_str(), str.length()); g_VFS->CreateFile(path, buf.Data(), buf.Size()); } bool JSI_VFS::PathRestrictionMet(ScriptInterface::CxPrivate* pCxPrivate, const std::vector& validPaths, const CStrW& filePath) { for (const CStrW& validPath : validPaths) if (filePath.find(validPath) == 0) return true; CStrW allowedPaths; for (std::size_t i = 0; i < validPaths.size(); ++i) { if (i != 0) allowedPaths += L", "; allowedPaths += L"\"" + validPaths[i] + L"\""; } - JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS_ReportError(cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str()); + ScriptInterface::Request rq(pCxPrivate); + JS_ReportError(rq.cx, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str()); return false; } #define VFS_ScriptFunctions(context)\ JS::Value Script_ReadJSONFile_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\ {\ return JSI_VFS::ReadJSONFile(pCxPrivate, PathRestriction_##context, filePath);\ }\ JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\ {\ return JSI_VFS::BuildDirEntList(pCxPrivate, PathRestriction_##context, path, filterStr, recurse);\ }\ bool Script_FileExists_##context(ScriptInterface::CxPrivate* pCxPrivate, const std::wstring& filePath)\ {\ return JSI_VFS::FileExists(pCxPrivate, PathRestriction_##context, filePath);\ }\ VFS_ScriptFunctions(GUI); VFS_ScriptFunctions(Simulation); VFS_ScriptFunctions(Maps); #undef VFS_ScriptFunctions void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("GetFileMTime"); scriptInterface.RegisterFunction("GetFileSize"); scriptInterface.RegisterFunction("ReadFile"); scriptInterface.RegisterFunction("ReadFileLines"); scriptInterface.RegisterFunction("ReadJSONFile"); scriptInterface.RegisterFunction("WriteJSONFile"); } void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("ReadJSONFile"); } void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("ListDirectoryFiles"); scriptInterface.RegisterFunction("FileExists"); scriptInterface.RegisterFunction("ReadJSONFile"); } Index: ps/trunk/source/scriptinterface/NativeWrapperDefns.h =================================================================== --- ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24175) +++ ps/trunk/source/scriptinterface/NativeWrapperDefns.h (revision 24176) @@ -1,239 +1,235 @@ /* Copyright (C) 2020 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 . */ // Use the macro below to define types that will be passed by value to C++ functions. // NOTE: References are used just to avoid superfluous copy constructor calls // in the script wrapper code. They cannot be used as out-parameters. // They are const T& by default to avoid confusion about this, especially // because sometimes the function is not just exposed to scripts, but also // called from C++ code. template struct ScriptInterface::MaybeRef { typedef const T& Type; }; #define PASS_BY_VALUE_IN_NATIVE_WRAPPER(T) \ template <> struct ScriptInterface::MaybeRef \ { \ typedef T Type; \ }; \ PASS_BY_VALUE_IN_NATIVE_WRAPPER(JS::HandleValue) PASS_BY_VALUE_IN_NATIVE_WRAPPER(bool) PASS_BY_VALUE_IN_NATIVE_WRAPPER(int) PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint8_t) PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint16_t) PASS_BY_VALUE_IN_NATIVE_WRAPPER(uint32_t) PASS_BY_VALUE_IN_NATIVE_WRAPPER(fixed) PASS_BY_VALUE_IN_NATIVE_WRAPPER(float) PASS_BY_VALUE_IN_NATIVE_WRAPPER(double) #undef PASS_BY_VALUE_IN_NATIVE_WRAPPER // This works around a bug in Visual Studio (error C2244 if ScriptInterface:: is included in the // type specifier of MaybeRef::Type for parameters inside the member function declaration). // It's probably the bug described here, but I'm not quite sure (at least the example there still // cause error C2244): // https://connect.microsoft.com/VisualStudio/feedback/details/611863/vs2010-c-fails-with-error-c2244-gcc-4-3-4-compiles-ok // // TODO: When dropping support for VS 2015, check if this bug is still present in the supported // Visual Studio versions (replace the macro definitions in NativeWrapperDecls.h with these ones, // remove them from here and check if this causes error C2244 when compiling. #undef NUMBERED_LIST_TAIL_MAYBE_REF #undef NUMBERED_LIST_BALANCED_MAYBE_REF #define NUMBERED_LIST_TAIL_MAYBE_REF(z, i, data) , typename ScriptInterface::MaybeRef::Type #define NUMBERED_LIST_BALANCED_MAYBE_REF(z, i, data) BOOST_PP_COMMA_IF(i) typename ScriptInterface::MaybeRef::Type // (NativeWrapperDecls.h set up a lot of the macros we use here) -// ScriptInterface_NativeWrapper::call(cx, rval, fptr, args...) will call fptr(cbdata, args...), +// ScriptInterface_NativeWrapper::call(rq, rval, fptr, args...) will call fptr(cbdata, args...), // and if T != void then it will store the result in rval: // Templated on the return type so void can be handled separately template struct ScriptInterface_NativeWrapper { template - static void call(JSContext* cx, JS::MutableHandleValue rval, F fptr, Ts... params) + static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue rval, F fptr, Ts... params) { - ScriptInterface::AssignOrToJSValUnrooted(cx, rval, fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...)); + ScriptInterface::AssignOrToJSValUnrooted(rq, rval, fptr(ScriptInterface::GetScriptInterfaceAndCBData(rq.cx), params...)); } }; // Overloaded to ignore the return value from void functions template <> struct ScriptInterface_NativeWrapper { template - static void call(JSContext* cx, JS::MutableHandleValue UNUSED(rval), F fptr, Ts... params) + static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue UNUSED(rval), F fptr, Ts... params) { - fptr(ScriptInterface::GetScriptInterfaceAndCBData(cx), params...); + fptr(ScriptInterface::GetScriptInterfaceAndCBData(rq.cx), params...); } }; // Same idea but for method calls: template struct ScriptInterface_NativeMethodWrapper { template - static void call(JSContext* cx, JS::MutableHandleValue rval, TC* c, F fptr, Ts... params) + static void call(const ScriptInterface::Request& rq, JS::MutableHandleValue rval, TC* c, F fptr, Ts... params) { - ScriptInterface::AssignOrToJSValUnrooted(cx, rval, (c->*fptr)(params...)); + ScriptInterface::AssignOrToJSValUnrooted(rq, rval, (c->*fptr)(params...)); } }; template struct ScriptInterface_NativeMethodWrapper { template - static void call(JSContext* UNUSED(cx), JS::MutableHandleValue UNUSED(rval), TC* c, F fptr, Ts... params) + static void call(const ScriptInterface::Request& UNUSED(rq), JS::MutableHandleValue UNUSED(rval), TC* c, F fptr, Ts... params) { (c->*fptr)(params...); } }; // JSFastNative-compatible function that wraps the function identified in the template argument list #define OVERLOADS(z, i, data) \ template \ bool ScriptInterface::call(JSContext* cx, uint argc, JS::Value* vp) \ { \ JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ - JSAutoRequest rq(cx); \ + ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \ - JS::RootedValue rval(cx); \ - ScriptInterface_NativeWrapper::template call(cx, &rval, fptr A0_TAIL(z,i)); \ + JS::RootedValue rval(rq.cx); \ + ScriptInterface_NativeWrapper::template call(rq, &rval, fptr A0_TAIL(z,i)); \ args.rval().set(rval); \ - return !ScriptInterface::IsExceptionPending(cx); \ + return !ScriptInterface::IsExceptionPending(rq); \ } BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // Same idea but for methods #define OVERLOADS(z, i, data) \ template \ bool ScriptInterface::callMethod(JSContext* cx, uint argc, JS::Value* vp) \ { \ JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ - JSAutoRequest rq(cx); \ - TC* c = ScriptInterface::GetPrivate(cx, args, CLS); \ + ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + TC* c = ScriptInterface::GetPrivate(rq, args, CLS); \ if (! c) return false; \ BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \ - JS::RootedValue rval(cx); \ - ScriptInterface_NativeMethodWrapper::template call(cx, &rval, c, fptr A0_TAIL(z,i)); \ + JS::RootedValue rval(rq.cx); \ + ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \ args.rval().set(rval); \ - return !ScriptInterface::IsExceptionPending(cx); \ + return !ScriptInterface::IsExceptionPending(rq); \ } BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS // const methods #define OVERLOADS(z, i, data) \ template \ bool ScriptInterface::callMethodConst(JSContext* cx, uint argc, JS::Value* vp) \ { \ JS::CallArgs args = JS::CallArgsFromVp(argc, vp); \ - JSAutoRequest rq(cx); \ - TC* c = ScriptInterface::GetPrivate(cx, args, CLS); \ + ScriptInterface::Request rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ + TC* c = ScriptInterface::GetPrivate(rq, args, CLS); \ if (! c) return false; \ BOOST_PP_REPEAT_##z (i, CONVERT_ARG, ~) \ - JS::RootedValue rval(cx); \ - ScriptInterface_NativeMethodWrapper::template call(cx, &rval, c, fptr A0_TAIL(z,i)); \ + JS::RootedValue rval(rq.cx); \ + ScriptInterface_NativeMethodWrapper::template call(rq, &rval, c, fptr A0_TAIL(z,i)); \ args.rval().set(rval); \ - return !ScriptInterface::IsExceptionPending(cx); \ + return !ScriptInterface::IsExceptionPending(rq); \ } BOOST_PP_REPEAT(SCRIPT_INTERFACE_MAX_ARGS, OVERLOADS, ~) #undef OVERLOADS template -static void AssignOrToJSValHelper(JSContext* cx, JS::AutoValueVector& argv, const T& a, const Ts&... params) +static void AssignOrToJSValHelper(const ScriptInterface::Request& rq, JS::AutoValueVector& argv, const T& a, const Ts&... params) { - ScriptInterface::AssignOrToJSVal(cx, argv[i], a); - AssignOrToJSValHelper(cx, argv, params...); + ScriptInterface::AssignOrToJSVal(rq, argv[i], a); + AssignOrToJSValHelper(rq, argv, params...); } template -static void AssignOrToJSValHelper(JSContext* UNUSED(cx), JS::AutoValueVector& UNUSED(argv)) +static void AssignOrToJSValHelper(const ScriptInterface::Request& UNUSED(rq), JS::AutoValueVector& UNUSED(argv)) { cassert(sizeof...(Ts) == 0); // Nop, for terminating the template recursion. } template bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, R& ret, const Ts&... params) const { - JSContext* cx = GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue jsRet(cx); - JS::AutoValueVector argv(cx); + ScriptInterface::Request rq(this); + JS::RootedValue jsRet(rq.cx); + JS::AutoValueVector argv(rq.cx); argv.resize(sizeof...(Ts)); - AssignOrToJSValHelper<0>(cx, argv, params...); + AssignOrToJSValHelper<0>(rq, argv, params...); if (!CallFunction_(val, name, argv, &jsRet)) return false; - return FromJSVal(cx, jsRet, ret); + return FromJSVal(rq, jsRet, ret); } template bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const Ts&... params) const { - JSContext* cx = GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(this); JS::MutableHandle jsRet(ret); - JS::AutoValueVector argv(cx); + JS::AutoValueVector argv(rq.cx); argv.resize(sizeof...(Ts)); - AssignOrToJSValHelper<0>(cx, argv, params...); + AssignOrToJSValHelper<0>(rq, argv, params...); return CallFunction_(val, name, argv, jsRet); } template bool ScriptInterface::CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const Ts&... params) const { - JSContext* cx = GetContext(); - JSAutoRequest rq(cx); - JS::AutoValueVector argv(cx); + ScriptInterface::Request rq(this); + JS::AutoValueVector argv(rq.cx); argv.resize(sizeof...(Ts)); - AssignOrToJSValHelper<0>(cx, argv, params...); + AssignOrToJSValHelper<0>(rq, argv, params...); return CallFunction_(val, name, argv, ret); } // Call the named property on the given object, with void return type template bool ScriptInterface::CallFunctionVoid(JS::HandleValue val, const char* name, const Ts&... params) const { - JSContext* cx = GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue jsRet(cx); - JS::AutoValueVector argv(cx); + ScriptInterface::Request rq(this); + JS::RootedValue jsRet(rq.cx); + JS::AutoValueVector argv(rq.cx); argv.resize(sizeof...(Ts)); - AssignOrToJSValHelper<0>(cx, argv, params...); + AssignOrToJSValHelper<0>(rq, argv, params...); return CallFunction_(val, name, argv, &jsRet); } // Clean up our mess #undef NUMBERED_LIST_HEAD #undef NUMBERED_LIST_TAIL #undef NUMBERED_LIST_TAIL_MAYBE_REF #undef NUMBERED_LIST_BALANCED #undef NUMBERED_LIST_BALANCED_MAYBE_REF #undef CONVERT_ARG #undef TYPENAME_T0_HEAD #undef T0 #undef T0_MAYBE_REF #undef T0_TAIL #undef T0_TAIL_MAYBE_REF #undef A0_TAIL Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 24175) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24176) @@ -1,440 +1,437 @@ /* Copyright (C) 2020 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 "MapGenerator.h" #include "graphics/MapIO.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "lib/external_libraries/libsdl.h" #include "lib/status.h" #include "lib/timer.h" #include "lib/file/vfs/vfs_path.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/FileIo.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "scriptinterface/ScriptRuntime.h" #include "scriptinterface/ScriptConversions.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/helpers/MapEdgeTiles.h" #include #include // TODO: Maybe this should be optimized depending on the map size. constexpr int RMS_RUNTIME_SIZE = 96 * 1024 * 1024; extern bool IsQuitRequested(); static bool MapGeneratorInterruptCallback(JSContext* UNUSED(cx)) { // This may not use SDL_IsQuitRequested(), because it runs in a thread separate to SDL, see SDL_PumpEvents if (IsQuitRequested()) { LOGWARNING("Quit requested!"); return false; } return true; } CMapGeneratorWorker::CMapGeneratorWorker(ScriptInterface* scriptInterface) : m_ScriptInterface(scriptInterface) { // If something happens before we initialize, that's a failure m_Progress = -1; } CMapGeneratorWorker::~CMapGeneratorWorker() { // Wait for thread to end if (m_WorkerThread.joinable()) m_WorkerThread.join(); } void CMapGeneratorWorker::Initialize(const VfsPath& scriptFile, const std::string& settings) { std::lock_guard lock(m_WorkerMutex); // Set progress to positive value m_Progress = 1; m_ScriptPath = scriptFile; m_Settings = settings; // Launch the worker thread m_WorkerThread = std::thread(RunThread, this); } void* CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); shared_ptr mapgenRuntime = ScriptRuntime::CreateRuntime(RMS_RUNTIME_SIZE); // Enable the script to be aborted JS_SetInterruptCallback(mapgenRuntime->m_rt, MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenRuntime); // Run map generation scripts if (!self->Run() || self->m_Progress > 0) { // Don't leave progress in an unknown state, if generator failed, set it to -1 std::lock_guard lock(self->m_WorkerMutex); self->m_Progress = -1; } SAFE_DELETE(self->m_ScriptInterface); // At this point the random map scripts are done running, so the thread has no further purpose // and can die. The data will be stored in m_MapData already if successful, or m_Progress // will contain an error value on failure. return NULL; } bool CMapGeneratorWorker::Run() { - JSContext* cx = m_ScriptInterface->GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(m_ScriptInterface); // Parse settings - JS::RootedValue settingsVal(cx); + JS::RootedValue settingsVal(rq.cx); if (!m_ScriptInterface->ParseJSON(m_Settings, &settingsVal) && settingsVal.isUndefined()) { LOGERROR("CMapGeneratorWorker::Run: Failed to parse settings"); return false; } // Prevent unintentional modifications to the settings object by random map scripts if (!m_ScriptInterface->FreezeObject(settingsVal, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to deepfreeze settings"); return false; } // Init RNG seed u32 seed = 0; if (!m_ScriptInterface->HasProperty(settingsVal, "Seed") || !m_ScriptInterface->GetProperty(settingsVal, "Seed", seed)) LOGWARNING("CMapGeneratorWorker::Run: No seed value specified - using 0"); InitScriptInterface(seed); RegisterScriptFunctions_MapGenerator(); // Copy settings to global variable - JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); + JS::RootedValue global(rq.cx, m_ScriptInterface->GetGlobalObject()); if (!m_ScriptInterface->SetProperty(global, "g_MapSettings", settingsVal, true, true)) { LOGERROR("CMapGeneratorWorker::Run: Failed to define g_MapSettings"); return false; } // Load RMS LOGMESSAGE("Loading RMS '%s'", m_ScriptPath.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(m_ScriptPath)) { LOGERROR("CMapGeneratorWorker::Run: Failed to load RMS '%s'", m_ScriptPath.string8()); return false; } return true; } void CMapGeneratorWorker::InitScriptInterface(const u32 seed) { m_ScriptInterface->SetCallbackData(static_cast(this)); m_ScriptInterface->ReplaceNondeterministicRNG(m_MapGenRNG); m_MapGenRNG.seed(seed); // VFS JSI_VFS::RegisterScriptFunctions_Maps(*m_ScriptInterface); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); // File loading m_ScriptInterface->RegisterFunction("LoadLibrary"); m_ScriptInterface->RegisterFunction("LoadHeightmapImage"); m_ScriptInterface->RegisterFunction("LoadMapTerrain"); // Engine constants // Length of one tile of the terrain grid in metres. // Useful to transform footprint sizes to the tilegrid coordinate system. m_ScriptInterface->SetGlobal("TERRAIN_TILE_SIZE", static_cast(TERRAIN_TILE_SIZE)); // Number of impassable tiles at the map border m_ScriptInterface->SetGlobal("MAP_BORDER_WIDTH", static_cast(MAP_EDGE_TILES)); } void CMapGeneratorWorker::RegisterScriptFunctions_MapGenerator() { // Template functions m_ScriptInterface->RegisterFunction("GetTemplate"); m_ScriptInterface->RegisterFunction("TemplateExists"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindTemplates>("FindTemplates"); m_ScriptInterface->RegisterFunction, std::string, bool, CMapGeneratorWorker::FindActorTemplates>("FindActorTemplates"); // Progression and profiling m_ScriptInterface->RegisterFunction("SetProgress"); m_ScriptInterface->RegisterFunction("GetMicroseconds"); m_ScriptInterface->RegisterFunction("ExportMap"); } int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CxPrivate* UNUSED(pCxPrivate)) { return JS_Now(); } shared_ptr CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& name) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->LoadScripts(name); } void CMapGeneratorWorker::ExportMap(ScriptInterface::CxPrivate* pCxPrivate, JS::HandleValue data) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy results std::lock_guard lock(self->m_WorkerMutex); self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); self->m_Progress = 0; } void CMapGeneratorWorker::SetProgress(ScriptInterface::CxPrivate* pCxPrivate, int progress) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); // Copy data std::lock_guard lock(self->m_WorkerMutex); if (progress >= self->m_Progress) self->m_Progress = progress; else LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress); } CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CxPrivate* pCxPrivate, const std::string& templateName) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.TemplateExists(templateName); } std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CxPrivate* pCxPrivate, const std::string& path, bool includeSubdirectories) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); } bool CMapGeneratorWorker::LoadScripts(const VfsPath& libraryName) { // Ignore libraries that are already loaded if (m_LoadedLibraries.find(libraryName) != m_LoadedLibraries.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedLibraries.insert(libraryName); VfsPath path = VfsPath(L"maps/random/") / libraryName / VfsPath(); VfsPaths pathnames; // Load all scripts in mapgen directory Status ret = vfs::GetPathnames(g_VFS, path, L"*.js", pathnames); if (ret == INFO::OK) { for (const VfsPath& p : pathnames) { LOGMESSAGE("Loading map generator script '%s'", p.string8()); if (!m_ScriptInterface->LoadGlobalScriptFile(p)) { LOGERROR("CMapGeneratorWorker::LoadScripts: Failed to load script '%s'", p.string8()); return false; } } } else { // Some error reading directory wchar_t error[200]; LOGERROR("CMapGeneratorWorker::LoadScripts: Error reading scripts in directory '%s': %s", path.string8(), utf8_from_wstring(StatusDescription(ret, error, ARRAY_SIZE(error)))); return false; } return true; } JS::Value CMapGeneratorWorker::LoadHeightmap(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename) { std::vector heightmap; if (LoadHeightmapImageVfs(filename, heightmap) != INFO::OK) { LOGERROR("Could not load heightmap file '%s'", filename.string8()); return JS::UndefinedValue(); } CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); - JSContext* cx = self->m_ScriptInterface->GetContext(); - JSAutoRequest rq(cx); - JS::RootedValue returnValue(cx); - ToJSVal_vector(cx, &returnValue, heightmap); + ScriptInterface::Request rq(self->m_ScriptInterface); + JS::RootedValue returnValue(rq.cx); + ToJSVal_vector(rq, &returnValue, heightmap); return returnValue; } // See CMapReader::UnpackTerrain, CMapReader::ParseTerrain for the reordering JS::Value CMapGeneratorWorker::LoadMapTerrain(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& filename) { CMapGeneratorWorker* self = static_cast(pCxPrivate->pCBData); - JSContext* cx = self->m_ScriptInterface->GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(self->m_ScriptInterface); if (!VfsFileExists(filename)) { self->m_ScriptInterface->ReportError( ("Terrain file \"" + filename.string8() + "\" does not exist!").c_str()); return JS::UndefinedValue(); } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) { self->m_ScriptInterface->ReportError( ("Could not load terrain file \"" + filename.string8() + "\" too old version!").c_str()); return JS::UndefinedValue(); } // unpack size ssize_t patchesPerSide = (ssize_t)unpacker.UnpackSize(); size_t verticesPerSide = patchesPerSide * PATCH_SIZE + 1; // unpack heightmap std::vector heightmap; heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&heightmap[0], SQR(verticesPerSide) * sizeof(u16)); // unpack texture names size_t textureCount = unpacker.UnpackSize(); std::vector textureNames; textureNames.reserve(textureCount); for (size_t i = 0; i < textureCount; ++i) { CStr texturename; unpacker.UnpackString(texturename); textureNames.push_back(texturename); } // unpack texture IDs per tile ssize_t tilesPerSide = patchesPerSide * PATCH_SIZE; std::vector tiles; tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&tiles[0], sizeof(CMapIO::STileDesc) * tiles.size()); // reorder by patches and store and save texture IDs per tile std::vector textureIDs; for (ssize_t x = 0; x < tilesPerSide; ++x) { size_t patchX = x / PATCH_SIZE; size_t offX = x % PATCH_SIZE; for (ssize_t y = 0; y < tilesPerSide; ++y) { size_t patchY = y / PATCH_SIZE; size_t offY = y % PATCH_SIZE; // m_Priority and m_Tex2Index unused textureIDs.push_back(tiles[(patchY * patchesPerSide + patchX) * SQR(PATCH_SIZE) + (offY * PATCH_SIZE + offX)].m_Tex1Index); } } - JS::RootedValue returnValue(cx); + JS::RootedValue returnValue(rq.cx); ScriptInterface::CreateObject( - cx, + rq, &returnValue, "height", heightmap, "textureNames", textureNames, "textureIDs", textureIDs); return returnValue; } ////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////// CMapGenerator::CMapGenerator() : m_Worker(new CMapGeneratorWorker(nullptr)) { } CMapGenerator::~CMapGenerator() { delete m_Worker; } void CMapGenerator::GenerateMap(const VfsPath& scriptFile, const std::string& settings) { m_Worker->Initialize(scriptFile, settings); } int CMapGenerator::GetProgress() { return m_Worker->GetProgress(); } shared_ptr CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/graphics/MapReader.cpp =================================================================== --- ps/trunk/source/graphics/MapReader.cpp (revision 24175) +++ ps/trunk/source/graphics/MapReader.cpp (revision 24176) @@ -1,1615 +1,1610 @@ /* Copyright (C) 2020 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 "MapReader.h" #include "graphics/Camera.h" #include "graphics/CinemaManager.h" #include "graphics/Entity.h" #include "graphics/GameView.h" #include "graphics/MapGenerator.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/PostprocManager.h" #include "renderer/SkyManager.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpCinemaManager.h" #include "simulation2/components/ICmpGarrisonHolder.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpTurretHolder.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpWaterManager.h" #include CMapReader::CMapReader() : xml_reader(0), m_PatchesPerSide(0), m_MapGen(0) { cur_terrain_tex = 0; // important - resets generator state } // LoadMap: try to load the map from given file; reinitialise the scene to new data if successful void CMapReader::LoadMap(const VfsPath& pathname, JSRuntime* rt, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, const CSimContext* pSimContext_, int playerID_, bool skipEntities) { pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; pSimulation2 = pSimulation2_; pSimContext = pSimContext_; m_PlayerID = playerID_; m_SkipEntities = skipEntities; m_StartingCameraTarget = INVALID_ENTITY; m_ScriptSettings.init(rt, settings); filename_xml = pathname.ChangeExtension(L".xml"); // In some cases (particularly tests) we don't want to bother storing a large // mostly-empty .pmp file, so we let the XML file specify basic terrain instead. // If there's an .xml file and no .pmp, then we're probably in this XML-only mode only_xml = false; if (!VfsFileExists(pathname) && VfsFileExists(filename_xml)) { only_xml = true; } file_format_version = CMapIO::FILE_VERSION; // default if there's no .pmp if (!only_xml) { // [25ms] unpacker.Read(pathname, "PSMP"); file_format_version = unpacker.GetVersion(); } // check oldest supported version if (file_format_version < FILE_READ_VERSION) throw PSERROR_Game_World_MapLoadFailed("Could not load terrain file - too old version!"); // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); // reset post effects if (pPostproc) pPostproc->SetPostEffect(L"default"); // load map or script settings script if (settings.isUndefined()) RegMemFun(this, &CMapReader::LoadScriptSettings, L"CMapReader::LoadScriptSettings", 50); else RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50); // unpack the data if (!only_xml) RegMemFun(this, &CMapReader::UnpackMap, L"CMapReader::UnpackMap", 1200); // read the corresponding XML file RegMemFun(this, &CMapReader::ReadXML, L"CMapReader::ReadXML", 50); // apply terrain data to the world RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5); // read entities RegMemFun(this, &CMapReader::ReadXMLEntities, L"CMapReader::ReadXMLEntities", 5800); // apply misc data to the world RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5); } // LoadRandomMap: try to load the map data; reinitialise the scene to new data if successful void CMapReader::LoadRandomMap(const CStrW& scriptFile, JSRuntime* rt, JS::HandleValue settings, CTerrain *pTerrain_, WaterManager* pWaterMan_, SkyManager* pSkyMan_, CLightEnv *pLightEnv_, CGameView *pGameView_, CCinemaManager* pCinema_, CTriggerManager* pTrigMan_, CPostprocManager* pPostproc_, CSimulation2 *pSimulation2_, int playerID_) { m_ScriptFile = scriptFile; pSimulation2 = pSimulation2_; pSimContext = pSimulation2 ? &pSimulation2->GetSimContext() : NULL; m_ScriptSettings.init(rt, settings); pTerrain = pTerrain_; pLightEnv = pLightEnv_; pGameView = pGameView_; pWaterMan = pWaterMan_; pSkyMan = pSkyMan_; pCinema = pCinema_; pTrigMan = pTrigMan_; pPostproc = pPostproc_; m_PlayerID = playerID_; m_SkipEntities = false; m_StartingCameraTarget = INVALID_ENTITY; // delete all existing entities if (pSimulation2) pSimulation2->ResetState(); only_xml = false; // copy random map settings (before entity creation) RegMemFun(this, &CMapReader::LoadRMSettings, L"CMapReader::LoadRMSettings", 50); // load player settings script (must be done before reading map) RegMemFun(this, &CMapReader::LoadPlayerSettings, L"CMapReader::LoadPlayerSettings", 50); // load map generator with random map script RegMemFun(this, &CMapReader::GenerateMap, L"CMapReader::GenerateMap", 20000); // parse RMS results into terrain structure RegMemFun(this, &CMapReader::ParseTerrain, L"CMapReader::ParseTerrain", 500); // parse RMS results into environment settings RegMemFun(this, &CMapReader::ParseEnvironment, L"CMapReader::ParseEnvironment", 5); // parse RMS results into camera settings RegMemFun(this, &CMapReader::ParseCamera, L"CMapReader::ParseCamera", 5); // apply terrain data to the world RegMemFun(this, &CMapReader::ApplyTerrainData, L"CMapReader::ApplyTerrainData", 5); // parse RMS results into entities RegMemFun(this, &CMapReader::ParseEntities, L"CMapReader::ParseEntities", 1000); // apply misc data to the world RegMemFun(this, &CMapReader::ApplyData, L"CMapReader::ApplyData", 5); // load map settings script (must be done after reading map) RegMemFun(this, &CMapReader::LoadMapSettings, L"CMapReader::LoadMapSettings", 5); } // UnpackMap: unpack the given data from the raw data stream into local variables int CMapReader::UnpackMap() { return UnpackTerrain(); } // UnpackTerrain: unpack the terrain from the end of the input data stream // - data: map size, heightmap, list of textures used by map, texture tile assignments int CMapReader::UnpackTerrain() { // yield after this time is reached. balances increased progress bar // smoothness vs. slowing down loading. const double end_time = timer_Time() + 200e-3; // first call to generator (this is skipped after first call, // i.e. when the loop below was interrupted) if (cur_terrain_tex == 0) { m_PatchesPerSide = (ssize_t)unpacker.UnpackSize(); // unpack heightmap [600us] size_t verticesPerSide = m_PatchesPerSide*PATCH_SIZE+1; m_Heightmap.resize(SQR(verticesPerSide)); unpacker.UnpackRaw(&m_Heightmap[0], SQR(verticesPerSide)*sizeof(u16)); // unpack # textures num_terrain_tex = unpacker.UnpackSize(); m_TerrainTextures.reserve(num_terrain_tex); } // unpack texture names; find handle for each texture. // interruptible. while (cur_terrain_tex < num_terrain_tex) { CStr texturename; unpacker.UnpackString(texturename); ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled) CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texturename); m_TerrainTextures.push_back(texentry); cur_terrain_tex++; LDR_CHECK_TIMEOUT(cur_terrain_tex, num_terrain_tex); } // unpack tile data [3ms] ssize_t tilesPerSide = m_PatchesPerSide*PATCH_SIZE; m_Tiles.resize(size_t(SQR(tilesPerSide))); unpacker.UnpackRaw(&m_Tiles[0], sizeof(STileDesc)*m_Tiles.size()); // reset generator state. cur_terrain_tex = 0; return 0; } int CMapReader::ApplyTerrainData() { if (m_PatchesPerSide == 0) { // we'll probably crash when trying to use this map later throw PSERROR_Game_World_MapLoadFailed("Error loading map: no terrain data.\nCheck application log for details."); } if (!only_xml) { // initialise the terrain pTerrain->Initialize(m_PatchesPerSide, &m_Heightmap[0]); // setup the textures on the minipatches STileDesc* tileptr = &m_Tiles[0]; for (ssize_t j=0; jGetPatch(i,j)->m_MiniPatches[m][k]; // can't fail mp.Tex = m_TerrainTextures[tileptr->m_Tex1Index]; mp.Priority = tileptr->m_Priority; tileptr++; } } } } } CmpPtr cmpTerrain(*pSimContext, SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); return 0; } // ApplyData: take all the input data, and rebuild the scene from it int CMapReader::ApplyData() { // copy over the lighting parameters if (pLightEnv) *pLightEnv = m_LightEnv; CmpPtr cmpPlayerManager(*pSimContext, SYSTEM_ENTITY); if (pGameView && cmpPlayerManager) { // Default to global camera (with constraints) pGameView->ResetCameraTarget(pGameView->GetCamera()->GetFocus()); // TODO: Starting rotation? CmpPtr cmpPlayer(*pSimContext, cmpPlayerManager->GetPlayerByID(m_PlayerID)); if (cmpPlayer && cmpPlayer->HasStartingCamera()) { // Use player starting camera CFixedVector3D pos = cmpPlayer->GetStartingCameraPos(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } else if (m_StartingCameraTarget != INVALID_ENTITY) { // Point camera at entity CmpPtr cmpPosition(*pSimContext, m_StartingCameraTarget); if (cmpPosition) { CFixedVector3D pos = cmpPosition->GetPosition(); pGameView->ResetCameraTarget(CVector3D(pos.X.ToFloat(), pos.Y.ToFloat(), pos.Z.ToFloat())); } } } return 0; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// PSRETURN CMapSummaryReader::LoadMap(const VfsPath& pathname) { VfsPath filename_xml = pathname.ChangeExtension(L".xml"); CXeromyces xmb_file; if (xmb_file.Load(g_VFS, filename_xml, "scenario") != PSRETURN_OK) return PSRETURN_File_ReadFailed; // Define all the relevant elements used in the XML file #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(scenario); EL(scriptsettings); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(root.GetNodeName() == el_scenario); XERO_ITER_EL(root, child) { int child_name = child.GetNodeName(); if (child_name == el_scriptsettings) { m_ScriptSettings = child.GetText(); } } return PSRETURN_OK; } void CMapSummaryReader::GetMapSettings(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) { - JSContext* cx = scriptInterface.GetContext(); - JSAutoRequest rq(cx); + ScriptInterface::Request rq(scriptInterface); - ScriptInterface::CreateObject(cx, ret); + ScriptInterface::CreateObject(rq, ret); if (m_ScriptSettings.empty()) return; - JS::RootedValue scriptSettingsVal(cx); + JS::RootedValue scriptSettingsVal(rq.cx); scriptInterface.ParseJSON(m_ScriptSettings, &scriptSettingsVal); scriptInterface.SetProperty(ret, "settings", scriptSettingsVal, false); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Holds various state data while reading maps, so that loading can be // interrupted (e.g. to update the progress display) then later resumed. class CXMLReader { NONCOPYABLE(CXMLReader); public: CXMLReader(const VfsPath& xml_filename, CMapReader& mapReader) : m_MapReader(mapReader), nodes(NULL, 0, NULL) { Init(xml_filename); } CStr ReadScriptSettings(); // read everything except for entities void ReadXML(); // return semantics: see Loader.cpp!LoadFunc. int ProgressiveReadEntities(); private: CXeromyces xmb_file; CMapReader& m_MapReader; int el_entity; int el_tracks; int el_template, el_player; int el_position, el_orientation, el_obstruction; int el_garrison; int el_turrets; int el_actor; int at_x, at_y, at_z; int at_group, at_group2; int at_angle; int at_uid; int at_seed; int at_turret; XMBElementList nodes; // children of root // loop counters size_t node_idx; size_t entity_idx; // # entities+nonentities processed and total (for progress calc) int completed_jobs, total_jobs; // maximum used entity ID, so we can safely allocate new ones entity_id_t max_uid; void Init(const VfsPath& xml_filename); void ReadTerrain(XMBElement parent); void ReadEnvironment(XMBElement parent); void ReadCamera(XMBElement parent); void ReadPaths(XMBElement parent); void ReadTriggers(XMBElement parent); int ReadEntities(XMBElement parent, double end_time); }; void CXMLReader::Init(const VfsPath& xml_filename) { // must only assign once, so do it here node_idx = entity_idx = 0; if (xmb_file.Load(g_VFS, xml_filename, "scenario") != PSRETURN_OK) throw PSERROR_Game_World_MapLoadFailed("Could not read map XML file!"); // define the elements and attributes that are frequently used in the XML file, // so we don't need to do lots of string construction and comparison when // reading the data. // (Needs to be synchronised with the list in CXMLReader - ugh) #define EL(x) el_##x = xmb_file.GetElementID(#x) #define AT(x) at_##x = xmb_file.GetAttributeID(#x) EL(entity); EL(tracks); EL(template); EL(player); EL(position); EL(garrison); EL(turrets); EL(orientation); EL(obstruction); EL(actor); AT(x); AT(y); AT(z); AT(group); AT(group2); AT(angle); AT(uid); AT(seed); AT(turret); #undef AT #undef EL XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementString(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); // find out total number of entities+nonentities // (used when calculating progress) completed_jobs = 0; total_jobs = 0; for (XMBElement node : nodes) total_jobs += node.GetChildNodes().size(); // Find the maximum entity ID, so we can safely allocate new IDs without conflicts max_uid = SYSTEM_ENTITY; XMBElement ents = nodes.GetFirstNamedItem(xmb_file.GetElementID("Entities")); XERO_ITER_EL(ents, ent) { CStr uid = ent.GetAttributes().GetNamedItem(at_uid); max_uid = std::max(max_uid, (entity_id_t)uid.ToUInt()); } } CStr CXMLReader::ReadScriptSettings() { XMBElement root = xmb_file.GetRoot(); ENSURE(xmb_file.GetElementString(root.GetNodeName()) == "Scenario"); nodes = root.GetChildNodes(); XMBElement settings = nodes.GetFirstNamedItem(xmb_file.GetElementID("ScriptSettings")); return settings.GetText(); } void CXMLReader::ReadTerrain(XMBElement parent) { #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) AT(patches); AT(texture); AT(priority); AT(height); #undef AT ssize_t patches = 9; CStr texture = "grass1_spring"; int priority = 0; u16 height = 16384; XERO_ITER_ATTR(parent, attr) { if (attr.Name == at_patches) patches = attr.Value.ToInt(); else if (attr.Name == at_texture) texture = attr.Value; else if (attr.Name == at_priority) priority = attr.Value.ToInt(); else if (attr.Name == at_height) height = (u16)attr.Value.ToInt(); } m_MapReader.m_PatchesPerSide = patches; // Load the texture ENSURE(CTerrainTextureManager::IsInitialised()); // we need this for the terrain properties (even when graphics are disabled) CTerrainTextureEntry* texentry = g_TexMan.FindTexture(texture); m_MapReader.pTerrain->Initialize(patches, NULL); // Fill the heightmap u16* heightmap = m_MapReader.pTerrain->GetHeightMap(); ssize_t verticesPerSide = m_MapReader.pTerrain->GetVerticesPerSide(); for (ssize_t i = 0; i < SQR(verticesPerSide); ++i) heightmap[i] = height; // Fill the texture map for (ssize_t pz = 0; pz < patches; ++pz) { for (ssize_t px = 0; px < patches; ++px) { CPatch* patch = m_MapReader.pTerrain->GetPatch(px, pz); // can't fail for (ssize_t z = 0; z < PATCH_SIZE; ++z) { for (ssize_t x = 0; x < PATCH_SIZE; ++x) { patch->m_MiniPatches[z][x].Tex = texentry; patch->m_MiniPatches[z][x].Priority = priority; } } } } } void CXMLReader::ReadEnvironment(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(posteffect); EL(skyset); EL(suncolor); EL(sunelevation); EL(sunrotation); EL(terrainambientcolor); EL(unitsambientcolor); EL(water); EL(waterbody); EL(type); EL(color); EL(tint); EL(height); EL(waviness); EL(murkiness); EL(windangle); EL(fog); EL(fogcolor); EL(fogfactor); EL(fogthickness); EL(postproc); EL(brightness); EL(contrast); EL(saturation); EL(bloom); AT(r); AT(g); AT(b); #undef AT #undef EL XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_skyset) { if (m_MapReader.pSkyMan) m_MapReader.pSkyMan->SetSkySet(element.GetText().FromUTF8()); } else if (element_name == el_suncolor) { m_MapReader.m_LightEnv.m_SunColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_sunelevation) { m_MapReader.m_LightEnv.m_Elevation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_sunrotation) { m_MapReader.m_LightEnv.m_Rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_terrainambientcolor) { m_MapReader.m_LightEnv.m_TerrainAmbientColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_unitsambientcolor) { m_MapReader.m_LightEnv.m_UnitsAmbientColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_fog) { XERO_ITER_EL(element, fog) { int element_name = fog.GetNodeName(); if (element_name == el_fogcolor) { XMBAttributeList attrs = fog.GetAttributes(); m_MapReader.m_LightEnv.m_FogColor = RGBColor( attrs.GetNamedItem(at_r).ToFloat(), attrs.GetNamedItem(at_g).ToFloat(), attrs.GetNamedItem(at_b).ToFloat()); } else if (element_name == el_fogfactor) { m_MapReader.m_LightEnv.m_FogFactor = fog.GetText().ToFloat(); } else if (element_name == el_fogthickness) { m_MapReader.m_LightEnv.m_FogMax = fog.GetText().ToFloat(); } } } else if (element_name == el_postproc) { XERO_ITER_EL(element, postproc) { int element_name = postproc.GetNodeName(); if (element_name == el_brightness) { m_MapReader.m_LightEnv.m_Brightness = postproc.GetText().ToFloat(); } else if (element_name == el_contrast) { m_MapReader.m_LightEnv.m_Contrast = postproc.GetText().ToFloat(); } else if (element_name == el_saturation) { m_MapReader.m_LightEnv.m_Saturation = postproc.GetText().ToFloat(); } else if (element_name == el_bloom) { m_MapReader.m_LightEnv.m_Bloom = postproc.GetText().ToFloat(); } else if (element_name == el_posteffect) { if (m_MapReader.pPostproc) m_MapReader.pPostproc->SetPostEffect(postproc.GetText().FromUTF8()); } } } else if (element_name == el_water) { XERO_ITER_EL(element, waterbody) { ENSURE(waterbody.GetNodeName() == el_waterbody); XERO_ITER_EL(waterbody, waterelement) { int element_name = waterelement.GetNodeName(); if (element_name == el_height) { CmpPtr cmpWaterManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); ENSURE(cmpWaterManager); cmpWaterManager->SetWaterLevel(entity_pos_t::FromString(waterelement.GetText())); continue; } // The rest are purely graphical effects, and should be ignored if // graphics are disabled if (!m_MapReader.pWaterMan) continue; if (element_name == el_type) { if (waterelement.GetText() == "default") m_MapReader.pWaterMan->m_WaterType = L"ocean"; else m_MapReader.pWaterMan->m_WaterType = waterelement.GetText().FromUTF8(); } #define READ_COLOR(el, out) \ else if (element_name == el) \ { \ XMBAttributeList attrs = waterelement.GetAttributes(); \ out = CColor( \ attrs.GetNamedItem(at_r).ToFloat(), \ attrs.GetNamedItem(at_g).ToFloat(), \ attrs.GetNamedItem(at_b).ToFloat(), \ 1.f); \ } #define READ_FLOAT(el, out) \ else if (element_name == el) \ { \ out = waterelement.GetText().ToFloat(); \ } \ READ_COLOR(el_color, m_MapReader.pWaterMan->m_WaterColor) READ_COLOR(el_tint, m_MapReader.pWaterMan->m_WaterTint) READ_FLOAT(el_waviness, m_MapReader.pWaterMan->m_Waviness) READ_FLOAT(el_murkiness, m_MapReader.pWaterMan->m_Murkiness) READ_FLOAT(el_windangle, m_MapReader.pWaterMan->m_WindAngle) #undef READ_FLOAT #undef READ_COLOR else debug_warn(L"Invalid map XML data"); } } } else debug_warn(L"Invalid map XML data"); } m_MapReader.m_LightEnv.CalculateSunDirection(); } void CXMLReader::ReadCamera(XMBElement parent) { // defaults if we don't find player starting camera #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(declination); EL(rotation); EL(position); AT(angle); AT(x); AT(y); AT(z); #undef AT #undef EL float declination = DEGTORAD(30.f), rotation = DEGTORAD(-45.f); CVector3D translation = CVector3D(100, 150, -100); XERO_ITER_EL(parent, element) { int element_name = element.GetNodeName(); XMBAttributeList attrs = element.GetAttributes(); if (element_name == el_declination) { declination = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_rotation) { rotation = attrs.GetNamedItem(at_angle).ToFloat(); } else if (element_name == el_position) { translation = CVector3D( attrs.GetNamedItem(at_x).ToFloat(), attrs.GetNamedItem(at_y).ToFloat(), attrs.GetNamedItem(at_z).ToFloat()); } else debug_warn(L"Invalid map XML data"); } if (m_MapReader.pGameView) { m_MapReader.pGameView->GetCamera()->m_Orientation.SetXRotation(declination); m_MapReader.pGameView->GetCamera()->m_Orientation.RotateY(rotation); m_MapReader.pGameView->GetCamera()->m_Orientation.Translate(translation); m_MapReader.pGameView->GetCamera()->UpdateFrustum(); } } void CXMLReader::ReadPaths(XMBElement parent) { #define EL(x) int el_##x = xmb_file.GetElementID(#x) #define AT(x) int at_##x = xmb_file.GetAttributeID(#x) EL(path); EL(rotation); EL(node); EL(position); EL(target); AT(name); AT(timescale); AT(orientation); AT(mode); AT(style); AT(x); AT(y); AT(z); AT(deltatime); #undef EL #undef AT CmpPtr cmpCinemaManager(*m_MapReader.pSimContext, SYSTEM_ENTITY); XERO_ITER_EL(parent, element) { int elementName = element.GetNodeName(); if (elementName == el_path) { CCinemaData pathData; XMBAttributeList attrs = element.GetAttributes(); CStrW pathName(attrs.GetNamedItem(at_name).FromUTF8()); pathData.m_Name = pathName; pathData.m_Timescale = fixed::FromString(attrs.GetNamedItem(at_timescale)); pathData.m_Orientation = attrs.GetNamedItem(at_orientation).FromUTF8(); pathData.m_Mode = attrs.GetNamedItem(at_mode).FromUTF8(); pathData.m_Style = attrs.GetNamedItem(at_style).FromUTF8(); TNSpline positionSpline, targetSpline; fixed lastPositionTime = fixed::Zero(); fixed lastTargetTime = fixed::Zero(); XERO_ITER_EL(element, pathChild) { elementName = pathChild.GetNodeName(); attrs = pathChild.GetAttributes(); // Load node data used for spline if (elementName == el_node) { lastPositionTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); lastTargetTime += fixed::FromString(attrs.GetNamedItem(at_deltatime)); XERO_ITER_EL(pathChild, nodeChild) { elementName = nodeChild.GetNodeName(); attrs = nodeChild.GetAttributes(); if (elementName == el_position) { CFixedVector3D position(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); positionSpline.AddNode(position, CFixedVector3D(), lastPositionTime); lastPositionTime = fixed::Zero(); } else if (elementName == el_rotation) { // TODO: Implement rotation slerp/spline as another object } else if (elementName == el_target) { CFixedVector3D targetPosition(fixed::FromString(attrs.GetNamedItem(at_x)), fixed::FromString(attrs.GetNamedItem(at_y)), fixed::FromString(attrs.GetNamedItem(at_z))); targetSpline.AddNode(targetPosition, CFixedVector3D(), lastTargetTime); lastTargetTime = fixed::Zero(); } else LOGWARNING("Invalid cinematic element for node child"); } } else LOGWARNING("Invalid cinematic element for path child"); } // Construct cinema path with data gathered CCinemaPath path(pathData, positionSpline, targetSpline); if (path.Empty()) { LOGWARNING("Path with name '%s' is empty", pathName.ToUTF8()); return; } if (!cmpCinemaManager) continue; if (!cmpCinemaManager->HasPath(pathName)) cmpCinemaManager->AddPath(path); else LOGWARNING("Path with name '%s' already exists", pathName.ToUTF8()); } else LOGWARNING("Invalid path child with name '%s'", element.GetText()); } } void CXMLReader::ReadTriggers(XMBElement UNUSED(parent)) { } int CXMLReader::ReadEntities(XMBElement parent, double end_time) { XMBElementList entities = parent.GetChildNodes(); ENSURE(m_MapReader.pSimulation2); CSimulation2& sim = *m_MapReader.pSimulation2; CmpPtr cmpPlayerManager(sim, SYSTEM_ENTITY); while (entity_idx < entities.size()) { // all new state at this scope and below doesn't need to be // wrapped, since we only yield after a complete iteration. XMBElement entity = entities[entity_idx++]; ENSURE(entity.GetNodeName() == el_entity); XMBAttributeList attrs = entity.GetAttributes(); CStr uid = attrs.GetNamedItem(at_uid); ENSURE(!uid.empty()); int EntityUid = uid.ToInt(); CStrW TemplateName; int PlayerID = 0; std::vector Garrison; std::vector > Turrets; CFixedVector3D Position; CFixedVector3D Orientation; long Seed = -1; // Obstruction control groups. entity_id_t ControlGroup = INVALID_ENTITY; entity_id_t ControlGroup2 = INVALID_ENTITY; XERO_ITER_EL(entity, setting) { int element_name = setting.GetNodeName(); //