Index: ps/trunk/source/ps/scripting/JSInterface_Debug.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Debug.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Debug.cpp (revision 24983) @@ -1,97 +1,101 @@ /* 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_Debug.h" #include "i18n/L10n.h" #include "lib/svn_revision.h" #include "lib/debug.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include #include +namespace JSI_Debug +{ /** * Microseconds since the epoch. */ -double JSI_Debug::GetMicroseconds(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +double GetMicroseconds() { return JS_Now(); } // Deliberately cause the game to crash. // Currently implemented via access violation (read of address 0). // Useful for testing the crashlog/stack trace code. -int JSI_Debug::Crash(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +int Crash() { debug_printf("Crashing at user's request.\n"); return *(volatile int*)0; } -void JSI_Debug::DebugWarn(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void DebugWarn() { debug_warn(L"Warning at user's request."); } -void JSI_Debug::DisplayErrorDialog(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& msg) +void DisplayErrorDialog(const std::wstring& msg) { debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL); } // Return the date at which the current executable was compiled. // - Displayed on main menu screen; tells non-programmers which auto-build // they are running. Could also be determined via .EXE file properties, // but that's a bit more trouble. -std::wstring JSI_Debug::GetBuildDate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::wstring GetBuildDate() { UDate buildDate = g_L10n.ParseDateTime(__DATE__, "MMM d yyyy", icu::Locale::getUS()); return wstring_from_utf8(g_L10n.LocalizeDateTime(buildDate, L10n::Date, icu::SimpleDateFormat::MEDIUM)); } -double JSI_Debug::GetBuildTimestamp(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +double GetBuildTimestamp() { UDate buildDate = g_L10n.ParseDateTime(__DATE__ " " __TIME__, "MMM d yyyy HH:mm:ss", icu::Locale::getUS()); if (buildDate) return buildDate / 1000.0; return std::time(nullptr); } // Return the revision number at which the current executable was compiled. // - svn revision is generated by calling svnversion and cached in // lib/svn_revision.cpp. it is useful to know when attempting to // reproduce bugs (the main EXE and PDB should be temporarily reverted to // that revision so that they match user-submitted crashdumps). -std::wstring JSI_Debug::GetBuildRevision(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::wstring GetBuildRevision() { std::wstring svnRevision(svn_revision); if (svnRevision == L"custom build") return wstring_from_utf8(g_L10n.Translate("custom build")); return svnRevision; } -void JSI_Debug::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GetMicroseconds"); - scriptInterface.RegisterFunction("Crash"); - scriptInterface.RegisterFunction("DebugWarn"); - scriptInterface.RegisterFunction("DisplayErrorDialog"); - scriptInterface.RegisterFunction("GetBuildDate"); - scriptInterface.RegisterFunction("GetBuildTimestamp"); - scriptInterface.RegisterFunction("GetBuildRevision"); + ScriptFunction::Register<&GetMicroseconds>(rq, "GetMicroseconds"); + ScriptFunction::Register<&Crash>(rq, "Crash"); + ScriptFunction::Register<&DebugWarn>(rq, "DebugWarn"); + ScriptFunction::Register<&DisplayErrorDialog>(rq, "DisplayErrorDialog"); + ScriptFunction::Register<&GetBuildDate>(rq, "GetBuildDate"); + ScriptFunction::Register<&GetBuildTimestamp>(rq, "GetBuildTimestamp"); + ScriptFunction::Register<&GetBuildRevision>(rq, "GetBuildRevision"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_Game.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24983) @@ -1,188 +1,190 @@ /* Copyright (C) 2021 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/GameSetup/GameSetup.h" #include "ps/Replay.h" #include "ps/World.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/system/TurnManager.h" #include "simulation2/Simulation2.h" #include "soundmanager/SoundManager.h" -bool JSI_Game::IsGameStarted(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +namespace JSI_Game +{ +bool IsGameStarted() { return g_Game; } -void JSI_Game::StartGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs, int playerID) +void StartGame(ScriptInterface::CmptPrivate* pCmptPrivate, 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(); ScriptRequest rqSim(sim->GetScriptInterface()); JS::RootedValue gameAttribs(rqSim.cx, sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), attribs)); g_Game->SetPlayerID(playerID); g_Game->StartGame(&gameAttribs, ""); } -void JSI_Game::Script_EndGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void Script_EndGame() { EndGame(); } -int JSI_Game::GetPlayerID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +int GetPlayerID() { if (!g_Game) return -1; return g_Game->GetPlayerID(); } -void JSI_Game::SetPlayerID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int id) +void SetPlayerID(int id) { if (!g_Game) return; g_Game->SetPlayerID(id); } -void JSI_Game::SetViewedPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int id) +void SetViewedPlayer(int id) { if (!g_Game) return; g_Game->SetViewedPlayerID(id); } -float JSI_Game::GetSimRate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +float GetSimRate() { return g_Game->GetSimRate(); } -void JSI_Game::SetSimRate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float rate) +void SetSimRate(float rate) { g_Game->SetSimRate(rate); } -bool JSI_Game::IsPaused(ScriptInterface::CmptPrivate* pCmptPrivate) +bool IsPaused(const ScriptRequest& rq) { if (!g_Game) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Game is not started"); return false; } return g_Game->m_Paused; } -void JSI_Game::SetPaused(ScriptInterface::CmptPrivate* pCmptPrivate, bool pause, bool sendMessage) +void SetPaused(const ScriptRequest& rq, bool pause, bool sendMessage) { if (!g_Game) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Game is not started"); return; } g_Game->m_Paused = pause; #if CONFIG2_AUDIO if (g_SoundManager) { g_SoundManager->PauseAmbient(pause); g_SoundManager->PauseAction(pause); } #endif if (g_NetClient && sendMessage) g_NetClient->SendPausedMessage(pause); } -bool JSI_Game::IsVisualReplay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool IsVisualReplay() { if (!g_Game) return false; return g_Game->IsVisualReplay(); } -std::wstring JSI_Game::GetCurrentReplayDirectory(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::wstring GetCurrentReplayDirectory() { 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::CmptPrivate* UNUSED(pCmptPrivate), unsigned int numTurns) +void EnableTimeWarpRecording(unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } -void JSI_Game::RewindTimeWarp(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void RewindTimeWarp() { g_Game->GetTurnManager()->RewindTimeWarp(); } -void JSI_Game::DumpTerrainMipmap(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void DumpTerrainMipmap() { 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) +void RegisterScriptFunctions(const ScriptRequest& rq) { - 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"); + ScriptFunction::Register<&IsGameStarted>(rq, "IsGameStarted"); + ScriptFunction::Register<&StartGame>(rq, "StartGame"); + ScriptFunction::Register<&Script_EndGame>(rq, "EndGame"); + ScriptFunction::Register<&GetPlayerID>(rq, "GetPlayerID"); + ScriptFunction::Register<&SetPlayerID>(rq, "SetPlayerID"); + ScriptFunction::Register<&SetViewedPlayer>(rq, "SetViewedPlayer"); + ScriptFunction::Register<&GetSimRate>(rq, "GetSimRate"); + ScriptFunction::Register<&SetSimRate>(rq, "SetSimRate"); + ScriptFunction::Register<&IsPaused>(rq, "IsPaused"); + ScriptFunction::Register<&SetPaused>(rq, "SetPaused"); + ScriptFunction::Register<&IsVisualReplay>(rq, "IsVisualReplay"); + ScriptFunction::Register<&GetCurrentReplayDirectory>(rq, "GetCurrentReplayDirectory"); + ScriptFunction::Register<&EnableTimeWarpRecording>(rq, "EnableTimeWarpRecording"); + ScriptFunction::Register<&RewindTimeWarp>(rq, "RewindTimeWarp"); + ScriptFunction::Register<&DumpTerrainMipmap>(rq, "DumpTerrainMipmap"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_Main.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Main.cpp (revision 24983) @@ -1,138 +1,142 @@ /* 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/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "tools/atlas/GameInterface/GameLoop.h" extern void QuitEngine(); extern void StartAtlas(); -void JSI_Main::QuitEngine(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +namespace JSI_Main +{ +void QuitEngine() { ::QuitEngine(); } -void JSI_Main::StartAtlas(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void StartAtlas() { ::StartAtlas(); } -bool JSI_Main::AtlasIsAvailable(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool AtlasIsAvailable() { return ATLAS_IsAvailable(); } -bool JSI_Main::IsAtlasRunning(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool IsAtlasRunning() { return g_AtlasGameLoop && g_AtlasGameLoop->running; } -void JSI_Main::OpenURL(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& url) +void OpenURL(const std::string& url) { sys_open_url(url); } -std::wstring JSI_Main::GetSystemUsername(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::wstring GetSystemUsername() { return sys_get_user_name(); } -std::wstring JSI_Main::GetMatchID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::wstring GetMatchID() { return ps_generate_guid().FromUTF8(); } -JS::Value JSI_Main::LoadMapSettings(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) +JS::Value LoadMapSettings(const ScriptInterface& scriptInterface, const VfsPath& pathname) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); CMapSummaryReader reader; if (reader.LoadMap(pathname) != PSRETURN_OK) return JS::UndefinedValue(); JS::RootedValue settings(rq.cx); - reader.GetMapSettings(*(pCmptPrivate->pScriptInterface), &settings); + reader.GetMapSettings(scriptInterface, &settings); return settings; } -bool JSI_Main::HotkeyIsPressed_(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& hotkeyName) +bool HotkeyIsPressed_(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::CmptPrivate* UNUSED(pCmptPrivate)) +int GetFps() { if (!g_frequencyFilter) return 0; return g_frequencyFilter->StableFrequency(); } -int JSI_Main::GetTextWidth(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& fontName, const std::wstring& text) +int GetTextWidth(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::CmptPrivate* UNUSED(pCmptPrivate), const std::string& input) +std::string CalculateMD5(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) +void RegisterScriptFunctions(const ScriptRequest& rq) { - 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"); + ScriptFunction::Register<&QuitEngine>(rq, "Exit"); + ScriptFunction::Register<&StartAtlas>(rq, "RestartInAtlas"); + ScriptFunction::Register<&AtlasIsAvailable>(rq, "AtlasIsAvailable"); + ScriptFunction::Register<&IsAtlasRunning>(rq, "IsAtlasRunning"); + ScriptFunction::Register<&OpenURL>(rq, "OpenURL"); + ScriptFunction::Register<&GetSystemUsername>(rq, "GetSystemUsername"); + ScriptFunction::Register<&GetMatchID>(rq, "GetMatchID"); + ScriptFunction::Register<&LoadMapSettings>(rq, "LoadMapSettings"); + ScriptFunction::Register<&HotkeyIsPressed_>(rq, "HotkeyIsPressed"); + ScriptFunction::Register<&GetFps>(rq, "GetFPS"); + ScriptFunction::Register<&GetTextWidth>(rq, "GetTextWidth"); + ScriptFunction::Register<&CalculateMD5>(rq, "CalculateMD5"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_ModIo.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ModIo.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_ModIo.cpp (revision 24983) @@ -1,157 +1,127 @@ /* 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_ModIo.h" #include "ps/CLogger.h" #include "ps/ModIo.h" +#include "scriptinterface/FunctionWrapper.h" -void JSI_ModIo::StartGetGameId(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +namespace JSI_ModIo { - if (!g_ModIo) - g_ModIo = new ModIo(); - - ENSURE(g_ModIo); - - g_ModIo->StartGetGameId(); -} - -void JSI_ModIo::StartListMods(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - if (!g_ModIo) - { - LOGERROR("ModIoStartListMods called before ModIoStartGetGameId"); - return; - } - - g_ModIo->StartListMods(); -} - -void JSI_ModIo::StartDownloadMod(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), uint32_t idx) +ModIo* ModIoGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_ModIo) { - LOGERROR("ModIoStartDownloadMod called before ModIoStartGetGameId"); - return; + LOGERROR("Trying to access ModIO when it's not initialized!"); + return nullptr; } - - g_ModIo->StartDownloadMod(idx); + return g_ModIo; } -bool JSI_ModIo::AdvanceRequest(ScriptInterface::CmptPrivate* pCmptPrivate) +void StartGetGameId() { if (!g_ModIo) - { - LOGERROR("ModIoAdvanceRequest called before ModIoGetMods"); - return false; - } - - ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; - return g_ModIo->AdvanceRequest(*scriptInterface); -} + g_ModIo = new ModIo(); -void JSI_ModIo::CancelRequest(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - if (!g_ModIo) - { - LOGERROR("ModIoCancelRequest called before ModIoGetMods"); - return; - } + ENSURE(g_ModIo); - g_ModIo->CancelRequest(); + g_ModIo->StartGetGameId(); } -JS::Value JSI_ModIo::GetMods(ScriptInterface::CmptPrivate* pCmptPrivate) +// TODO: could provide a FromJSVal for ModIoModData +JS::Value GetMods(const ScriptInterface& scriptInterface) { if (!g_ModIo) { LOGERROR("ModIoGetMods called before ModIoStartGetGameId"); return JS::NullValue(); } - ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; ScriptRequest rq(scriptInterface); const std::vector& availableMods = g_ModIo->GetMods(); JS::RootedValue mods(rq.cx); ScriptInterface::CreateArray(rq, &mods, availableMods.size()); u32 i = 0; for (const ModIoModData& mod : availableMods) { JS::RootedValue m(rq.cx); ScriptInterface::CreateObject(rq, &m); for (const std::pair& prop : mod.properties) - scriptInterface->SetProperty(m, prop.first.c_str(), prop.second, true); + scriptInterface.SetProperty(m, prop.first.c_str(), prop.second, true); - scriptInterface->SetProperty(m, "dependencies", mod.dependencies, true); - scriptInterface->SetPropertyInt(mods, i++, m); + scriptInterface.SetProperty(m, "dependencies", mod.dependencies, true); + scriptInterface.SetPropertyInt(mods, i++, m); } return mods; } const std::map statusStrings = { { DownloadProgressStatus::NONE, "none" }, { DownloadProgressStatus::GAMEID, "gameid" }, { DownloadProgressStatus::READY, "ready" }, { DownloadProgressStatus::LISTING, "listing" }, { DownloadProgressStatus::LISTED, "listed" }, { DownloadProgressStatus::DOWNLOADING, "downloading" }, { DownloadProgressStatus::SUCCESS, "success" }, { DownloadProgressStatus::FAILED_GAMEID, "failed_gameid" }, { DownloadProgressStatus::FAILED_LISTING, "failed_listing" }, { DownloadProgressStatus::FAILED_DOWNLOADING, "failed_downloading" }, { DownloadProgressStatus::FAILED_FILECHECK, "failed_filecheck" } }; -JS::Value JSI_ModIo::GetDownloadProgress(ScriptInterface::CmptPrivate* pCmptPrivate) +// TODO: could provide a FromJSVal for DownloadProgressData +JS::Value GetDownloadProgress(ScriptInterface::CmptPrivate* pCmptPrivate) { if (!g_ModIo) { LOGERROR("ModIoGetDownloadProgress called before ModIoGetMods"); return JS::NullValue(); } ScriptInterface* scriptInterface = pCmptPrivate->pScriptInterface; ScriptRequest rq(scriptInterface); const DownloadProgressData& progress = g_ModIo->GetDownloadProgress(); JS::RootedValue progressData(rq.cx); ScriptInterface::CreateObject(rq, &progressData); scriptInterface->SetProperty(progressData, "status", statusStrings.at(progress.status), true); scriptInterface->SetProperty(progressData, "progress", progress.progress, true); scriptInterface->SetProperty(progressData, "error", progress.error, true); return progressData; } -void JSI_ModIo::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("ModIoStartGetGameId"); - scriptInterface.RegisterFunction("ModIoStartListMods"); - scriptInterface.RegisterFunction("ModIoStartDownloadMod"); - scriptInterface.RegisterFunction("ModIoAdvanceRequest"); - scriptInterface.RegisterFunction("ModIoCancelRequest"); - scriptInterface.RegisterFunction("ModIoGetMods"); - scriptInterface.RegisterFunction("ModIoGetDownloadProgress"); + ScriptFunction::Register<&StartGetGameId>(rq, "ModIoStartGetGameId"); + ScriptFunction::Register<&ModIo::StartListMods, &ModIoGetter>(rq, "ModIoStartListMods"); + ScriptFunction::Register<&ModIo::StartDownloadMod, &ModIoGetter>(rq, "ModIoStartDownloadMod"); + ScriptFunction::Register<&ModIo::AdvanceRequest, &ModIoGetter>(rq, "ModIoAdvanceRequest"); + ScriptFunction::Register<&ModIo::CancelRequest, &ModIoGetter>(rq, "ModIoCancelRequest"); + ScriptFunction::Register<&GetMods>(rq, "ModIoGetMods"); + ScriptFunction::Register<&GetDownloadProgress>(rq, "ModIoGetDownloadProgress"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.cpp (revision 24983) @@ -1,133 +1,137 @@ /* 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/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" -JS::Value JSI_SavedGame::GetSavedGames(ScriptInterface::CmptPrivate* pCmptPrivate) +namespace JSI_SavedGame { - return SavedGames::GetSavedGames(*(pCmptPrivate->pScriptInterface)); +JS::Value GetSavedGames(const ScriptInterface& scriptInterface) +{ + return SavedGames::GetSavedGames(scriptInterface); } -bool JSI_SavedGame::DeleteSavedGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name) +bool DeleteSavedGame(const std::wstring& name) { return SavedGames::DeleteSavedGame(name); } -void JSI_SavedGame::SaveGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata) +void SaveGame(const ScriptInterface& scriptInterface, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata) { - ScriptInterface::StructuredClone GUIMetadataClone = pCmptPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata); + ScriptInterface::StructuredClone GUIMetadataClone = scriptInterface.WriteStructuredClone(GUIMetadata); if (SavedGames::Save(filename, description, *g_Game->GetSimulation2(), GUIMetadataClone) < 0) LOGERROR("Failed to save game"); } -void JSI_SavedGame::SaveGamePrefix(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata) +void SaveGamePrefix(const ScriptInterface& scriptInterface, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata) { - ScriptInterface::StructuredClone GUIMetadataClone = pCmptPrivate->pScriptInterface->WriteStructuredClone(GUIMetadata); + ScriptInterface::StructuredClone GUIMetadataClone = scriptInterface.WriteStructuredClone(GUIMetadata); if (SavedGames::SavePrefix(prefix, description, *g_Game->GetSimulation2(), GUIMetadataClone) < 0) LOGERROR("Failed to save game"); } -void JSI_SavedGame::QuickSave(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), JS::HandleValue GUIMetadata) +void QuickSave(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::CmptPrivate* UNUSED(pCmptPrivate)) +void QuickLoad() { 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::CmptPrivate* pCmptPrivate, const std::wstring& name) +JS::Value StartSavedGame(const ScriptInterface& scriptInterface, 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. - ScriptRequest rqGui(pCmptPrivate->pScriptInterface); + ScriptRequest rqGui(scriptInterface); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); // Load the saved game data from disk JS::RootedValue guiContextMetadata(rqGui.cx); std::string savedState; - Status err = SavedGames::Load(name, *(pCmptPrivate->pScriptInterface), &guiContextMetadata, savedState); + Status err = SavedGames::Load(name, scriptInterface, &guiContextMetadata, savedState); if (err < 0) return JS::UndefinedValue(); g_Game = new CGame(true); { CSimulation2* sim = g_Game->GetSimulation2(); ScriptRequest rqGame(sim->GetScriptInterface()); JS::RootedValue gameContextMetadata(rqGame.cx, - sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), guiContextMetadata)); + sim->GetScriptInterface().CloneValueFromOtherCompartment(scriptInterface, guiContextMetadata)); 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 ActivateRejoinTest(ScriptInterface::CmptPrivate*) +void ActivateRejoinTest() { if (!g_Game || !g_Game->GetSimulation2() || !g_Game->GetTurnManager()) return; g_Game->GetSimulation2()->ActivateRejoinTest(g_Game->GetTurnManager()->GetCurrentTurn() + 1); } -void JSI_SavedGame::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GetSavedGames"); - scriptInterface.RegisterFunction("DeleteSavedGame"); - scriptInterface.RegisterFunction("SaveGame"); - scriptInterface.RegisterFunction("SaveGamePrefix"); - scriptInterface.RegisterFunction("QuickSave"); - scriptInterface.RegisterFunction("QuickLoad"); - scriptInterface.RegisterFunction("ActivateRejoinTest"); - scriptInterface.RegisterFunction("StartSavedGame"); + ScriptFunction::Register<&GetSavedGames>(rq, "GetSavedGames"); + ScriptFunction::Register<&DeleteSavedGame>(rq, "DeleteSavedGame"); + ScriptFunction::Register<&SaveGame>(rq, "SaveGame"); + ScriptFunction::Register<&SaveGamePrefix>(rq, "SaveGamePrefix"); + ScriptFunction::Register<&QuickSave>(rq, "QuickSave"); + ScriptFunction::Register<&QuickLoad>(rq, "QuickLoad"); + ScriptFunction::Register<&ActivateRejoinTest>(rq, "ActivateRejoinTest"); + ScriptFunction::Register<&StartSavedGame>(rq, "StartSavedGame"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_UserReport.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_UserReport.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_UserReport.h (revision 24983) @@ -1,36 +1,28 @@ /* Copyright (C) 2018 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_JSI_USERREPORT #define INCLUDED_JSI_USERREPORT -#include "scriptinterface/ScriptInterface.h" - -#include +class ScriptRequest; namespace JSI_UserReport { - bool IsUserReportEnabled(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetUserReportEnabled(ScriptInterface::CmptPrivate* pCmptPrivate, bool enabled); - std::string GetUserReportStatus(ScriptInterface::CmptPrivate* pCmptPrivate); - std::string GetUserReportLogPath(ScriptInterface::CmptPrivate* pCmptPrivate); - std::string GetUserReportConfigPath(ScriptInterface::CmptPrivate* pCmptPrivate); - - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_USERREPORT Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.cpp (revision 24983) @@ -1,76 +1,45 @@ /* Copyright (C) 2019 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_VisualReplay.h" #include "ps/CStr.h" #include "ps/VisualReplay.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" -bool JSI_VisualReplay::StartVisualReplay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& directory) +namespace JSI_VisualReplay { - return VisualReplay::StartVisualReplay(directory); -} - -bool JSI_VisualReplay::DeleteReplay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& replayFile) -{ - return VisualReplay::DeleteReplay(replayFile); -} - -JS::Value JSI_VisualReplay::GetReplays(ScriptInterface::CmptPrivate* pCmptPrivate, bool compareFiles) +CStrW GetReplayDirectoryName(const CStrW& directoryName) { - return VisualReplay::GetReplays(*(pCmptPrivate->pScriptInterface), compareFiles); + return OsPath(VisualReplay::GetDirectoryPath() / directoryName).string(); } -JS::Value JSI_VisualReplay::GetReplayAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName) +void RegisterScriptFunctions(const ScriptRequest& rq) { - return VisualReplay::GetReplayAttributes(pCmptPrivate, directoryName); + ScriptFunction::Register<&VisualReplay::GetReplays>(rq, "GetReplays"); + ScriptFunction::Register<&VisualReplay::DeleteReplay>(rq, "DeleteReplay"); + ScriptFunction::Register<&VisualReplay::StartVisualReplay>(rq, "StartVisualReplay"); + ScriptFunction::Register<&VisualReplay::GetReplayAttributes>(rq, "GetReplayAttributes"); + ScriptFunction::Register<&VisualReplay::GetReplayMetadata>(rq, "GetReplayMetadata"); + ScriptFunction::Register<&VisualReplay::HasReplayMetadata>(rq, "HasReplayMetadata"); + ScriptFunction::Register<&VisualReplay::AddReplayToCache>(rq, "AddReplayToCache"); + ScriptFunction::Register<&GetReplayDirectoryName>(rq, "GetReplayDirectoryName"); } - -bool JSI_VisualReplay::HasReplayMetadata(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& directoryName) -{ - return VisualReplay::HasReplayMetadata(directoryName); -} - -JS::Value JSI_VisualReplay::GetReplayMetadata(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName) -{ - return VisualReplay::GetReplayMetadata(pCmptPrivate, directoryName); -} - -void JSI_VisualReplay::AddReplayToCache(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName) -{ - VisualReplay::AddReplayToCache(*(pCmptPrivate->pScriptInterface), directoryName); -} - -CStrW JSI_VisualReplay::GetReplayDirectoryName(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& directoryName) -{ - return wstring_from_utf8(OsPath(VisualReplay::GetDirectoryPath() / directoryName).string8()); -} - -void JSI_VisualReplay::RegisterScriptFunctions(const ScriptInterface& scriptInterface) -{ - scriptInterface.RegisterFunction("GetReplays"); - scriptInterface.RegisterFunction("DeleteReplay"); - scriptInterface.RegisterFunction("StartVisualReplay"); - scriptInterface.RegisterFunction("GetReplayAttributes"); - scriptInterface.RegisterFunction("GetReplayMetadata"); - scriptInterface.RegisterFunction("HasReplayMetadata"); - scriptInterface.RegisterFunction("AddReplayToCache"); - scriptInterface.RegisterFunction("GetReplayDirectoryName"); } Index: ps/trunk/source/ps/scripting/JSInterface_SavedGame.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_SavedGame.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_SavedGame.h (revision 24983) @@ -1,36 +1,28 @@ /* Copyright (C) 2019 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_JSI_SAVEDGAME #define INCLUDED_JSI_SAVEDGAME -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_SavedGame { - JS::Value GetSavedGames(ScriptInterface::CmptPrivate* pCmptPrivate); - bool DeleteSavedGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name); - void SaveGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename, const std::wstring& description, JS::HandleValue GUIMetadata); - void SaveGamePrefix(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& prefix, const std::wstring& description, JS::HandleValue GUIMetadata); - void QuickSave(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue GUIMetadata); - void QuickLoad(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value StartSavedGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name); - - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_SAVEDGAME Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24983) @@ -1,289 +1,290 @@ /* Copyright (C) 2021 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/FunctionWrapper.h" #include "scriptinterface/ScriptExtraHeaders.h" #include "scriptinterface/ScriptInterface.h" #include +namespace JSI_VFS +{ // 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 */ +// Tests whether the current script context is allowed to read from the given directory +bool PathRestrictionMet(const ScriptRequest& rq, 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"\""; + } + + ScriptException::Raise(rq, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str()); + + return false; +} + + // state held across multiple BuildDirEntListCB calls; init by BuildDirEntList. struct BuildDirEntListState { - ScriptInterface* pScriptInterface; + const ScriptRequest& rq; JS::PersistentRootedObject filename_array; int cur_idx; - BuildDirEntListState(ScriptInterface* scriptInterface) - : pScriptInterface(scriptInterface), - filename_array(scriptInterface->GetGeneralJSContext()), + BuildDirEntListState(const ScriptRequest& rq) + : rq(rq), + filename_array(rq.cx), cur_idx(0) { - ScriptRequest 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; - ScriptRequest rq(s->pScriptInterface); - 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); + JS::RootedObject filenameArrayObj(s->rq.cx, s->filename_array); + JS::RootedValue val(s->rq.cx); + ScriptInterface::ToJSVal(s->rq, &val, CStrW(pathname.string()) ); + JS_SetElement(s->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::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse) +JS::Value BuildDirEntList(const ScriptRequest& rq, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse) { - if (!PathRestrictionMet(pCmptPrivate, validPaths, path)) + if (!PathRestrictionMet(rq, 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; // build array in the callback function - BuildDirEntListState state(pCmptPrivate->pScriptInterface); + BuildDirEntListState state(rq); 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::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filename) +bool FileExists(const ScriptRequest& rq, const std::vector& validPaths, const CStrW& filename) { - return PathRestrictionMet(pCmptPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK; + return PathRestrictionMet(rq, 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::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename) +double GetFileMTime(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::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename) +unsigned int GetFileSize(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::CmptPrivate* pCmptPrivate, const std::wstring& filename) +JS::Value ReadFile(const ScriptRequest& rq, const std::wstring& filename) { 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 - ScriptRequest rq(pCmptPrivate->pScriptInterface); 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::CmptPrivate* pCmptPrivate, const std::wstring& filename) +JS::Value ReadFileLines(const ScriptInterface& scriptInterface, const std::wstring& filename) { 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); - const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface; ScriptRequest 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(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::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath) +// Return file contents parsed as a JS Object +JS::Value ReadJSONFile(const ScriptInterface& scriptInterface, const std::vector& validPaths, const CStrW& filePath) { - if (!PathRestrictionMet(pCmptPrivate, validPaths, filePath)) + ScriptRequest rq(scriptInterface); + if (!PathRestrictionMet(rq, validPaths, filePath)) return JS::NullValue(); - const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface; - ScriptRequest rq(scriptInterface); JS::RootedValue out(rq.cx); scriptInterface.ReadJSONFile(filePath, &out); return out; } -void JSI_VFS::WriteJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath, JS::HandleValue val1) +// Save given JS Object to a JSON file +void WriteJSONFile(const ScriptInterface& scriptInterface, const std::wstring& filePath, JS::HandleValue val1) { - const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface; ScriptRequest rq(scriptInterface); // TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON. JS::RootedValue val(rq.cx, val1); 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::DeleteCampaignSave(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& filePath) +bool DeleteCampaignSave(const CStrW& filePath) { OsPath realPath; if (filePath.Left(16) != L"saves/campaigns/" || filePath.Right(12) != L".0adcampaign") return false; return VfsFileExists(filePath) && g_VFS->GetRealPath(filePath, realPath) == INFO::OK && g_VFS->RemoveFile(filePath) == INFO::OK && wunlink(realPath) == 0; } -bool JSI_VFS::PathRestrictionMet(ScriptInterface::CmptPrivate* pCmptPrivate, 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"\""; - } - - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "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::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\ +JS::Value Script_ReadJSONFile_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\ {\ - return JSI_VFS::ReadJSONFile(pCmptPrivate, PathRestriction_##context, filePath);\ + return ReadJSONFile(scriptInterface, PathRestriction_##context, filePath);\ }\ -JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\ +JS::Value Script_ListDirectoryFiles_##context(const ScriptInterface& scriptInterface, const std::wstring& path, const std::wstring& filterStr, bool recurse)\ {\ - return JSI_VFS::BuildDirEntList(pCmptPrivate, PathRestriction_##context, path, filterStr, recurse);\ + return BuildDirEntList(scriptInterface, PathRestriction_##context, path, filterStr, recurse);\ }\ -bool Script_FileExists_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\ +bool Script_FileExists_##context(const ScriptInterface& scriptInterface, const std::wstring& filePath)\ {\ - return JSI_VFS::FileExists(pCmptPrivate, PathRestriction_##context, filePath);\ + return FileExists(scriptInterface, PathRestriction_##context, filePath);\ }\ VFS_ScriptFunctions(GUI); VFS_ScriptFunctions(Simulation); VFS_ScriptFunctions(Maps); #undef VFS_ScriptFunctions -void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions_GUI(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("ListDirectoryFiles"); - scriptInterface.RegisterFunction("FileExists"); - scriptInterface.RegisterFunction("GetFileMTime"); - scriptInterface.RegisterFunction("GetFileSize"); - scriptInterface.RegisterFunction("ReadFile"); - scriptInterface.RegisterFunction("ReadFileLines"); - scriptInterface.RegisterFunction("ReadJSONFile"); - scriptInterface.RegisterFunction("WriteJSONFile"); - scriptInterface.RegisterFunction("DeleteCampaignSave"); + ScriptFunction::Register<&Script_ListDirectoryFiles_GUI>(rq, "ListDirectoryFiles"); + ScriptFunction::Register<&Script_FileExists_GUI>(rq, "FileExists"); + ScriptFunction::Register<&GetFileMTime>(rq, "GetFileMTime"); + ScriptFunction::Register<&GetFileSize>(rq, "GetFileSize"); + ScriptFunction::Register<&ReadFile>(rq, "ReadFile"); + ScriptFunction::Register<&ReadFileLines>(rq, "ReadFileLines"); + ScriptFunction::Register<&Script_ReadJSONFile_GUI>(rq, "ReadJSONFile"); + ScriptFunction::Register<&WriteJSONFile>(rq, "WriteJSONFile"); + ScriptFunction::Register<&DeleteCampaignSave>(rq, "DeleteCampaignSave"); } -void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions_Simulation(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("ListDirectoryFiles"); - scriptInterface.RegisterFunction("FileExists"); - scriptInterface.RegisterFunction("ReadJSONFile"); + ScriptFunction::Register<&Script_ListDirectoryFiles_Simulation>(rq, "ListDirectoryFiles"); + ScriptFunction::Register<&Script_FileExists_Simulation>(rq, "FileExists"); + ScriptFunction::Register<&Script_ReadJSONFile_Simulation>(rq, "ReadJSONFile"); } -void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions_Maps(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("ListDirectoryFiles"); - scriptInterface.RegisterFunction("FileExists"); - scriptInterface.RegisterFunction("ReadJSONFile"); + ScriptFunction::Register<&Script_ListDirectoryFiles_Maps>(rq, "ListDirectoryFiles"); + ScriptFunction::Register<&Script_FileExists_Maps>(rq, "FileExists"); + ScriptFunction::Register<&Script_ReadJSONFile_Maps>(rq, "ReadJSONFile"); +} } Index: ps/trunk/source/graphics/CameraController.cpp =================================================================== --- ps/trunk/source/graphics/CameraController.cpp (revision 24982) +++ ps/trunk/source/graphics/CameraController.cpp (revision 24983) @@ -1,719 +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 "CameraController.h" #include "graphics/HFTracer.h" #include "graphics/Terrain.h" -#include "graphics/scripting/JSInterface_GameView.h" #include "lib/input.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" extern int g_xres, g_yres; // Maximum distance outside the edge of the map that the camera's // focus point can be moved static const float CAMERA_EDGE_MARGIN = 2.0f * TERRAIN_TILE_SIZE; CCameraController::CCameraController(CCamera& camera) : ICameraController(camera), m_ConstrainCamera(true), m_FollowEntity(INVALID_ENTITY), m_FollowFirstPerson(false), // Dummy values (these will be filled in by the config file) m_ViewScrollSpeed(0), m_ViewScrollSpeedModifier(1), m_ViewRotateXSpeed(0), m_ViewRotateXMin(0), m_ViewRotateXMax(0), m_ViewRotateXDefault(0), m_ViewRotateYSpeed(0), m_ViewRotateYSpeedWheel(0), m_ViewRotateYDefault(0), m_ViewRotateSpeedModifier(1), m_ViewDragSpeed(0), m_ViewZoomSpeed(0), m_ViewZoomSpeedWheel(0), m_ViewZoomMin(0), m_ViewZoomMax(0), m_ViewZoomDefault(0), m_ViewZoomSpeedModifier(1), m_ViewFOV(DEGTORAD(45.f)), m_ViewNear(2.f), m_ViewFar(4096.f), m_JoystickPanX(-1), m_JoystickPanY(-1), m_JoystickRotateX(-1), m_JoystickRotateY(-1), m_JoystickZoomIn(-1), m_JoystickZoomOut(-1), m_HeightSmoothness(0.5f), m_HeightMin(16.f), m_PosX(0, 0, 0.01f), m_PosY(0, 0, 0.01f), m_PosZ(0, 0, 0.01f), m_Zoom(0, 0, 0.1f), m_RotateX(0, 0, 0.001f), m_RotateY(0, 0, 0.001f) { SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; m_Camera.SetViewPort(vp); SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CCameraController::~CCameraController() = default; void CCameraController::LoadConfig() { CFG_GET_VAL("view.scroll.speed", m_ViewScrollSpeed); CFG_GET_VAL("view.scroll.speed.modifier", m_ViewScrollSpeedModifier); CFG_GET_VAL("view.rotate.x.speed", m_ViewRotateXSpeed); CFG_GET_VAL("view.rotate.x.min", m_ViewRotateXMin); CFG_GET_VAL("view.rotate.x.max", m_ViewRotateXMax); CFG_GET_VAL("view.rotate.x.default", m_ViewRotateXDefault); CFG_GET_VAL("view.rotate.y.speed", m_ViewRotateYSpeed); CFG_GET_VAL("view.rotate.y.speed.wheel", m_ViewRotateYSpeedWheel); CFG_GET_VAL("view.rotate.y.default", m_ViewRotateYDefault); CFG_GET_VAL("view.rotate.speed.modifier", m_ViewRotateSpeedModifier); CFG_GET_VAL("view.drag.speed", m_ViewDragSpeed); CFG_GET_VAL("view.zoom.speed", m_ViewZoomSpeed); CFG_GET_VAL("view.zoom.speed.wheel", m_ViewZoomSpeedWheel); CFG_GET_VAL("view.zoom.min", m_ViewZoomMin); CFG_GET_VAL("view.zoom.max", m_ViewZoomMax); CFG_GET_VAL("view.zoom.default", m_ViewZoomDefault); CFG_GET_VAL("view.zoom.speed.modifier", m_ViewZoomSpeedModifier); CFG_GET_VAL("joystick.camera.pan.x", m_JoystickPanX); CFG_GET_VAL("joystick.camera.pan.y", m_JoystickPanY); CFG_GET_VAL("joystick.camera.rotate.x", m_JoystickRotateX); CFG_GET_VAL("joystick.camera.rotate.y", m_JoystickRotateY); CFG_GET_VAL("joystick.camera.zoom.in", m_JoystickZoomIn); CFG_GET_VAL("joystick.camera.zoom.out", m_JoystickZoomOut); CFG_GET_VAL("view.height.smoothness", m_HeightSmoothness); CFG_GET_VAL("view.height.min", m_HeightMin); #define SETUP_SMOOTHNESS(CFG_PREFIX, SMOOTHED_VALUE) \ { \ float smoothness = SMOOTHED_VALUE.GetSmoothness(); \ CFG_GET_VAL(CFG_PREFIX ".smoothness", smoothness); \ SMOOTHED_VALUE.SetSmoothness(smoothness); \ } SETUP_SMOOTHNESS("view.pos", m_PosX); SETUP_SMOOTHNESS("view.pos", m_PosY); SETUP_SMOOTHNESS("view.pos", m_PosZ); SETUP_SMOOTHNESS("view.zoom", m_Zoom); SETUP_SMOOTHNESS("view.rotate.x", m_RotateX); SETUP_SMOOTHNESS("view.rotate.y", m_RotateY); #undef SETUP_SMOOTHNESS CFG_GET_VAL("view.near", m_ViewNear); CFG_GET_VAL("view.far", m_ViewFar); CFG_GET_VAL("view.fov", m_ViewFOV); // Convert to radians m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_ViewFOV = DEGTORAD(m_ViewFOV); } void CCameraController::SetViewport(const SViewPort& vp) { m_Camera.SetViewPort(vp); SetCameraProjection(); } void CCameraController::Update(const float deltaRealTime) { // Calculate mouse movement static int mouse_last_x = 0; static int mouse_last_y = 0; int mouse_dx = g_mouse_x - mouse_last_x; int mouse_dy = g_mouse_y - mouse_last_y; mouse_last_x = g_mouse_x; mouse_last_y = g_mouse_y; if (HotkeyIsPressed("camera.rotate.cw")) m_RotateY.AddSmoothly(m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.ccw")) m_RotateY.AddSmoothly(-m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.up")) m_RotateX.AddSmoothly(-m_ViewRotateXSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.down")) m_RotateX.AddSmoothly(m_ViewRotateXSpeed * deltaRealTime); float moveRightward = 0.f; float moveForward = 0.f; if (HotkeyIsPressed("camera.pan")) { moveRightward += m_ViewDragSpeed * mouse_dx; moveForward += m_ViewDragSpeed * -mouse_dy; } if (g_mouse_active) { if (g_mouse_x >= g_xres - 2 && g_mouse_x < g_xres) moveRightward += m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_x <= 3 && g_mouse_x >= 0) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (g_mouse_y >= g_yres - 2 && g_mouse_y < g_yres) moveForward -= m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_y <= 3 && g_mouse_y >= 0) moveForward += m_ViewScrollSpeed * deltaRealTime; } if (HotkeyIsPressed("camera.right")) moveRightward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.left")) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.up")) moveForward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.down")) moveForward -= m_ViewScrollSpeed * deltaRealTime; if (g_Joystick.IsEnabled()) { // This could all be improved with extra speed and sensitivity settings // (maybe use pow to allow finer control?), and inversion settings moveRightward += g_Joystick.GetAxisValue(m_JoystickPanX) * m_ViewScrollSpeed * deltaRealTime; moveForward -= g_Joystick.GetAxisValue(m_JoystickPanY) * m_ViewScrollSpeed * deltaRealTime; m_RotateX.AddSmoothly(g_Joystick.GetAxisValue(m_JoystickRotateX) * m_ViewRotateXSpeed * deltaRealTime); m_RotateY.AddSmoothly(-g_Joystick.GetAxisValue(m_JoystickRotateY) * m_ViewRotateYSpeed * deltaRealTime); // Use a +1 bias for zoom because I want this to work with trigger buttons that default to -1 m_Zoom.AddSmoothly((g_Joystick.GetAxisValue(m_JoystickZoomIn) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime); m_Zoom.AddSmoothly(-(g_Joystick.GetAxisValue(m_JoystickZoomOut) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime); } if (moveRightward || moveForward) { // Break out of following mode when the user starts scrolling m_FollowEntity = INVALID_ENTITY; float s = sin(m_RotateY.GetSmoothedValue()); float c = cos(m_RotateY.GetSmoothedValue()); m_PosX.AddSmoothly(c * moveRightward); m_PosZ.AddSmoothly(-s * moveRightward); m_PosX.AddSmoothly(s * moveForward); m_PosZ.AddSmoothly(c * moveForward); } if (m_FollowEntity) { CmpPtr cmpPosition(*(g_Game->GetSimulation2()), m_FollowEntity); CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpPosition && cmpPosition->IsInWorld() && cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FollowEntity, g_Game->GetViewedPlayerID()) == LosVisibility::VISIBLE) { // Get the most recent interpolated position float frameOffset = g_Game->GetSimulation2()->GetLastFrameOffset(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset); CVector3D pos = transform.GetTranslation(); if (m_FollowFirstPerson) { float x, z, angle; cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle); float height = 4.f; m_Camera.m_Orientation.SetIdentity(); m_Camera.m_Orientation.RotateX(static_cast(M_PI) / 24.f); m_Camera.m_Orientation.RotateY(angle); m_Camera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z); m_Camera.UpdateFrustum(); return; } else { // Move the camera to match the unit CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pos - pivot; m_PosX.AddSmoothly(delta.X); m_PosY.AddSmoothly(delta.Y); m_PosZ.AddSmoothly(delta.Z); } } else { // The unit disappeared (died or garrisoned etc), so stop following it m_FollowEntity = INVALID_ENTITY; } } if (HotkeyIsPressed("camera.zoom.in")) m_Zoom.AddSmoothly(-m_ViewZoomSpeed * deltaRealTime); if (HotkeyIsPressed("camera.zoom.out")) m_Zoom.AddSmoothly(m_ViewZoomSpeed * deltaRealTime); if (m_ConstrainCamera) m_Zoom.ClampSmoothly(m_ViewZoomMin, m_ViewZoomMax); float zoomDelta = -m_Zoom.Update(deltaRealTime); if (zoomDelta) { CVector3D forwards = m_Camera.GetOrientation().GetIn(); m_PosX.AddSmoothly(forwards.X * zoomDelta); m_PosY.AddSmoothly(forwards.Y * zoomDelta); m_PosZ.AddSmoothly(forwards.Z * zoomDelta); } if (m_ConstrainCamera) m_RotateX.ClampSmoothly(DEGTORAD(m_ViewRotateXMin), DEGTORAD(m_ViewRotateXMax)); FocusHeight(true); // Ensure the ViewCamera focus is inside the map with the chosen margins // if not so - apply margins to the camera if (m_ConstrainCamera) { CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); CTerrain* pTerrain = g_Game->GetWorld()->GetTerrain(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CVector3D desiredPivot = pivot; CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpRangeManager && cmpRangeManager->GetLosCircular()) { // Clamp to a circular region around the center of the map float r = pTerrain->GetMaxX() / 2; CVector3D center(r, desiredPivot.Y, r); float dist = (desiredPivot - center).Length(); if (dist > r - CAMERA_EDGE_MARGIN) desiredPivot = center + (desiredPivot - center).Normalized() * (r - CAMERA_EDGE_MARGIN); } else { // Clamp to the square edges of the map desiredPivot.X = Clamp(desiredPivot.X, pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN); desiredPivot.Z = Clamp(desiredPivot.Z, pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN); } // Update the position so that pivot is within the margin m_PosX.SetValueSmoothly(desiredPivot.X + delta.X); m_PosZ.SetValueSmoothly(desiredPivot.Z + delta.Z); } m_PosX.Update(deltaRealTime); m_PosY.Update(deltaRealTime); m_PosZ.Update(deltaRealTime); // Handle rotation around the Y (vertical) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateYDelta = m_RotateY.Update(deltaRealTime); if (rotateYDelta) { // We've updated RotateY, and need to adjust Pos so that it's still // facing towards the original focus point (the terrain in the center // of the screen). CVector3D upwards(0.0f, 1.0f, 0.0f); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(upwards, rotateYDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } // Handle rotation around the X (sideways, relative to camera) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateXDelta = m_RotateX.Update(deltaRealTime); if (rotateXDelta) { CVector3D rightwards = targetCam.GetOrientation().GetLeft() * -1.0f; CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(rightwards, rotateXDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } /* This is disabled since it doesn't seem necessary: // Ensure the camera's near point is never inside the terrain if (m_ConstrainCamera) { CMatrix3D target; target.SetIdentity(); target.RotateX(m_RotateX.GetValue()); target.RotateY(m_RotateY.GetValue()); target.Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); CVector3D nearPoint = target.GetTranslation() + target.GetIn() * defaultNear; float ground = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z); float limit = ground + 16.f; if (nearPoint.Y < limit) m_PosY.AddSmoothly(limit - nearPoint.Y); } */ m_RotateY.Wrap(-static_cast(M_PI), static_cast(M_PI)); // Update the camera matrix SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CVector3D CCameraController::GetSmoothPivot(CCamera& camera) const { return camera.GetOrientation().GetTranslation() + camera.GetOrientation().GetIn() * m_Zoom.GetSmoothedValue(); } CVector3D CCameraController::GetCameraPivot() const { return GetSmoothPivot(m_Camera); } CVector3D CCameraController::GetCameraPosition() const { return CVector3D(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } CVector3D CCameraController::GetCameraRotation() const { // The angle of rotation around the Z axis is not used. return CVector3D(m_RotateX.GetValue(), m_RotateY.GetValue(), 0.0f); } float CCameraController::GetCameraZoom() const { return m_Zoom.GetValue(); } void CCameraController::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom) { m_PosX.SetValue(pos.X); m_PosY.SetValue(pos.Y); m_PosZ.SetValue(pos.Z); m_RotateX.SetValue(rotX); m_RotateY.SetValue(rotY); m_Zoom.SetValue(zoom); FocusHeight(false); SetupCameraMatrixNonSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::MoveCameraTarget(const CVector3D& target) { // Maintain the same orientation and level of zoom, if we can // (do this by working out the point the camera is looking at, saving // the difference between that position and the camera point, and restoring // that difference to our new target) CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = target - pivot; m_PosX.SetValueSmoothly(delta.X + m_PosX.GetValue()); m_PosZ.SetValueSmoothly(delta.Z + m_PosZ.GetValue()); FocusHeight(false); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::ResetCameraTarget(const CVector3D& target) { CMatrix3D orientation; orientation.SetIdentity(); orientation.RotateX(DEGTORAD(m_ViewRotateXDefault)); orientation.RotateY(DEGTORAD(m_ViewRotateYDefault)); CVector3D delta = orientation.GetIn() * m_ViewZoomDefault; m_PosX.SetValue(target.X - delta.X); m_PosY.SetValue(target.Y - delta.Y); m_PosZ.SetValue(target.Z - delta.Z); m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_Zoom.SetValue(m_ViewZoomDefault); FocusHeight(false); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::FollowEntity(entity_id_t entity, bool firstPerson) { m_FollowEntity = entity; m_FollowFirstPerson = firstPerson; } entity_id_t CCameraController::GetFollowedEntity() { return m_FollowEntity; } void CCameraController::SetCameraProjection() { m_Camera.SetPerspectiveProjection(m_ViewNear, m_ViewFar, m_ViewFOV); } void CCameraController::ResetCameraAngleZoom() { CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); // Compute the zoom adjustment to get us back to the default CVector3D forwards = targetCam.GetOrientation().GetIn(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pivot - targetCam.GetOrientation().GetTranslation(); float dist = delta.Dot(forwards); m_Zoom.AddSmoothly(m_ViewZoomDefault - dist); // Reset orientations to default m_RotateX.SetValueSmoothly(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValueSmoothly(DEGTORAD(m_ViewRotateYDefault)); } void CCameraController::SetupCameraMatrixSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetSmoothedValue(), m_PosY.GetSmoothedValue(), m_PosZ.GetSmoothedValue()); } void CCameraController::SetupCameraMatrixSmoothRot(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::SetupCameraMatrixNonSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetValue()); orientation->RotateY(m_RotateY.GetValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::FocusHeight(bool smooth) { /* The camera pivot height is moved towards ground level. To prevent excessive zoom when looking over a cliff, the target ground level is the maximum of the ground level at the camera's near and pivot points. The ground levels are filtered to achieve smooth camera movement. The filter radius is proportional to the zoom level. The camera height is clamped to prevent map penetration. */ if (!m_ConstrainCamera) return; CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); const CVector3D position = targetCam.GetOrientation().GetTranslation(); const CVector3D forwards = targetCam.GetOrientation().GetIn(); // horizontal view radius const float radius = sqrtf(forwards.X * forwards.X + forwards.Z * forwards.Z) * m_Zoom.GetSmoothedValue(); const float near_radius = radius * m_HeightSmoothness; const float pivot_radius = radius * m_HeightSmoothness; const CVector3D nearPoint = position + forwards * m_ViewNear; const CVector3D pivotPoint = position + forwards * m_Zoom.GetSmoothedValue(); const float ground = std::max( g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z), g_Renderer.GetWaterManager()->m_WaterHeight); // filter ground levels for smooth camera movement const float filtered_near_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius); const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius); // filtered maximum visible ground level in view const float filtered_ground = std::max( std::max(filtered_near_ground, filtered_pivot_ground), g_Renderer.GetWaterManager()->m_WaterHeight); // target camera height above pivot point const float pivot_height = -forwards.Y * (m_Zoom.GetSmoothedValue() - m_ViewNear); // minimum camera height above filtered ground level const float min_height = (m_HeightMin + ground - filtered_ground); const float target_height = std::max(pivot_height, min_height); const float height = (nearPoint.Y - filtered_ground); const float diff = target_height - height; if (fabsf(diff) < 0.0001f) return; if (smooth) m_PosY.AddSmoothly(diff); else m_PosY.Add(diff); } InReaction CCameraController::HandleEvent(const SDL_Event_* ev) { switch (ev->ev.type) { case SDL_HOTKEYPRESS: { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "camera.reset") { ResetCameraAngleZoom(); return IN_HANDLED; } return IN_PASS; } case SDL_HOTKEYDOWN: { std::string hotkey = static_cast(ev->ev.user.data1); // Mouse wheel must be treated using events instead of polling, // because SDL auto-generates a sequence of mousedown/mouseup events // and we never get to see the "down" state inside Update(). if (hotkey == "camera.zoom.wheel.in") { m_Zoom.AddSmoothly(-m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.zoom.wheel.out") { m_Zoom.AddSmoothly(m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.cw") { m_RotateY.AddSmoothly(m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.ccw") { m_RotateY.AddSmoothly(-m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.increase") { m_ViewScrollSpeed *= m_ViewScrollSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.decrease") { m_ViewScrollSpeed /= m_ViewScrollSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.increase") { m_ViewRotateXSpeed *= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed *= m_ViewRotateSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.decrease") { m_ViewRotateXSpeed /= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed /= m_ViewRotateSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.increase") { m_ViewZoomSpeed *= m_ViewZoomSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.decrease") { m_ViewZoomSpeed /= m_ViewZoomSpeedModifier; return IN_HANDLED; } return IN_PASS; } } return IN_PASS; } Index: ps/trunk/source/graphics/GameView.cpp =================================================================== --- ps/trunk/source/graphics/GameView.cpp (revision 24982) +++ ps/trunk/source/graphics/GameView.cpp (revision 24983) @@ -1,415 +1,414 @@ /* 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 "GameView.h" #include "graphics/CameraController.h" #include "graphics/CinemaManager.h" #include "graphics/ColladaManager.h" #include "graphics/HFTracer.h" #include "graphics/LOSTexture.h" #include "graphics/LightEnv.h" #include "graphics/Model.h" #include "graphics/ObjectManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/SmoothedValue.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/Unit.h" #include "graphics/UnitManager.h" -#include "graphics/scripting/JSInterface_GameView.h" #include "lib/input.h" #include "lib/timer.h" #include "lobby/IXmppClient.h" #include "maths/BoundingBoxAligned.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include class CGameViewImpl { NONCOPYABLE(CGameViewImpl); public: CGameViewImpl(CGame* game) : Game(game), ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()), LOSTexture(*game->GetSimulation2()), TerritoryTexture(*game->GetSimulation2()), ViewCamera(), CullCamera(), LockCullCamera(false), Culling(true), CameraController(new CCameraController(ViewCamera)) { } CGame* Game; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; /** * this camera controls the eye position when rendering */ CCamera ViewCamera; /** * this camera controls the frustum that is used for culling * and shadow calculations * * Note that all code that works with camera movements should only change * m_ViewCamera. The render functions automatically sync the cull camera to * the view camera depending on the value of m_LockCullCamera. */ CCamera CullCamera; /** * When @c true, the cull camera is locked in place. * When @c false, the cull camera follows the view camera. * * Exposed to JS as gameView.lockCullCamera */ bool LockCullCamera; /** * When @c true, culling is enabled so that only models that have a chance of * being visible are sent to the renderer. * Otherwise, the entire world is sent to the renderer. * * Exposed to JS as gameView.culling */ bool Culling; /** * Cache global lighting environment. This is used to check whether the * environment has changed during the last frame, so that vertex data can be updated etc. */ CLightEnv CachedLightEnv; CCinemaManager CinemaManager; /** * Controller of the view's camera. We use a std::unique_ptr for an easy * on the fly replacement. It's guaranteed that the pointer is never nulllptr. */ std::unique_ptr CameraController; }; #define IMPLEMENT_BOOLEAN_SETTING(NAME) \ bool CGameView::Get##NAME##Enabled() const \ { \ return m->NAME; \ } \ \ void CGameView::Set##NAME##Enabled(bool Enabled) \ { \ m->NAME = Enabled; \ } IMPLEMENT_BOOLEAN_SETTING(Culling); IMPLEMENT_BOOLEAN_SETTING(LockCullCamera); bool CGameView::GetConstrainCameraEnabled() const { return m->CameraController->GetConstrainCamera(); } void CGameView::SetConstrainCameraEnabled(bool enabled) { m->CameraController->SetConstrainCamera(enabled); } #undef IMPLEMENT_BOOLEAN_SETTING CGameView::CGameView(CGame *pGame): m(new CGameViewImpl(pGame)) { m->CullCamera = m->ViewCamera; g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); } CGameView::~CGameView() { UnloadResources(); delete m; } void CGameView::SetViewport(const SViewPort& vp) { m->CameraController->SetViewport(vp); } CObjectManager& CGameView::GetObjectManager() { return m->ObjectManager; } CCamera* CGameView::GetCamera() { return &m->ViewCamera; } CCinemaManager* CGameView::GetCinema() { return &m->CinemaManager; }; CLOSTexture& CGameView::GetLOSTexture() { return m->LOSTexture; } CTerritoryTexture& CGameView::GetTerritoryTexture() { return m->TerritoryTexture; } int CGameView::Initialize() { m->CameraController->LoadConfig(); return 0; } void CGameView::RegisterInit() { // CGameView init RegMemFun(this, &CGameView::Initialize, L"CGameView init", 1); RegMemFun(g_TexMan.GetSingletonPtr(), &CTerrainTextureManager::LoadTerrainTextures, L"LoadTerrainTextures", 60); RegMemFun(g_Renderer.GetSingletonPtr(), &CRenderer::LoadAlphaMaps, L"LoadAlphaMaps", 5); } void CGameView::BeginFrame() { if (m->LockCullCamera == false) { // Set up cull camera m->CullCamera = m->ViewCamera; } g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); CheckLightEnv(); m->Game->CachePlayerColors(); } void CGameView::Render() { g_Renderer.RenderScene(*this); } /////////////////////////////////////////////////////////// // This callback is part of the Scene interface // Submit all objects visible in the given frustum void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c) { { PROFILE3("submit terrain"); CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight + 0.001f; const ssize_t patchesPerSide = pTerrain->GetPatchesPerSide(); // find out which patches will be drawn for (ssize_t j=0; jGetPatch(i,j); // can't fail // If the patch is underwater, calculate a bounding box that also contains the water plane CBoundingBoxAligned bounds = patch->GetWorldBounds(); if(bounds[1].Y < waterHeight) bounds[1].Y = waterHeight; if (!m->Culling || frustum.IsBoxVisible(bounds)) c->Submit(patch); } } } m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling); } void CGameView::CheckLightEnv() { if (m->CachedLightEnv == g_LightEnv) return; m->CachedLightEnv = g_LightEnv; CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); if (!pTerrain) return; PROFILE("update light env"); pTerrain->MakeDirty(RENDERDATA_UPDATE_COLOR); const std::vector& units = m->Game->GetWorld()->GetUnitManager().GetUnits(); for (size_t i = 0; i < units.size(); ++i) units[i]->GetModel().SetDirtyRec(RENDERDATA_UPDATE_COLOR); } void CGameView::UnloadResources() { g_TexMan.UnloadTerrainTextures(); g_Renderer.UnloadAlphaMaps(); g_Renderer.GetWaterManager()->UnloadWaterTextures(); } void CGameView::Update(const float deltaRealTime) { // If camera movement is being handled by the touch-input system, // then we should stop to avoid conflicting with it if (g_TouchInput.IsEnabled()) return; if (!g_app_has_focus) return; m->CinemaManager.Update(deltaRealTime); if (m->CinemaManager.IsEnabled()) return; m->CameraController->Update(deltaRealTime); } CVector3D CGameView::GetCameraPivot() const { return m->CameraController->GetCameraPivot(); } CVector3D CGameView::GetCameraPosition() const { return m->CameraController->GetCameraPosition(); } CVector3D CGameView::GetCameraRotation() const { return m->CameraController->GetCameraRotation(); } float CGameView::GetCameraZoom() const { return m->CameraController->GetCameraZoom(); } void CGameView::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom) { m->CameraController->SetCamera(pos, rotX, rotY, zoom); } void CGameView::MoveCameraTarget(const CVector3D& target) { m->CameraController->MoveCameraTarget(target); } void CGameView::ResetCameraTarget(const CVector3D& target) { m->CameraController->ResetCameraTarget(target); } void CGameView::FollowEntity(entity_id_t entity, bool firstPerson) { m->CameraController->FollowEntity(entity, firstPerson); } entity_id_t CGameView::GetFollowedEntity() { return m->CameraController->GetFollowedEntity(); } InReaction game_view_handler(const SDL_Event_* ev) { // put any events that must be processed even if inactive here if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsEnabled()) return IN_PASS; CGameView *pView=g_Game->GetView(); return pView->HandleEvent(ev); } InReaction CGameView::HandleEvent(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_HOTKEYPRESS: { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "wireframe") { if (g_XmppClient && g_rankedGame == true) break; else if (g_Renderer.GetModelRenderMode() == SOLID) { g_Renderer.SetTerrainRenderMode(EDGED_FACES); g_Renderer.SetWaterRenderMode(EDGED_FACES); g_Renderer.SetModelRenderMode(EDGED_FACES); g_Renderer.SetOverlayRenderMode(EDGED_FACES); } else if (g_Renderer.GetModelRenderMode() == EDGED_FACES) { g_Renderer.SetTerrainRenderMode(WIREFRAME); g_Renderer.SetWaterRenderMode(WIREFRAME); g_Renderer.SetModelRenderMode(WIREFRAME); g_Renderer.SetOverlayRenderMode(WIREFRAME); } else { g_Renderer.SetTerrainRenderMode(SOLID); g_Renderer.SetWaterRenderMode(SOLID); g_Renderer.SetModelRenderMode(SOLID); g_Renderer.SetOverlayRenderMode(SOLID); } return IN_HANDLED; } } } return m->CameraController->HandleEvent(ev); } Index: ps/trunk/source/graphics/MapGenerator.cpp =================================================================== --- ps/trunk/source/graphics/MapGenerator.cpp (revision 24982) +++ ps/trunk/source/graphics/MapGenerator.cpp (revision 24983) @@ -1,432 +1,428 @@ /* Copyright (C) 2021 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/Threading.h" #include "ps/scripting/JSInterface_VFS.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.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_CONTEXT_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(Threading::HandleExceptions::Wrapper, this); } void CMapGeneratorWorker::RunThread(CMapGeneratorWorker* self) { debug_SetThreadName("MapGenerator"); g_Profiler2.RegisterCurrentThread("MapGenerator"); shared_ptr mapgenContext = ScriptContext::CreateContext(RMS_CONTEXT_SIZE); // Enable the script to be aborted JS_AddInterruptCallback(mapgenContext->GetGeneralJSContext(), MapGeneratorInterruptCallback); self->m_ScriptInterface = new ScriptInterface("Engine", "MapGenerator", mapgenContext); // 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. } bool CMapGeneratorWorker::Run() { ScriptRequest rq(m_ScriptInterface); // Parse settings 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(rq.cx, rq.globalValue()); 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; } +#define REGISTER_MAPGEN_FUNC(func) \ + ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptFunction::ObjectFromCBData>(rq, #func); +#define REGISTER_MAPGEN_FUNC_NAME(func, name) \ + ScriptFunction::Register<&CMapGeneratorWorker::func, ScriptFunction::ObjectFromCBData>(rq, name); + 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"); + ScriptRequest rq(m_ScriptInterface); + REGISTER_MAPGEN_FUNC_NAME(LoadScripts, "LoadLibrary"); + REGISTER_MAPGEN_FUNC_NAME(LoadHeightmap, "LoadHeightmapImage"); + REGISTER_MAPGEN_FUNC(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() { + ScriptRequest rq(m_ScriptInterface); + // 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"); + REGISTER_MAPGEN_FUNC(GetTemplate); + REGISTER_MAPGEN_FUNC(TemplateExists); + REGISTER_MAPGEN_FUNC(FindTemplates); + REGISTER_MAPGEN_FUNC(FindActorTemplates); // Progression and profiling - m_ScriptInterface->RegisterFunction("SetProgress"); - m_ScriptInterface->RegisterFunction("GetMicroseconds"); - m_ScriptInterface->RegisterFunction("ExportMap"); + REGISTER_MAPGEN_FUNC(SetProgress); + REGISTER_MAPGEN_FUNC(GetMicroseconds); + REGISTER_MAPGEN_FUNC(ExportMap); } +#undef REGISTER_MAPGEN_FUNC +#undef REGISTER_MAPGEN_FUNC_NAME + int CMapGeneratorWorker::GetProgress() { std::lock_guard lock(m_WorkerMutex); return m_Progress; } -double CMapGeneratorWorker::GetMicroseconds(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +double CMapGeneratorWorker::GetMicroseconds() { return JS_Now(); } ScriptInterface::StructuredClone CMapGeneratorWorker::GetResults() { std::lock_guard lock(m_WorkerMutex); return m_MapData; } -bool CMapGeneratorWorker::LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name) -{ - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - return self->LoadScripts(name); -} - -void CMapGeneratorWorker::ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) +void CMapGeneratorWorker::ExportMap(JS::HandleValue data) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - // Copy results - std::lock_guard lock(self->m_WorkerMutex); - self->m_MapData = self->m_ScriptInterface->WriteStructuredClone(data); - self->m_Progress = 0; + std::lock_guard lock(m_WorkerMutex); + m_MapData = m_ScriptInterface->WriteStructuredClone(data); + m_Progress = 0; } -void CMapGeneratorWorker::SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress) +void CMapGeneratorWorker::SetProgress(int progress) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - // Copy data - std::lock_guard lock(self->m_WorkerMutex); + std::lock_guard lock(m_WorkerMutex); - if (progress >= self->m_Progress) - self->m_Progress = progress; + if (progress >= m_Progress) + m_Progress = progress; else - LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", self->m_Progress, progress); + LOGWARNING("The random map script tried to reduce the loading progress from %d to %d", m_Progress, progress); } -CParamNode CMapGeneratorWorker::GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) +CParamNode CMapGeneratorWorker::GetTemplate(const std::string& templateName) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - const CParamNode& templateRoot = self->m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); + const CParamNode& templateRoot = m_TemplateLoader.GetTemplateFileData(templateName).GetChild("Entity"); if (!templateRoot.IsOk()) LOGERROR("Invalid template found for '%s'", templateName.c_str()); return templateRoot; } -bool CMapGeneratorWorker::TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName) +bool CMapGeneratorWorker::TemplateExists(const std::string& templateName) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - return self->m_TemplateLoader.TemplateExists(templateName); + return m_TemplateLoader.TemplateExists(templateName); } -std::vector CMapGeneratorWorker::FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) +std::vector CMapGeneratorWorker::FindTemplates(const std::string& path, bool includeSubdirectories) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); + return m_TemplateLoader.FindTemplates(path, includeSubdirectories, SIMULATION_TEMPLATES); } -std::vector CMapGeneratorWorker::FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories) +std::vector CMapGeneratorWorker::FindActorTemplates(const std::string& path, bool includeSubdirectories) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - return self->m_TemplateLoader.FindTemplates(path, includeSubdirectories, ACTOR_TEMPLATES); + return 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::CmptPrivate* pCmptPrivate, const VfsPath& filename) +JS::Value CMapGeneratorWorker::LoadHeightmap(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(pCmptPrivate->pCBData); - ScriptRequest rq(self->m_ScriptInterface); + ScriptRequest rq(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::CmptPrivate* pCmptPrivate, const VfsPath& filename) +JS::Value CMapGeneratorWorker::LoadMapTerrain(const VfsPath& filename) { - CMapGeneratorWorker* self = static_cast(pCmptPrivate->pCBData); - ScriptRequest rq(self->m_ScriptInterface); + ScriptRequest rq(m_ScriptInterface); if (!VfsFileExists(filename)) { ScriptException::Raise(rq, "Terrain file \"%s\" does not exist!", filename.string8().c_str()); return JS::UndefinedValue(); } CFileUnpacker unpacker; unpacker.Read(filename, "PSMP"); if (unpacker.GetVersion() < CMapIO::FILE_READ_VERSION) { ScriptException::Raise(rq, "Could not load terrain file \"%s\" too old version!", filename.string8().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(rq.cx); ScriptInterface::CreateObject( 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(); } ScriptInterface::StructuredClone CMapGenerator::GetResults() { return m_Worker->GetResults(); } Index: ps/trunk/source/graphics/MapGenerator.h =================================================================== --- ps/trunk/source/graphics/MapGenerator.h (revision 24982) +++ ps/trunk/source/graphics/MapGenerator.h (revision 24983) @@ -1,248 +1,243 @@ /* Copyright (C) 2021 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_MAPGENERATOR #define INCLUDED_MAPGENERATOR #include "lib/posix/posix_pthread.h" #include "ps/FileIo.h" #include "ps/TemplateLoader.h" #include "scriptinterface/ScriptInterface.h" #include #include #include #include #include class CMapGeneratorWorker; /** * Random map generator interface. Initialized by CMapReader and then checked * periodically during loading, until it's finished (progress value is 0). * * The actual work is performed by CMapGeneratorWorker in a separate thread. */ class CMapGenerator { NONCOPYABLE(CMapGenerator); public: CMapGenerator(); ~CMapGenerator(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void GenerateMap(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ ScriptInterface::StructuredClone GetResults(); private: CMapGeneratorWorker* m_Worker; }; /** * Random map generator worker thread. * (This is run in a thread so that the GUI remains responsive while loading) * * Thread-safety: * - Initialize and constructor/destructor must be called from the main thread. * - ScriptInterface created and destroyed by thread * - StructuredClone used to return JS map data - JS:Values can't be used across threads/contexts. */ class CMapGeneratorWorker { public: CMapGeneratorWorker(ScriptInterface* scriptInterface); ~CMapGeneratorWorker(); /** * Start the map generator thread * * @param scriptFile The VFS path for the script, e.g. "maps/random/latium.js" * @param settings JSON string containing settings for the map generator */ void Initialize(const VfsPath& scriptFile, const std::string& settings); /** * Get status of the map generator thread * * @return Progress percentage 1-100 if active, 0 when finished, or -1 on error */ int GetProgress(); /** * Get random map data, according to this format: * http://trac.wildfiregames.com/wiki/Random_Map_Generator_Internals#Dataformat * * @return StructuredClone containing map data */ ScriptInterface::StructuredClone GetResults(); /** * Set initial seed, callback data. * Expose functions, globals and classes defined in this class relevant to the map and test scripts. */ void InitScriptInterface(const u32 seed); private: /** * Expose functions defined in this class that are relevant to mapscripts but not the tests. */ void RegisterScriptFunctions_MapGenerator(); /** * Load all scripts of the given library * * @param libraryName VfsPath specifying name of the library (subfolder of ../maps/random/) * @return true if all scripts ran successfully, false if there's an error */ bool LoadScripts(const VfsPath& libraryName); /** - * Recursively load all script files in the given folder. - */ - static bool LoadLibrary(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& name); - - /** * Finalize map generation and pass results from the script to the engine. */ - static void ExportMap(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data); + void ExportMap(JS::HandleValue data); /** * Load an image file and return it as a height array. */ - static JS::Value LoadHeightmap(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& src); + JS::Value LoadHeightmap(const VfsPath& src); /** * Load an Atlas terrain file (PMP) returning textures and heightmap. */ - static JS::Value LoadMapTerrain(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& filename); + JS::Value LoadMapTerrain(const VfsPath& filename); /** * Sets the map generation progress, which is one of multiple stages determining the loading screen progress. */ - static void SetProgress(ScriptInterface::CmptPrivate* pCmptPrivate, int progress); + void SetProgress(int progress); /** * Microseconds since the epoch. */ - static double GetMicroseconds(ScriptInterface::CmptPrivate* pCmptPrivate); + double GetMicroseconds(); /** * Return the template data of the given template name. */ - static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); + CParamNode GetTemplate(const std::string& templateName); /** * Check whether the given template exists. */ - static bool TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); + bool TemplateExists(const std::string& templateName); /** * Returns all template names of simulation entity templates. */ - static std::vector FindTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); + std::vector FindTemplates(const std::string& path, bool includeSubdirectories); /** * Returns all template names of actors. */ - static std::vector FindActorTemplates(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& path, bool includeSubdirectories); + std::vector FindActorTemplates(const std::string& path, bool includeSubdirectories); /** * Perform map generation in an independent thread. */ static void RunThread(CMapGeneratorWorker* self); /** * Perform the map generation. */ bool Run(); /** * Currently loaded script librarynames. */ std::set m_LoadedLibraries; /** * Result of the mapscript generation including terrain, entities and environment settings. */ ScriptInterface::StructuredClone m_MapData; /** * Deterministic random number generator. */ boost::rand48 m_MapGenRNG; /** * Current map generation progress. */ int m_Progress; /** * Provides the script context. */ ScriptInterface* m_ScriptInterface; /** * Map generation script to run. */ VfsPath m_ScriptPath; /** * Map and simulation settings chosen in the gamesetup stage. */ std::string m_Settings; /** * Backend to loading template data. */ CTemplateLoader m_TemplateLoader; /** * Holds the mapgeneration thread identifier. */ std::thread m_WorkerThread; /** * Avoids thread synchronization issues. */ std::mutex m_WorkerMutex; }; #endif //INCLUDED_MAPGENERATOR Index: ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp =================================================================== --- ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp (revision 24982) +++ ps/trunk/source/graphics/scripting/JSInterface_GameView.cpp (revision 24983) @@ -1,174 +1,179 @@ /* 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_GameView.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/Terrain.h" +#include "maths/FixedVector3D.h" #include "ps/Game.h" #include "ps/World.h" #include "ps/CLogger.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" +#include "simulation2/helpers/Position.h" +namespace JSI_GameView +{ #define IMPLEMENT_BOOLEAN_SCRIPT_SETTING(NAME) \ -bool JSI_GameView::Get##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) \ +bool Get##NAME##Enabled() \ { \ if (!g_Game || !g_Game->GetView()) \ { \ LOGERROR("Trying to get a setting from GameView when it's not initialized!"); \ return false; \ } \ return g_Game->GetView()->Get##NAME##Enabled(); \ } \ \ -void JSI_GameView::Set##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool Enabled) \ +void Set##NAME##Enabled(bool Enabled) \ { \ if (!g_Game || !g_Game->GetView()) \ { \ LOGERROR("Trying to set a setting of GameView when it's not initialized!"); \ return; \ } \ g_Game->GetView()->Set##NAME##Enabled(Enabled); \ } IMPLEMENT_BOOLEAN_SCRIPT_SETTING(Culling); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(LockCullCamera); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(ConstrainCamera); #undef IMPLEMENT_BOOLEAN_SCRIPT_SETTING #define REGISTER_BOOLEAN_SCRIPT_SETTING(NAME) \ -scriptInterface.RegisterFunction("GameView_Get" #NAME "Enabled"); \ -scriptInterface.RegisterFunction("GameView_Set" #NAME "Enabled"); + ScriptFunction::Register<&Get##NAME##Enabled>(rq, "GameView_Get" #NAME "Enabled"); \ + ScriptFunction::Register<&Set##NAME##Enabled>(rq, "GameView_Set" #NAME "Enabled"); -void JSI_GameView::RegisterScriptFunctions_Settings(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions_Settings(const ScriptRequest& rq) { REGISTER_BOOLEAN_SCRIPT_SETTING(Culling); REGISTER_BOOLEAN_SCRIPT_SETTING(LockCullCamera); REGISTER_BOOLEAN_SCRIPT_SETTING(ConstrainCamera); } #undef REGISTER_BOOLEAN_SCRIPT_SETTING -JS::Value JSI_GameView::GetCameraPivot(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value GetCameraPivot(const ScriptRequest& rq) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); CVector3D pivot(-1, -1, -1); if (g_Game && g_Game->GetView()) pivot = g_Game->GetView()->GetCameraPivot(); JS::RootedValue pivotValue(rq.cx); ScriptInterface::CreateObject(rq, &pivotValue, "x", pivot.X, "z", pivot.Z); return pivotValue; } /** * Move camera to a 2D location. */ -void JSI_GameView::CameraMoveTo(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), entity_pos_t x, entity_pos_t z) +void CameraMoveTo(entity_pos_t x, entity_pos_t z) { if (!g_Game || !g_Game->GetWorld() || !g_Game->GetView() || !g_Game->GetWorld()->GetTerrain()) return; CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; target.X = x.ToFloat(); target.Z = z.ToFloat(); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } /** * Set the camera to look at the given location. */ -void JSI_GameView::SetCameraTarget(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float x, float y, float z) +void SetCameraTarget(float x, float y, float z) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->MoveCameraTarget(CVector3D(x, y, z)); } /** * Set the data (position, orientation and zoom) of the camera. */ -void JSI_GameView::SetCameraData(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom) +void SetCameraData(entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom) { if (!g_Game || !g_Game->GetView()) return; CVector3D pos(x.ToFloat(), y.ToFloat(), z.ToFloat()); g_Game->GetView()->SetCamera(pos, rotx.ToFloat(), roty.ToFloat(), zoom.ToFloat()); } /** * Start / stop camera following mode. * @param entityid unit id to follow. If zero, stop following mode */ -void JSI_GameView::CameraFollow(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), entity_id_t entityid) +void CameraFollow(entity_id_t entityid) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->FollowEntity(entityid, false); } /** * Start / stop first-person camera following mode. * @param entityid unit id to follow. If zero, stop following mode. */ -void JSI_GameView::CameraFollowFPS(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), entity_id_t entityid) +void CameraFollowFPS(entity_id_t entityid) { if (!g_Game || !g_Game->GetView()) return; g_Game->GetView()->FollowEntity(entityid, true); } -entity_id_t JSI_GameView::GetFollowedEntity(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +entity_id_t GetFollowedEntity() { if (!g_Game || !g_Game->GetView()) return INVALID_ENTITY; return g_Game->GetView()->GetFollowedEntity(); } -CFixedVector3D JSI_GameView::GetTerrainAtScreenPoint(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int x, int y) +CFixedVector3D GetTerrainAtScreenPoint(int x, int y) { CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true); return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z)); } -void JSI_GameView::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - RegisterScriptFunctions_Settings(scriptInterface); + RegisterScriptFunctions_Settings(rq); - scriptInterface.RegisterFunction("GetCameraPivot"); - scriptInterface.RegisterFunction("CameraMoveTo"); - scriptInterface.RegisterFunction("SetCameraTarget"); - scriptInterface.RegisterFunction("SetCameraData"); - scriptInterface.RegisterFunction("CameraFollow"); - scriptInterface.RegisterFunction("CameraFollowFPS"); - scriptInterface.RegisterFunction("GetFollowedEntity"); - scriptInterface.RegisterFunction("GetTerrainAtScreenPoint"); + ScriptFunction::Register<&GetCameraPivot>(rq, "GetCameraPivot"); + ScriptFunction::Register<&CameraMoveTo>(rq, "CameraMoveTo"); + ScriptFunction::Register<&SetCameraTarget>(rq, "SetCameraTarget"); + ScriptFunction::Register<&SetCameraData>(rq, "SetCameraData"); + ScriptFunction::Register<&CameraFollow>(rq, "CameraFollow"); + ScriptFunction::Register<&CameraFollowFPS>(rq, "CameraFollowFPS"); + ScriptFunction::Register<&GetFollowedEntity>(rq, "GetFollowedEntity"); + ScriptFunction::Register<&GetTerrainAtScreenPoint>(rq, "GetTerrainAtScreenPoint"); +} } Index: ps/trunk/source/graphics/scripting/JSInterface_GameView.h =================================================================== --- ps/trunk/source/graphics/scripting/JSInterface_GameView.h (revision 24982) +++ ps/trunk/source/graphics/scripting/JSInterface_GameView.h (revision 24983) @@ -1,51 +1,28 @@ /* Copyright (C) 2019 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_JSINTERFACE_GAMEVIEW #define INCLUDED_JSINTERFACE_GAMEVIEW -#include "maths/FixedVector3D.h" -#include "scriptinterface/ScriptInterface.h" -#include "simulation2/helpers/Position.h" -#include "simulation2/system/Entity.h" - -#define DECLARE_BOOLEAN_SCRIPT_SETTING(NAME) \ - bool Get##NAME##Enabled(ScriptInterface::CmptPrivate* pCmptPrivate); \ - void Set##NAME##Enabled(ScriptInterface::CmptPrivate* pCmptPrivate, bool Enabled); +class ScriptRequest; namespace JSI_GameView { - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); - void RegisterScriptFunctions_Settings(const ScriptInterface& scriptInterface); - - DECLARE_BOOLEAN_SCRIPT_SETTING(Culling); - DECLARE_BOOLEAN_SCRIPT_SETTING(LockCullCamera); - DECLARE_BOOLEAN_SCRIPT_SETTING(ConstrainCamera); - - JS::Value GetCameraPivot(ScriptInterface::CmptPrivate* pCmptPrivate); - void CameraMoveTo(ScriptInterface::CmptPrivate* pCmptPrivate, entity_pos_t x, entity_pos_t z); - void SetCameraTarget(ScriptInterface::CmptPrivate* pCmptPrivate, float x, float y, float z); - void SetCameraData(ScriptInterface::CmptPrivate* pCmptPrivate, entity_pos_t x, entity_pos_t y, entity_pos_t z, entity_pos_t rotx, entity_pos_t roty, entity_pos_t zoom); - void CameraFollow(ScriptInterface::CmptPrivate* pCmptPrivate, entity_id_t entityid); - void CameraFollowFPS(ScriptInterface::CmptPrivate* pCmptPrivate, entity_id_t entityid); - entity_id_t GetFollowedEntity(ScriptInterface::CmptPrivate* pCmptPrivate); - CFixedVector3D GetTerrainAtScreenPoint(ScriptInterface::CmptPrivate* pCmptPrivate, int x, int y); + void RegisterScriptFunctions(const ScriptRequest& rq); } -#undef DECLARE_BOOLEAN_SCRIPT_SETTING - #endif // INCLUDED_JSINTERFACE_GAMEVIEW Index: ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp (revision 24982) +++ ps/trunk/source/gui/Scripting/JSInterface_GUIManager.cpp (revision 24983) @@ -1,109 +1,92 @@ /* 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_GUIManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/ObjectBases/IGUIObject.h" #include "ps/GameSetup/Config.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" +namespace JSI_GUIManager +{ // Note that the initData argument may only contain clonable data. // Functions aren't supported for example! -void JSI_GUIManager::PushGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction) +void PushGuiPage(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction) { - g_GUI->PushPage(name, pCmptPrivate->pScriptInterface->WriteStructuredClone(initData), callbackFunction); + g_GUI->PushPage(name, scriptInterface.WriteStructuredClone(initData), callbackFunction); } -void JSI_GUIManager::SwitchGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData) +void SwitchGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData) { g_GUI->SwitchPage(name, pCmptPrivate->pScriptInterface, initData); } -void JSI_GUIManager::PopGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue args) +void PopGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue args) { if (g_GUI->GetPageCount() < 2) { ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Can't pop GUI pages when less than two pages are opened!"); return; } g_GUI->PopPage(pCmptPrivate->pScriptInterface->WriteStructuredClone(args)); } -JS::Value JSI_GUIManager::GetGUIObjectByName(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) -{ - CGUI* guiPage = static_cast(pCmptPrivate->pCBData); - - IGUIObject* guiObj = guiPage->FindObjectByName(name); - if (!guiObj) - return JS::UndefinedValue(); - - return JS::ObjectValue(*guiObj->GetJSObject()); -} - -void JSI_GUIManager::SetGlobalHotkey(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& hotkeyTag, const std::string& eventName, JS::HandleValue function) -{ - CGUI* guiPage = static_cast(pCmptPrivate->pCBData); - guiPage->SetGlobalHotkey(hotkeyTag, eventName, function); -} - -void JSI_GUIManager::UnsetGlobalHotkey(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& hotkeyTag, const std::string& eventName) -{ - CGUI* guiPage = static_cast(pCmptPrivate->pCBData); - guiPage->UnsetGlobalHotkey(hotkeyTag, eventName); -} - -std::wstring JSI_GUIManager::SetCursor(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name) +std::wstring SetCursor(const std::wstring& name) { std::wstring old = g_CursorName; g_CursorName = name; return old; } -void JSI_GUIManager::ResetCursor(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void ResetCursor() { g_CursorName = g_DefaultCursor; } -bool JSI_GUIManager::TemplateExists(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& templateName) +bool TemplateExists(const std::string& templateName) { return g_GUI->TemplateExists(templateName); } -CParamNode JSI_GUIManager::GetTemplate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& templateName) +CParamNode GetTemplate(const std::string& templateName) { return g_GUI->GetTemplate(templateName); } -void JSI_GUIManager::RegisterScriptFunctions(const ScriptInterface& scriptInterface) + +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("PushGuiPage"); - scriptInterface.RegisterFunction("SwitchGuiPage"); - scriptInterface.RegisterFunction("SetGlobalHotkey"); - scriptInterface.RegisterFunction("UnsetGlobalHotkey"); - scriptInterface.RegisterFunction("PopGuiPage"); - scriptInterface.RegisterFunction("GetGUIObjectByName"); - scriptInterface.RegisterFunction("SetCursor"); - scriptInterface.RegisterFunction("ResetCursor"); - scriptInterface.RegisterFunction("TemplateExists"); - scriptInterface.RegisterFunction("GetTemplate"); + ScriptFunction::Register<&PushGuiPage>(rq, "PushGuiPage"); + ScriptFunction::Register<&SwitchGuiPage>(rq, "SwitchGuiPage"); + ScriptFunction::Register<&PopGuiPage>(rq, "PopGuiPage"); + ScriptFunction::Register<&SetCursor>(rq, "SetCursor"); + ScriptFunction::Register<&ResetCursor>(rq, "ResetCursor"); + ScriptFunction::Register<&TemplateExists>(rq, "TemplateExists"); + ScriptFunction::Register<&GetTemplate>(rq, "GetTemplate"); + + ScriptFunction::Register<&CGUI::FindObjectByName, &ScriptFunction::ObjectFromCBData>(rq, "GetGUIObjectByName"); + ScriptFunction::Register<&CGUI::SetGlobalHotkey, &ScriptFunction::ObjectFromCBData>(rq, "SetGlobalHotkey"); + ScriptFunction::Register<&CGUI::UnsetGlobalHotkey, &ScriptFunction::ObjectFromCBData>(rq, "UnsetGlobalHotkey"); +} } Index: ps/trunk/source/gui/Scripting/JSInterface_GUIManager.h =================================================================== --- ps/trunk/source/gui/Scripting/JSInterface_GUIManager.h (revision 24982) +++ ps/trunk/source/gui/Scripting/JSInterface_GUIManager.h (revision 24983) @@ -1,40 +1,28 @@ /* 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_JSI_GUIMANAGER #define INCLUDED_JSI_GUIMANAGER -#include "scriptinterface/ScriptInterface.h" -#include "simulation2/system/ParamNode.h" +class ScriptRequest; namespace JSI_GUIManager { - void PushGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData, JS::HandleValue callbackFunction); - void SwitchGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue initData); - void PopGuiPage(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue args); - JS::Value GetGUIObjectByName(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name); - void SetGlobalHotkey(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& hotkeyTag, const std::string& eventName, JS::HandleValue function); - void UnsetGlobalHotkey(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& hotkeyTag, const std::string& eventName); - std::wstring SetCursor(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name); - void ResetCursor(ScriptInterface::CmptPrivate* pCmptPrivate); - bool TemplateExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); - CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName); - - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_GUIMANAGER Index: ps/trunk/source/gui/Scripting/ScriptFunctions.cpp =================================================================== --- ps/trunk/source/gui/Scripting/ScriptFunctions.cpp (revision 24982) +++ ps/trunk/source/gui/Scripting/ScriptFunctions.cpp (revision 24983) @@ -1,76 +1,76 @@ /* Copyright (C) 2021 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 "ScriptFunctions.h" #include "graphics/scripting/JSInterface_GameView.h" #include "gui/Scripting/JSInterface_GUIManager.h" #include "gui/Scripting/JSInterface_GUISize.h" #include "i18n/scripting/JSInterface_L10n.h" #include "lobby/scripting/JSInterface_Lobby.h" #include "network/scripting/JSInterface_Network.h" #include "ps/scripting/JSInterface_ConfigDB.h" #include "ps/scripting/JSInterface_Console.h" #include "ps/scripting/JSInterface_Debug.h" #include "ps/scripting/JSInterface_Game.h" #include "ps/scripting/JSInterface_Hotkey.h" #include "ps/scripting/JSInterface_Main.h" #include "ps/scripting/JSInterface_Mod.h" #include "ps/scripting/JSInterface_ModIo.h" #include "ps/scripting/JSInterface_SavedGame.h" #include "ps/scripting/JSInterface_UserReport.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/scripting/JSInterface_VisualReplay.h" #include "renderer/scripting/JSInterface_Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/scripting/JSInterface_Simulation.h" #include "soundmanager/scripting/JSInterface_Sound.h" /* * This file defines a set of functions that are available to GUI scripts, to allow * interaction with the rest of the engine. * Functions are exposed to scripts within the global object 'Engine', so * scripts should call "Engine.FunctionName(...)" etc. */ void GuiScriptingInit(ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JSI_GUISize::RegisterScriptClass(scriptInterface); - JSI_ConfigDB::RegisterScriptFunctions(scriptInterface); + JSI_ConfigDB::RegisterScriptFunctions(rq); JSI_Console::RegisterScriptFunctions(rq); - JSI_Debug::RegisterScriptFunctions(scriptInterface); - JSI_GUIManager::RegisterScriptFunctions(scriptInterface); - JSI_Game::RegisterScriptFunctions(scriptInterface); - JSI_GameView::RegisterScriptFunctions(scriptInterface); - JSI_Hotkey::RegisterScriptFunctions(scriptInterface); - JSI_L10n::RegisterScriptFunctions(scriptInterface); - JSI_Lobby::RegisterScriptFunctions(scriptInterface); - JSI_Main::RegisterScriptFunctions(scriptInterface); - JSI_Mod::RegisterScriptFunctions(scriptInterface); - JSI_ModIo::RegisterScriptFunctions(scriptInterface); - JSI_Network::RegisterScriptFunctions(scriptInterface); - JSI_Renderer::RegisterScriptFunctions(scriptInterface); - JSI_SavedGame::RegisterScriptFunctions(scriptInterface); - JSI_Simulation::RegisterScriptFunctions(scriptInterface); - JSI_Sound::RegisterScriptFunctions(scriptInterface); - JSI_UserReport::RegisterScriptFunctions(scriptInterface); - JSI_VFS::RegisterScriptFunctions_GUI(scriptInterface); - JSI_VisualReplay::RegisterScriptFunctions(scriptInterface); + JSI_Debug::RegisterScriptFunctions(rq); + JSI_GUIManager::RegisterScriptFunctions(rq); + JSI_Game::RegisterScriptFunctions(rq); + JSI_GameView::RegisterScriptFunctions(rq); + JSI_Hotkey::RegisterScriptFunctions(rq); + JSI_L10n::RegisterScriptFunctions(rq); + JSI_Lobby::RegisterScriptFunctions(rq); + JSI_Main::RegisterScriptFunctions(rq); + JSI_Mod::RegisterScriptFunctions(rq); + JSI_ModIo::RegisterScriptFunctions(rq); + JSI_Network::RegisterScriptFunctions(rq); + JSI_Renderer::RegisterScriptFunctions(rq); + JSI_SavedGame::RegisterScriptFunctions(rq); + JSI_Simulation::RegisterScriptFunctions(rq); + JSI_Sound::RegisterScriptFunctions(rq); + JSI_UserReport::RegisterScriptFunctions(rq); + JSI_VFS::RegisterScriptFunctions_GUI(rq); + JSI_VisualReplay::RegisterScriptFunctions(rq); } Index: ps/trunk/source/i18n/scripting/JSInterface_L10n.cpp =================================================================== --- ps/trunk/source/i18n/scripting/JSInterface_L10n.cpp (revision 24982) +++ ps/trunk/source/i18n/scripting/JSInterface_L10n.cpp (revision 24983) @@ -1,189 +1,101 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2021 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_L10n.h" #include "i18n/L10n.h" #include "lib/utf8.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" -// Returns a translation of the specified English string into the current language. -std::wstring JSI_L10n::Translate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& sourceString) +namespace JSI_L10n { - return wstring_from_utf8(g_L10n.Translate(utf8_from_wstring(sourceString))); -} - -// Returns a translation of the specified English string, for the specified context. -std::wstring JSI_L10n::TranslateWithContext(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& context, const std::wstring& sourceString) -{ - return wstring_from_utf8(g_L10n.TranslateWithContext(context, utf8_from_wstring(sourceString))); -} - -// Return a translated version of the given strings (singular and plural) depending on an integer value. -std::wstring JSI_L10n::TranslatePlural(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& singularSourceString, const std::wstring& pluralSourceString, int number) -{ - return wstring_from_utf8(g_L10n.TranslatePlural(utf8_from_wstring(singularSourceString), utf8_from_wstring(pluralSourceString), number)); -} - -// Return a translated version of the given strings (singular and plural) depending on an integer value, for the specified context. -std::wstring JSI_L10n::TranslatePluralWithContext(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& context, const std::wstring& singularSourceString, const std::wstring& pluralSourceString, int number) +L10n* L10nGetter(const ScriptRequest&, JS::CallArgs&) { - return wstring_from_utf8(g_L10n.TranslatePluralWithContext(context, utf8_from_wstring(singularSourceString), utf8_from_wstring(pluralSourceString), number)); + if (!g_L10n.IsInitialised()) + { + LOGERROR("Trying to access g_L10n when it's not initialized!"); + return nullptr; + } + return &g_L10n.GetSingleton(); } -// Return a translated version of the given string, localizing it line by line. -std::wstring JSI_L10n::TranslateLines(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& sourceString) +std::vector TranslateArray(const std::vector& sourceArray) { - return wstring_from_utf8(g_L10n.TranslateLines(utf8_from_wstring(sourceString))); -} - -// Return a translated version of the items in the specified array. -std::vector JSI_L10n::TranslateArray(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::vector& sourceArray) -{ - std::vector translatedArray; - for (const std::wstring& elem : sourceArray) - translatedArray.push_back(wstring_from_utf8(g_L10n.Translate(utf8_from_wstring(elem)))); + std::vector translatedArray; + if (g_L10n.IsInitialised()) + for (const std::string& elem : sourceArray) + translatedArray.push_back(g_L10n.Translate(elem)); return translatedArray; } -std::wstring JSI_L10n::GetFallbackToAvailableDictLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.GetFallbackToAvailableDictLocale(locale); -} - // Return a localized version of a time given in milliseconds. -std::wstring JSI_L10n::FormatMillisecondsIntoDateStringLocal(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), UDate milliseconds, const std::wstring& formatString) +std::string FormatMillisecondsIntoDateStringLocal(UDate milliseconds, const std::string& formatString) { - return wstring_from_utf8(g_L10n.FormatMillisecondsIntoDateString(milliseconds, utf8_from_wstring(formatString), true)); + return g_L10n.FormatMillisecondsIntoDateString(milliseconds, formatString, true); } // Return a localized version of a duration or a time in GMT given in milliseconds. -std::wstring JSI_L10n::FormatMillisecondsIntoDateStringGMT(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), UDate milliseconds, const std::wstring& formatString) -{ - return wstring_from_utf8(g_L10n.FormatMillisecondsIntoDateString(milliseconds, utf8_from_wstring(formatString), false)); -} - -// Return a localized version of the given decimal number. -std::wstring JSI_L10n::FormatDecimalNumberIntoString(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), double number) -{ - return wstring_from_utf8(g_L10n.FormatDecimalNumberIntoString(number)); -} - -std::vector JSI_L10n::GetSupportedLocaleBaseNames(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - return g_L10n.GetSupportedLocaleBaseNames(); -} - -std::vector JSI_L10n::GetSupportedLocaleDisplayNames(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - return g_L10n.GetSupportedLocaleDisplayNames(); -} - -std::string JSI_L10n::GetCurrentLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - return g_L10n.GetCurrentLocaleString(); -} - -bool JSI_L10n::UseLongStrings(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - return g_L10n.UseLongStrings(); -} - -std::vector JSI_L10n::GetAllLocales(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - return g_L10n.GetAllLocales(); -} - -std::string JSI_L10n::GetDictionaryLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& configLocale) -{ - return g_L10n.GetDictionaryLocale(configLocale); -} - -std::vector JSI_L10n::GetDictionariesForLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.GetDictionariesForLocale(locale); -} - -std::string JSI_L10n::GetLocaleLanguage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.GetLocaleLanguage(locale); -} - -std::string JSI_L10n::GetLocaleBaseName(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.GetLocaleBaseName(locale); -} - -std::string JSI_L10n::GetLocaleCountry(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) +std::string FormatMillisecondsIntoDateStringGMT(UDate milliseconds, const std::string& formatString) { - return g_L10n.GetLocaleCountry(locale); + return g_L10n.FormatMillisecondsIntoDateString(milliseconds, formatString, false); } -std::string JSI_L10n::GetLocaleScript(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) +void RegisterScriptFunctions(const ScriptRequest& rq) { - return g_L10n.GetLocaleScript(locale); +#define REGISTER_L10N(name) \ + ScriptFunction::Register<&L10n::name, &L10nGetter>(rq, #name); +#define REGISTER_L10N_FUNC(func, name) \ + ScriptFunction::Register(rq, name); + + REGISTER_L10N(Translate) + REGISTER_L10N(TranslateWithContext) + REGISTER_L10N(TranslatePlural) + REGISTER_L10N(TranslatePluralWithContext) + REGISTER_L10N(TranslateLines) + ScriptFunction::Register<&TranslateArray>(rq, "TranslateArray"); + ScriptFunction::Register<&FormatMillisecondsIntoDateStringLocal>(rq, "FormatMillisecondsIntoDateStringLocal"); + ScriptFunction::Register<&FormatMillisecondsIntoDateStringGMT>(rq, "FormatMillisecondsIntoDateStringGMT"); + REGISTER_L10N(FormatDecimalNumberIntoString) + + REGISTER_L10N(GetSupportedLocaleBaseNames) + REGISTER_L10N(GetSupportedLocaleDisplayNames) + REGISTER_L10N_FUNC(&L10n::GetCurrentLocaleString, "GetCurrentLocale"); + REGISTER_L10N(GetAllLocales) + // Select the appropriate overload. + REGISTER_L10N_FUNC(static_cast(&L10n::GetDictionaryLocale), "GetDictionaryLocale"); + REGISTER_L10N(GetDictionariesForLocale) + + REGISTER_L10N(UseLongStrings) + REGISTER_L10N(GetLocaleLanguage) + REGISTER_L10N(GetLocaleBaseName) + REGISTER_L10N(GetLocaleCountry) + REGISTER_L10N(GetLocaleScript) + // Select the appropriate overload. + REGISTER_L10N_FUNC(static_cast(&L10n::GetFallbackToAvailableDictLocale), "GetFallbackToAvailableDictLocale"); + + // Select the appropriate overloads. + REGISTER_L10N_FUNC(static_cast(&L10n::ValidateLocale), "ValidateLocale"); + REGISTER_L10N_FUNC(static_cast(&L10n::SaveLocale), "SaveLocale"); + REGISTER_L10N(ReevaluateCurrentLocaleAndReload) +#undef REGISTER_L10N +#undef REGISTER_L10N_FUNC } - -bool JSI_L10n::ValidateLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.ValidateLocale(locale); -} - -bool JSI_L10n::SaveLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale) -{ - return g_L10n.SaveLocale(locale); -} - -void JSI_L10n::ReevaluateCurrentLocaleAndReload(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) -{ - g_L10n.ReevaluateCurrentLocaleAndReload(); -} - - -void JSI_L10n::RegisterScriptFunctions(const ScriptInterface& scriptInterface) -{ - scriptInterface.RegisterFunction("Translate"); - scriptInterface.RegisterFunction("TranslateWithContext"); - scriptInterface.RegisterFunction("TranslatePlural"); - scriptInterface.RegisterFunction("TranslatePluralWithContext"); - scriptInterface.RegisterFunction("TranslateLines"); - scriptInterface.RegisterFunction, std::vector, &TranslateArray>("TranslateArray"); - scriptInterface.RegisterFunction("FormatMillisecondsIntoDateStringLocal"); - scriptInterface.RegisterFunction("FormatMillisecondsIntoDateStringGMT"); - scriptInterface.RegisterFunction("FormatDecimalNumberIntoString"); - - scriptInterface.RegisterFunction, &GetSupportedLocaleBaseNames>("GetSupportedLocaleBaseNames"); - scriptInterface.RegisterFunction, &GetSupportedLocaleDisplayNames>("GetSupportedLocaleDisplayNames"); - scriptInterface.RegisterFunction("GetCurrentLocale"); - scriptInterface.RegisterFunction, &GetAllLocales>("GetAllLocales"); - scriptInterface.RegisterFunction("GetDictionaryLocale"); - scriptInterface.RegisterFunction, std::string, &GetDictionariesForLocale>("GetDictionariesForLocale"); - - scriptInterface.RegisterFunction("UseLongStrings"); - scriptInterface.RegisterFunction("GetLocaleLanguage"); - scriptInterface.RegisterFunction("GetLocaleBaseName"); - scriptInterface.RegisterFunction("GetLocaleCountry"); - scriptInterface.RegisterFunction("GetLocaleScript"); - scriptInterface.RegisterFunction("GetFallbackToAvailableDictLocale"); - - scriptInterface.RegisterFunction("ValidateLocale"); - scriptInterface.RegisterFunction("SaveLocale"); - scriptInterface.RegisterFunction("ReevaluateCurrentLocaleAndReload"); } Index: ps/trunk/source/i18n/scripting/JSInterface_L10n.h =================================================================== --- ps/trunk/source/i18n/scripting/JSInterface_L10n.h (revision 24982) +++ ps/trunk/source/i18n/scripting/JSInterface_L10n.h (revision 24983) @@ -1,455 +1,47 @@ /* Copyright (C) 2021 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_JSINTERFACE_L10N #define INCLUDED_JSINTERFACE_L10N -#include "lib/external_libraries/icu.h" -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; /** * Namespace for the functions of the JavaScript interface for * internationalization and localization. * * This namespace defines JavaScript interfaces to functions defined in L10n and * related helper functions. * * @sa http://trac.wildfiregames.com/wiki/Internationalization_and_Localization */ namespace JSI_L10n { /** * Registers the functions of the JavaScript interface for * internationalization and localization into the specified JavaScript * context. * - * @param ScriptInterface JavaScript context where RegisterScriptFunctions() + * @param ScriptRequest Script Request where RegisterScriptFunctions() * registers the functions. * * @sa GuiScriptingInit() */ - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); - - /** - * Returns the translation of the specified string to the - * @link L10n::GetCurrentLocale() current locale@endlink. - * - * This is a JavaScript interface to L10n::Translate(). - * - * @param pCmptPrivate JavaScript context. - * @param sourceString String to translate to the current locale. - * @return Translation of @p sourceString to the current locale, or - * @p sourceString if there is no translation available. - */ - std::wstring Translate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& sourceString); - - /** - * Returns the translation of the specified string to the - * @link L10n::GetCurrentLocale() current locale@endlink in the specified - * context. - * - * This is a JavaScript interface to L10n::TranslateWithContext(). - * - * @param pCmptPrivate JavaScript context. - * @param context Context where the string is used. See - * http://www.gnu.org/software/gettext/manual/html_node/Contexts.html - * @param sourceString String to translate to the current locale. - * @return Translation of @p sourceString to the current locale in the - * specified @p context, or @p sourceString if there is no - * translation available. - */ - std::wstring TranslateWithContext(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& context, const std::wstring& sourceString); - - /** - * Returns the translation of the specified string to the - * @link L10n::GetCurrentLocale() current locale@endlink based on the - * specified number. - * - * This is a JavaScript interface to L10n::TranslatePlural(). - * - * @param pCmptPrivate JavaScript context. - * @param singularSourceString String to translate to the current locale, - * in English' singular form. - * @param pluralSourceString String to translate to the current locale, in - * English' plural form. - * @param number Number that determines the required form of the translation - * (or the English string if no translation is available). - * @return Translation of the source string to the current locale for the - * specified @p number, or either @p singularSourceString (if - * @p number is 1) or @p pluralSourceString (if @p number is not 1) - * if there is no translation available. - */ - std::wstring TranslatePlural(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& singularSourceString, const std::wstring& pluralSourceString, int number); - - /** - * Returns the translation of the specified string to the - * @link L10n::GetCurrentLocale() current locale@endlink in the specified - * context, based on the specified number. - * - * This is a JavaScript interface to L10n::TranslatePluralWithContext(). - * - * @param pCmptPrivate JavaScript context. - * @param context Context where the string is used. See - * http://www.gnu.org/software/gettext/manual/html_node/Contexts.html - * @param singularSourceString String to translate to the current locale, - * in English' singular form. - * @param pluralSourceString String to translate to the current locale, in - * English' plural form. - * @param number Number that determines the required form of the translation - * (or the English string if no translation is available). * - * @return Translation of the source string to the current locale in the - * specified @p context and for the specified @p number, or either - * @p singularSourceString (if @p number is 1) or - * @p pluralSourceString (if @p number is not 1) if there is no - * translation available. - */ - std::wstring TranslatePluralWithContext(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& context, const std::wstring& singularSourceString, const std::wstring& pluralSourceString, int number); - - /** - * Translates a text line by line to the - * @link L10n::GetCurrentLocale() current locale@endlink. - * - * TranslateLines() is helpful when you need to translate a plain text file - * after you load it. - * - * This is a JavaScript interface to L10n::TranslateLines(). - * - * @param pCmptPrivate JavaScript context. - * @param sourceString Text to translate to the current locale. - * @return Line by line translation of @p sourceString to the current - * locale. Some of the lines in the returned text may be in English - * because there was not translation available for them. - */ - std::wstring TranslateLines(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& sourceString); - - /** - * Translate each of the strings of a JavaScript array to the - * @link L10n::GetCurrentLocale() current locale@endlink. - * - * This is a helper function that loops through the items of the input array - * and calls L10n::Translate() on each of them. - * - * @param pCmptPrivate JavaScript context. - * @param sourceArray JavaScript array of strings to translate to the - * current locale. - * @return Item by item translation of @p sourceArray to the current locale. - * Some of the items in the returned array may be in English because - * there was not translation available for them. - */ - std::vector TranslateArray(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::vector& sourceArray); - - /** - * Returns the specified date converted to the local timezone using the specified date format. - * - * This is a JavaScript interface to - * L10n::FormatMillisecondsIntoDateString(). - * - * @param pCmptPrivate JavaScript context. - * @param milliseconds Date specified as a UNIX timestamp in milliseconds - * (not seconds). If you have a JavaScript @c Date object, you can - * use @c Date.getTime() to obtain the UNIX time in milliseconds. - * @param formatString Date format string defined using ICU date formatting - * symbols. Usually, you internationalize the format string and - * get it translated before you pass it to - * FormatMillisecondsIntoDateString(). - * @return String containing the specified date with the specified date - * format. - * - * @sa http://en.wikipedia.org/wiki/Unix_time - * @sa https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table - */ - std::wstring FormatMillisecondsIntoDateStringLocal(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), UDate milliseconds, const std::wstring& formatString); - - /** - * Returns the specified date in GMT using the specified date format. - * - * This is a JavaScript interface to - * L10n::FormatMillisecondsIntoDateString(). - * - * @param pCmptPrivate JavaScript context. - * @param milliseconds Date specified as a UNIX timestamp in milliseconds - * (not seconds). If you have a JavaScript @c Date object, you can - * use @c Date.getTime() to obtain the UNIX time in milliseconds. - * @param formatString Date format string defined using ICU date formatting - * symbols. Usually, you internationalize the format string and - * get it translated before you pass it to - * FormatMillisecondsIntoDateString(). - * @return String containing the specified date with the specified date - * format. - * - * @sa http://en.wikipedia.org/wiki/Unix_time - * @sa https://sites.google.com/site/icuprojectuserguide/formatparse/datetime?pli=1#TOC-Date-Field-Symbol-Table - */ - std::wstring FormatMillisecondsIntoDateStringGMT(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), UDate milliseconds, const std::wstring& formatString); - - /** - * Returns the specified floating-point number as a string, with the number - * formatted as a decimal number using the - * @link L10n::GetCurrentLocale() current locale@endlink. - * - * This is a JavaScript interface to L10n::FormatDecimalNumberIntoString(). - * - * @param pCmptPrivate JavaScript context. - * @param number Number to format. - * @return Decimal number formatted using the current locale. - */ - std::wstring FormatDecimalNumberIntoString(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), double number); - - /** - * Returns an array of supported locale codes sorted alphabetically. - * - * A locale code is a string such as "de" or "pt_BR". - * - * If yours is a development copy (the 'config/dev.cfg' file is found in the - * virtual filesystem), the output array may include the special "long" - * locale code. - * - * This is a JavaScript interface to L10n::GetSupportedLocaleBaseNames(). - * - * @param pCmptPrivate JavaScript context. - * @return Array of supported locale codes. - * - * @sa GetSupportedLocaleDisplayNames() - * @sa GetAllLocales() - * @sa GetCurrentLocale() - * - * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale - */ - std::vector GetSupportedLocaleBaseNames(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - /** - * Returns an array of supported locale names sorted alphabetically by - * locale code. - * - * A locale code is a string such as "de" or "pt_BR". - * - * If yours is a development copy (the 'config/dev.cfg' file is found in the - * virtual filesystem), the output array may include the special "Long - * Strings" locale name. - * - * This is a JavaScript interface to L10n::GetSupportedLocaleDisplayNames(). - * - * @param pCmptPrivate JavaScript context. - * @return Array of supported locale codes. - * - * @sa GetSupportedLocaleBaseNames() - * - * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale - */ - std::vector GetSupportedLocaleDisplayNames(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - /** - * Returns the code of the current locale. - * - * A locale code is a string such as "de" or "pt_BR". - * - * This is a JavaScript interface to L10n::GetCurrentLocaleString(). - * - * @param pCmptPrivate JavaScript context. - * - * @sa GetSupportedLocaleBaseNames() - * @sa GetAllLocales() - * @sa ReevaluateCurrentLocaleAndReload() - */ - std::string GetCurrentLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - /** - * Returns an array of locale codes supported by ICU. - * - * A locale code is a string such as "de" or "pt_BR". - * - * This is a JavaScript interface to L10n::GetAllLocales(). - * - * @param pCmptPrivate JavaScript context. - * @return Array of supported locale codes. - * - * @sa GetSupportedLocaleBaseNames() - * @sa GetCurrentLocale() - * - * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a073d70df8c9c8d119c0d42d70de24137 - */ - std::vector GetAllLocales(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - /** - * Returns the code of the recommended locale for the current user that the - * game supports. - * - * "That the game supports" means both that a translation file is available - * for that locale and that the locale code is either supported by ICU or - * the special "long" locale code. - * - * The mechanism to select a recommended locale follows this logic: - * 1. First see if the game supports the specified locale,\n - * @p configLocale. - * 2. Otherwise, check the system locale and see if the game supports\n - * that locale. - * 3. Else, return the default locale, 'en_US'. - * - * This is a JavaScript interface to L10n::GetDictionaryLocale(std::string). - * - * @param pCmptPrivate JavaScript context. - * @param configLocale Locale to check for support first. Pass an empty - * string to check the system locale directly. - * @return Code of a locale that the game supports. - * - * @sa http://trac.wildfiregames.com/wiki/Implementation_of_Internationalization_and_Localization#LongStringsLocale - */ - std::string GetDictionaryLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& configLocale); - - /** - * Returns an array of paths to files in the virtual filesystem that provide - * translations for the specified locale code. - * - * This is a JavaScript interface to L10n::GetDictionariesForLocale(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale code. - * @return Array of paths to files in the virtual filesystem that provide - * translations for @p locale. - */ - std::vector GetDictionariesForLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Returns the ISO-639 language code of the specified locale code. - * - * For example, if you specify the 'en_US' locate, it returns 'en'. - * - * This is a JavaScript interface to L10n::GetLocaleLanguage(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale code. - * @return Language code. - * - * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#af36d821adced72a870d921ebadd0ca93 - */ - std::string GetLocaleLanguage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Returns the programmatic code of the entire locale without keywords. - * - * This is a JavaScript interface to L10n::GetLocaleBaseName(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale code. - * @return Locale code without keywords. - * - * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a4c1acbbdf95dc15599db5f322fa4c4d0 - */ - std::string GetLocaleBaseName(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Returns the ISO-3166 country code of the specified locale code. - * - * For example, if you specify the 'en_US' locate, it returns 'US'. - * - * This is a JavaScript interface to L10n::GetLocaleCountry(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale code. - * @return Country code. - * - * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#ae3f1fc415c00d4f0ab33288ceadccbf9 - */ - std::string GetLocaleCountry(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Returns the ISO-15924 abbreviation script code of the specified locale code. - * - * This is a JavaScript interface to L10n::GetLocaleScript(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale code. - * @return Script code. - * - * @sa http://www.icu-project.org/apiref/icu4c/classicu_1_1Locale.html#a5e0145a339d30794178a1412dcc55abe - */ - std::string GetLocaleScript(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - - std::wstring GetFallbackToAvailableDictLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Returns @c true if the current locale is the special "Long Strings" - * locale. It returns @c false otherwise. - * - * This is a JavaScript interface to L10n::UseLongStrings(). - * - * @param pCmptPrivate JavaScript context. * - * @return Whether the current locale is the special "Long Strings" - * (@c true) or not (@c false). - */ - bool UseLongStrings(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - /** - * Returns @c true if the locale is supported by both ICU and the game. It - * returns @c false otherwise. - * - * It returns @c true if both of these conditions are true: - * 1. ICU has resources for that locale (which also ensures it's a valid\n - * locale string). - * 2. Either a dictionary for language_country or for language is\n - * available. - * - * This is a JavaScript interface to L10n::ValidateLocale(const std::string&). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale to check. - * @return Whether @p locale is supported by both ICU and the game (@c true) - * or not (@c false). - */ - bool ValidateLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Saves the specified locale in the game configuration file. - * - * The next time that the game starts, the game uses the locale in the - * configuration file if there are translation files available for it. - * - * SaveLocale() checks the validity of the specified locale with - * ValidateLocale(). If the specified locale is not valid, SaveLocale() - * returns @c false and does not save the locale to the configuration file. - * - * This is a JavaScript interface to L10n::SaveLocale(). - * - * @param pCmptPrivate JavaScript context. - * @param locale Locale to save to the configuration file. - * @return Whether the specified locale is valid (@c true) or not - * (@c false). - */ - bool SaveLocale(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& locale); - - /** - * Determines the best, supported locale for the current user, makes it the - * current game locale and reloads the translations dictionary with - * translations for that locale. - * - * To determine the best locale, ReevaluateCurrentLocaleAndReload(): - * 1. Checks the user game configuration. - * 2. If the locale is not defined there, it checks the system locale. - * 3. If none of those locales are supported by the game, the default\n - * locale, 'en_US', is used. - * - * This is a JavaScript interface to L10n::ReevaluateCurrentLocaleAndReload(). - * - * @param pCmptPrivate JavaScript context. - * - * @sa GetCurrentLocale() - */ - void ReevaluateCurrentLocaleAndReload(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSINTERFACE_L10N Index: ps/trunk/source/lobby/IXmppClient.h =================================================================== --- ps/trunk/source/lobby/IXmppClient.h (revision 24982) +++ ps/trunk/source/lobby/IXmppClient.h (revision 24983) @@ -1,71 +1,71 @@ /* Copyright (C) 2021 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 IXMPPCLIENT_H #define IXMPPCLIENT_H #include "scriptinterface/ScriptTypes.h" class ScriptInterface; namespace StunClient { struct StunEndpoint; } class IXmppClient { public: static IXmppClient* create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, bool regOpt = false); virtual ~IXmppClient() {} virtual void connect() = 0; virtual void disconnect() = 0; virtual bool isConnected() = 0; virtual void recv() = 0; virtual void SendIqGetBoardList() = 0; virtual void SendIqGetProfile(const std::string& player) = 0; virtual void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) = 0; virtual void SendIqGetConnectionData(const std::string& jid, const std::string& password) = 0; virtual void SendIqUnregisterGame() = 0; virtual void SendIqChangeStateGame(const std::string& nbp, const std::string& players) = 0; virtual void SendIqLobbyAuth(const std::string& to, const std::string& token) = 0; virtual void SetNick(const std::string& nick) = 0; - virtual void GetNick(std::string& nick) = 0; + virtual std::string GetNick() = 0; virtual void kick(const std::string& nick, const std::string& reason) = 0; virtual void ban(const std::string& nick, const std::string& reason) = 0; virtual void SetPresence(const std::string& presence) = 0; virtual const char* GetPresence(const std::string& nickname) = 0; virtual const char* GetRole(const std::string& nickname) = 0; virtual std::wstring GetRating(const std::string& nickname) = 0; virtual const std::wstring& GetSubject() = 0; - virtual void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; - virtual void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; - virtual void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; - virtual void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) = 0; + virtual JS::Value GUIGetPlayerList(const ScriptInterface& scriptInterface) = 0; + virtual JS::Value GUIGetGameList(const ScriptInterface& scriptInterface) = 0; + virtual JS::Value GUIGetBoardList(const ScriptInterface& scriptInterface) = 0; + virtual JS::Value GUIGetProfile(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) = 0; virtual JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface) = 0; virtual bool GuiPollHasPlayerListUpdate() = 0; virtual void SendMUCMessage(const std::string& message) = 0; virtual void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID) = 0; }; extern IXmppClient *g_XmppClient; extern bool g_rankedGame; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/XmppClient.cpp =================================================================== --- ps/trunk/source/lobby/XmppClient.cpp (revision 24982) +++ ps/trunk/source/lobby/XmppClient.cpp (revision 24983) @@ -1,1494 +1,1500 @@ /* Copyright (C) 2021 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 "XmppClient.h" #include "StanzaExtensions.h" #ifdef WIN32 # include #endif #include "i18n/L10n.h" #include "lib/external_libraries/enet.h" #include "lib/utf8.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/StunClient.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptExtraHeaders.h" // StructuredClone #include "scriptinterface/ScriptInterface.h" #include #include //debug #if 1 #define DbgXMPP(x) #else #define DbgXMPP(x) std::cout << x << std::endl; static std::string tag_xml(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->xml().to_string(); glooxwrapper::Tag::free(tag); return ret; } #endif static std::string tag_name(const glooxwrapper::IQ& iq) { std::string ret; glooxwrapper::Tag* tag = iq.tag(); ret = tag->name().to_string(); glooxwrapper::Tag::free(tag); return ret; } IXmppClient* IXmppClient::create(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize,bool regOpt) { return new XmppClient(scriptInterface, sUsername, sPassword, sRoom, sNick, historyRequestSize, regOpt); } /** * Construct the XMPP client. * * @param scriptInterface - ScriptInterface to be used for storing GUI messages. * Can be left blank for non-visual applications. * @param sUsername Username to login with of register. * @param sPassword Password to login with or register. * @param sRoom MUC room to join. * @param sNick Nick to join with. * @param historyRequestSize Number of stanzas of room history to request. * @param regOpt If we are just registering or not. */ XmppClient::XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize, bool regOpt) : m_ScriptInterface(scriptInterface), m_client(nullptr), m_mucRoom(nullptr), m_registration(nullptr), m_username(sUsername), m_password(sPassword), m_room(sRoom), m_nick(sNick), m_initialLoadComplete(false), m_isConnected(false), m_sessionManager(nullptr), m_certStatus(gloox::CertStatus::CertOk), m_PlayerMapUpdate(false), m_connectionDataJid(), m_connectionDataIqId() { if (m_ScriptInterface) JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this); // Read lobby configuration from default.cfg std::string sXpartamupp; std::string sEchelon; CFG_GET_VAL("lobby.server", m_server); CFG_GET_VAL("lobby.xpartamupp", sXpartamupp); CFG_GET_VAL("lobby.echelon", sEchelon); m_xpartamuppId = sXpartamupp + "@" + m_server + "/CC"; m_echelonId = sEchelon + "@" + m_server + "/CC"; glooxwrapper::JID clientJid(sUsername + "@" + m_server + "/0ad"); glooxwrapper::JID roomJid(m_room + "@conference." + m_server + "/" + sNick); // If we are connecting, use the full jid and a password // If we are registering, only use the server name if (!regOpt) m_client = new glooxwrapper::Client(clientJid, sPassword); else m_client = new glooxwrapper::Client(m_server); // Optionally join without a TLS certificate, so a local server can be tested quickly. // Security risks from malicious JS mods can be mitigated if this option and also the hostname and login are shielded from JS access. bool tls = true; CFG_GET_VAL("lobby.tls", tls); m_client->setTls(tls ? gloox::TLSRequired : gloox::TLSDisabled); // Disable use of the SASL PLAIN mechanism, to prevent leaking credentials // if the server doesn't list any supported SASL mechanism or the response // has been modified to exclude those. const int mechs = gloox::SaslMechAll ^ gloox::SaslMechPlain; m_client->setSASLMechanisms(mechs); m_client->registerConnectionListener(this); m_client->setPresence(gloox::Presence::Available, -1); m_client->disco()->setVersion("Pyrogenesis", engine_version); m_client->disco()->setIdentity("client", "bot"); m_client->setCompression(false); m_client->registerStanzaExtension(new GameListQuery()); m_client->registerIqHandler(this, EXTGAMELISTQUERY); m_client->registerStanzaExtension(new BoardListQuery()); m_client->registerIqHandler(this, EXTBOARDLISTQUERY); m_client->registerStanzaExtension(new ProfileQuery()); m_client->registerIqHandler(this, EXTPROFILEQUERY); m_client->registerStanzaExtension(new LobbyAuth()); m_client->registerIqHandler(this, EXTLOBBYAUTH); m_client->registerStanzaExtension(new ConnectionData()); m_client->registerIqHandler(this, EXTCONNECTIONDATA); m_client->registerMessageHandler(this); // Uncomment to see the raw stanzas //m_client->getWrapped()->logInstance().registerLogHandler( gloox::LogLevelDebug, gloox::LogAreaAll, this ); if (!regOpt) { // Create a Multi User Chat Room m_mucRoom = new glooxwrapper::MUCRoom(m_client, roomJid, this, 0); // Get room history. m_mucRoom->setRequestHistory(historyRequestSize, gloox::MUCRoom::HistoryMaxStanzas); } else { // Registration m_registration = new glooxwrapper::Registration(m_client); m_registration->registerRegistrationHandler(this); } m_sessionManager = new glooxwrapper::SessionManager(m_client, this); // Register plugins to allow gloox parse them in incoming sessions m_sessionManager->registerPlugins(); } /** * Destroy the xmpp client */ XmppClient::~XmppClient() { DbgXMPP("XmppClient destroyed"); delete m_registration; delete m_mucRoom; delete m_sessionManager; // Workaround for memory leak in gloox 1.0/1.0.1 m_client->removePresenceExtension(gloox::ExtCaps); delete m_client; for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); if (m_ScriptInterface) JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), XmppClient::Trace, this); } void XmppClient::TraceMember(JSTracer* trc) { for (JS::Heap& guiMessage : m_GuiMessageQueue) JS::TraceEdge(trc, &guiMessage, "m_GuiMessageQueue"); for (JS::Heap& guiMessage : m_HistoricGuiMessages) JS::TraceEdge(trc, &guiMessage, "m_HistoricGuiMessages"); } /// Network void XmppClient::connect() { m_initialLoadComplete = false; m_client->connect(false); } void XmppClient::disconnect() { m_client->disconnect(); } bool XmppClient::isConnected() { return m_isConnected; } void XmppClient::recv() { m_client->recv(1); } /** * Log (debug) Handler */ void XmppClient::handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) { std::cout << "log: level: " << level << ", area: " << area << ", message: " << message << std::endl; } /***************************************************** * Connection handlers * *****************************************************/ /** * Handle connection */ void XmppClient::onConnect() { if (m_mucRoom) { m_isConnected = true; CreateGUIMessage("system", "connected", std::time(nullptr)); m_mucRoom->join(); } if (m_registration) m_registration->fetchRegistrationFields(); } /** * Handle disconnection */ void XmppClient::onDisconnect(gloox::ConnectionError error) { // Make sure we properly leave the room so that // everything works if we decide to come back later if (m_mucRoom) m_mucRoom->leave(); // Clear game, board and player lists. for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_BoardList.clear(); m_GameList.clear(); m_PlayerMap.clear(); m_PlayerMapUpdate = true; m_Profile.clear(); m_HistoricGuiMessages.clear(); m_isConnected = false; m_initialLoadComplete = false; CreateGUIMessage( "system", "disconnected", std::time(nullptr), "reason", error, "certificate_status", m_certStatus); } /** * Handle TLS connection. */ bool XmppClient::onTLSConnect(const glooxwrapper::CertInfo& info) { DbgXMPP("onTLSConnect"); DbgXMPP( "status: " << info.status << "\nissuer: " << info.issuer << "\npeer: " << info.server << "\nprotocol: " << info.protocol << "\nmac: " << info.mac << "\ncipher: " << info.cipher << "\ncompression: " << info.compression ); m_certStatus = static_cast(info.status); // Optionally accept invalid certificates, see require_tls option. bool verify_certificate = true; CFG_GET_VAL("lobby.verify_certificate", verify_certificate); return info.status == gloox::CertOk || !verify_certificate; } /** * Handle MUC room errors */ void XmppClient::handleMUCError(glooxwrapper::MUCRoom& UNUSED(room), gloox::StanzaError err) { DbgXMPP("MUC Error " << ": " << StanzaErrorToString(err)); CreateGUIMessage("system", "error", std::time(nullptr), "text", err); } /***************************************************** * Requests to server * *****************************************************/ /** * Request the leaderboard data from the server. */ void XmppClient::SendIqGetBoardList() { glooxwrapper::JID echelonJid(m_echelonId); // Send IQ BoardListQuery* b = new BoardListQuery(); b->m_Command = "getleaderboard"; glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID()); iq.addExtension(b); DbgXMPP("SendIqGetBoardList [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Request the profile data from the server. */ void XmppClient::SendIqGetProfile(const std::string& player) { glooxwrapper::JID echelonJid(m_echelonId); // Send IQ ProfileQuery* b = new ProfileQuery(); b->m_Command = player; glooxwrapper::IQ iq(gloox::IQ::Get, echelonJid, m_client->getID()); iq.addExtension(b); DbgXMPP("SendIqGetProfile [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Request the Connection data (ip, port...) from the server. */ void XmppClient::SendIqGetConnectionData(const std::string& jid, const std::string& password) { glooxwrapper::JID targetJID(jid); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Password = password; glooxwrapper::IQ iq(gloox::IQ::Get, targetJID, m_client->getID()); iq.addExtension(connectionData); m_connectionDataJid = iq.from().full(); m_connectionDataIqId = iq.id().to_string(); DbgXMPP("SendIqGetConnectionData [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send game report containing numerous game properties to the server. * * @param data A JS array of game statistics */ void XmppClient::SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID echelonJid(m_echelonId); // Setup some base stanza attributes GameReport* game = new GameReport(); glooxwrapper::Tag* report = glooxwrapper::Tag::allocate("game"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNames(data, true, properties); for (const std::string& p : properties) { std::wstring value; scriptInterface.GetProperty(data, p.c_str(), value); report->addAttribute(p, utf8_from_wstring(value)); } // Add stanza to IQ game->m_GameReport.emplace_back(report); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, echelonJid, m_client->getID()); iq.addExtension(game); DbgXMPP("SendGameReport [" << tag_xml(iq) << "]"); m_client->send(iq); }; /** * Send a request to register a game to the server. * * @param data A JS array of game attributes */ void XmppClient::SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Setup some base stanza attributes GameListQuery* g = new GameListQuery(); g->m_Command = "register"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); // Iterate through all the properties reported and add them to the stanza. std::vector properties; scriptInterface.EnumeratePropertyNames(data, true, properties); for (const std::string& p : properties) { std::wstring value; scriptInterface.GetProperty(data, p.c_str(), value); game->addAttribute(p, utf8_from_wstring(value)); } // Push the stanza onto the IQ g->m_GameList.emplace_back(game); // Send IQ glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); iq.addExtension(g); DbgXMPP("SendIqRegisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to unregister a game to the server. */ void XmppClient::SendIqUnregisterGame() { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "unregister"; g->m_GameList.emplace_back(glooxwrapper::Tag::allocate("game")); glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); iq.addExtension(g); DbgXMPP("SendIqUnregisterGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /** * Send a request to change the state of a registered game on the server. * * A game can either be in the 'running' or 'waiting' state - the server * decides which - but we need to update the current players that are * in-game so the server can make the calculation. */ void XmppClient::SendIqChangeStateGame(const std::string& nbp, const std::string& players) { glooxwrapper::JID xpartamuppJid(m_xpartamuppId); // Send IQ GameListQuery* g = new GameListQuery(); g->m_Command = "changestate"; glooxwrapper::Tag* game = glooxwrapper::Tag::allocate("game"); game->addAttribute("nbp", nbp); game->addAttribute("players", players); g->m_GameList.emplace_back(game); glooxwrapper::IQ iq(gloox::IQ::Set, xpartamuppJid, m_client->getID()); iq.addExtension(g); DbgXMPP("SendIqChangeStateGame [" << tag_xml(iq) << "]"); m_client->send(iq); } /***************************************************** * iq to clients * *****************************************************/ /** * Send lobby authentication token. */ void XmppClient::SendIqLobbyAuth(const std::string& to, const std::string& token) { LobbyAuth* auth = new LobbyAuth(); auth->m_Token = token; glooxwrapper::JID clientJid(to + "@" + m_server + "/0ad"); glooxwrapper::IQ iq(gloox::IQ::Set, clientJid, m_client->getID()); iq.addExtension(auth); DbgXMPP("SendIqLobbyAuth [" << tag_xml(iq) << "]"); m_client->send(iq); } /***************************************************** * Account registration * *****************************************************/ void XmppClient::handleRegistrationFields(const glooxwrapper::JID&, int fields, glooxwrapper::string) { glooxwrapper::RegistrationFields vals; vals.username = m_username; vals.password = m_password; m_registration->createAccount(fields, vals); } void XmppClient::handleRegistrationResult(const glooxwrapper::JID&, gloox::RegistrationResult result) { if (result == gloox::RegistrationSuccess) CreateGUIMessage("system", "registered", std::time(nullptr)); else CreateGUIMessage("system", "error", std::time(nullptr), "text", result); disconnect(); } void XmppClient::handleAlreadyRegistered(const glooxwrapper::JID&) { DbgXMPP("the account already exists"); } void XmppClient::handleDataForm(const glooxwrapper::JID&, const glooxwrapper::DataForm&) { DbgXMPP("dataForm received"); } void XmppClient::handleOOB(const glooxwrapper::JID&, const glooxwrapper::OOB&) { DbgXMPP("OOB registration requested"); } /***************************************************** * Requests from GUI * *****************************************************/ /** * Handle requests from the GUI for the list of players. * * @return A JS array containing all known players and their presences */ -void XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) +JS::Value XmppClient::GUIGetPlayerList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); - ScriptInterface::CreateArray(rq, ret); + JS::RootedValue ret(rq.cx); + ScriptInterface::CreateArray(rq, &ret); int j = 0; for (const std::pair& p : m_PlayerMap) { JS::RootedValue player(rq.cx); ScriptInterface::CreateObject( rq, &player, "name", p.first, "presence", p.second.m_Presence, "rating", p.second.m_Rating, "role", p.second.m_Role); scriptInterface.SetPropertyInt(ret, j++, player); } + return ret; } /** * Handle requests from the GUI for the list of all active games. * * @return A JS array containing all known games */ -void XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) +JS::Value XmppClient::GUIGetGameList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); - ScriptInterface::CreateArray(rq, ret); + JS::RootedValue ret(rq.cx); + ScriptInterface::CreateArray(rq, &ret); int j = 0; const char* stats[] = { "name", "hostUsername", "state", "hasPassword", "nbp", "maxnbp", "players", "mapName", "niceMapName", "mapSize", "mapType", "victoryConditions", "startTime", "mods" }; for(const glooxwrapper::Tag* const& t : m_GameList) { JS::RootedValue game(rq.cx); ScriptInterface::CreateObject(rq, &game); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(game, stats[i], t->findAttribute(stats[i])); scriptInterface.SetPropertyInt(ret, j++, game); } + return ret; } /** * Handle requests from the GUI for leaderboard data. * * @return A JS array containing all known leaderboard data */ -void XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) +JS::Value XmppClient::GUIGetBoardList(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); - ScriptInterface::CreateArray(rq, ret); + JS::RootedValue ret(rq.cx); + ScriptInterface::CreateArray(rq, &ret); int j = 0; const char* attributes[] = { "name", "rank", "rating" }; for(const glooxwrapper::Tag* const& t : m_BoardList) { JS::RootedValue board(rq.cx); ScriptInterface::CreateObject(rq, &board); for (size_t i = 0; i < ARRAY_SIZE(attributes); ++i) scriptInterface.SetProperty(board, attributes[i], t->findAttribute(attributes[i])); scriptInterface.SetPropertyInt(ret, j++, board); } + return ret; } /** * Handle requests from the GUI for profile data. * * @return A JS array containing the specific user's profile data */ -void XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret) +JS::Value XmppClient::GUIGetProfile(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); - ScriptInterface::CreateArray(rq, ret); + JS::RootedValue ret(rq.cx); + ScriptInterface::CreateArray(rq, &ret); int j = 0; const char* stats[] = { "player", "rating", "totalGamesPlayed", "highestRating", "wins", "losses", "rank" }; for (const glooxwrapper::Tag* const& t : m_Profile) { JS::RootedValue profile(rq.cx); ScriptInterface::CreateObject(rq, &profile); for (size_t i = 0; i < ARRAY_SIZE(stats); ++i) scriptInterface.SetProperty(profile, stats[i], t->findAttribute(stats[i])); scriptInterface.SetPropertyInt(ret, j++, profile); } + return ret; } /***************************************************** * Message interfaces * *****************************************************/ void SetGUIMessageProperty(const ScriptRequest& UNUSED(rq), JS::HandleObject UNUSED(messageObj)) { } template void SetGUIMessageProperty(const ScriptRequest& rq, JS::HandleObject messageObj, const std::string& propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue scriptPropertyValue(rq.cx); ScriptInterface::AssignOrToJSVal(rq, &scriptPropertyValue, propertyValue); JS_DefineProperty(rq.cx, messageObj, propertyName.c_str(), scriptPropertyValue, JSPROP_ENUMERATE); SetGUIMessageProperty(rq, messageObj, args...); } template void XmppClient::CreateGUIMessage( const std::string& type, const std::string& level, const std::time_t time, Args const&... args) { if (!m_ScriptInterface) return; ScriptRequest rq(m_ScriptInterface); JS::RootedValue message(rq.cx); ScriptInterface::CreateObject( rq, &message, "type", type, "level", level, "historic", false, "time", static_cast(time)); JS::RootedObject messageObj(rq.cx, message.toObjectOrNull()); SetGUIMessageProperty(rq, messageObj, args...); m_ScriptInterface->FreezeObject(message, true); m_GuiMessageQueue.push_back(JS::Heap(message)); } bool XmppClient::GuiPollHasPlayerListUpdate() { // The initial playerlist will be received in multiple messages // Only inform the GUI after all of these playerlist fragments were received. if (!m_initialLoadComplete) return false; bool hasUpdate = m_PlayerMapUpdate; m_PlayerMapUpdate = false; return hasUpdate; } JS::Value XmppClient::GuiPollNewMessages(const ScriptInterface& scriptInterface) { if ((m_isConnected && !m_initialLoadComplete) || m_GuiMessageQueue.empty()) return JS::UndefinedValue(); ScriptRequest rq(m_ScriptInterface); // Optimize for batch message processing that is more // performance demanding than processing a lone message. JS::RootedValue messages(rq.cx); ScriptInterface::CreateArray(rq, &messages); int j = 0; for (const JS::Heap& message : m_GuiMessageQueue) { m_ScriptInterface->SetPropertyInt(messages, j++, message); // Store historic chat messages. // Only store relevant messages to minimize memory footprint. JS::RootedValue rootedMessage(rq.cx, message); std::string type; m_ScriptInterface->GetProperty(rootedMessage, "type", type); if (type != "chat") continue; std::string level; m_ScriptInterface->GetProperty(rootedMessage, "level", level); if (level != "room-message" && level != "private-message") continue; JS::RootedValue historicMessage(rq.cx); if (JS_StructuredClone(rq.cx, rootedMessage, &historicMessage, nullptr, nullptr)) { m_ScriptInterface->SetProperty(historicMessage, "historic", true); m_ScriptInterface->FreezeObject(historicMessage, true); m_HistoricGuiMessages.push_back(JS::Heap(historicMessage)); } else LOGERROR("Could not clone historic lobby GUI message!"); } m_GuiMessageQueue.clear(); // Copy the messages over to the caller script interface. return scriptInterface.CloneValueFromOtherCompartment(*m_ScriptInterface, messages); } JS::Value XmppClient::GuiPollHistoricMessages(const ScriptInterface& scriptInterface) { if (m_HistoricGuiMessages.empty()) return JS::UndefinedValue(); ScriptRequest rq(m_ScriptInterface); JS::RootedValue messages(rq.cx); ScriptInterface::CreateArray(rq, &messages); int j = 0; for (const JS::Heap& message : m_HistoricGuiMessages) m_ScriptInterface->SetPropertyInt(messages, j++, message); // Copy the messages over to the caller script interface. return scriptInterface.CloneValueFromOtherCompartment(*m_ScriptInterface, messages); } /** * Send a standard MUC textual message. */ void XmppClient::SendMUCMessage(const std::string& message) { m_mucRoom->send(message); } /** * Handle a room message. */ void XmppClient::handleMUCMessage(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::Message& msg, bool priv) { DbgXMPP(msg.from().resource() << " said " << msg.body()); CreateGUIMessage( "chat", priv ? "private-message" : "room-message", ComputeTimestamp(msg), "from", msg.from().resource(), "text", msg.body()); } /** * Handle a private message. */ void XmppClient::handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession*) { DbgXMPP("type " << msg.subtype() << ", subject " << msg.subject() << ", message " << msg.body() << ", thread id " << msg.thread()); CreateGUIMessage( "chat", "private-message", ComputeTimestamp(msg), "from", msg.from().resource(), "text", msg.body()); } /** * Handle portions of messages containing custom stanza extensions. */ bool XmppClient::handleIq(const glooxwrapper::IQ& iq) { DbgXMPP("handleIq [" << tag_xml(iq) << "]"); if (iq.subtype() == gloox::IQ::Result) { const GameListQuery* gq = iq.findExtension(EXTGAMELISTQUERY); const BoardListQuery* bq = iq.findExtension(EXTBOARDLISTQUERY); const ProfileQuery* pq = iq.findExtension(EXTPROFILEQUERY); const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA); if (cd) { if (g_NetServer || !g_NetClient) return true; if (!m_connectionDataJid.empty() && m_connectionDataJid.compare(iq.from().full()) != 0) { LOGMESSAGE("XmppClient: Received connection data from invalid host: %s", iq.from().username()); return true; } if (!m_connectionDataIqId.empty() && m_connectionDataIqId.compare(iq.id().to_string()) != 0) { LOGWARNING("XmppClient: Received connection data with invalid id"); return true; } if (!cd->m_Error.empty()) { g_NetClient->HandleGetServerDataFailed(cd->m_Error.c_str()); return true; } g_NetClient->SetupServerData(cd->m_Ip.to_string(), stoi(cd->m_Port.to_string()), !cd->m_UseSTUN.empty()); g_NetClient->TryToConnect(iq.from().full()); } if (gq) { if (iq.from().full() == m_xpartamuppId && gq->m_Command == "register" && g_NetServer && !g_NetServer->GetUseSTUN()) { if (gq->m_GameList.empty()) { LOGWARNING("XmppClient: Received empty game list in response to Game Register"); return true; } std::string publicIP = gq->m_GameList.front()->findAttribute("ip").to_string(); if (publicIP.empty()) { LOGWARNING("XmppClient: Received game with no IP in response to Game Register"); return true; } g_NetServer->SetConnectionData(publicIP, g_NetServer->GetPublicPort(), false); return true; } for (const glooxwrapper::Tag* const& t : m_GameList) glooxwrapper::Tag::free(t); m_GameList.clear(); for (const glooxwrapper::Tag* const& t : gq->m_GameList) m_GameList.emplace_back(t->clone()); CreateGUIMessage("game", "gamelist", std::time(nullptr)); } if (bq) { if (bq->m_Command == "boardlist") { for (const glooxwrapper::Tag* const& t : m_BoardList) glooxwrapper::Tag::free(t); m_BoardList.clear(); for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) m_BoardList.emplace_back(t->clone()); CreateGUIMessage("game", "leaderboard", std::time(nullptr)); } else if (bq->m_Command == "ratinglist") { for (const glooxwrapper::Tag* const& t : bq->m_StanzaBoardList) { const PlayerMap::iterator it = m_PlayerMap.find(t->findAttribute("name")); if (it != m_PlayerMap.end()) { it->second.m_Rating = t->findAttribute("rating"); m_PlayerMapUpdate = true; } } CreateGUIMessage("game", "ratinglist", std::time(nullptr)); } } if (pq) { for (const glooxwrapper::Tag* const& t : m_Profile) glooxwrapper::Tag::free(t); m_Profile.clear(); for (const glooxwrapper::Tag* const& t : pq->m_StanzaProfile) m_Profile.emplace_back(t->clone()); CreateGUIMessage("game", "profile", std::time(nullptr)); } } else if (iq.subtype() == gloox::IQ::Set) { const LobbyAuth* lobbyAuth = iq.findExtension(EXTLOBBYAUTH); if (lobbyAuth) { LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username()); glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); m_client->send(response); if (g_NetServer) g_NetServer->OnLobbyAuth(iq.from().username(), lobbyAuth->m_Token.to_string()); else LOGERROR("Received lobby authentication request, but not hosting currently!"); } } else if (iq.subtype() == gloox::IQ::Get) { const ConnectionData* cd = iq.findExtension(EXTCONNECTIONDATA); if (cd) { LOGMESSAGE("XmppClient: Recieved request for connection data from %s", iq.from().username()); if (!g_NetServer) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "not_server"; response.addExtension(connectionData); m_client->send(response); return true; } if (g_NetServer->IsBanned(iq.from().username())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "banned"; response.addExtension(connectionData); m_client->send(response); return true; } if (!g_NetServer->CheckPasswordAndIncrement(CStr(cd->m_Password.c_str()), iq.from().username())) { glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Error = "invalid_password"; response.addExtension(connectionData); m_client->send(response); return true; } glooxwrapper::IQ response(gloox::IQ::Result, iq.from(), iq.id()); ConnectionData* connectionData = new ConnectionData(); connectionData->m_Ip = g_NetServer->GetPublicIp();; connectionData->m_Port = std::to_string(g_NetServer->GetPublicPort()); connectionData->m_UseSTUN = g_NetServer->GetUseSTUN() ? "true" : ""; response.addExtension(connectionData); m_client->send(response); } } else if (iq.subtype() == gloox::IQ::Error) CreateGUIMessage("system", "error", std::time(nullptr), "text", iq.error_error()); else { CreateGUIMessage("system", "error", std::time(nullptr), "text", wstring_from_utf8(g_L10n.Translate("unknown subtype (see logs)"))); LOGMESSAGE("unknown subtype '%s'", tag_name(iq).c_str()); } return true; } /** * Update local data when a user changes presence. */ void XmppClient::handleMUCParticipantPresence(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::MUCRoomParticipant participant, const glooxwrapper::Presence& presence) { const glooxwrapper::string& nick = participant.nick->resource(); if (presence.presence() == gloox::Presence::Unavailable) { if (!participant.newNick.empty() && (participant.flags & (gloox::UserNickChanged | gloox::UserSelf))) { // we have a nick change if (m_PlayerMap.find(participant.newNick) == m_PlayerMap.end()) m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(participant.newNick), std::forward_as_tuple(presence.presence(), participant.role, std::move(m_PlayerMap.at(nick).m_Rating))); else LOGERROR("Nickname changed to an existing nick!"); DbgXMPP(nick << " is now known as " << participant.newNick); CreateGUIMessage( "chat", "nick", std::time(nullptr), "oldnick", nick, "newnick", participant.newNick); } else if (participant.flags & gloox::UserKicked) { DbgXMPP(nick << " was kicked. Reason: " << participant.reason); CreateGUIMessage( "chat", "kicked", std::time(nullptr), "nick", nick, "reason", participant.reason); } else if (participant.flags & gloox::UserBanned) { DbgXMPP(nick << " was banned. Reason: " << participant.reason); CreateGUIMessage( "chat", "banned", std::time(nullptr), "nick", nick, "reason", participant.reason); } else { DbgXMPP(nick << " left the room (flags " << participant.flags << ")"); CreateGUIMessage( "chat", "leave", std::time(nullptr), "nick", nick); } m_PlayerMap.erase(nick); } else { const PlayerMap::iterator it = m_PlayerMap.find(nick); /* During the initialization process, we receive join messages for everyone * currently in the room. We don't want to display these, so we filter them * out. We will always be the last to join during initialization. */ if (!m_initialLoadComplete) { if (m_mucRoom->nick() == nick) m_initialLoadComplete = true; } else if (it == m_PlayerMap.end()) { CreateGUIMessage( "chat", "join", std::time(nullptr), "nick", nick); } else if (it->second.m_Role != participant.role) { CreateGUIMessage( "chat", "role", std::time(nullptr), "nick", nick, "oldrole", it->second.m_Role, "newrole", participant.role); } else { // Don't create a GUI message for regular presence changes, because // several hundreds of them accumulate during a match, impacting performance terribly and // the only way they are used is to determine whether to update the playerlist. } DbgXMPP( nick << " is in the room, " "presence: " << GetPresenceString(presence.presence()) << ", " "role: "<< GetRoleString(participant.role)); if (it == m_PlayerMap.end()) { m_PlayerMap.emplace( std::piecewise_construct, std::forward_as_tuple(nick), std::forward_as_tuple(presence.presence(), participant.role, std::string())); } else { it->second.m_Presence = presence.presence(); it->second.m_Role = participant.role; } } m_PlayerMapUpdate = true; } /** * Update local cache when subject changes. */ void XmppClient::handleMUCSubject(glooxwrapper::MUCRoom& UNUSED(room), const glooxwrapper::string& nick, const glooxwrapper::string& subject) { m_Subject = wstring_from_utf8(subject.to_string()); CreateGUIMessage( "chat", "subject", std::time(nullptr), "nick", nick, "subject", m_Subject); } /** * Get current subject. */ const std::wstring& XmppClient::GetSubject() { return m_Subject; } /** * Request nick change, real change via mucRoomHandler. * * @param nick Desired nickname */ void XmppClient::SetNick(const std::string& nick) { m_mucRoom->setNick(nick); } /** * Get current nickname. - * - * @param nick Variable to store the nickname in. */ -void XmppClient::GetNick(std::string& nick) +std::string XmppClient::GetNick() { - nick = m_mucRoom->nick().to_string(); + return m_mucRoom->nick().to_string(); } /** * Kick a player from the current room. * * @param nick Nickname to be kicked * @param reason Reason the player was kicked */ void XmppClient::kick(const std::string& nick, const std::string& reason) { m_mucRoom->kick(nick, reason); } /** * Ban a player from the current room. * * @param nick Nickname to be banned * @param reason Reason the player was banned */ void XmppClient::ban(const std::string& nick, const std::string& reason) { m_mucRoom->ban(nick, reason); } /** * Change the xmpp presence of the client. * * @param presence A string containing the desired presence */ void XmppClient::SetPresence(const std::string& presence) { #define IF(x,y) if (presence == x) m_mucRoom->setPresence(gloox::Presence::y) IF("available", Available); else IF("chat", Chat); else IF("away", Away); else IF("playing", DND); else IF("offline", Unavailable); // The others are not to be set #undef IF else LOGERROR("Unknown presence '%s'", presence.c_str()); } /** * Get the current xmpp presence of the given nick. */ const char* XmppClient::GetPresence(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return "offline"; return GetPresenceString(it->second.m_Presence); } /** * Get the current xmpp role of the given nick. */ const char* XmppClient::GetRole(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return ""; return GetRoleString(it->second.m_Role); } /** * Get the most recent received rating of the given nick. * Notice that this doesn't request a rating profile if it hasn't been received yet. */ std::wstring XmppClient::GetRating(const std::string& nick) { const PlayerMap::iterator it = m_PlayerMap.find(nick); if (it == m_PlayerMap.end()) return std::wstring(); return wstring_from_utf8(it->second.m_Rating.to_string()); } /***************************************************** * Utilities * *****************************************************/ /** * Parse and return the timestamp of a historic chat message and return the current time for new chat messages. * Historic chat messages are implement as DelayedDelivers as specified in XEP-0203. * Hence, their timestamp MUST be in UTC and conform to the DateTime format XEP-0082. * * @returns Seconds since the epoch. */ std::time_t XmppClient::ComputeTimestamp(const glooxwrapper::Message& msg) { // Only historic messages contain a timestamp! if (!msg.when()) return std::time(nullptr); // The locale is irrelevant, because the XMPP date format doesn't contain written month names for (const std::string& format : std::vector{ "Y-M-d'T'H:m:sZ", "Y-M-d'T'H:m:s.SZ" }) { UDate dateTime = g_L10n.ParseDateTime(msg.when()->stamp().to_string(), format, icu::Locale::getUS()); if (dateTime) return dateTime / 1000.0; } return std::time(nullptr); } /** * Convert a gloox presence type to an untranslated string literal to be used as an identifier by the scripts. */ const char* XmppClient::GetPresenceString(const gloox::Presence::PresenceType presenceType) { switch (presenceType) { #define CASE(X,Y) case gloox::Presence::X: return Y CASE(Available, "available"); CASE(Chat, "chat"); CASE(Away, "away"); CASE(DND, "playing"); CASE(XA, "away"); CASE(Unavailable, "offline"); CASE(Probe, "probe"); CASE(Error, "error"); CASE(Invalid, "invalid"); default: LOGERROR("Unknown presence type '%d'", static_cast(presenceType)); return ""; #undef CASE } } /** * Convert a gloox role type to an untranslated string literal to be used as an identifier by the scripts. */ const char* XmppClient::GetRoleString(const gloox::MUCRoomRole role) { switch (role) { #define CASE(X, Y) case gloox::X: return Y CASE(RoleNone, "none"); CASE(RoleVisitor, "visitor"); CASE(RoleParticipant, "participant"); CASE(RoleModerator, "moderator"); CASE(RoleInvalid, "invalid"); default: LOGERROR("Unknown role type '%d'", static_cast(role)); return ""; #undef CASE } } /** * Translates a gloox certificate error codes, i.e. gloox certificate statuses except CertOk. * Keep in sync with specifications. */ std::string XmppClient::CertificateErrorToString(gloox::CertStatus status) { std::map certificateErrorStrings = { { gloox::CertInvalid, g_L10n.Translate("The certificate is not trusted.") }, { gloox::CertSignerUnknown, g_L10n.Translate("The certificate hasn't got a known issuer.") }, { gloox::CertRevoked, g_L10n.Translate("The certificate has been revoked.") }, { gloox::CertExpired, g_L10n.Translate("The certificate has expired.") }, { gloox::CertNotActive, g_L10n.Translate("The certificate is not yet active.") }, { gloox::CertWrongPeer, g_L10n.Translate("The certificate has not been issued for the peer connected to.") }, { gloox::CertSignerNotCa, g_L10n.Translate("The certificate signer is not a certificate authority.") } }; std::string result; for (std::map::iterator it = certificateErrorStrings.begin(); it != certificateErrorStrings.end(); ++it) if (status & it->first) result += "\n" + it->second; return result; } /** * Convert a gloox stanza error type to string. * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::StanzaErrorToString(gloox::StanzaError err) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(StanzaErrorUndefined, g_L10n.Translate("No error")); DEBUG_CASE(StanzaErrorBadRequest, "Server received malformed XML"); CASE(StanzaErrorConflict, g_L10n.Translate("Player already logged in")); DEBUG_CASE(StanzaErrorFeatureNotImplemented, "Server does not implement requested feature"); CASE(StanzaErrorForbidden, g_L10n.Translate("Forbidden")); DEBUG_CASE(StanzaErrorGone, "Unable to find message receipiant"); CASE(StanzaErrorInternalServerError, g_L10n.Translate("Internal server error")); DEBUG_CASE(StanzaErrorItemNotFound, "Message receipiant does not exist"); DEBUG_CASE(StanzaErrorJidMalformed, "JID (XMPP address) malformed"); DEBUG_CASE(StanzaErrorNotAcceptable, "Receipiant refused message. Possible policy issue"); CASE(StanzaErrorNotAllowed, g_L10n.Translate("Not allowed")); CASE(StanzaErrorNotAuthorized, g_L10n.Translate("Not authorized")); DEBUG_CASE(StanzaErrorNotModified, "Requested item has not changed since last request"); DEBUG_CASE(StanzaErrorPaymentRequired, "This server requires payment"); CASE(StanzaErrorRecipientUnavailable, g_L10n.Translate("Recipient temporarily unavailable")); DEBUG_CASE(StanzaErrorRedirect, "Request redirected"); CASE(StanzaErrorRegistrationRequired, g_L10n.Translate("Registration required")); DEBUG_CASE(StanzaErrorRemoteServerNotFound, "Remote server not found"); DEBUG_CASE(StanzaErrorRemoteServerTimeout, "Remote server timed out"); DEBUG_CASE(StanzaErrorResourceConstraint, "The recipient is unable to process the message due to resource constraints"); CASE(StanzaErrorServiceUnavailable, g_L10n.Translate("Service unavailable")); DEBUG_CASE(StanzaErrorSubscribtionRequired, "Service requires subscription"); DEBUG_CASE(StanzaErrorUnexpectedRequest, "Attempt to send from invalid stanza address"); DEBUG_CASE(StanzaErrorUnknownSender, "Invalid 'from' address"); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox connection error enum to string * Keep in sync with Gloox documentation * * @param err Error to be converted * @return Converted error string */ std::string XmppClient::ConnectionErrorToString(gloox::ConnectionError err) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (err) { CASE(ConnNoError, g_L10n.Translate("No error")); CASE(ConnStreamError, g_L10n.Translate("Stream error")); CASE(ConnStreamVersionError, g_L10n.Translate("The incoming stream version is unsupported")); CASE(ConnStreamClosed, g_L10n.Translate("The stream has been closed by the server")); DEBUG_CASE(ConnProxyAuthRequired, "The HTTP/SOCKS5 proxy requires authentication"); DEBUG_CASE(ConnProxyAuthFailed, "HTTP/SOCKS5 proxy authentication failed"); DEBUG_CASE(ConnProxyNoSupportedAuth, "The HTTP/SOCKS5 proxy requires an unsupported authentication mechanism"); CASE(ConnIoError, g_L10n.Translate("An I/O error occurred")); DEBUG_CASE(ConnParseError, "An XML parse error occurred"); CASE(ConnConnectionRefused, g_L10n.Translate("The connection was refused by the server")); CASE(ConnDnsError, g_L10n.Translate("Resolving the server's hostname failed")); CASE(ConnOutOfMemory, g_L10n.Translate("This system is out of memory")); DEBUG_CASE(ConnNoSupportedAuth, "The authentication mechanisms the server offered are not supported or no authentication mechanisms were available"); CASE(ConnTlsFailed, g_L10n.Translate("The server's certificate could not be verified or the TLS handshake did not complete successfully")); CASE(ConnTlsNotAvailable, g_L10n.Translate("The server did not offer required TLS encryption")); DEBUG_CASE(ConnCompressionFailed, "Negotiation/initializing compression failed"); CASE(ConnAuthenticationFailed, g_L10n.Translate("Authentication failed. Incorrect password or account does not exist")); CASE(ConnUserDisconnected, g_L10n.Translate("The user or system requested a disconnect")); CASE(ConnNotConnected, g_L10n.Translate("There is no active connection")); default: return g_L10n.Translate("Unknown error"); } #undef DEBUG_CASE #undef CASE } /** * Convert a gloox registration result enum to string * Keep in sync with Gloox documentation * * @param err Enum to be converted * @return Converted string */ std::string XmppClient::RegistrationResultToString(gloox::RegistrationResult res) { #define CASE(X, Y) case gloox::X: return Y #define DEBUG_CASE(X, Y) case gloox::X: return g_L10n.Translate("Error") + " (" + Y + ")" switch (res) { CASE(RegistrationSuccess, g_L10n.Translate("Your account has been successfully registered")); CASE(RegistrationNotAcceptable, g_L10n.Translate("Not all necessary information provided")); CASE(RegistrationConflict, g_L10n.Translate("Username already exists")); DEBUG_CASE(RegistrationNotAuthorized, "Account removal timeout or insufficiently secure channel for password change"); DEBUG_CASE(RegistrationBadRequest, "Server received an incomplete request"); DEBUG_CASE(RegistrationForbidden, "Registration forbidden"); DEBUG_CASE(RegistrationRequired, "Account cannot be removed as it does not exist"); DEBUG_CASE(RegistrationUnexpectedRequest, "This client is unregistered with the server"); DEBUG_CASE(RegistrationNotAllowed, "Server does not permit password changes"); default: return ""; } #undef DEBUG_CASE #undef CASE } void XmppClient::SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJIDStr) { DbgXMPP("SendStunEndpointToHost " << hostJIDStr); char ipStr[256] = "(error)"; ENetAddress addr; addr.host = ntohl(stunEndpoint.ip); enet_address_get_host_ip(&addr, ipStr, ARRAY_SIZE(ipStr)); glooxwrapper::JID hostJID(hostJIDStr); glooxwrapper::Jingle::Session session = m_sessionManager->createSession(hostJID); session.sessionInitiate(ipStr, stunEndpoint.port); } void XmppClient::handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle) { if (action == gloox::Jingle::SessionInitiate) handleSessionInitiation(session, jingle); } void XmppClient::handleSessionInitiation(glooxwrapper::Jingle::Session& UNUSED(session), const glooxwrapper::Jingle::Session::Jingle& jingle) { glooxwrapper::Jingle::ICEUDP::Candidate candidate = jingle.getCandidate(); if (candidate.ip.empty()) { LOGERROR("Failed to retrieve Jingle candidate"); return; } if (!g_NetServer) { LOGERROR("Received STUN connection request, but not hosting currently!"); return; } g_NetServer->SendHolePunchingMessage(candidate.ip.to_string(), candidate.port); } Index: ps/trunk/source/lobby/XmppClient.h =================================================================== --- ps/trunk/source/lobby/XmppClient.h (revision 24982) +++ ps/trunk/source/lobby/XmppClient.h (revision 24983) @@ -1,206 +1,206 @@ /* Copyright (C) 2021 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 XXXMPPCLIENT_H #define XXXMPPCLIENT_H #include "IXmppClient.h" #include "glooxwrapper/glooxwrapper.h" #include #include #include #include class ScriptInterface; namespace glooxwrapper { class Client; struct CertInfo; } class XmppClient : public IXmppClient, public glooxwrapper::ConnectionListener, public glooxwrapper::MUCRoomHandler, public glooxwrapper::IqHandler, public glooxwrapper::RegistrationHandler, public glooxwrapper::MessageHandler, public glooxwrapper::Jingle::SessionHandler { NONCOPYABLE(XmppClient); private: // Components glooxwrapper::Client* m_client; glooxwrapper::MUCRoom* m_mucRoom; glooxwrapper::Registration* m_registration; glooxwrapper::SessionManager* m_sessionManager; // Account infos std::string m_username; std::string m_password; std::string m_server; std::string m_room; std::string m_nick; std::string m_xpartamuppId; std::string m_echelonId; // Security std::string m_connectionDataJid; std::string m_connectionDataIqId; // State gloox::CertStatus m_certStatus; bool m_initialLoadComplete; bool m_isConnected; public: // Basic XmppClient(const ScriptInterface* scriptInterface, const std::string& sUsername, const std::string& sPassword, const std::string& sRoom, const std::string& sNick, const int historyRequestSize = 0, const bool regOpt = false); virtual ~XmppClient(); // JS::Heap is better for GC performance than JS::PersistentRooted static void Trace(JSTracer *trc, void *data) { static_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc); // Network void connect(); void disconnect(); bool isConnected(); void recv(); void SendIqGetBoardList(); void SendIqGetProfile(const std::string& player); void SendIqGameReport(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqRegisterGame(const ScriptInterface& scriptInterface, JS::HandleValue data); void SendIqGetConnectionData(const std::string& jid, const std::string& password); void SendIqUnregisterGame(); void SendIqChangeStateGame(const std::string& nbp, const std::string& players); void SendIqLobbyAuth(const std::string& to, const std::string& token); void SetNick(const std::string& nick); - void GetNick(std::string& nick); + std::string GetNick(); void kick(const std::string& nick, const std::string& reason); void ban(const std::string& nick, const std::string& reason); void SetPresence(const std::string& presence); const char* GetPresence(const std::string& nickname); const char* GetRole(const std::string& nickname); std::wstring GetRating(const std::string& nickname); const std::wstring& GetSubject(); - void GUIGetPlayerList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); - void GUIGetGameList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); - void GUIGetBoardList(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); - void GUIGetProfile(const ScriptInterface& scriptInterface, JS::MutableHandleValue ret); + JS::Value GUIGetPlayerList(const ScriptInterface& scriptInterface); + JS::Value GUIGetGameList(const ScriptInterface& scriptInterface); + JS::Value GUIGetBoardList(const ScriptInterface& scriptInterface); + JS::Value GUIGetProfile(const ScriptInterface& scriptInterface); void SendStunEndpointToHost(const StunClient::StunEndpoint& stunEndpoint, const std::string& hostJID); /** * Convert gloox values to string or time. */ static const char* GetPresenceString(const gloox::Presence::PresenceType presenceType); static const char* GetRoleString(const gloox::MUCRoomRole role); static std::string StanzaErrorToString(gloox::StanzaError err); static std::string RegistrationResultToString(gloox::RegistrationResult res); static std::string ConnectionErrorToString(gloox::ConnectionError err); static std::string CertificateErrorToString(gloox::CertStatus status); static std::time_t ComputeTimestamp(const glooxwrapper::Message& msg); protected: /* Xmpp handlers */ /* MUC handlers */ virtual void handleMUCParticipantPresence(glooxwrapper::MUCRoom& room, const glooxwrapper::MUCRoomParticipant, const glooxwrapper::Presence&); virtual void handleMUCError(glooxwrapper::MUCRoom& room, gloox::StanzaError); virtual void handleMUCMessage(glooxwrapper::MUCRoom& room, const glooxwrapper::Message& msg, bool priv); virtual void handleMUCSubject(glooxwrapper::MUCRoom& room, const glooxwrapper::string& nick, const glooxwrapper::string& subject); /* MUC handlers not supported by glooxwrapper */ // virtual bool handleMUCRoomCreation(glooxwrapper::MUCRoom*) {return false;} // virtual void handleMUCInviteDecline(glooxwrapper::MUCRoom*, const glooxwrapper::JID&, const std::string&) {} // virtual void handleMUCInfo(glooxwrapper::MUCRoom*, int, const std::string&, const glooxwrapper::DataForm*) {} // virtual void handleMUCItems(glooxwrapper::MUCRoom*, const std::list >&) {} /* Log handler */ virtual void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message); /* ConnectionListener handlers*/ virtual void onConnect(); virtual void onDisconnect(gloox::ConnectionError e); virtual bool onTLSConnect(const glooxwrapper::CertInfo& info); /* Iq Handlers */ virtual bool handleIq(const glooxwrapper::IQ& iq); virtual void handleIqID(const glooxwrapper::IQ&, int) {} /* Registration Handlers */ virtual void handleRegistrationFields(const glooxwrapper::JID& /*from*/, int fields, glooxwrapper::string instructions ); virtual void handleRegistrationResult(const glooxwrapper::JID& /*from*/, gloox::RegistrationResult result); virtual void handleAlreadyRegistered(const glooxwrapper::JID& /*from*/); virtual void handleDataForm(const glooxwrapper::JID& /*from*/, const glooxwrapper::DataForm& /*form*/); virtual void handleOOB(const glooxwrapper::JID& /*from*/, const glooxwrapper::OOB& oob); /* Message Handler */ virtual void handleMessage(const glooxwrapper::Message& msg, glooxwrapper::MessageSession* session); /* Session Handler */ virtual void handleSessionAction(gloox::Jingle::Action action, glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle); virtual void handleSessionInitiation(glooxwrapper::Jingle::Session& session, const glooxwrapper::Jingle::Session::Jingle& jingle); public: JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface); JS::Value GuiPollHistoricMessages(const ScriptInterface& scriptInterface); bool GuiPollHasPlayerListUpdate(); void SendMUCMessage(const std::string& message); protected: template void CreateGUIMessage( const std::string& type, const std::string& level, const std::time_t time, Args const&... args); private: struct SPlayer { SPlayer(const gloox::Presence::PresenceType presence, const gloox::MUCRoomRole role, const glooxwrapper::string& rating) : m_Presence(presence), m_Role(role), m_Rating(rating) { } gloox::Presence::PresenceType m_Presence; gloox::MUCRoomRole m_Role; glooxwrapper::string m_Rating; }; using PlayerMap = std::map; /// Map of players PlayerMap m_PlayerMap; /// Whether or not the playermap has changed since the last time the GUI checked. bool m_PlayerMapUpdate; /// List of games std::vector m_GameList; /// List of rankings std::vector m_BoardList; /// Profile data std::vector m_Profile; /// ScriptInterface to root the values const ScriptInterface* m_ScriptInterface; /// Queue of messages for the GUI std::deque > m_GuiMessageQueue; /// Cache of all GUI messages received since the login std::vector > m_HistoricGuiMessages; /// Current room subject/topic. std::wstring m_Subject; }; #endif // XMPPCLIENT_H Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 24982) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.cpp (revision 24983) @@ -1,520 +1,232 @@ /* 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_Lobby.h" #include "gui/GUIManager.h" #include "lib/utf8.h" #include "lobby/IXmppClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Util.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "third_party/encryption/pkcs5_pbkdf2.h" #include -void JSI_Lobby::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +namespace JSI_Lobby { - // Lobby functions - scriptInterface.RegisterFunction("HasXmppClient"); - scriptInterface.RegisterFunction("SetRankedGame"); -#if CONFIG2_LOBBY // Allow the lobby to be disabled - scriptInterface.RegisterFunction("StartXmppClient"); - scriptInterface.RegisterFunction("StartRegisterXmppClient"); - scriptInterface.RegisterFunction("StopXmppClient"); - scriptInterface.RegisterFunction("ConnectXmppClient"); - scriptInterface.RegisterFunction("DisconnectXmppClient"); - scriptInterface.RegisterFunction("IsXmppClientConnected"); - scriptInterface.RegisterFunction("SendGetBoardList"); - scriptInterface.RegisterFunction("SendGetProfile"); - scriptInterface.RegisterFunction("SendRegisterGame"); - scriptInterface.RegisterFunction("SendGameReport"); - scriptInterface.RegisterFunction("SendUnregisterGame"); - scriptInterface.RegisterFunction("SendChangeStateGame"); - scriptInterface.RegisterFunction("GetPlayerList"); - scriptInterface.RegisterFunction("GetGameList"); - scriptInterface.RegisterFunction("GetBoardList"); - scriptInterface.RegisterFunction("GetProfile"); - scriptInterface.RegisterFunction("LobbyGuiPollNewMessages"); - scriptInterface.RegisterFunction("LobbyGuiPollHistoricMessages"); - scriptInterface.RegisterFunction("LobbyGuiPollHasPlayerListUpdate"); - scriptInterface.RegisterFunction("LobbySendMessage"); - scriptInterface.RegisterFunction("LobbySetPlayerPresence"); - scriptInterface.RegisterFunction("LobbySetNick"); - scriptInterface.RegisterFunction("LobbyGetNick"); - scriptInterface.RegisterFunction("LobbyKick"); - scriptInterface.RegisterFunction("LobbyBan"); - scriptInterface.RegisterFunction("LobbyGetPlayerPresence"); - scriptInterface.RegisterFunction("LobbyGetPlayerRole"); - scriptInterface.RegisterFunction("LobbyGetPlayerRating"); - scriptInterface.RegisterFunction("EncryptPassword"); - scriptInterface.RegisterFunction("LobbyGetRoomSubject"); -#endif // CONFIG2_LOBBY -} - -bool JSI_Lobby::HasXmppClient(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool HasXmppClient() { return g_XmppClient; } -void JSI_Lobby::SetRankedGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool isRanked) +void SetRankedGame(bool isRanked) { g_rankedGame = isRanked; } #if CONFIG2_LOBBY -void JSI_Lobby::StartXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize) +void StartXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize) { if (g_XmppClient) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Cannot call StartXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), utf8_from_wstring(room), utf8_from_wstring(nick), historyRequestSize); g_rankedGame = true; } -void JSI_Lobby::StartRegisterXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& username, const std::wstring& password) +void StartRegisterXmppClient(const ScriptRequest& rq, const std::wstring& username, const std::wstring& password) { if (g_XmppClient) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Cannot call StartRegisterXmppClient with an already initialized XmppClient!"); return; } g_XmppClient = IXmppClient::create( g_GUI->GetScriptInterface().get(), utf8_from_wstring(username), utf8_from_wstring(password), std::string(), std::string(), 0, true); } -void JSI_Lobby::StopXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate) +void StopXmppClient(const ScriptRequest& rq) { if (!g_XmppClient) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Cannot call StopXmppClient without an initialized XmppClient!"); return; } SAFE_DELETE(g_XmppClient); g_rankedGame = false; } -void JSI_Lobby::ConnectXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call ConnectXmppClient without an initialized XmppClient!"); - return; - } - - g_XmppClient->connect(); -} - -void JSI_Lobby::DisconnectXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call DisconnectXmppClient without an initialized XmppClient!"); - return; - } - - g_XmppClient->disconnect(); -} +//////////////////////////////////////////////// +//////////////////////////////////////////////// -bool JSI_Lobby::IsXmppClientConnected(ScriptInterface::CmptPrivate* pCmptPrivate) +IXmppClient* XmppGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_XmppClient) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call IsXmppClientConnected without an initialized XmppClient!"); - return false; + LOGERROR("Cannot use XMPPClient functions without an initialized XmppClient!"); + return nullptr; } - - return g_XmppClient->isConnected(); -} - -void JSI_Lobby::SendGetBoardList(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call SendGetBoardList without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendIqGetBoardList(); -} - -void JSI_Lobby::SendGetProfile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& player) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call SendGetProfile without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendIqGetProfile(utf8_from_wstring(player)); -} - -void JSI_Lobby::SendGameReport(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call SendGameReport without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendIqGameReport(*(pCmptPrivate->pScriptInterface), data); + return g_XmppClient; } -void JSI_Lobby::SendRegisterGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) +void SendRegisterGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data) { if (!g_XmppClient) { ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Cannot call SendRegisterGame without an initialized XmppClient!"); return; } // Prevent JS mods to register matches in the lobby that were started with lobby authentication disabled if (!g_NetServer || !g_NetServer->UseLobbyAuth()) { LOGERROR("Registering games in the lobby requires lobby authentication to be enabled!"); return; } g_XmppClient->SendIqRegisterGame(*(pCmptPrivate->pScriptInterface), data); } -void JSI_Lobby::SendUnregisterGame(ScriptInterface::CmptPrivate* pCmptPrivate) +// Unlike other functions, this one just returns Undefined if XmppClient isn't initialised. +JS::Value GuiPollNewMessages(const ScriptInterface& scriptInterface) { if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call SendUnregisterGame without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendIqUnregisterGame(); -} - -void JSI_Lobby::SendChangeStateGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nbp, const std::wstring& players) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call SendChangeStateGame without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendIqChangeStateGame(utf8_from_wstring(nbp), utf8_from_wstring(players)); -} - -JS::Value JSI_Lobby::GetPlayerList(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - ScriptRequest rq(pCmptPrivate->pScriptInterface); - - if (!g_XmppClient) - { - ScriptException::Raise(rq, "Cannot call GetPlayerList without an initialized XmppClient!"); return JS::UndefinedValue(); - } - JS::RootedValue playerList(rq.cx); - g_XmppClient->GUIGetPlayerList(*(pCmptPrivate->pScriptInterface), &playerList); - - return playerList; + return g_XmppClient->GuiPollNewMessages(scriptInterface); } -JS::Value JSI_Lobby::GetGameList(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - ScriptRequest rq(pCmptPrivate->pScriptInterface); - - if (!g_XmppClient) - { - ScriptException::Raise(rq, "Cannot call GetGameList without an initialized XmppClient!"); - return JS::UndefinedValue(); - } - - JS::RootedValue gameList(rq.cx); - g_XmppClient->GUIGetGameList(*(pCmptPrivate->pScriptInterface), &gameList); - - return gameList; -} - -JS::Value JSI_Lobby::GetBoardList(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - ScriptRequest rq(pCmptPrivate->pScriptInterface); - - if (!g_XmppClient) - { - ScriptException::Raise(rq, "Cannot call GetBoardList without an initialized XmppClient!"); - return JS::UndefinedValue(); - } - - JS::RootedValue boardList(rq.cx); - g_XmppClient->GUIGetBoardList(*(pCmptPrivate->pScriptInterface), &boardList); - - return boardList; -} - -JS::Value JSI_Lobby::GetProfile(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - ScriptRequest rq(pCmptPrivate->pScriptInterface); - - if (!g_XmppClient) - { - ScriptException::Raise(rq, "Cannot call GetProfile without an initialized XmppClient!"); - return JS::UndefinedValue(); - } - - JS::RootedValue profileFetch(rq.cx); - g_XmppClient->GUIGetProfile(*(pCmptPrivate->pScriptInterface), &profileFetch); - - return profileFetch; -} - -bool JSI_Lobby::LobbyGuiPollHasPlayerListUpdate(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGuiPollHasPlayerListUpdate without an initialized XmppClient!"); - return false; - } - - return g_XmppClient->GuiPollHasPlayerListUpdate(); -} - -JS::Value JSI_Lobby::LobbyGuiPollNewMessages(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - return JS::UndefinedValue(); - - return g_XmppClient->GuiPollNewMessages(*(pCmptPrivate->pScriptInterface)); -} - -JS::Value JSI_Lobby::LobbyGuiPollHistoricMessages(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGuiPollHistoricMessages without an initialized XmppClient!"); - return JS::UndefinedValue(); - } - - return g_XmppClient->GuiPollHistoricMessages(*(pCmptPrivate->pScriptInterface)); -} - -void JSI_Lobby::LobbySendMessage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& message) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbySendMessage without an initialized XmppClient!"); - return; - } - - g_XmppClient->SendMUCMessage(utf8_from_wstring(message)); -} - -void JSI_Lobby::LobbySetPlayerPresence(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& presence) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbySetPlayerPresence without an initialized XmppClient!"); - return; - } - - g_XmppClient->SetPresence(utf8_from_wstring(presence)); -} - -void JSI_Lobby::LobbySetNick(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbySetNick without an initialized XmppClient!"); - return; - } - - g_XmppClient->SetNick(utf8_from_wstring(nick)); -} - -std::wstring JSI_Lobby::LobbyGetNick(ScriptInterface::CmptPrivate* pCmptPrivate) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGetNick without an initialized XmppClient!"); - return std::wstring(); - } - - std::string nick; - g_XmppClient->GetNick(nick); - return wstring_from_utf8(nick); -} - -void JSI_Lobby::LobbyKick(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick, const std::wstring& reason) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyKick without an initialized XmppClient!"); - return; - } - - g_XmppClient->kick(utf8_from_wstring(nick), utf8_from_wstring(reason)); -} - -void JSI_Lobby::LobbyBan(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick, const std::wstring& reason) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyBan without an initialized XmppClient!"); - return; - } - - g_XmppClient->ban(utf8_from_wstring(nick), utf8_from_wstring(reason)); -} - -const char* JSI_Lobby::LobbyGetPlayerPresence(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGetPlayerPresence without an initialized XmppClient!"); - return ""; - } - - return g_XmppClient->GetPresence(utf8_from_wstring(nickname)); -} - -const char* JSI_Lobby::LobbyGetPlayerRole(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGetPlayerRole without an initialized XmppClient!"); - return ""; - } - - return g_XmppClient->GetRole(utf8_from_wstring(nickname)); -} - -std::wstring JSI_Lobby::LobbyGetPlayerRating(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname) -{ - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGetPlayerRating without an initialized XmppClient!"); - return std::wstring(); - } - - return g_XmppClient->GetRating(utf8_from_wstring(nickname)); -} // Non-public secure PBKDF2 hash function with salting and 1,337 iterations // // TODO: We should use libsodium's crypto_pwhash instead of this. The first reason is that // libsodium doesn't propose a bare PBKDF2 hash in its API and it's too bad to rely on custom // code when we have a fully-fledged library available; the second reason is that Argon2 (the // default algorithm for crypto_pwhash) is better than what we use (and it's the default one // in the lib for a reason). // However changing the hashing method should be planned carefully, by trying to login with a // password hashed the old way, and, if successful, updating the password in the database using // the new hashing method. Dropping the old hashing code can only be done either by giving users // a way to reset their password, or by keeping track of successful password updates and dropping // old unused accounts after some time. -std::string JSI_Lobby::EncryptPassword(const std::string& password, const std::string& username) +std::string EncryptPassword(const std::string& password, const std::string& username) { ENSURE(sodium_init() >= 0); const int DIGESTSIZE = crypto_hash_sha256_BYTES; const int ITERATIONS = 1337; cassert(DIGESTSIZE == 32); static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 34, 35, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 133, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = {0}; crypto_hash_sha256_state state; crypto_hash_sha256_init(&state); crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); crypto_hash_sha256_update(&state, (unsigned char*)username.c_str(), username.length()); crypto_hash_sha256_final(&state, salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); } -std::wstring JSI_Lobby::EncryptPassword(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& pass, const std::wstring& user) -{ - return wstring_from_utf8(JSI_Lobby::EncryptPassword(utf8_from_wstring(pass), utf8_from_wstring(user))); -} +#endif -std::wstring JSI_Lobby::LobbyGetRoomSubject(ScriptInterface::CmptPrivate* pCmptPrivate) +void RegisterScriptFunctions(const ScriptRequest& rq) { - if (!g_XmppClient) - { - ScriptRequest rq(pCmptPrivate->pScriptInterface); - ScriptException::Raise(rq, "Cannot call LobbyGetRoomSubject without an initialized XmppClient!"); - return std::wstring(); - } + // Lobby functions + ScriptFunction::Register<&HasXmppClient>(rq, "HasXmppClient"); + ScriptFunction::Register<&SetRankedGame>(rq, "SetRankedGame"); +#if CONFIG2_LOBBY // Allow the lobby to be disabled + ScriptFunction::Register<&StartXmppClient>(rq, "StartXmppClient"); + ScriptFunction::Register<&StartRegisterXmppClient>(rq, "StartRegisterXmppClient"); + ScriptFunction::Register<&StopXmppClient>(rq, "StopXmppClient"); + +#define REGISTER_XMPP(func, name) \ + ScriptFunction::Register<&IXmppClient::func, &XmppGetter>(rq, name) + + REGISTER_XMPP(connect, "ConnectXmppClient"); + REGISTER_XMPP(disconnect, "DisconnectXmppClient"); + REGISTER_XMPP(isConnected, "IsXmppClientConnected"); + REGISTER_XMPP(SendIqGetBoardList, "SendGetBoardList"); + REGISTER_XMPP(SendIqGetProfile, "SendGetProfile"); + REGISTER_XMPP(SendIqGameReport, "SendGameReport"); + ScriptFunction::Register<&SendRegisterGame>(rq, "SendRegisterGame"); + REGISTER_XMPP(SendIqUnregisterGame, "SendUnregisterGame"); + REGISTER_XMPP(SendIqChangeStateGame, "SendChangeStateGame"); + REGISTER_XMPP(GUIGetPlayerList, "GetPlayerList"); + REGISTER_XMPP(GUIGetGameList, "GetGameList"); + REGISTER_XMPP(GUIGetBoardList, "GetBoardList"); + REGISTER_XMPP(GUIGetProfile, "GetProfile"); + + ScriptFunction::Register<&GuiPollNewMessages>(rq, "LobbyGuiPollNewMessages"); + REGISTER_XMPP(GuiPollHistoricMessages, "LobbyGuiPollHistoricMessages"); + REGISTER_XMPP(GuiPollHasPlayerListUpdate, "LobbyGuiPollHasPlayerListUpdate"); + REGISTER_XMPP(SendMUCMessage, "LobbySendMessage"); + REGISTER_XMPP(SetPresence, "LobbySetPlayerPresence"); + REGISTER_XMPP(SetNick, "LobbySetNick"); + REGISTER_XMPP(GetNick, "LobbyGetNick"); + REGISTER_XMPP(kick, "LobbyKick"); + REGISTER_XMPP(ban, "LobbyBan"); + REGISTER_XMPP(GetPresence, "LobbyGetPlayerPresence"); + REGISTER_XMPP(GetRole, "LobbyGetPlayerRole"); + REGISTER_XMPP(GetRating, "LobbyGetPlayerRating"); + REGISTER_XMPP(GetSubject, "LobbyGetRoomSubject"); +#undef REGISTER_XMPP - return g_XmppClient->GetSubject(); + ScriptFunction::Register<&EncryptPassword>(rq, "EncryptPassword"); +#endif // CONFIG2_LOBBY +} } - -#endif Index: ps/trunk/source/lobby/scripting/JSInterface_Lobby.h =================================================================== --- ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 24982) +++ ps/trunk/source/lobby/scripting/JSInterface_Lobby.h (revision 24983) @@ -1,73 +1,28 @@ /* Copyright (C) 2019 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_JSI_LOBBY #define INCLUDED_JSI_LOBBY -#include "lib/config2.h" -#include "scriptinterface/ScriptInterface.h" - -#include +class ScriptRequest; namespace JSI_Lobby { - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); - - bool HasXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate); - bool IsRankedGame(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetRankedGame(ScriptInterface::CmptPrivate* pCmptPrivate, bool isRanked); - -#if CONFIG2_LOBBY - void StartXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& username, const std::wstring& password, const std::wstring& room, const std::wstring& nick, int historyRequestSize); - void StartRegisterXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& username, const std::wstring& password); - void StopXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate); - void ConnectXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate); - void DisconnectXmppClient(ScriptInterface::CmptPrivate* pCmptPrivate); - bool IsXmppClientConnected(ScriptInterface::CmptPrivate* pCmptPrivate); - void SendGetBoardList(ScriptInterface::CmptPrivate* pCmptPrivate); - void SendGetProfile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& player); - void SendGameReport(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data); - void SendRegisterGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue data); - void SendUnregisterGame(ScriptInterface::CmptPrivate* pCmptPrivate); - void SendChangeStateGame(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nbp, const std::wstring& players); - JS::Value GetPlayerList(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetGameList(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetBoardList(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetProfile(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value LobbyGuiPollNewMessages(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value LobbyGuiPollHistoricMessages(ScriptInterface::CmptPrivate* pCmptPrivate); - bool LobbyGuiPollHasPlayerListUpdate(ScriptInterface::CmptPrivate* pCmptPrivate); - void LobbySendMessage(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& message); - void LobbySetPlayerPresence(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& presence); - void LobbySetNick(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick); - std::wstring LobbyGetNick(ScriptInterface::CmptPrivate* pCmptPrivate); - void LobbyKick(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick, const std::wstring& reason); - void LobbyBan(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nick, const std::wstring& reason); - const char* LobbyGetPlayerPresence(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname); - const char* LobbyGetPlayerRole(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname); - std::wstring LobbyGetPlayerRating(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& nickname); - std::wstring LobbyGetRoomSubject(ScriptInterface::CmptPrivate* pCmptPrivate); - - // Non-public secure PBKDF2 hash function with salting and 1,337 iterations - std::string EncryptPassword(const std::string& password, const std::string& username); - - // Public hash interface. - std::wstring EncryptPassword(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& pass, const std::wstring& user); -#endif // CONFIG2_LOBBY + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_LOBBY Index: ps/trunk/source/network/scripting/JSInterface_Network.cpp =================================================================== --- ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24982) +++ ps/trunk/source/network/scripting/JSInterface_Network.cpp (revision 24983) @@ -1,300 +1,305 @@ /* Copyright (C) 2021 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 "ps/GUID.h" #include "ps/Util.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "third_party/encryption/pkcs5_pbkdf2.h" -u16 JSI_Network::GetDefaultPort(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +namespace JSI_Network +{ +u16 GetDefaultPort() { return PS_DEFAULT_PORT; } -bool JSI_Network::IsNetController(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool IsNetController() { return !!g_NetClient && g_NetClient->IsController(); } -bool JSI_Network::HasNetServer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool HasNetServer() { return !!g_NetServer; } -bool JSI_Network::HasNetClient(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +bool HasNetClient() { return !!g_NetClient; } -CStr JSI_Network::HashPassword(const CStr& password) +CStr HashPassword(const CStr& password) { if (password.empty()) return password; ENSURE(sodium_init() >= 0); const int DIGESTSIZE = crypto_hash_sha256_BYTES; constexpr int ITERATIONS = 1737; cassert(DIGESTSIZE == 32); static const unsigned char salt_base[DIGESTSIZE] = { 244, 243, 249, 244, 32, 33, 19, 35, 16, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 32, 33, 244, 224, 127, 129, 130, 140, 153, 88, 123, 234, 123 }; // initialize the salt buffer unsigned char salt_buffer[DIGESTSIZE] = { 0 }; crypto_hash_sha256_state state; crypto_hash_sha256_init(&state); crypto_hash_sha256_update(&state, salt_base, sizeof(salt_base)); crypto_hash_sha256_final(&state, salt_buffer); // PBKDF2 to create the buffer unsigned char encrypted[DIGESTSIZE]; pbkdf2(encrypted, (unsigned char*)password.c_str(), password.length(), salt_buffer, DIGESTSIZE, ITERATIONS); return CStr(Hexify(encrypted, DIGESTSIZE)).UpperCase(); } -void JSI_Network::StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password) +void StartNetworkHost(const ScriptRequest& rq, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password) { 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. bool hasLobby = !!g_XmppClient; g_NetServer = new CNetServer(hasLobby); // In lobby, we send our public ip and port on request to the players, who want to connect. // In either case we need to know our public IP. If using STUN, we'll use that, // otherwise, the lobby's reponse to the game registration stanza will tell us our public IP. if (hasLobby) { CStr ip; if (!useSTUN) // Don't store IP - the lobby bot will send it later. // (if a client tries to connect before it's setup, they'll be disconnected) g_NetServer->SetConnectionData("", serverPort, false); else { u16 port = serverPort; // This is using port variable to store return value, do not pass serverPort itself. if (!StunClient::FindStunEndpointHost(ip, port)) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Failed to host via STUN."); SAFE_DELETE(g_NetServer); return; } g_NetServer->SetConnectionData(ip, port, true); } } if (!g_NetServer->SetupConnection(serverPort)) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Failed to start server"); SAFE_DELETE(g_NetServer); return; } // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); // We will get hashed password from clients, so hash it once for server CStr hashedPass = HashPassword(password); g_NetServer->SetPassword(hashedPass); g_NetServer->SetControllerSecret(secret); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostLobbyName); g_NetClient->SetGamePassword(hashedPass); g_NetClient->SetupServerData("127.0.0.1", serverPort, false); g_NetClient->SetControllerSecret(secret); if (!g_NetClient->SetupConnection(nullptr)) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } -void JSI_Network::StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID) +void StartNetworkJoin(const ScriptRequest& rq, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID) { ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@"))); g_NetClient->SetupServerData(serverAddress, serverPort, useSTUN); if (!g_NetClient->SetupConnection(nullptr)) { - ScriptRequest rq(pCmptPrivate->pScriptInterface); ScriptException::Raise(rq, "Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } -void JSI_Network::StartNetworkJoinLobby(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& playerName, const CStr& hostJID, const CStr& password) +/** + * Requires XmppClient to send iq request to the server to get server's ip and port based on passed password. + * This is needed to not force server to share it's public ip with all potential clients in the lobby. + * XmppClient will also handle logic after receiving the answer. + */ +void StartNetworkJoinLobby(const CStrW& playerName, const CStr& hostJID, const CStr& password) { ENSURE(!!g_XmppClient); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); CStr hashedPass = HashPassword(password); g_Game = new CGame(true); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); g_NetClient->SetHostingPlayerName(hostJID.substr(0, hostJID.find("@"))); g_NetClient->SetGamePassword(hashedPass); g_XmppClient->SendIqGetConnectionData(hostJID, hashedPass.c_str()); } -void JSI_Network::DisconnectNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void DisconnectNetworkGame() { // 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::CmptPrivate* UNUSED(pCmptPrivate)) +CStr GetPlayerGUID() { if (!g_NetClient) return "local"; return g_NetClient->GetGUID(); } -JS::Value JSI_Network::PollNetworkClient(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value PollNetworkClient(const ScriptInterface& scriptInterface) { if (!g_NetClient) return JS::UndefinedValue(); // Convert from net client context to GUI script context ScriptRequest rqNet(g_NetClient->GetScriptInterface()); JS::RootedValue pollNet(rqNet.cx); g_NetClient->GuiPoll(&pollNet); - return pCmptPrivate->pScriptInterface->CloneValueFromOtherCompartment(g_NetClient->GetScriptInterface(), pollNet); + return scriptInterface.CloneValueFromOtherCompartment(g_NetClient->GetScriptInterface(), pollNet); } -void JSI_Network::SetNetworkGameAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs1) +void SetNetworkGameAttributes(const ScriptInterface& scriptInterface, 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). - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx, attribs1); - g_NetClient->SendGameSetupMessage(&attribs, *(pCmptPrivate->pScriptInterface)); + g_NetClient->SendGameSetupMessage(&attribs, scriptInterface); } -void JSI_Network::AssignNetworkPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int playerID, const CStr& guid) +void AssignNetworkPlayer(int playerID, const CStr& guid) { ENSURE(g_NetClient); g_NetClient->SendAssignPlayerMessage(playerID, guid); } -void JSI_Network::KickPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& playerName, bool ban) +void KickPlayer(const CStrW& playerName, bool ban) { ENSURE(g_NetClient); g_NetClient->SendKickPlayerMessage(playerName, ban); } -void JSI_Network::SendNetworkChat(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& message) +void SendNetworkChat(const CStrW& message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } -void JSI_Network::SendNetworkReady(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int message) +void SendNetworkReady(int message) { ENSURE(g_NetClient); g_NetClient->SendReadyMessage(message); } -void JSI_Network::ClearAllPlayerReady (ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void ClearAllPlayerReady () { ENSURE(g_NetClient); g_NetClient->SendClearAllReadyMessage(); } -void JSI_Network::StartNetworkGame(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void StartNetworkGame() { ENSURE(g_NetClient); g_NetClient->SendStartGameMessage(); } -void JSI_Network::SetTurnLength(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int length) +void SetTurnLength(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) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GetDefaultPort"); - scriptInterface.RegisterFunction("IsNetController"); - scriptInterface.RegisterFunction("HasNetServer"); - scriptInterface.RegisterFunction("HasNetClient"); - scriptInterface.RegisterFunction("StartNetworkHost"); - scriptInterface.RegisterFunction("StartNetworkJoin"); - scriptInterface.RegisterFunction("StartNetworkJoinLobby"); - 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"); + ScriptFunction::Register<&GetDefaultPort>(rq, "GetDefaultPort"); + ScriptFunction::Register<&IsNetController>(rq, "IsNetController"); + ScriptFunction::Register<&HasNetServer>(rq, "HasNetServer"); + ScriptFunction::Register<&HasNetClient>(rq, "HasNetClient"); + ScriptFunction::Register<&StartNetworkHost>(rq, "StartNetworkHost"); + ScriptFunction::Register<&StartNetworkJoin>(rq, "StartNetworkJoin"); + ScriptFunction::Register<&StartNetworkJoinLobby>(rq, "StartNetworkJoinLobby"); + ScriptFunction::Register<&DisconnectNetworkGame>(rq, "DisconnectNetworkGame"); + ScriptFunction::Register<&GetPlayerGUID>(rq, "GetPlayerGUID"); + ScriptFunction::Register<&PollNetworkClient>(rq, "PollNetworkClient"); + ScriptFunction::Register<&SetNetworkGameAttributes>(rq, "SetNetworkGameAttributes"); + ScriptFunction::Register<&AssignNetworkPlayer>(rq, "AssignNetworkPlayer"); + ScriptFunction::Register<&KickPlayer>(rq, "KickPlayer"); + ScriptFunction::Register<&SendNetworkChat>(rq, "SendNetworkChat"); + ScriptFunction::Register<&SendNetworkReady>(rq, "SendNetworkReady"); + ScriptFunction::Register<&ClearAllPlayerReady>(rq, "ClearAllPlayerReady"); + ScriptFunction::Register<&StartNetworkGame>(rq, "StartNetworkGame"); + ScriptFunction::Register<&SetTurnLength>(rq, "SetTurnLength"); +} } Index: ps/trunk/source/network/scripting/JSInterface_Network.h =================================================================== --- ps/trunk/source/network/scripting/JSInterface_Network.h (revision 24982) +++ ps/trunk/source/network/scripting/JSInterface_Network.h (revision 24983) @@ -1,55 +1,28 @@ /* Copyright (C) 2021 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_JSI_NETWORK #define INCLUDED_JSI_NETWORK -#include "lib/types.h" -#include "ps/CStr.h" -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Network { - u16 GetDefaultPort(ScriptInterface::CmptPrivate* pCmptPrivate); - bool IsNetController(ScriptInterface::CmptPrivate* pCmptPrivate); - bool HasNetServer(ScriptInterface::CmptPrivate* pCmptPrivate); - bool HasNetClient(ScriptInterface::CmptPrivate* pCmptPrivate); - void StartNetworkGame(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetNetworkGameAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs1); - void StartNetworkHost(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const u16 serverPort, const CStr& hostLobbyName, bool useSTUN, const CStr& password); - void StartNetworkJoin(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& serverAddress, u16 serverPort, bool useSTUN, const CStr& hostJID); - /** - * Requires XmppClient to send iq request to the server to get server's ip and port based on passed password. - * This is needed to not force server to share it's public ip with all potential clients in the lobby. - * XmppClient will also handle logic after receiving the answer. - */ - void StartNetworkJoinLobby(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, const CStr& hostJID, const CStr& password); - void DisconnectNetworkGame(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value PollNetworkClient(ScriptInterface::CmptPrivate* pCmptPrivate); - CStr GetPlayerGUID(ScriptInterface::CmptPrivate* pCmptPrivate); - void KickPlayer(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& playerName, bool ban); - void AssignNetworkPlayer(ScriptInterface::CmptPrivate* pCmptPrivate, int playerID, const CStr& guid); - void ClearAllPlayerReady (ScriptInterface::CmptPrivate* pCmptPrivate); - void SendNetworkChat(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& message); - void SendNetworkReady(ScriptInterface::CmptPrivate* pCmptPrivate, int message); - void SetTurnLength(ScriptInterface::CmptPrivate* pCmptPrivate, int length); - - CStr HashPassword(const CStr& password); - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_NETWORK Index: ps/trunk/source/ps/ModIo.cpp =================================================================== --- ps/trunk/source/ps/ModIo.cpp (revision 24982) +++ ps/trunk/source/ps/ModIo.cpp (revision 24983) @@ -1,854 +1,854 @@ /* 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) +void ModIo::StartDownloadMod(u32 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; if (message->data.result == CURLE_OK) continue; std::string error = fmt::sprintf( g_L10n.Translate("Download failure. Server response: %s; %s."), curl_easy_strerror(message->data.result), 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_ScriptContext, 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; ScriptRequest rq(scriptInterface); 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(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(rq.cx, dataVal.toObjectOrNull()); u32 length; bool isArray; 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(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(rq.cx, &first.toObject()); bool hasIdProperty; if (!JS_HasProperty(rq.cx, firstObj, "id", &hasIdProperty) || !hasIdProperty) FAIL("No id property in first element."); 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(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(); ScriptRequest rq(scriptInterface); 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(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(rq.cx, dataVal.toObjectOrNull()); u32 length; bool isArray; 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(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(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(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(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(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(rq, modFile, "metadata_blob", metadata_blob, true)) INVALIDATE_DATA_AND_CONTINUE("Failed to get metadata_blob from modFile."); 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(rq, metadata, "dependencies", data.dependencies, true)) INVALIDATE_DATA_AND_CONTINUE("Failed to get dependencies from metadata_blob."); std::vector minisigs; 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/ModIo.h =================================================================== --- ps/trunk/source/ps/ModIo.h (revision 24982) +++ ps/trunk/source/ps/ModIo.h (revision 24983) @@ -1,209 +1,209 @@ /* Copyright (C) 2019 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. */ #ifndef INCLUDED_MODIO #define INCLUDED_MODIO #include "lib/external_libraries/curl.h" #include "lib/os_path.h" #include "scriptinterface/ScriptInterface.h" #include #include // TODO: Allocate instance of the below two using sodium_malloc? struct PKStruct { unsigned char sig_alg[2] = {}; // == "Ed" unsigned char keynum[8] = {}; // should match the keynum in the sigstruct, else this is the wrong key unsigned char pk[crypto_sign_PUBLICKEYBYTES] = {}; }; struct SigStruct { unsigned char sig_alg[2] = {}; // "ED" (since we only support the hashed mode) unsigned char keynum[8] = {}; // should match the keynum in the PKStruct unsigned char sig[crypto_sign_BYTES] = {}; }; struct ModIoModData { std::map properties; std::vector dependencies; SigStruct sig; }; enum class DownloadProgressStatus { NONE, // Default state GAMEID, // The game ID is being downloaded READY, // The game ID has been downloaded LISTING, // The mod list is being downloaded LISTED, // The mod list has been downloaded DOWNLOADING, // A mod file is being downloaded SUCCESS, // A mod file has been downloaded FAILED_GAMEID, // Game ID couldn't be retrieved FAILED_LISTING, // Mod list couldn't be retrieved FAILED_DOWNLOADING, // File couldn't be retrieved FAILED_FILECHECK // The file is corrupted }; struct DownloadProgressData { DownloadProgressStatus status; double progress; std::string error; }; struct DownloadCallbackData; /** * mod.io API interfacing code. * * Overview * * This class interfaces with a remote API provider that returns a list of mod files. * These can then be downloaded after some cursory checking of well-formedness of the returned * metadata. * Downloaded files are checked for well formedness by validating that they fit the size and hash * indicated by the API, then we check if the file is actually signed by a trusted key, and only * if all of that is success the file is actually possible to be loaded as a mod. * * Security considerations * * This both distrusts the loaded JS mods, and the API as much as possible. * We do not want a malicious mod to use this to download arbitrary files, nor do we want the API * to make us download something we have not verified. * Therefore we only allow mods to download one of the mods returned by this class (using indices). * * This (mostly) necessitates parsing the API responses here, as opposed to in JS. * One could alternatively parse the responses in a locked down JS context, but that would require * storing that code in here, or making sure nobody can overwrite it. Also this would possibly make * some of the needed accesses for downloading and verifying files a bit more complicated. * * Everything downloaded from the API has its signature verified against our public key. * This is a requirement, as otherwise a compromise of the API would result in users installing * possibly malicious files. * So a compromised API can just serve old files that we signed, so in that case there would need * to be an issue in that old file that was missed. * * To limit the extend to how old those files could be the signing key should be rotated * regularly (e.g. every release). To allow old versions of the engine to still use the API * files can be signed by both the old and the new key for some amount of time, that however * only makes sense in case a mod is compatible with both engine versions. * * Note that this does not prevent all possible attacks a package manager/update system should * defend against. This is intentionally not an update system since proper package managers already * exist. However there is some possible overlap in attack vectors and these should be evalutated * whether they apply and to what extend we can fix that on our side (or how to get the API provider * to help us do so). For a list of some possible issues see: * https://github.com/theupdateframework/specification/blob/master/tuf-spec.md * * The mod.io settings are also locked down such that only mods that have been authorized by us * show up in API queries. This is both done so that all required information (dependencies) * are stored for the files, and that only mods that have been checked for being ok are actually * shown to users. */ class ModIo { NONCOPYABLE(ModIo); public: ModIo(); ~ModIo(); // Async requests void StartGetGameId(); void StartListMods(); - void StartDownloadMod(size_t idx); + void StartDownloadMod(u32 idx); /** * Advance the current async request and perform final steps if the download is complete. * * @param scriptInterface used for parsing the data and possibly install the mod. * @return true if the download is complete (successful or not), false otherwise. */ bool AdvanceRequest(const ScriptInterface& scriptInterface); /** * Cancel the current async request and clean things up */ void CancelRequest(); const std::vector& GetMods() const { return m_ModData; } const DownloadProgressData& GetDownloadProgress() const { return m_DownloadProgressData; } private: static size_t ReceiveCallback(void* buffer, size_t size, size_t nmemb, void* userp); static size_t DownloadCallback(void* buffer, size_t size, size_t nmemb, void* userp); static int DownloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow); CURLMcode SetupRequest(const std::string& url, bool fileDownload); void TearDownRequest(); bool ParseGameId(const ScriptInterface& scriptInterface, std::string& err); bool ParseMods(const ScriptInterface& scriptInterface, std::string& err); void DeleteDownloadedFile(); bool VerifyDownloadedFile(std::string& err); // Utility methods for parsing mod.io responses and metadata static bool ParseGameIdResponse(const ScriptInterface& scriptInterface, const std::string& responseData, int& id, std::string& err); static bool ParseModsResponse(const ScriptInterface& scriptInterface, const std::string& responseData, std::vector& modData, const PKStruct& pk, std::string& err); static bool ParseSignature(const std::vector& minisigs, SigStruct& sig, const PKStruct& pk, std::string& err); // Url parts std::string m_BaseUrl; std::string m_GamesRequest; std::string m_GameId; // Query parameters std::string m_ApiKey; std::string m_IdQuery; CURL* m_Curl; CURLM* m_CurlMulti; curl_slist* m_Headers; char m_ErrorBuffer[CURL_ERROR_SIZE]; std::string m_ResponseData; // Current mod download int m_DownloadModID; OsPath m_DownloadFilePath; DownloadCallbackData* m_CallbackData; DownloadProgressData m_DownloadProgressData; PKStruct m_pk; std::vector m_ModData; friend class TestModIo; }; extern ModIo* g_ModIo; #endif // INCLUDED_MODIO Index: ps/trunk/source/ps/VisualReplay.cpp =================================================================== --- ps/trunk/source/ps/VisualReplay.cpp (revision 24982) +++ ps/trunk/source/ps/VisualReplay.cpp (revision 24983) @@ -1,502 +1,502 @@ /* 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 "VisualReplay.h" #include "graphics/GameView.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/allocators/shared_ptr.h" #include "lib/external_libraries/libsdl.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Paths.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/Util.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptExtraHeaders.h" /** * Filter too short replays (value in seconds). */ const u8 minimumReplayDuration = 3; OsPath VisualReplay::GetDirectoryPath() { return Paths(g_args).UserData() / "replays" / engine_version; } OsPath VisualReplay::GetCacheFilePath() { return GetDirectoryPath() / L"replayCache.json"; } OsPath VisualReplay::GetTempCacheFilePath() { return GetDirectoryPath() / L"replayCache_temp.json"; } bool VisualReplay::StartVisualReplay(const OsPath& directory) { ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); const OsPath replayFile = VisualReplay::GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return false; g_Game = new CGame(false); return g_Game->StartVisualReplay(replayFile); } bool VisualReplay::ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject) { if (!FileExists(GetCacheFilePath())) return false; std::ifstream cacheStream(OsString(GetCacheFilePath()).c_str()); CStr cacheStr((std::istreambuf_iterator(cacheStream)), std::istreambuf_iterator()); cacheStream.close(); ScriptRequest rq(scriptInterface); JS::RootedValue cachedReplays(rq.cx); if (scriptInterface.ParseJSON(cacheStr, &cachedReplays)) { cachedReplaysObject.set(&cachedReplays.toObject()); bool isArray; if (JS::IsArrayObject(rq.cx, cachedReplaysObject, &isArray) && isArray) return true; } LOGWARNING("The replay cache file is corrupted, it will be deleted"); wunlink(GetCacheFilePath()); return false; } void VisualReplay::StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays) { ScriptRequest rq(scriptInterface); JS::RootedValue replaysRooted(rq.cx, JS::ObjectValue(*replays)); std::ofstream cacheStream(OsString(GetTempCacheFilePath()).c_str(), std::ofstream::out | std::ofstream::trunc); cacheStream << scriptInterface.StringifyJSON(&replaysRooted); cacheStream.close(); wunlink(GetCacheFilePath()); if (wrename(GetTempCacheFilePath(), GetCacheFilePath())) LOGERROR("Could not store the replay cache"); } JS::HandleObject VisualReplay::ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"ReloadReplayCache"); ScriptRequest rq(scriptInterface); // Maps the filename onto the index and size typedef std::map> replayCacheMap; replayCacheMap fileList; JS::RootedObject cachedReplaysObject(rq.cx); if (ReadCacheFile(scriptInterface, &cachedReplaysObject)) { // Create list of files included in the cache u32 cacheLength = 0; JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength); for (u32 j = 0; j < cacheLength; ++j) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, cachedReplaysObject, j, &replay); JS::RootedValue file(rq.cx); OsPath fileName; double fileSize; scriptInterface.GetProperty(replay, "directory", fileName); scriptInterface.GetProperty(replay, "fileSize", fileSize); fileList[fileName] = std::make_pair(j, fileSize); } } JS::RootedObject replays(rq.cx, JS::NewArrayObject(rq.cx, 0)); DirectoryNames directories; if (GetDirectoryEntries(GetDirectoryPath(), nullptr, &directories) != INFO::OK) return replays; bool newReplays = false; std::vector copyFromOldCache; // Specifies where the next replay should be kept u32 i = 0; for (const OsPath& directory : directories) { // This cannot use IsQuitRequested(), because the current loop and that function both run in the main thread. // So SDL events are not processed unless called explicitly here. if (SDL_QuitRequested()) // Don't return, because we want to save our progress break; const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; bool isNew = true; replayCacheMap::iterator it = fileList.find(directory); if (it != fileList.end()) { if (compareFiles) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); if (fileInfo.Size() == it->second.second) isNew = false; } else isNew = false; } if (isNew) { JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, directory)); if (replayData.isNull()) { if (!FileExists(replayFile)) continue; CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); ScriptInterface::CreateObject( rq, &replayData, "directory", directory.string(), "fileSize", static_cast(fileInfo.Size())); } JS_SetElement(rq.cx, replays, i++, replayData); newReplays = true; } else copyFromOldCache.push_back(it->second.first); } debug_printf( "Loading %lu cached replays, removed %lu outdated entries, loaded %i new entries\n", (unsigned long)fileList.size(), (unsigned long)(fileList.size() - copyFromOldCache.size()), i); if (!newReplays && fileList.empty()) return replays; // No replay was changed, so just return the cache if (!newReplays && fileList.size() == copyFromOldCache.size()) return cachedReplaysObject; { // Copy the replays from the old cache that are not deleted if (!copyFromOldCache.empty()) for (u32 j : copyFromOldCache) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, cachedReplaysObject, j, &replay); JS_SetElement(rq.cx, replays, i++, replay); } } StoreCacheFile(scriptInterface, replays); return replays; } JS::Value VisualReplay::GetReplays(const ScriptInterface& scriptInterface, bool compareFiles) { TIMER(L"GetReplays"); ScriptRequest rq(scriptInterface); JS::RootedObject replays(rq.cx, ReloadReplayCache(scriptInterface, compareFiles)); // Only take entries with data JS::RootedValue replaysWithoutNullEntries(rq.cx); ScriptInterface::CreateArray(rq, &replaysWithoutNullEntries); u32 replaysLength = 0; JS::GetArrayLength(rq.cx, replays, &replaysLength); for (u32 j = 0, i = 0; j < replaysLength; ++j) { JS::RootedValue replay(rq.cx); JS_GetElement(rq.cx, replays, j, &replay); if (scriptInterface.HasProperty(replay, "attribs")) scriptInterface.SetPropertyInt(replaysWithoutNullEntries, i++, replay); } return replaysWithoutNullEntries; } /** * Move the cursor backwards until a newline was read or the beginning of the file was found. * Either way the cursor points to the beginning of a newline. * * @return The current cursor position or -1 on error. */ inline off_t goBackToLineBeginning(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { int currentPos; char character; for (int characters = 0; characters < 10000; ++characters) { currentPos = (int) replayStream->tellg(); // Stop when reached the beginning of the file if (currentPos == 0) return currentPos; if (!replayStream->good()) { LOGERROR("Unknown error when returning to the last line (%i of %lu) of %s", currentPos, fileSize, fileName.string8().c_str()); return -1; } // Stop when reached newline replayStream->get(character); if (character == '\n') return currentPos; // Otherwise go back one character. // Notice: -1 will set the cursor back to the most recently read character. replayStream->seekg(-2, std::ios_base::cur); } LOGERROR("Infinite loop when going back to a line beginning in %s", fileName.string8().c_str()); return -1; } /** * Compute game duration in seconds. Assume constant turn length. * Find the last line that starts with "turn" by reading the file backwards. * * @return seconds or -1 on error */ inline int getReplayDuration(std::istream* replayStream, const OsPath& fileName, off_t fileSize) { CStr type; // Move one character before the file-end replayStream->seekg(-2, std::ios_base::end); // Infinite loop protection, should never occur. // There should be about 5 lines to read until a turn is found. for (int linesRead = 1; linesRead < 1000; ++linesRead) { off_t currentPosition = goBackToLineBeginning(replayStream, fileName, fileSize); // Read error or reached file beginning. No turns exist. if (currentPosition < 1) return -1; if (!replayStream->good()) { LOGERROR("Read error when determining replay duration at %i of %llu in %s", currentPosition - 2, fileSize, fileName.string8().c_str()); return -1; } // Found last turn, compute duration. if (currentPosition + 4 < fileSize && (*replayStream >> type).good() && type == "turn") { u32 turn = 0, turnLength = 0; *replayStream >> turn >> turnLength; return (turn+1) * turnLength / 1000; // add +1 as turn numbers starts with 0 } // Otherwise move cursor back to the character before the last newline replayStream->seekg(currentPosition - 2, std::ios_base::beg); } LOGERROR("Infinite loop when determining replay duration for %s", fileName.string8().c_str()); return -1; } JS::Value VisualReplay::LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory) { // The directory argument must not be constant, otherwise concatenating will fail const OsPath replayFile = GetDirectoryPath() / directory / L"commands.txt"; if (!FileExists(replayFile)) return JS::NullValue(); // Get file size and modification date CFileInfo fileInfo; GetFileInfo(replayFile, &fileInfo); const off_t fileSize = fileInfo.Size(); if (fileSize == 0) return JS::NullValue(); std::ifstream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type; if (!(*replayStream >> type).good()) { LOGERROR("Couldn't open %s.", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } if (type != "start") { LOGWARNING("The replay %s doesn't begin with 'start'!", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Parse header / first line CStr header; std::getline(*replayStream, header); ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); if (!scriptInterface.ParseJSON(header, &attribs)) { LOGERROR("Couldn't parse replay header of %s", replayFile.string8().c_str()); SAFE_DELETE(replayStream); return JS::NullValue(); } // Ensure "turn" after header if (!(*replayStream >> type).good() || type != "turn") { SAFE_DELETE(replayStream); return JS::NullValue(); // there are no turns at all } // Don't process files of rejoined clients u32 turn = 1; *replayStream >> turn; if (turn != 0) { SAFE_DELETE(replayStream); return JS::NullValue(); } int duration = getReplayDuration(replayStream, replayFile, fileSize); SAFE_DELETE(replayStream); // Ensure minimum duration if (duration < minimumReplayDuration) return JS::NullValue(); // Return the actual data JS::RootedValue replayData(rq.cx); ScriptInterface::CreateObject( rq, &replayData, "directory", directory.string(), "fileSize", static_cast(fileSize), "duration", duration); scriptInterface.SetProperty(replayData, "attribs", attribs); return replayData; } bool VisualReplay::DeleteReplay(const OsPath& replayDirectory) { if (replayDirectory.empty()) return false; const OsPath directory = GetDirectoryPath() / replayDirectory; return DirectoryExists(directory) && DeleteDirectory(directory) == INFO::OK; } -JS::Value VisualReplay::GetReplayAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, const OsPath& directoryName) +JS::Value VisualReplay::GetReplayAttributes(const ScriptInterface& scriptInterface, const OsPath& directoryName) { // Create empty JS object - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); ScriptInterface::CreateObject(rq, &attribs); // Return empty object if file doesn't exist const OsPath replayFile = GetDirectoryPath() / directoryName / L"commands.txt"; if (!FileExists(replayFile)) return attribs; // Open file std::istream* replayStream = new std::ifstream(OsString(replayFile).c_str()); CStr type, line; ENSURE((*replayStream >> type).good() && type == "start"); // Read and return first line std::getline(*replayStream, line); - pCmptPrivate->pScriptInterface->ParseJSON(line, &attribs); + scriptInterface.ParseJSON(line, &attribs); SAFE_DELETE(replayStream);; return attribs; } void VisualReplay::AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName) { TIMER(L"AddReplayToCache"); ScriptRequest rq(scriptInterface); JS::RootedValue replayData(rq.cx, LoadReplayData(scriptInterface, OsPath(directoryName))); if (replayData.isNull()) return; JS::RootedObject cachedReplaysObject(rq.cx); if (!ReadCacheFile(scriptInterface, &cachedReplaysObject)) cachedReplaysObject = JS::NewArrayObject(rq.cx, 0); u32 cacheLength = 0; JS::GetArrayLength(rq.cx, cachedReplaysObject, &cacheLength); JS_SetElement(rq.cx, cachedReplaysObject, cacheLength, replayData); StoreCacheFile(scriptInterface, cachedReplaysObject); } bool VisualReplay::HasReplayMetadata(const OsPath& directoryName) { const OsPath filePath(GetDirectoryPath() / directoryName / L"metadata.json"); if (!FileExists(filePath)) return false; CFileInfo fileInfo; GetFileInfo(filePath, &fileInfo); return fileInfo.Size() > 0; } -JS::Value VisualReplay::GetReplayMetadata(ScriptInterface::CmptPrivate* pCmptPrivate, const OsPath& directoryName) +JS::Value VisualReplay::GetReplayMetadata(const ScriptInterface& scriptInterface, const OsPath& directoryName) { if (!HasReplayMetadata(directoryName)) return JS::NullValue(); - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); JS::RootedValue metadata(rq.cx); std::ifstream* stream = new std::ifstream(OsString(GetDirectoryPath() / directoryName / L"metadata.json").c_str()); ENSURE(stream->good()); CStr line; std::getline(*stream, line); stream->close(); SAFE_DELETE(stream); - pCmptPrivate->pScriptInterface->ParseJSON(line, &metadata); + scriptInterface.ParseJSON(line, &metadata); return metadata; } Index: ps/trunk/source/ps/VisualReplay.h =================================================================== --- ps/trunk/source/ps/VisualReplay.h (revision 24982) +++ ps/trunk/source/ps/VisualReplay.h (revision 24983) @@ -1,125 +1,126 @@ /* Copyright (C) 2019 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_REPlAY -#define INCLUDED_REPlAY +#ifndef INCLUDED_VISUAL_REPLAY +#define INCLUDED_VISUAL_REPLAY #include "lib/os_path.h" -#include "scriptinterface/ScriptInterface.h" class CSimulation2; class CGUIManager; +class ScriptInterface; + /** * Contains functions for visually replaying past games. */ namespace VisualReplay { /** * Returns the absolute path to the sim-log directory (that contains the directories with the replay files. */ OsPath GetDirectoryPath(); /** * Returns the absolute path to the replay cache file. */ OsPath GetCacheFilePath(); /** * Returns the absolute path to the temporary replay cache file used to * always have a valid cache file in place even if bad things happen. */ OsPath GetTempCacheFilePath(); /** * Replays the commands.txt file in the given subdirectory visually. */ bool StartVisualReplay(const OsPath& directory); /** * Reads the replay Cache file and parses it into a jsObject * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param cachedReplaysObject - the cached replays. * @return true on succes */ bool ReadCacheFile(const ScriptInterface& scriptInterface, JS::MutableHandleObject cachedReplaysObject); /** * Stores the replay list in the replay cache file * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param replays - the replay list to store. */ void StoreCacheFile(const ScriptInterface& scriptInterface, JS::HandleObject replays); /** * Load the replay cache and check if there are new/deleted replays. If so, update the cache. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - compare the directory name and the FileSize of the replays and the cache. * @return cache entries */ JS::HandleObject ReloadReplayCache(const ScriptInterface& scriptInterface, bool compareFiles); /** * Get a list of replays to display in the GUI. * * @param scriptInterface - the ScriptInterface in which to create the return data. * @param compareFiles - reload the cache, which takes more time, * but nearly ensures, that no changed replay is missed. * @return array of objects containing replay data */ JS::Value GetReplays(const ScriptInterface& scriptInterface, bool compareFiles); /** * Parses a commands.txt file and extracts metadata. * Works similarly to CGame::LoadReplayData(). */ JS::Value LoadReplayData(const ScriptInterface& scriptInterface, const OsPath& directory); /** * Permanently deletes the visual replay (including the parent directory) * * @param replayFile - path to commands.txt, whose parent directory will be deleted. * @return true if deletion was successful, false on error */ bool DeleteReplay(const OsPath& replayFile); /** * Returns the parsed header of the replay file (commands.txt). */ -JS::Value GetReplayAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, const OsPath& directoryName); +JS::Value GetReplayAttributes(const ScriptInterface& scriptInterface, const OsPath& directoryName); /** * Returns whether or not the metadata / summary screen data has been saved properly when the game ended. */ bool HasReplayMetadata(const OsPath& directoryName); /** * Returns the metadata of a replay. */ -JS::Value GetReplayMetadata(ScriptInterface::CmptPrivate* pCmptPrivate, const OsPath& directoryName); +JS::Value GetReplayMetadata(const ScriptInterface& scriptInterface, const OsPath& directoryName); /** * Adds a replay to the replayCache. */ void AddReplayToCache(const ScriptInterface& scriptInterface, const CStrW& directoryName); } #endif Index: ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_ConfigDB.cpp (revision 24983) @@ -1,201 +1,205 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 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_ConfigDB.h" #include "ps/ConfigDB.h" #include "ps/CLogger.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include #include +namespace JSI_ConfigDB +{ // These entries will not be readable nor writable for JS, so that e.g. malicious mods can't leak personal or sensitive data static const std::unordered_set g_ProtectedConfigNames = { "modio.public_key", // See ModIO.cpp "modio.v1.baseurl", "modio.v1.api_key", "modio.v1.name_id", "userreport.id" // Acts as authentication token for GDPR personal data requests. }; -bool JSI_ConfigDB::IsProtectedConfigName(const std::string& name) +bool IsProtectedConfigName(const std::string& name) { if (g_ProtectedConfigNames.find(name) != g_ProtectedConfigNames.end()) { LOGERROR("Access denied (%s)", name.c_str()); return true; } return false; } -bool JSI_ConfigDB::GetConfigNamespace(const std::wstring& cfgNsString, EConfigNamespace& cfgNs) +bool GetConfigNamespace(const std::wstring& cfgNsString, EConfigNamespace& cfgNs) { if (cfgNsString == L"default") cfgNs = CFG_DEFAULT; else if (cfgNsString == L"mod") cfgNs = CFG_MOD; else if (cfgNsString == L"system") cfgNs = CFG_SYSTEM; else if (cfgNsString == L"user") cfgNs = CFG_USER; else if (cfgNsString == L"hwdetect") cfgNs = CFG_HWDETECT; else { LOGERROR("Invalid namespace name passed to the ConfigDB!"); cfgNs = CFG_DEFAULT; return false; } return true; } -bool JSI_ConfigDB::HasChanges(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString) +bool HasChanges(const std::wstring& cfgNsString) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; return g_ConfigDB.HasChanges(cfgNs); } -bool JSI_ConfigDB::SetChanges(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, bool value) +bool SetChanges(const std::wstring& cfgNsString, bool value) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetChanges(cfgNs, value); return true; } -std::string JSI_ConfigDB::GetValue(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name) +std::string GetValue(const std::wstring& cfgNsString, const std::string& name) { if (IsProtectedConfigName(name)) return ""; EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return std::string(); std::string value; g_ConfigDB.GetValue(cfgNs, name, value); return value; } -bool JSI_ConfigDB::CreateValue(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name, const std::string& value) +bool CreateValue(const std::wstring& cfgNsString, const std::string& name, const std::string& value) { if (IsProtectedConfigName(name)) return false; EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetValueString(cfgNs, name, value); return true; } -bool JSI_ConfigDB::CreateValues(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name, const std::vector& values) +bool CreateValues(const std::wstring& cfgNsString, const std::string& name, const std::vector& values) { if (IsProtectedConfigName(name)) return false; EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetValueList(cfgNs, name, values); return true; } -bool JSI_ConfigDB::RemoveValue(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name) +bool RemoveValue(const std::wstring& cfgNsString, const std::string& name) { if (IsProtectedConfigName(name)) return false; EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.RemoveValue(cfgNs, name); return true; } -bool JSI_ConfigDB::WriteFile(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const Path& path) +bool WriteFile(const std::wstring& cfgNsString, const Path& path) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; return g_ConfigDB.WriteFile(cfgNs, path); } -bool JSI_ConfigDB::WriteValueToFile(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path) +bool WriteValueToFile(const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path) { if (IsProtectedConfigName(name)) return false; EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; return g_ConfigDB.WriteValueToFile(cfgNs, name, value, path); } -void JSI_ConfigDB::CreateAndWriteValueToFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path) +void CreateAndWriteValueToFile(const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path) { - CreateValue(pCmptPrivate, cfgNsString, name, value); - WriteValueToFile(pCmptPrivate, cfgNsString, name, value, path); + CreateValue(cfgNsString, name, value); + WriteValueToFile(cfgNsString, name, value, path); } -bool JSI_ConfigDB::Reload(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString) +bool Reload(const std::wstring& cfgNsString) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; return g_ConfigDB.Reload(cfgNs); } -bool JSI_ConfigDB::SetFile(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& cfgNsString, const Path& path) +bool SetFile(const std::wstring& cfgNsString, const Path& path) { EConfigNamespace cfgNs; if (!GetConfigNamespace(cfgNsString, cfgNs)) return false; g_ConfigDB.SetConfigFile(cfgNs, path); return true; } -void JSI_ConfigDB::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("ConfigDB_HasChanges"); - scriptInterface.RegisterFunction("ConfigDB_SetChanges"); - scriptInterface.RegisterFunction("ConfigDB_GetValue"); - scriptInterface.RegisterFunction("ConfigDB_CreateValue"); - scriptInterface.RegisterFunction, &JSI_ConfigDB::CreateValues>("ConfigDB_CreateValues"); - scriptInterface.RegisterFunction("ConfigDB_RemoveValue"); - scriptInterface.RegisterFunction("ConfigDB_WriteFile"); - scriptInterface.RegisterFunction("ConfigDB_WriteValueToFile"); - scriptInterface.RegisterFunction("ConfigDB_CreateAndWriteValueToFile"); - scriptInterface.RegisterFunction("ConfigDB_SetFile"); - scriptInterface.RegisterFunction("ConfigDB_Reload"); + ScriptFunction::Register<&HasChanges>(rq, "ConfigDB_HasChanges"); + ScriptFunction::Register<&SetChanges>(rq, "ConfigDB_SetChanges"); + ScriptFunction::Register<&GetValue>(rq, "ConfigDB_GetValue"); + ScriptFunction::Register<&CreateValue>(rq, "ConfigDB_CreateValue"); + ScriptFunction::Register<&CreateValues>(rq, "ConfigDB_CreateValues"); + ScriptFunction::Register<&RemoveValue>(rq, "ConfigDB_RemoveValue"); + ScriptFunction::Register<&WriteFile>(rq, "ConfigDB_WriteFile"); + ScriptFunction::Register<&WriteValueToFile>(rq, "ConfigDB_WriteValueToFile"); + ScriptFunction::Register<&CreateAndWriteValueToFile>(rq, "ConfigDB_CreateAndWriteValueToFile"); + ScriptFunction::Register<&SetFile>(rq, "ConfigDB_SetFile"); + ScriptFunction::Register<&Reload>(rq, "ConfigDB_Reload"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_ConfigDB.h (revision 24983) @@ -1,44 +1,28 @@ /* Copyright (C) 2019 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_JSI_CONFIGDB #define INCLUDED_JSI_CONFIGDB -#include "ps/ConfigDB.h" -#include "scriptinterface/ScriptInterface.h" - -#include +class ScriptRequest; namespace JSI_ConfigDB { - bool IsProtectedConfigName(const std::string& name); - bool GetConfigNamespace(const std::wstring& cfgNsString, EConfigNamespace& cfgNs); - bool HasChanges(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString); - bool SetChanges(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, bool value); - std::string GetValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name); - bool CreateValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value); - bool CreateValues(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::vector& values); - bool RemoveValue(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name); - bool WriteFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const Path& path); - bool WriteValueToFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path); - void CreateAndWriteValueToFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const std::string& name, const std::string& value, const Path& path); - bool Reload(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString); - bool SetFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& cfgNsString, const Path& path); - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_CONFIGDB Index: ps/trunk/source/ps/scripting/JSInterface_Console.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Console.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Console.cpp (revision 24983) @@ -1,43 +1,43 @@ /* Copyright (C) 2021 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_Console.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "scriptinterface/FunctionWrapper.h" -namespace +namespace JSI_Console { CConsole* ConsoleGetter(const ScriptRequest&, JS::CallArgs&) { if (!g_Console) { LOGERROR("Trying to access the console when it's not initialized!"); return nullptr; } return g_Console; } -} -void JSI_Console::RegisterScriptFunctions(const ScriptRequest& rq) +void RegisterScriptFunctions(const ScriptRequest& rq) { ScriptFunction::Register<&CConsole::IsActive, ConsoleGetter>(rq, "Console_GetVisibleEnabled"); ScriptFunction::Register<&CConsole::SetVisible, ConsoleGetter>(rq, "Console_SetVisibleEnabled"); } +} Index: ps/trunk/source/ps/scripting/JSInterface_Debug.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Debug.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Debug.h (revision 24983) @@ -1,38 +1,28 @@ /* Copyright (C) 2019 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_JSI_DEBUG #define INCLUDED_JSI_DEBUG -#include "scriptinterface/ScriptInterface.h" - -#include +class ScriptRequest; namespace JSI_Debug { - int Crash(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - void DebugWarn(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - void DisplayErrorDialog(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& msg); - std::wstring GetBuildDate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - double GetBuildTimestamp(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - std::wstring GetBuildRevision(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - double GetMicroseconds(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)); - - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_DEBUG Index: ps/trunk/source/ps/scripting/JSInterface_Game.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Game.h (revision 24983) @@ -1,44 +1,28 @@ /* Copyright (C) 2018 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_JSI_GAME #define INCLUDED_JSI_GAME -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Game { - bool IsGameStarted(ScriptInterface::CmptPrivate* pCmptPrivate); - void StartGame(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue attribs, int playerID); - void Script_EndGame(ScriptInterface::CmptPrivate* pCmptPrivate); - int GetPlayerID(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetPlayerID(ScriptInterface::CmptPrivate* pCmptPrivate, int id); - void SetViewedPlayer(ScriptInterface::CmptPrivate* pCmptPrivate, int id); - float GetSimRate(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetSimRate(ScriptInterface::CmptPrivate* pCmptPrivate, float rate); - bool IsPaused(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetPaused(ScriptInterface::CmptPrivate* pCmptPrivate, bool pause, bool sendMessage); - bool IsVisualReplay(ScriptInterface::CmptPrivate* pCmptPrivate); - std::wstring GetCurrentReplayDirectory(ScriptInterface::CmptPrivate* pCmptPrivate); - void RewindTimeWarp(ScriptInterface::CmptPrivate* pCmptPrivate); - void EnableTimeWarpRecording(ScriptInterface::CmptPrivate* pCmptPrivate, unsigned int numTurns); - void DumpTerrainMipmap(ScriptInterface::CmptPrivate* pCmptPrivate); - - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_GAME Index: ps/trunk/source/ps/scripting/JSInterface_Main.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Main.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_Main.h (revision 24983) @@ -1,41 +1,28 @@ /* Copyright (C) 2018 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_JSI_MAIN #define INCLUDED_JSI_MAIN -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Main { - void QuitEngine(ScriptInterface::CmptPrivate* pCmptPrivate); - void StartAtlas(ScriptInterface::CmptPrivate* pCmptPrivate); - bool AtlasIsAvailable(ScriptInterface::CmptPrivate* pCmptPrivate); - bool IsAtlasRunning(ScriptInterface::CmptPrivate* pCmptPrivate); - void OpenURL(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& url); - std::wstring GetSystemUsername(ScriptInterface::CmptPrivate* pCmptPrivate); - std::wstring GetMatchID(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value LoadMapSettings(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname); - bool HotkeyIsPressed_(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& hotkeyName); - int GetFps(ScriptInterface::CmptPrivate* pCmptPrivate); - int GetTextWidth(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& fontName, const std::wstring& text); - std::string CalculateMD5(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& input); - - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_MAIN Index: ps/trunk/source/ps/scripting/JSInterface_ModIo.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_ModIo.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_ModIo.h (revision 24983) @@ -1,36 +1,28 @@ /* Copyright (C) 2018 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_JSI_MODIO #define INCLUDED_JSI_MODIO -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_ModIo { - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); - - void StartGetGameId(ScriptInterface::CmptPrivate* pCmptPrivate); - void StartListMods(ScriptInterface::CmptPrivate* pCmptPrivate); - void StartDownloadMod(ScriptInterface::CmptPrivate* pCmptPrivate, uint32_t idx); - bool AdvanceRequest(ScriptInterface::CmptPrivate* pCmptPrivate); - void CancelRequest(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetMods(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetDownloadProgress(ScriptInterface::CmptPrivate* pCmptPrivate); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_MODIO Index: ps/trunk/source/ps/scripting/JSInterface_UserReport.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_UserReport.cpp (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_UserReport.cpp (revision 24983) @@ -1,63 +1,67 @@ /* Copyright (C) 2018 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_UserReport.h" #include "ps/Filesystem.h" #include "ps/Pyrogenesis.h" #include "ps/UserReport.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include -bool JSI_UserReport::IsUserReportEnabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +namespace JSI_UserReport +{ +bool IsUserReportEnabled() { return g_UserReporter.IsReportingEnabled(); } -void JSI_UserReport::SetUserReportEnabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled) +void SetUserReportEnabled(bool enabled) { g_UserReporter.SetReportingEnabled(enabled); } -std::string JSI_UserReport::GetUserReportStatus(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::string GetUserReportStatus() { return g_UserReporter.GetStatus(); } -std::string JSI_UserReport::GetUserReportLogPath(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::string GetUserReportLogPath() { return psLogDir().string8(); } -std::string JSI_UserReport::GetUserReportConfigPath(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::string GetUserReportConfigPath() { OsPath configPath; WARN_IF_ERR(g_VFS->GetDirectoryRealPath("config/", configPath)); return configPath.string8(); } -void JSI_UserReport::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("IsUserReportEnabled"); - scriptInterface.RegisterFunction("SetUserReportEnabled"); - scriptInterface.RegisterFunction("GetUserReportStatus"); - scriptInterface.RegisterFunction("GetUserReportLogPath"); - scriptInterface.RegisterFunction("GetUserReportConfigPath"); + ScriptFunction::Register<&IsUserReportEnabled>(rq, "IsUserReportEnabled"); + ScriptFunction::Register<&SetUserReportEnabled>(rq, "SetUserReportEnabled"); + ScriptFunction::Register<&GetUserReportStatus>(rq, "GetUserReportStatus"); + ScriptFunction::Register<&GetUserReportLogPath>(rq, "GetUserReportLogPath"); + ScriptFunction::Register<&GetUserReportConfigPath>(rq, "GetUserReportConfigPath"); +} } Index: ps/trunk/source/ps/scripting/JSInterface_VFS.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 24983) @@ -1,62 +1,30 @@ /* Copyright (C) 2021 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_JSI_VFS #define INCLUDED_JSI_VFS -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_VFS { - // Return an array of pathname strings, one for each matching entry in the - // specified directory. - JS::Value BuildDirEntList(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse); - - // Return true iff the file exists - bool FileExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filename); - - // Return time [seconds since 1970] of the last modification to the specified file. - double GetFileMTime(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - - // Return current size of file. - unsigned int GetFileSize(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - - // Return file contents in a string. - JS::Value ReadFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - - // Return file contents as an array of lines. - JS::Value ReadFileLines(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - - // Return file contents parsed as a JS Object - JS::Value ReadJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath); - - // Save given JS Object to a JSON file - void WriteJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath, JS::HandleValue val1); - - // Delete the given campaign save. - // This is limited to campaign save to avoid mods deleting the wrong file. - bool DeleteCampaignSave(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& filePath); - - // Tests whether the current script context is allowed to read from the given directory - bool PathRestrictionMet(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath); - - void RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface); - void RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface); - void RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions_GUI(const ScriptRequest& rq); + void RegisterScriptFunctions_Simulation(const ScriptRequest& rq); + void RegisterScriptFunctions_Maps(const ScriptRequest& rq); } #endif // INCLUDED_JSI_VFS Index: ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 24982) +++ ps/trunk/source/ps/scripting/JSInterface_VisualReplay.h (revision 24983) @@ -1,36 +1,28 @@ /* 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 . */ #ifndef INCLUDED_JSI_VISUALREPLAY #define INCLUDED_JSI_VISUALREPLAY -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_VisualReplay { - bool StartVisualReplay(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directory); - bool DeleteReplay(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& replayFile); - JS::Value GetReplays(ScriptInterface::CmptPrivate* pCmptPrivate, bool compareFiles); - JS::Value GetReplayAttributes(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName); - bool HasReplayMetadata(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName); - JS::Value GetReplayMetadata(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName); - void AddReplayToCache(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName); - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); - CStrW GetReplayDirectoryName(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& directoryName); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_VISUALREPLAY Index: ps/trunk/source/scriptinterface/ScriptInterface.cpp =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24982) +++ ps/trunk/source/scriptinterface/ScriptInterface.cpp (revision 24983) @@ -1,1056 +1,1015 @@ /* Copyright (C) 2021 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 "FunctionWrapper.h" #include "ScriptContext.h" #include "ScriptExtraHeaders.h" #include "ScriptInterface.h" #include "ScriptStats.h" #include "lib/debug.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/utf16string.h" #include #include #define BOOST_MULTI_INDEX_DISABLE_SERIALIZATION #include #include #include #include #include #include #include #include "valgrind.h" /** * @file * Abstractions of various SpiderMonkey features. * Engine code should be using functions of these interfaces rather than * directly accessing the underlying JS api. */ struct ScriptInterface_impl { ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context); ~ScriptInterface_impl(); - void Register(const char* name, JSNative fptr, uint nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_context; friend ScriptRequest; private: JSContext* m_cx; JS::PersistentRootedObject m_glob; // global scope object public: boost::rand48* m_rng; JS::PersistentRootedObject m_nativeScope; // native function scope object }; ScriptRequest::ScriptRequest(const ScriptInterface& scriptInterface) : cx(scriptInterface.m->m_cx), nativeScope(scriptInterface.m->m_nativeScope) { m_formerRealm = JS::EnterRealm(cx, scriptInterface.m->m_glob); glob = JS::CurrentGlobalOrNull(cx); } JS::Value ScriptRequest::globalValue() const { return JS::ObjectValue(*glob); } ScriptRequest::~ScriptRequest() { JS::LeaveRealm(cx, m_formerRealm); } namespace { JSClassOps global_classops = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, JS_GlobalObjectTraceHook }; JSClass global_class = { "global", JSCLASS_GLOBAL_FLAGS, &global_classops }; // Functions in the global namespace: bool print(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ for (uint i = 0; i < args.length(); ++i) { std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[i], str)) return false; debug_printf("%s", utf8_from_wstring(str).c_str()); } fflush(stdout); args.rval().setUndefined(); return true; } bool logmsg(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGMESSAGE("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool warn(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGWARNING("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } bool error(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (args.length() < 1) { args.rval().setUndefined(); return true; } ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ std::wstring str; if (!ScriptInterface::FromJSVal(rq, args[0], str)) return false; LOGERROR("%s", utf8_from_wstring(str)); args.rval().setUndefined(); return true; } -bool deepcopy(JSContext* cx, uint argc, JS::Value* vp) +JS::Value deepcopy(const ScriptRequest& rq, JS::HandleValue val) { - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - if (args.length() < 1) + if (val.isNullOrUndefined()) { - args.rval().setUndefined(); - return true; + ScriptException::Raise(rq, "deepcopy requires one argument."); + return JS::UndefinedValue(); } - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ - JS::RootedValue ret(cx); - if (!JS_StructuredClone(rq.cx, args[0], &ret, NULL, NULL)) - return false; - - args.rval().set(ret); - return true; + JS::RootedValue ret(rq.cx); + if (!JS_StructuredClone(rq.cx, val, &ret, NULL, NULL)) + { + ScriptException::Raise(rq, "deepcopy StructureClone copy failed."); + return JS::UndefinedValue(); + } + return ret; } -bool deepfreeze(JSContext* cx, uint argc, JS::Value* vp) +JS::Value deepfreeze(const ScriptInterface& scriptInterface, JS::HandleValue val) { - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ - - if (args.length() != 1 || !args.get(0).isObject()) + ScriptRequest rq(scriptInterface); + if (!val.isObject()) { ScriptException::Raise(rq, "deepfreeze requires exactly one object as an argument."); - return false; + return JS::UndefinedValue(); } - ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->FreezeObject(args.get(0), true); - args.rval().set(args.get(0)); - return true; + scriptInterface.FreezeObject(val, true); + return val; } -bool ProfileStart(JSContext* cx, uint argc, JS::Value* vp) +void ProfileStart(const std::string& regionName) { const char* name = "(ProfileStart)"; - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ - if (args.length() >= 1) - { - std::string str; - if (!ScriptInterface::FromJSVal(rq, args[0], str)) - return false; - - typedef boost::flyweight< - std::string, - boost::flyweights::no_tracking, - boost::flyweights::no_locking - > StringFlyweight; + typedef boost::flyweight< + std::string, + boost::flyweights::no_tracking, + boost::flyweights::no_locking + > StringFlyweight; - name = StringFlyweight(str).get().c_str(); - } + if (!regionName.empty()) + name = StringFlyweight(regionName).get().c_str(); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.StartScript(name); g_Profiler2.RecordRegionEnter(name); - - args.rval().setUndefined(); - return true; } -bool ProfileStop(JSContext* UNUSED(cx), uint argc, JS::Value* vp) +void ProfileStop() { - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); if (CProfileManager::IsInitialised() && Threading::IsMainThread()) g_Profiler.Stop(); g_Profiler2.RecordRegionLeave(); - - args.rval().setUndefined(); - return true; } -bool ProfileAttribute(JSContext* cx, uint argc, JS::Value* vp) +void ProfileAttribute(const std::string& attr) { const char* name = "(ProfileAttribute)"; - JS::CallArgs args = JS::CallArgsFromVp(argc, vp); - ScriptRequest rq(*ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface); \ - if (args.length() >= 1) - { - std::string str; - if (!ScriptInterface::FromJSVal(rq, args[0], str)) - return false; - - typedef boost::flyweight< - std::string, - boost::flyweights::no_tracking, - boost::flyweights::no_locking - > StringFlyweight; + typedef boost::flyweight< + std::string, + boost::flyweights::no_tracking, + boost::flyweights::no_locking + > StringFlyweight; - name = StringFlyweight(str).get().c_str(); - } + if (!attr.empty()) + name = StringFlyweight(attr).get().c_str(); g_Profiler2.RecordAttribute("%s", name); - - args.rval().setUndefined(); - return true; } // Math override functions: // boost::uniform_real is apparently buggy in Boost pre-1.47 - for integer generators // it returns [min,max], not [min,max). The bug was fixed in 1.47. // We need consistent behaviour, so manually implement the correct version: static double generate_uniform_real(boost::rand48& rng, double min, double max) { while (true) { double n = (double)(rng() - rng.min()); double d = (double)(rng.max() - rng.min()) + 1.0; ENSURE(d > 0 && n >= 0 && n <= d); double r = n / d * (max - min) + min; if (r < max) return r; } } bool Math_random(JSContext* cx, uint argc, JS::Value* vp) { JS::CallArgs args = JS::CallArgsFromVp(argc, vp); double r; if (!ScriptInterface::GetScriptInterfaceAndCBData(cx)->pScriptInterface->MathRandom(r)) return false; args.rval().setNumber(r); return true; } } // anonymous namespace bool ScriptInterface::MathRandom(double& nbr) { if (m->m_rng == NULL) return false; nbr = generate_uniform_real(*(m->m_rng), 0.0, 1.0); return true; } ScriptInterface_impl::ScriptInterface_impl(const char* nativeScopeName, const shared_ptr& context) : m_context(context), m_cx(context->GetGeneralJSContext()), m_glob(context->GetGeneralJSContext()), m_nativeScope(context->GetGeneralJSContext()) { JS::RealmCreationOptions creationOpt; // Keep JIT code during non-shrinking GCs. This brings a quite big performance improvement. creationOpt.setPreserveJitCode(true); // Enable uneval creationOpt.setToSourceEnabled(true); JS::RealmOptions opt(creationOpt, JS::RealmBehaviors{}); m_glob = JS_NewGlobalObject(m_cx, &global_class, nullptr, JS::OnNewGlobalHookOption::FireOnNewGlobalHook, opt); JSAutoRealm autoRealm(m_cx, m_glob); ENSURE(JS::InitRealmStandardClasses(m_cx)); JS_DefineProperty(m_cx, m_glob, "global", m_glob, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); - m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); - + // These first 4 actually use CallArgs & thus don't use ScriptFunction JS_DefineFunction(m_cx, m_glob, "print", ::print, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "log", ::logmsg, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "warn", ::warn, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); JS_DefineFunction(m_cx, m_glob, "error", ::error, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); - JS_DefineFunction(m_cx, m_glob, "clone", ::deepcopy, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); - JS_DefineFunction(m_cx, m_glob, "deepfreeze", ::deepfreeze, 1, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); + ScriptFunction::Register(m_cx, m_glob, "clone"); + ScriptFunction::Register(m_cx, m_glob, "deepfreeze"); - Register("ProfileStart", ::ProfileStart, 1); - Register("ProfileStop", ::ProfileStop, 0); - Register("ProfileAttribute", ::ProfileAttribute, 1); + m_nativeScope = JS_DefineObject(m_cx, m_glob, nativeScopeName, nullptr, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); + + ScriptFunction::Register<&ProfileStart>(m_cx, m_nativeScope, "ProfileStart"); + ScriptFunction::Register<&ProfileStop>(m_cx, m_nativeScope, "ProfileStop"); + ScriptFunction::Register<&ProfileAttribute>(m_cx, m_nativeScope, "ProfileAttribute"); m_context->RegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } ScriptInterface_impl::~ScriptInterface_impl() { m_context->UnRegisterRealm(JS::GetObjectRealmOrNull(m_glob)); } -void ScriptInterface_impl::Register(const char* name, JSNative fptr, uint nargs) const -{ - JSAutoRealm autoRealm(m_cx, m_glob); - JS::RootedObject nativeScope(m_cx, m_nativeScope); - JS::RootedFunction func(m_cx, JS_DefineFunction(m_cx, nativeScope, name, fptr, nargs, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); -} - ScriptInterface::ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context) : m(std::make_unique(nativeScopeName, context)) { // Profiler stats table isn't thread-safe, so only enable this on the main thread if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Add(this, debugName); } ScriptRequest rq(this); m_CmptPrivate.pScriptInterface = this; JS_SetCompartmentPrivate(js::GetObjectCompartment(rq.glob), (void*)&m_CmptPrivate); } ScriptInterface::~ScriptInterface() { if (Threading::IsMainThread()) { if (g_ScriptStatsTable) g_ScriptStatsTable->Remove(this); } } void ScriptInterface::SetCallbackData(void* pCBData) { m_CmptPrivate.pCBData = pCBData; } ScriptInterface::CmptPrivate* ScriptInterface::GetScriptInterfaceAndCBData(JSContext* cx) { CmptPrivate* pCmptPrivate = (CmptPrivate*)JS_GetCompartmentPrivate(js::GetContextCompartment(cx)); return pCmptPrivate; } bool ScriptInterface::LoadGlobalScripts() { // Ignore this failure in tests if (!g_VFS) return false; // Load and execute *.js in the global scripts directory VfsPaths pathnames; vfs::GetPathnames(g_VFS, L"globalscripts/", L"*.js", pathnames); for (const VfsPath& path : pathnames) if (!LoadGlobalScriptFile(path)) { LOGERROR("LoadGlobalScripts: Failed to load script %s", path.string8()); return false; } return true; } bool ScriptInterface::ReplaceNondeterministicRNG(boost::rand48& rng) { ScriptRequest rq(this); JS::RootedValue math(rq.cx); JS::RootedObject global(rq.cx, rq.glob); if (JS_GetProperty(rq.cx, global, "Math", &math) && math.isObject()) { JS::RootedObject mathObj(rq.cx, &math.toObject()); JS::RootedFunction random(rq.cx, JS_DefineFunction(rq.cx, mathObj, "random", Math_random, 0, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)); if (random) { m->m_rng = &rng; return true; } } ScriptException::CatchPending(rq); LOGERROR("ReplaceNondeterministicRNG: failed to replace Math.random"); return false; } -void ScriptInterface::Register(const char* name, JSNative fptr, size_t nargs) const -{ - m->Register(name, fptr, (uint)nargs); -} - JSContext* ScriptInterface::GetGeneralJSContext() const { return m->m_context->GetGeneralJSContext(); } shared_ptr ScriptInterface::GetContext() const { return m->m_context; } void ScriptInterface::CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const { ScriptRequest rq(this); if (!ctor.isObject()) { LOGERROR("CallConstructor: ctor is not an object"); out.setNull(); return; } JS::RootedObject ctorObj(rq.cx, &ctor.toObject()); out.setObjectOrNull(JS_New(rq.cx, ctorObj, argv)); } void ScriptInterface::DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs) { ScriptRequest rq(this); std::string typeName = clasp->name; if (m_CustomObjectTypes.find(typeName) != m_CustomObjectTypes.end()) { // This type already exists throw PSERROR_Scripting_DefineType_AlreadyExists(); } JS::RootedObject global(rq.cx, rq.glob); JS::RootedObject obj(rq.cx, JS_InitClass(rq.cx, global, nullptr, clasp, constructor, minArgs, // Constructor, min args ps, fs, // Properties, methods static_ps, static_fs)); // Constructor properties, methods if (obj == nullptr) { ScriptException::CatchPending(rq); throw PSERROR_Scripting_DefineType_CreationFailed(); } CustomType& type = m_CustomObjectTypes[typeName]; type.m_Prototype.init(rq.cx, obj); type.m_Class = clasp; type.m_Constructor = constructor; } JSObject* ScriptInterface::CreateCustomObject(const std::string& typeName) const { std::map::const_iterator it = m_CustomObjectTypes.find(typeName); if (it == m_CustomObjectTypes.end()) throw PSERROR_Scripting_TypeDoesNotExist(); ScriptRequest rq(this); JS::RootedObject prototype(rq.cx, it->second.m_Prototype.get()); return JS_NewObjectWithGivenProto(rq.cx, it->second.m_Class, prototype); } bool ScriptInterface::CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::RootedObject obj(rq.cx); if (!JS_ValueToObject(rq.cx, val, &obj) || !obj) return false; // Check that the named function actually exists, to avoid ugly JS error reports // when calling an undefined value bool found; if (!JS_HasProperty(rq.cx, obj, name, &found) || !found) return false; if (JS_CallFunctionName(rq.cx, obj, name, argv, ret)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject object) { object.set(JS_NewPlainObject(rq.cx)); if (!object) throw PSERROR_Scripting_CreateObjectFailed(); return true; } void ScriptInterface::CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length) { objectValue.setObjectOrNull(JS::NewArrayObject(rq.cx, length)); if (!objectValue.isObject()) throw PSERROR_Scripting_CreateObjectFailed(); } bool ScriptInterface::SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); bool found; if (!JS_HasProperty(rq.cx, global, name, &found)) return false; if (found) { JS::Rooted desc(rq.cx); if (!JS_GetOwnPropertyDescriptor(rq.cx, global, name, &desc)) return false; if (!desc.writable()) { if (!replace) { ScriptException::Raise(rq, "SetGlobal \"%s\" called multiple times", name); return false; } // This is not supposed to happen, unless the user has called SetProperty with constant = true on the global object // instead of using SetGlobal. if (!desc.configurable()) { ScriptException::Raise(rq, "The global \"%s\" is permanent and cannot be hotloaded", name); return false; } LOGMESSAGE("Hotloading new value for global \"%s\".", name); ENSURE(JS_DeleteProperty(rq.cx, global, name)); } } uint attrs = 0; if (constant) attrs |= JSPROP_READONLY; if (enumerate) attrs |= JSPROP_ENUMERATE; return JS_DefineProperty(rq.cx, global, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_DefineProperty(rq.cx, object, name, value, attrs); } bool ScriptInterface::SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); utf16string name16(name, name + wcslen(name)); return JS_DefineUCProperty(rq.cx, object, reinterpret_cast(name16.c_str()), name16.length(), value, attrs); } bool ScriptInterface::SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const { ScriptRequest rq(this); uint attrs = 0; if (constant) attrs |= JSPROP_READONLY | JSPROP_PERMANENT; if (enumerate) attrs |= JSPROP_ENUMERATE; if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); JS::RootedId id(rq.cx, INT_TO_JSID(name)); return JS_DefinePropertyById(rq.cx, object, id, value, attrs); } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out) { JS::RootedValue val(rq.cx); if (!GetProperty(rq, obj, name, &val)) return false; if (!val.isObject()) { LOGERROR("GetProperty failed: trying to get an object, but the property is not an object!"); return false; } out.set(&val.toObject()); return true; } bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out) { if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetProperty(rq.cx, object, name, out); } bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const { ScriptRequest rq(this); return GetPropertyInt(rq,obj, name, out); } bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out) { JS::RootedId nameId(rq.cx, INT_TO_JSID(name)); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); return JS_GetPropertyById(rq.cx, object, nameId, out); } bool ScriptInterface::HasProperty(JS::HandleValue obj, const char* name) const { ScriptRequest rq(this); if (!obj.isObject()) return false; JS::RootedObject object(rq.cx, &obj.toObject()); bool found; if (!JS_HasProperty(rq.cx, object, name, &found)) return false; return found; } bool ScriptInterface::GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out) { // Try to get the object as a property of the global object. JS::RootedObject global(rq.cx, rq.glob); if (!JS_GetProperty(rq.cx, global, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; // Some objects, such as const definitions, or Class definitions, are hidden inside closures. // We must fetch those from the correct lexical scope. //JS::RootedValue glob(cx); JS::RootedObject lexical_environment(rq.cx, JS_GlobalLexicalEnvironment(rq.glob)); if (!JS_GetProperty(rq.cx, lexical_environment, name.c_str(), out)) { out.set(JS::NullHandleValue); return false; } if (!out.isNullOrUndefined()) return true; out.set(JS::NullHandleValue); return false; } bool ScriptInterface::EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const { ScriptRequest rq(this); if (!objVal.isObjectOrNull()) { LOGERROR("EnumeratePropertyNames expected object type!"); return false; } JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedIdVector props(rq.cx); // This recurses up the prototype chain on its own. if (!js::GetPropertyKeys(rq.cx, obj, enumerableOnly? 0 : JSITER_HIDDEN, &props)) return false; out.reserve(out.size() + props.length()); for (size_t i = 0; i < props.length(); ++i) { JS::RootedId id(rq.cx, props[i]); JS::RootedValue val(rq.cx); if (!JS_IdToValue(rq.cx, id, &val)) return false; // Ignore integer properties for now. // TODO: is this actually a thing in ECMAScript 6? if (!val.isString()) continue; std::string propName; if (!FromJSVal(rq, val, propName)) return false; out.emplace_back(std::move(propName)); } return true; } bool ScriptInterface::SetPrototype(JS::HandleValue objVal, JS::HandleValue protoVal) { ScriptRequest rq(this); if (!objVal.isObject() || !protoVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); JS::RootedObject proto(rq.cx, &protoVal.toObject()); return JS_SetPrototype(rq.cx, obj, proto); } bool ScriptInterface::FreezeObject(JS::HandleValue objVal, bool deep) const { ScriptRequest rq(this); if (!objVal.isObject()) return false; JS::RootedObject obj(rq.cx, &objVal.toObject()); if (deep) return JS_DeepFreezeObject(rq.cx, obj); else return JS_FreezeObject(rq.cx, obj); } bool ScriptInterface::LoadScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); JS::RootedObject global(rq.cx, rq.glob); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::CompileOptions options(rq.cx); // Set the line to 0 because CompileFunction silently adds a `(function() {` as the first line, // and errors get misreported. // TODO: it would probably be better to not implicitly introduce JS scopes. options.setFileAndLine(filenameStr.c_str(), 0); options.setIsRunOnce(false); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); JS::RootedObjectVector emptyScopeChain(rq.cx); JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, NULL, 0, NULL, src)); if (func == nullptr) { ScriptException::CatchPending(rq); return false; } JS::RootedValue rval(rq.cx); if (JS_CallFunction(rq.cx, nullptr, func, JS::HandleValueArray::empty(), &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScript(const VfsPath& filename, const std::string& code) const { ScriptRequest rq(this); // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = filename.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), 1); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::LoadGlobalScriptFile(const VfsPath& path) const { ScriptRequest rq(this); if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return false; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return false; } CStr code = file.DecodeUTF8(); // assume it's UTF-8 uint lineNo = 1; // CompileOptions does not copy the contents of the filename string pointer. // Passing a temporary string there will cause undefined behaviour, so we create a separate string to avoid the temporary. std::string filenameStr = path.string8(); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine(filenameStr.c_str(), lineNo); JS::SourceText src; ENSURE(src.init(rq.cx, code.c_str(), code.length(), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, &rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::Eval(const char* code, JS::MutableHandleValue rval) const { ScriptRequest rq(this); JS::CompileOptions opts(rq.cx); opts.setFileAndLine("(eval)", 1); JS::SourceText src; ENSURE(src.init(rq.cx, code, strlen(code), JS::SourceOwnership::Borrowed)); if (JS::Evaluate(rq.cx, opts, src, rval)) return true; ScriptException::CatchPending(rq); return false; } bool ScriptInterface::ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const { ScriptRequest rq(this); std::wstring attrsW = wstring_from_utf8(string_utf8); utf16string string(attrsW.begin(), attrsW.end()); if (JS_ParseJSON(rq.cx, reinterpret_cast(string.c_str()), (u32)string.size(), out)) return true; ScriptException::CatchPending(rq); return false; } void ScriptInterface::ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const { if (!VfsFileExists(path)) { LOGERROR("File '%s' does not exist", path.string8()); return; } CVFSFile file; PSRETURN ret = file.Load(g_VFS, path); if (ret != PSRETURN_OK) { LOGERROR("Failed to load file '%s': %s", path.string8(), GetErrorString(ret)); return; } std::string content(file.DecodeUTF8()); // assume it's UTF-8 if (!ParseJSON(content, out)) LOGERROR("Failed to parse '%s'", path.string8()); } struct Stringifier { static bool callback(const char16_t* buf, u32 len, void* data) { utf16string str(buf, buf+len); std::wstring strw(str.begin(), str.end()); Status err; // ignore Unicode errors static_cast(data)->stream << utf8_from_wstring(strw, &err); return true; } std::stringstream stream; }; // TODO: It's not quite clear why JS_Stringify needs JS::MutableHandleValue. |obj| should not get modified. // It probably has historical reasons and could be changed by SpiderMonkey in the future. std::string ScriptInterface::StringifyJSON(JS::MutableHandleValue obj, bool indent) const { ScriptRequest rq(this); Stringifier str; JS::RootedValue indentVal(rq.cx, indent ? JS::Int32Value(2) : JS::UndefinedValue()); if (!JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) { ScriptException::CatchPending(rq); return std::string(); } return str.stream.str(); } std::string ScriptInterface::ToString(JS::MutableHandleValue obj, bool pretty) const { ScriptRequest rq(this); if (obj.isUndefined()) return "(void 0)"; // Try to stringify as JSON if possible // (TODO: this is maybe a bad idea since it'll drop 'undefined' values silently) if (pretty) { Stringifier str; JS::RootedValue indentVal(rq.cx, JS::Int32Value(2)); if (JS_Stringify(rq.cx, obj, nullptr, indentVal, &Stringifier::callback, &str)) return str.stream.str(); // Drop exceptions raised by cyclic values before trying something else JS_ClearPendingException(rq.cx); } // Caller didn't want pretty output, or JSON conversion failed (e.g. due to cycles), // so fall back to obj.toSource() std::wstring source = L"(error)"; CallFunction(obj, "toSource", source); return utf8_from_wstring(source); } JS::Value ScriptInterface::CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const { PROFILE("CloneValueFromOtherCompartment"); ScriptRequest rq(this); JS::RootedValue out(rq.cx); ScriptInterface::StructuredClone structuredClone = otherCompartment.WriteStructuredClone(val); ReadStructuredClone(structuredClone, &out); return out.get(); } ScriptInterface::StructuredClone ScriptInterface::WriteStructuredClone(JS::HandleValue v) const { ScriptRequest rq(this); ScriptInterface::StructuredClone ret(new JSStructuredCloneData(JS::StructuredCloneScope::SameProcess)); JS::CloneDataPolicy policy; if (!JS_WriteStructuredClone(rq.cx, v, ret.get(), JS::StructuredCloneScope::SameProcess, policy, nullptr, nullptr, JS::UndefinedHandleValue)) { debug_warn(L"Writing a structured clone with JS_WriteStructuredClone failed!"); ScriptException::CatchPending(rq); return ScriptInterface::StructuredClone(); } return ret; } void ScriptInterface::ReadStructuredClone(const ScriptInterface::StructuredClone& ptr, JS::MutableHandleValue ret) const { ScriptRequest rq(this); JS::CloneDataPolicy policy; if (!JS_ReadStructuredClone(rq.cx, *ptr, JS_STRUCTURED_CLONE_VERSION, ptr->scope(), ret, policy, nullptr, nullptr)) ScriptException::CatchPending(rq); } Index: ps/trunk/source/scriptinterface/NativeWrapperDecls.h =================================================================== --- ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24982) +++ ps/trunk/source/scriptinterface/NativeWrapperDecls.h (revision 24983) @@ -1,112 +1,102 @@ /* 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( \ 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/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24982) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 24983) @@ -1,133 +1,134 @@ /* Copyright (C) 2021 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 "simulation2/system/ComponentTest.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) { ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); val = value; std::stringstream stream; CStdSerializer serializer(scriptInterface, stream); serializer.ScriptVal("", &val); CStdDeserializer deserializer(scriptInterface, stream); deserializer.ScriptVal("", &val); return val; } void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); - componentManager.GetScriptInterface().RegisterFunction ("SerializationRoundTrip"); + ScriptRequest rq(componentManager.GetScriptInterface()); + ScriptFunction::Register(rq, "SerializationRoundTrip"); load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "LoadComponentScript"); ScriptFunction::Register(rq, "LoadHelperScript"); ScriptFunction::Register(rq, "SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/renderer/scripting/JSInterface_Renderer.h =================================================================== --- ps/trunk/source/renderer/scripting/JSInterface_Renderer.h (revision 24982) +++ ps/trunk/source/renderer/scripting/JSInterface_Renderer.h (revision 24983) @@ -1,33 +1,28 @@ /* 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_JSINTERFACE_RENDERER #define INCLUDED_JSINTERFACE_RENDERER -#include "scriptinterface/ScriptInterface.h" +class ScriptRequest; namespace JSI_Renderer { - std::string GetRenderPath(ScriptInterface::CmptPrivate* pCmptPrivate); - bool TextureExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename); - - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } -#undef DECLARE_BOOLEAN_SCRIPT_SETTING - #endif // INCLUDED_JSINTERFACE_RENDERER Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24982) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 24983) @@ -1,1122 +1,1099 @@ /* 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 "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include "ps/scripting/JSInterface_VFS.h" #include "ps/TemplateLoader.h" #include "ps/Util.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/HierarchicalPathfinder.h" #include "simulation2/helpers/LongPathfinder.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/SerializedTypes.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" extern void QuitEngine(); /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS::Values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_Behavior(behavior), m_ScriptInterface(scriptInterface), m_Obj(scriptInterface->GetGeneralJSContext()) { } bool Initialise() { // LoadScripts will only load each script once even though we call it for each player if (!m_Worker.LoadScripts(m_AIName)) return false; ScriptRequest rq(m_ScriptInterface); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(rq.cx); m_Worker.LoadMetadata(path, &metadata); if (metadata.isUndefined()) { LOGERROR("Failed to create AI player: can't find %s", path.string8()); return false; } // Get the constructor name from the metadata std::string moduleName; std::string constructor; JS::RootedValue objectWithConstructor(rq.cx); // object that should contain the constructor function JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->HasProperty(metadata, "moduleName")) { LOGERROR("Failed to create AI player: %s: missing 'moduleName'", path.string8()); return false; } m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName); if (!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR("Failed to create AI player: %s: can't find the module that should contain the constructor: '%s'", path.string8(), moduleName); return false; } if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor)) { LOGERROR("Failed to create AI player: %s: missing 'constructor'", path.string8()); return false; } // Get the constructor function from the loaded scripts if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR("Failed to create AI player: %s: can't find constructor '%s'", path.string8(), constructor); return false; } m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent); // Set up the data to pass as the constructor argument JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "player", m_Player, "difficulty", m_Difficulty, "behavior", m_Behavior); if (!m_UseSharedComponent) { ENSURE(m_Worker.m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false); } JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings.get())); m_ScriptInterface->CallConstructor(ctor, argv, &m_Obj); if (m_Obj.get().isNull()) { LOGERROR("Failed to create AI player: %s: error calling constructor '%s'", path.string8(), constructor); return false; } return true; } void Run(JS::HandleValue state, int playerID) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, JS::HandleValue SharedAI) { m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(m_Obj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; u8 m_Difficulty; std::wstring m_Behavior; bool m_UseSharedComponent; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptInterface; JS::PersistentRootedValue m_Obj; std::vector m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector commands; }; CAIWorker() : m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptContext)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false), m_EntityTemplates(g_ScriptContext->GetGeneralJSContext()), m_SharedAIObj(g_ScriptContext->GetGeneralJSContext()), m_PassabilityMapVal(g_ScriptContext->GetGeneralJSContext()), m_TerritoryMapVal(g_ScriptContext->GetGeneralJSContext()) { m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->SetCallbackData(static_cast (this)); JS_AddExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); - m_ScriptInterface->RegisterFunction("PostCommand"); - m_ScriptInterface->RegisterFunction("IncludeModule"); - m_ScriptInterface->RegisterFunction("Exit"); + ScriptRequest rq(m_ScriptInterface); +#define REGISTER_FUNC_NAME(func, name) \ + ScriptFunction::Register<&CAIWorker::func, ScriptFunction::ObjectFromCBData>(rq, name); + + REGISTER_FUNC_NAME(PostCommand, "PostCommand"); + REGISTER_FUNC_NAME(LoadScripts, "IncludeModule"); + ScriptFunction::Register(rq, "Exit"); + + REGISTER_FUNC_NAME(ComputePathScript, "ComputePath"); - m_ScriptInterface->RegisterFunction("ComputePath"); + REGISTER_FUNC_NAME(DumpImage, "DumpImage"); + REGISTER_FUNC_NAME(GetTemplate, "GetTemplate"); - m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); - m_ScriptInterface->RegisterFunction("GetTemplate"); +#undef REGISTER_FUNC_NAME - JSI_VFS::RegisterScriptFunctions_Simulation(*(m_ScriptInterface.get())); + JSI_VFS::RegisterScriptFunctions_Simulation(rq); // Globalscripts may use VFS script functions m_ScriptInterface->LoadGlobalScripts(); } ~CAIWorker() { JS_RemoveExtraGCRootsTracer(m_ScriptInterface->GetGeneralJSContext(), Trace, this); } bool HasLoadedEntityTemplates() const { return m_HasLoadedEntityTemplates; } bool LoadScripts(const std::wstring& moduleName) { // Ignore modules that are already loaded if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedModules.insert(moduleName); // Load and execute *.js VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0) { LOGERROR("Failed to load AI scripts for module %s", utf8_from_wstring(moduleName)); return false; } for (const VfsPath& path : pathnames) { if (!m_ScriptInterface->LoadGlobalScriptFile(path)) { LOGERROR("Failed to load script %s", path.string8()); return false; } } return true; } - static void IncludeModule(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name) - { - ENSURE(pCmptPrivate->pCBData); - CAIWorker* self = static_cast (pCmptPrivate->pCBData); - self->LoadScripts(name); - } - - static void PostCommand(ScriptInterface::CmptPrivate* pCmptPrivate, int playerid, JS::HandleValue cmd) - { - ENSURE(pCmptPrivate->pCBData); - CAIWorker* self = static_cast (pCmptPrivate->pCBData); - self->PostCommand(playerid, cmd); - } - void PostCommand(int playerid, JS::HandleValue cmd) { for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd)); return; } } LOGERROR("Invalid playerid in PostCommand!"); } - static JS::Value ComputePath(ScriptInterface::CmptPrivate* pCmptPrivate, - JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) + JS::Value ComputePathScript(JS::HandleValue position, JS::HandleValue goal, pass_class_t passClass) { - ENSURE(pCmptPrivate->pCBData); - CAIWorker* self = static_cast (pCmptPrivate->pCBData); - ScriptRequest rq(self->m_ScriptInterface); + ScriptRequest rq(m_ScriptInterface); CFixedVector2D pos, goalPos; std::vector waypoints; JS::RootedValue retVal(rq.cx); - self->m_ScriptInterface->FromJSVal(rq, position, pos); - self->m_ScriptInterface->FromJSVal(rq, goal, goalPos); + m_ScriptInterface->FromJSVal(rq, position, pos); + m_ScriptInterface->FromJSVal(rq, goal, goalPos); - self->ComputePath(pos, goalPos, passClass, waypoints); - self->m_ScriptInterface->ToJSVal >(rq, &retVal, waypoints); + ComputePath(pos, goalPos, passClass, waypoints); + m_ScriptInterface->ToJSVal >(rq, &retVal, waypoints); return retVal; } void ComputePath(const CFixedVector2D& pos, const CFixedVector2D& goal, pass_class_t passClass, std::vector& waypoints) { WaypointPath ret; PathGoal pathGoal = { PathGoal::POINT, goal.X, goal.Y }; m_LongPathfinder.ComputePath(m_HierarchicalPathfinder, pos.X, pos.Y, pathGoal, passClass, ret); for (Waypoint& wp : ret.m_Waypoints) waypoints.emplace_back(wp.x, wp.z); } - static CParamNode GetTemplate(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& name) - { - ENSURE(pCmptPrivate->pCBData); - CAIWorker* self = static_cast (pCmptPrivate->pCBData); - - return self->GetTemplate(name); - } - CParamNode GetTemplate(const std::string& name) { if (!m_TemplateLoader.TemplateExists(name)) return CParamNode(false); return m_TemplateLoader.GetTemplateFileData(name).GetChild("Entity"); } - static void ExitProgram(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) - { - QuitEngine(); - } - /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ - static void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) + void DumpImage(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& name, const std::vector& data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } void SetRNGSeed(u32 seed) { m_RNG.seed(seed); } bool TryLoadSharedComponent() { ScriptRequest rq(m_ScriptInterface); // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; if (LoadScripts(L"common-api")) m_HasSharedComponent = true; else return false; // mainly here for the error messages OsPath path = L"simulation/ai/common-api/"; // Constructor name is SharedScript, it's in the module API3 // TODO: Hardcoding this is bad, we need a smarter way. JS::RootedValue AIModule(rq.cx); JS::RootedValue global(rq.cx, rq.globalValue()); JS::RootedValue ctor(rq.cx); if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find module '%s'", path.string8(), "API3"); return false; } if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR("Failed to create shared AI component: %s: can't find constructor '%s'", path.string8(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue playersID(rq.cx); ScriptInterface::CreateObject(rq, &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(rq.cx); m_ScriptInterface->ToJSVal(rq, &val, m_Players[i]->m_Player); m_ScriptInterface->SetPropertyInt(playersID, i, val, true); } ENSURE(m_HasLoadedEntityTemplates); JS::RootedValue settings(rq.cx); ScriptInterface::CreateObject( rq, &settings, "players", playersID, "templates", m_EntityTemplates); JS::RootedValueVector argv(rq.cx); ignore_result(argv.append(settings)); m_ScriptInterface->CallConstructor(ctor, argv, &m_SharedAIObj); if (m_SharedAIObj.get().isNull()) { LOGERROR("Failed to create shared AI component: %s: error calling constructor '%s'", path.string8(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, u8 difficulty, const std::wstring& behavior) { shared_ptr ai = std::make_shared(*this, aiName, player, difficulty, behavior, m_ScriptInterface); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const ScriptInterface::StructuredClone& gameState, const Grid& passabilityMap, const Grid& territoryMap, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { // this will be run last by InitGame.js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); m_ScriptInterface->ReadStructuredClone(gameState, &state); ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, passabilityMap); ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, territoryMap); m_PassabilityMap = passabilityMap; m_NonPathfindingPassClasses = nonPathfindingPassClassMasks; m_PathfindingPassClasses = pathfindingPassClassMasks; m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); if (m_HasSharedComponent) { m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void UpdateGameState(const ScriptInterface::StructuredClone& gameState) { ENSURE(m_CommandsComputed); m_GameState = gameState; } void UpdatePathfinder(const Grid& passabilityMap, bool globallyDirty, const Grid& dirtinessGrid, bool justDeserialized, const std::map& nonPathfindingPassClassMasks, const std::map& pathfindingPassClassMasks) { ENSURE(m_CommandsComputed); bool dimensionChange = m_PassabilityMap.m_W != passabilityMap.m_W || m_PassabilityMap.m_H != passabilityMap.m_H; m_PassabilityMap = passabilityMap; if (globallyDirty) { m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } else { m_LongPathfinder.Update(&m_PassabilityMap); m_HierarchicalPathfinder.Update(&m_PassabilityMap, dirtinessGrid); } ScriptRequest rq(m_ScriptInterface); if (dimensionChange || justDeserialized) ScriptInterface::ToJSVal(rq, &m_PassabilityMapVal, m_PassabilityMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_PassabilityMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(NavcellData)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint16ArrayData(dataObj, &sharedMemory, nogc), m_PassabilityMap.m_Data, nbytes); } } void UpdateTerritoryMap(const Grid& territoryMap) { ENSURE(m_CommandsComputed); bool dimensionChange = m_TerritoryMap.m_W != territoryMap.m_W || m_TerritoryMap.m_H != territoryMap.m_H; m_TerritoryMap = territoryMap; ScriptRequest rq(m_ScriptInterface); if (dimensionChange) ScriptInterface::ToJSVal(rq, &m_TerritoryMapVal, m_TerritoryMap); else { // Avoid a useless memory reallocation followed by a garbage collection. JS::RootedObject mapObj(rq.cx, &m_TerritoryMapVal.toObject()); JS::RootedValue mapData(rq.cx); ENSURE(JS_GetProperty(rq.cx, mapObj, "data", &mapData)); JS::RootedObject dataObj(rq.cx, &mapData.toObject()); u32 length = 0; ENSURE(JS::GetArrayLength(rq.cx, dataObj, &length)); u32 nbytes = (u32)(length * sizeof(u8)); bool sharedMemory; JS::AutoCheckCannotGC nogc; memcpy((void*)JS_GetUint8ArrayData(dataObj, &sharedMemory, nogc), m_TerritoryMap.m_Data, nbytes); } } void StartComputation() { m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void LoadEntityTemplates(const std::vector >& templates) { ScriptRequest rq(m_ScriptInterface); m_HasLoadedEntityTemplates = true; ScriptInterface::CreateObject(rq, &m_EntityTemplates); JS::RootedValue val(rq.cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(rq, false, &val); m_ScriptInterface->SetProperty(m_EntityTemplates, templates[i].first.c_str(), val, true); } } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { if (m_Players.empty()) return; ScriptRequest rq(m_ScriptInterface); std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) serializer.ScriptVal("sharedData", &m_SharedAIObj); for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.String("behavior", m_Players[i]->m_Behavior, 1, 256); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { JS::RootedValue val(rq.cx); m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } serializer.ScriptVal("data", &m_Players[i]->m_Obj); } // AI pathfinder Serializer(serializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(serializer, "pathfinding pass classes", m_PathfindingPassClasses); serializer.NumberU16_Unbounded("pathfinder grid w", m_PassabilityMap.m_W); serializer.NumberU16_Unbounded("pathfinder grid h", m_PassabilityMap.m_H); serializer.RawBytes("pathfinder grid data", (const u8*)m_PassabilityMap.m_Data, m_PassabilityMap.m_W*m_PassabilityMap.m_H*sizeof(NavcellData)); } void Deserialize(std::istream& stream, u32 numAis) { m_PlayerMetadata.clear(); m_Players.clear(); if (numAis == 0) return; ScriptRequest rq(m_ScriptInterface); ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad CStdDeserializer deserializer(*m_ScriptInterface, stream); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); deserializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { TryLoadSharedComponent(); deserializer.ScriptObjectAssign("sharedData", m_SharedAIObj); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; u8 difficulty; std::wstring behavior; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); deserializer.String("behavior", behavior, 1, 256); if (!AddPlayer(name, player, difficulty, behavior)) throw PSERROR_Deserialize_ScriptError(); u32 numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { JS::RootedValue val(rq.cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } deserializer.ScriptObjectAssign("data", m_Players.back()->m_Obj); } // AI pathfinder Serializer(deserializer, "non pathfinding pass classes", m_NonPathfindingPassClasses); Serializer(deserializer, "pathfinding pass classes", m_PathfindingPassClasses); u16 mapW, mapH; deserializer.NumberU16_Unbounded("pathfinder grid w", mapW); deserializer.NumberU16_Unbounded("pathfinder grid h", mapH); m_PassabilityMap = Grid(mapW, mapH); deserializer.RawBytes("pathfinder grid data", (u8*)m_PassabilityMap.m_Data, mapW*mapH*sizeof(NavcellData)); m_LongPathfinder.Reload(&m_PassabilityMap); m_HierarchicalPathfinder.Recompute(&m_PassabilityMap, m_NonPathfindingPassClasses, m_PathfindingPassClasses); } int getPlayerSize() { return m_Players.size(); } private: static void Trace(JSTracer *trc, void *data) { reinterpret_cast(data)->TraceMember(trc); } void TraceMember(JSTracer *trc) { for (std::pair>& metadata : m_PlayerMetadata) JS::TraceEdge(trc, &metadata.second, "CAIWorker::m_PlayerMetadata"); } void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata m_ScriptInterface->ReadJSONFile(path, out); m_PlayerMetadata[path] = JS::Heap(out); return; } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage ScriptRequest rq(m_ScriptInterface); JS::RootedValue state(rq.cx); { PROFILE3("AI compute read state"); m_ScriptInterface->ReadStructuredClone(m_GameState, &state); m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); m_ScriptInterface->CallFunctionVoid(m_SharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the context destructor. shared_ptr m_ScriptContext; shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; JS::PersistentRootedValue m_EntityTemplates; bool m_HasLoadedEntityTemplates; std::map > m_PlayerMetadata; std::vector > m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; JS::PersistentRootedValue m_SharedAIObj; std::vector m_Commands; std::set m_LoadedModules; ScriptInterface::StructuredClone m_GameState; Grid m_PassabilityMap; JS::PersistentRootedValue m_PassabilityMapVal; Grid m_TerritoryMap; JS::PersistentRootedValue m_TerritoryMapVal; std::map m_NonPathfindingPassClasses; std::map m_PathfindingPassClasses; HierarchicalPathfinder m_HierarchicalPathfinder; LongPathfinder m_LongPathfinder; bool m_CommandsComputed; CTemplateLoader m_TemplateLoader; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager)) { } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_TerritoriesDirtyBlinkingID = 0; m_JustDeserialized = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { serialize.NumberU32_Unbounded("num ais", m_Worker.getPlayerSize()); // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); u32 numAis; deserialize.NumberU32_Unbounded("num ais", numAis); if (numAis > 0) LoadUsedEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream(), numAis); m_JustDeserialized = true; } virtual void AddPlayer(const std::wstring& id, player_id_t player, u8 difficulty, const std::wstring& behavior) { LoadUsedEntityTemplates(); m_Worker.AddPlayer(id, player, difficulty, behavior); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } virtual void SetRNGSeed(u32 seed) { m_Worker.SetRNGSeed(seed); } virtual void TryLoadSharedComponent() { m_Worker.TryLoadSharedComponent(); } virtual void RunGamestateInit() { const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. JS::RootedValue state(rq.cx); cmpAIInterface->GetFullRepresentation(&state, true); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID)) territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); LoadPathfinderClasses(state); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; if (cmpPathfinder) cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } virtual void StartComputation() { PROFILE("AI setup"); const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(rq.cx); if (m_JustDeserialized) cmpAIInterface->GetFullRepresentation(&state, false); else cmpAIInterface->GetRepresentation(&state); LoadPathfinderClasses(state); // add the pathfinding classes to it // Update the game state m_Worker.UpdateGameState(scriptInterface.WriteStructuredClone(state)); // Update the pathfinding data CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) { const GridUpdateInformation& dirtinessInformations = cmpPathfinder->GetAIPathfinderDirtinessInformation(); if (dirtinessInformations.dirty || m_JustDeserialized) { const Grid& passabilityMap = cmpPathfinder->GetPassabilityGrid(); std::map nonPathfindingPassClassMasks, pathfindingPassClassMasks; cmpPathfinder->GetPassabilityClasses(nonPathfindingPassClassMasks, pathfindingPassClassMasks); m_Worker.UpdatePathfinder(passabilityMap, dirtinessInformations.globallyDirty, dirtinessInformations.dirtinessGrid, m_JustDeserialized, nonPathfindingPassClassMasks, pathfindingPassClassMasks); } cmpPathfinder->FlushAIPathfinderDirtinessInformation(); } // Update the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdateAI first CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && (cmpTerritoryManager->NeedUpdateAI(&m_TerritoriesDirtyID, &m_TerritoriesDirtyBlinkingID) || m_JustDeserialized)) { const Grid& territoryMap = cmpTerritoryManager->GetTerritoryGrid(); m_Worker.UpdateTerritoryMap(territoryMap); } m_Worker.StartComputation(); m_JustDeserialized = false; } virtual void PushCommands() { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue clonedCommandVal(rq.cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, clonedCommandVal); } } } private: size_t m_TerritoriesDirtyID; size_t m_TerritoriesDirtyBlinkingID; bool m_JustDeserialized; /** * Load the templates of all entities on the map (called when adding a new AI player for a new game * or when deserializing) */ void LoadUsedEntityTemplates() { if (m_Worker.HasLoadedEntityTemplates()) return; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); std::vector templateNames = cmpTemplateManager->FindUsedTemplates(); std::vector > usedTemplates; usedTemplates.reserve(templateNames.size()); for (const std::string& name : templateNames) { const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(name); if (node) usedTemplates.emplace_back(name, node); } // Send the data to the worker m_Worker.LoadEntityTemplates(usedTemplates); } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; const ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue classesVal(rq.cx); ScriptInterface::CreateObject(rq, &classesVal); std::map classes; cmpPathfinder->GetPassabilityClasses(classes); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true); scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp =================================================================== --- ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp (revision 24982) +++ ps/trunk/source/renderer/scripting/JSInterface_Renderer.cpp (revision 24983) @@ -1,65 +1,68 @@ /* 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_Renderer.h" #include "graphics/TextureManager.h" #include "renderer/RenderingOptions.h" #include "renderer/Renderer.h" -#include "scriptinterface/ScriptInterface.h" +#include "scriptinterface/FunctionWrapper.h" +namespace JSI_Renderer +{ #define IMPLEMENT_BOOLEAN_SCRIPT_SETTING(NAME) \ -bool Get##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) \ +bool Get##NAME##Enabled() \ { \ -return g_RenderingOptions.Get##NAME(); \ + return g_RenderingOptions.Get##NAME(); \ } \ \ -void Set##NAME##Enabled(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled) \ +void Set##NAME##Enabled(bool enabled) \ { \ g_RenderingOptions.Set##NAME(enabled); \ } IMPLEMENT_BOOLEAN_SCRIPT_SETTING(DisplayFrustum); IMPLEMENT_BOOLEAN_SCRIPT_SETTING(DisplayShadowsFrustum); #undef IMPLEMENT_BOOLEAN_SCRIPT_SETTING -std::string JSI_Renderer::GetRenderPath(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::string GetRenderPath() { return RenderPathEnum::ToString(g_RenderingOptions.GetRenderPath()); } -bool JSI_Renderer::TextureExists(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename) +bool TextureExists(const std::wstring& filename) { return g_Renderer.GetTextureManager().TextureExists(filename); } #define REGISTER_BOOLEAN_SCRIPT_SETTING(NAME) \ -scriptInterface.RegisterFunction("Renderer_Get" #NAME "Enabled"); \ -scriptInterface.RegisterFunction("Renderer_Set" #NAME "Enabled"); +ScriptFunction::Register<&Get##NAME##Enabled>(rq, "Renderer_Get" #NAME "Enabled"); \ +ScriptFunction::Register<&Set##NAME##Enabled>(rq, "Renderer_Set" #NAME "Enabled"); -void JSI_Renderer::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("Renderer_GetRenderPath"); - scriptInterface.RegisterFunction("TextureExists"); + ScriptFunction::Register<&GetRenderPath>(rq, "Renderer_GetRenderPath"); + ScriptFunction::Register<&TextureExists>(rq, "TextureExists"); REGISTER_BOOLEAN_SCRIPT_SETTING(DisplayFrustum); REGISTER_BOOLEAN_SCRIPT_SETTING(DisplayShadowsFrustum); } #undef REGISTER_BOOLEAN_SCRIPT_SETTING +} Index: ps/trunk/source/scriptinterface/ScriptInterface.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24982) +++ ps/trunk/source/scriptinterface/ScriptInterface.h (revision 24983) @@ -1,633 +1,628 @@ /* Copyright (C) 2021 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_SCRIPTINTERFACE #define INCLUDED_SCRIPTINTERFACE #include "lib/file/vfs/vfs_path.h" #include "maths/Fixed.h" #include "ps/Errors.h" #include "scriptinterface/ScriptExceptions.h" #include "scriptinterface/ScriptTypes.h" #include ERROR_GROUP(Scripting); ERROR_TYPE(Scripting, SetupFailed); ERROR_SUBGROUP(Scripting, LoadFile); ERROR_TYPE(Scripting_LoadFile, OpenFailed); ERROR_TYPE(Scripting_LoadFile, EvalErrors); ERROR_TYPE(Scripting, CallFunctionFailed); -ERROR_TYPE(Scripting, RegisterFunctionFailed); ERROR_TYPE(Scripting, DefineConstantFailed); ERROR_TYPE(Scripting, CreateObjectFailed); ERROR_TYPE(Scripting, TypeDoesNotExist); ERROR_SUBGROUP(Scripting, DefineType); ERROR_TYPE(Scripting_DefineType, AlreadyExists); ERROR_TYPE(Scripting_DefineType, CreationFailed); // Set the maximum number of function arguments that can be handled // (This should be as small as possible (for compiler efficiency), // but as large as necessary for all wrapped functions) #define SCRIPT_INTERFACE_MAX_ARGS 8 class JSStructuredCloneData; class ScriptInterface; struct ScriptInterface_impl; class ScriptContext; // Using a global object for the context is a workaround until Simulation, AI, etc, // use their own threads and also their own contexts. extern thread_local shared_ptr g_ScriptContext; namespace boost { namespace random { class rand48; } } /** * RAII structure which encapsulates an access to the context and compartment of a ScriptInterface. * This struct provides: * - a pointer to the context, while acting like JSAutoRequest * - a pointer to the global object of the compartment, while acting like JSAutoRealm * * This way, getting and using those pointers is safe with respect to the GC * and to the separation of compartments. */ class ScriptRequest { public: ScriptRequest() = delete; ScriptRequest(const ScriptRequest& rq) = delete; ScriptRequest& operator=(const ScriptRequest& rq) = delete; ScriptRequest(const ScriptInterface& scriptInterface); ScriptRequest(const ScriptInterface* scriptInterface) : ScriptRequest(*scriptInterface) {} ScriptRequest(shared_ptr scriptInterface) : ScriptRequest(*scriptInterface) {} ~ScriptRequest(); JS::Value globalValue() const; JSContext* cx; JSObject* glob; JS::HandleObject nativeScope; private: JS::Realm* m_formerRealm; }; /** * Abstraction around a SpiderMonkey JS::Realm. * * Thread-safety: * - May be used in non-main threads. * - Each ScriptInterface must be created, used, and destroyed, all in a single thread * (it must never be shared between threads). */ class ScriptInterface { NONCOPYABLE(ScriptInterface); friend class ScriptRequest; public: /** * Constructor. - * @param nativeScopeName Name of global object that functions (via RegisterFunction) will + * @param nativeScopeName Name of global object that functions (via ScriptFunction::Register) will * be placed into, as a scoping mechanism; typically "Engine" * @param debugName Name of this interface for CScriptStats purposes. * @param context ScriptContext to use when initializing this interface. */ ScriptInterface(const char* nativeScopeName, const char* debugName, const shared_ptr& context); ~ScriptInterface(); struct CmptPrivate { ScriptInterface* pScriptInterface; // the ScriptInterface object the compartment belongs to void* pCBData; // meant to be used as the "this" object for callback functions } m_CmptPrivate; void SetCallbackData(void* pCBData); static CmptPrivate* GetScriptInterfaceAndCBData(JSContext* cx); /** * GetGeneralJSContext returns the context without starting a GC request and without * entering the ScriptInterface compartment. It should only be used in specific situations, * for instance when initializing a persistent rooted. * If you need the compartmented context of the ScriptInterface, you should create a * ScriptInterface::Request and use the context from that. */ JSContext* GetGeneralJSContext() const; shared_ptr GetContext() const; /** * Load global scripts that most script interfaces need, * located in the /globalscripts directory. VFS must be initialized. */ bool LoadGlobalScripts(); /** * Replace the default JS random number geenrator with a seeded, network-sync'd one. */ bool ReplaceNondeterministicRNG(boost::random::rand48& rng); /** * Call a constructor function, equivalent to JS "new ctor(arg)". * @param ctor An object that can be used as constructor * @param argv Constructor arguments * @param out The new object; On error an error message gets logged and out is Null (out.isNull() == true). */ void CallConstructor(JS::HandleValue ctor, JS::HandleValueArray argv, JS::MutableHandleValue out) const; JSObject* CreateCustomObject(const std::string & typeName) const; void DefineCustomObjectType(JSClass *clasp, JSNative constructor, uint minArgs, JSPropertySpec *ps, JSFunctionSpec *fs, JSPropertySpec *static_ps, JSFunctionSpec *static_fs); /** * Sets the given value to a new plain JS::Object, converts the arguments to JS::Values and sets them as properties. * This is static so that callers like ToJSVal can use it with the JSContext directly instead of having to obtain the instance using GetScriptInterfaceAndCBData. * Can throw an exception. */ template static bool CreateObject(const ScriptRequest& rq, JS::MutableHandleValue objectValue, Args const&... args) { JS::RootedObject obj(rq.cx); if (!CreateObject_(rq, &obj, args...)) return false; objectValue.setObject(*obj); return true; } /** * Sets the given value to a new JS object or Null Value in case of out-of-memory. */ static void CreateArray(const ScriptRequest& rq, JS::MutableHandleValue objectValue, size_t length = 0); /** * Set the named property on the global object. * Optionally makes it {ReadOnly, DontEnum}. We do not allow to make it DontDelete, so that it can be hotloaded * by deleting it and re-creating it, which is done by setting @p replace to true. */ template bool SetGlobal(const char* name, const T& value, bool replace = false, bool constant = true, bool enumerate = true); /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant = false, bool enumerate = true) const; /** * Set the integer-named property on the given object. * Optionally makes it {ReadOnly, DontDelete, DontEnum}. */ template bool SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant = false, bool enumerate = true) const; /** * Get the named property on the given object. */ template bool GetProperty(JS::HandleValue obj, const char* name, T& out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleValue out) const; bool GetProperty(JS::HandleValue obj, const char* name, JS::MutableHandleObject out) const; template static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out); static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleValue out); static bool GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, JS::MutableHandleObject out); /** * Get the integer-named property on the given object. */ template bool GetPropertyInt(JS::HandleValue obj, int name, T& out) const; bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleValue out) const; bool GetPropertyInt(JS::HandleValue obj, int name, JS::MutableHandleObject out) const; template static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out); static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleValue out); static bool GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, JS::MutableHandleObject out); /** * Check the named property has been defined on the given object. */ bool HasProperty(JS::HandleValue obj, const char* name) const; /** * Get an object from the global scope or any lexical scope. * This can return globally accessible objects even if they are not properties * of the global object (e.g. ES6 class definitions). * @param name - Name of the property. * @param out The object or null. */ static bool GetGlobalProperty(const ScriptRequest& rq, const std::string& name, JS::MutableHandleValue out); /** * Returns all properties of the object, both own properties and inherited. * This is essentially equivalent to calling Object.getOwnPropertyNames() * and recursing up the prototype chain. * NB: this does not return properties with symbol or numeric keys, as that would * require a variant in the vector, and it's not useful for now. * @param enumerableOnly - only return enumerable properties. */ bool EnumeratePropertyNames(JS::HandleValue objVal, bool enumerableOnly, std::vector& out) const; bool SetPrototype(JS::HandleValue obj, JS::HandleValue proto); bool FreezeObject(JS::HandleValue objVal, bool deep) const; /** * Convert an object to a UTF-8 encoded string, either with JSON * (if pretty == true and there is no JSON error) or with toSource(). * * We have to use a mutable handle because JS_Stringify requires that for unknown reasons. */ std::string ToString(JS::MutableHandleValue obj, bool pretty = false) const; /** * Parse a UTF-8-encoded JSON string. Returns the unmodified value on error * and prints an error message. * @return true on success; false otherwise */ bool ParseJSON(const std::string& string_utf8, JS::MutableHandleValue out) const; /** * Read a JSON file. Returns the unmodified value on error and prints an error message. */ void ReadJSONFile(const VfsPath& path, JS::MutableHandleValue out) const; /** * Stringify to a JSON string, UTF-8 encoded. Returns an empty string on error. */ std::string StringifyJSON(JS::MutableHandleValue obj, bool indent = true) const; /** * Load and execute the given script in a new function scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @param filename Name for debugging purposes (not used to load the file) * @param code JS code to execute * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScript(const VfsPath& filename, const std::string& code) const; /** * Load and execute the given script in the global scope. * @return true on successful compilation and execution; false otherwise */ bool LoadGlobalScriptFile(const VfsPath& path) const; /** * Evaluate some JS code in the global scope. * @return true on successful compilation and execution; false otherwise */ bool Eval(const char* code) const; bool Eval(const char* code, JS::MutableHandleValue out) const; template bool Eval(const char* code, T& out) const; /** * Convert a JS::Value to a C++ type. (This might trigger GC.) */ template static bool FromJSVal(const ScriptRequest& rq, const JS::HandleValue val, T& ret); /** * Convert a C++ type to a JS::Value. (This might trigger GC. The return * value must be rooted if you don't want it to be collected.) * NOTE: We are passing the JS::Value by reference instead of returning it by value. * The reason is a memory corruption problem that appears to be caused by a bug in Visual Studio. * Details here: http://www.wildfiregames.com/forum/index.php?showtopic=17289&p=285921 */ template static void ToJSVal(const ScriptRequest& rq, JS::MutableHandleValue ret, T const& val); /** * Convert a named property of an object to a C++ type. */ template static bool FromJSProperty(const ScriptRequest& rq, const JS::HandleValue val, const char* name, T& ret, bool strict = false); /** * MathRandom (this function) calls the random number generator assigned to this ScriptInterface instance and * returns the generated number. * Math_random (with underscore, not this function) is a global function, but different random number generators can be * stored per ScriptInterface. It calls MathRandom of the current ScriptInterface instance. */ bool MathRandom(double& nbr); /** * Structured clones are a way to serialize 'simple' JS::Values into a buffer * that can safely be passed between compartments and between threads. * A StructuredClone can be stored and read multiple times if desired. * We wrap them in shared_ptr so memory management is automatic and * thread-safe. */ using StructuredClone = shared_ptr; StructuredClone WriteStructuredClone(JS::HandleValue v) const; void ReadStructuredClone(const StructuredClone& ptr, JS::MutableHandleValue ret) const; /** * Construct a new value (usable in this ScriptInterface's compartment) by cloning * a value from a different compartment. * Complex values (functions, XML, etc) won't be cloned correctly, but basic * types and cyclic references should be fine. */ JS::Value CloneValueFromOtherCompartment(const ScriptInterface& otherCompartment, JS::HandleValue val) const; /** * Retrieve the private data field of a JSObject that is an instance of the given JSClass. */ template static T* GetPrivate(const ScriptRequest& rq, JS::HandleObject thisobj, JSClass* jsClass) { T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisobj, jsClass, nullptr)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Retrieve the private data field of a JS Object that is an instance of the given JSClass. * If an error occurs, GetPrivate will report it with the according stack. */ template static T* GetPrivate(const ScriptRequest& rq, JS::CallArgs& callArgs, JSClass* jsClass) { if (!callArgs.thisv().isObject()) { ScriptException::Raise(rq, "Cannot retrieve private JS class data because from a non-object value!"); return nullptr; } JS::RootedObject thisObj(rq.cx, &callArgs.thisv().toObject()); T* value = static_cast(JS_GetInstancePrivate(rq.cx, thisObj, jsClass, &callArgs)); if (value == nullptr) ScriptException::Raise(rq, "Private data of the given object is null!"); return value; } /** * Converts |a| if needed and assigns it to |handle|. * This is meant for use in other templates where we want to use the same code for JS::RootedValue&/JS::HandleValue and * other types. Note that functions are meant to take JS::HandleValue instead of JS::RootedValue&, but this implicit * conversion does not work for templates (exact type matches required for type deduction). * A similar functionality could also be implemented as a ToJSVal specialization. The current approach was preferred * because "conversions" from JS::HandleValue to JS::MutableHandleValue are unusual and should not happen "by accident". */ template static void AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a); /** * The same as AssignOrToJSVal, but also allows JS::Value for T. * In most cases it's not safe to use the plain (unrooted) JS::Value type, but this can happen quite * easily with template functions. The idea is that the linker prints an error if AssignOrToJSVal is * used with JS::Value. If the specialization for JS::Value should be allowed, you can use this * "unrooted" version of AssignOrToJSVal. */ template static void AssignOrToJSValUnrooted(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { AssignOrToJSVal(rq, handle, a); } /** * Converts |val| to T if needed or just returns it if it's a handle. * This is meant for use in other templates where we want to use the same code for JS::HandleValue and * other types. */ template static T AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret); private: static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj); template static bool CreateObject_(const ScriptRequest& rq, JS::MutableHandleObject obj, const char* propertyName, const T& propertyValue, Args const&... args) { JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, propertyValue); return CreateObject_(rq, obj, args...) && JS_DefineProperty(rq.cx, obj, propertyName, val, JSPROP_ENUMERATE); } bool CallFunction_(JS::HandleValue val, const char* name, JS::HandleValueArray argv, JS::MutableHandleValue ret) const; bool SetGlobal_(const char* name, JS::HandleValue value, bool replace, bool constant, bool enumerate); bool SetProperty_(JS::HandleValue obj, const char* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetProperty_(JS::HandleValue obj, const wchar_t* name, JS::HandleValue value, bool constant, bool enumerate) const; bool SetPropertyInt_(JS::HandleValue obj, int name, JS::HandleValue value, bool constant, bool enumerate) const; struct CustomType { JS::PersistentRootedObject m_Prototype; JSClass* m_Class; JSNative m_Constructor; }; - void Register(const char* name, JSNative fptr, size_t nargs) const; // Take care to keep this declaration before heap rooted members. Destructors of heap rooted // members have to be called before the custom destructor of ScriptInterface_impl. std::unique_ptr m; boost::random::rand48* m_rng; std::map m_CustomObjectTypes; // The nasty macro/template bits are split into a separate file so you don't have to look at them public: #include "NativeWrapperDecls.h" // This declares: // // template - // void RegisterFunction(const char* functionName) const; - // - // template // static JSNative call; // // template // static JSNative callMethod; // // template // static JSNative callMethodConst; // // template // static size_t nargs(); // // template // bool CallFunction(JS::HandleValue val, const char* name, R& ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::Rooted* ret, const T0&...) const; // // template // bool CallFunction(JS::HandleValue val, const char* name, JS::MutableHandle ret, const T0&...) const; // // template // bool CallFunctionVoid(JS::HandleValue val, const char* name, const T0&...) const; }; // Implement those declared functions #include "NativeWrapperDefns.h" template inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& rq, JS::MutableHandleValue handle, const T& a) { ToJSVal(rq, handle, a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::PersistentRootedValue& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal >(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Heap& a) { handle.set(a); } template<> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::RootedValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSVal(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::HandleValue& a) { handle.set(a); } template <> inline void ScriptInterface::AssignOrToJSValUnrooted(const ScriptRequest& UNUSED(rq), JS::MutableHandleValue handle, const JS::Value& a) { handle.set(a); } template inline T ScriptInterface::AssignOrFromJSVal(const ScriptRequest& rq, const JS::HandleValue& val, bool& ret) { T retVal; ret = FromJSVal(rq, val, retVal); return retVal; } template<> inline JS::HandleValue ScriptInterface::AssignOrFromJSVal(const ScriptRequest& UNUSED(rq), const JS::HandleValue& val, bool& ret) { ret = true; return val; } template bool ScriptInterface::SetGlobal(const char* name, const T& value, bool replace, bool constant, bool enumerate) { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetGlobal_(name, val, replace, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const char* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetProperty(JS::HandleValue obj, const wchar_t* name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetProperty_(obj, name, val, constant, enumerate); } template bool ScriptInterface::SetPropertyInt(JS::HandleValue obj, int name, const T& value, bool constant, bool enumerate) const { ScriptRequest rq(this); JS::RootedValue val(rq.cx); AssignOrToJSVal(rq, &val, value); return SetPropertyInt_(obj, name, val, constant, enumerate); } template bool ScriptInterface::GetProperty(JS::HandleValue obj, const char* name, T& out) const { ScriptRequest rq(this); return GetProperty(rq, obj, name, out); } template bool ScriptInterface::GetProperty(const ScriptRequest& rq, JS::HandleValue obj, const char* name, T& out) { JS::RootedValue val(rq.cx); if (!GetProperty(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::GetPropertyInt(JS::HandleValue obj, int name, T& out) const { ScriptRequest rq(this); return GetPropertyInt(rq, obj, name, out); } template bool ScriptInterface::GetPropertyInt(const ScriptRequest& rq, JS::HandleValue obj, int name, T& out) { JS::RootedValue val(rq.cx); if (!GetPropertyInt(rq, obj, name, &val)) return false; return FromJSVal(rq, val, out); } template bool ScriptInterface::Eval(const char* code, T& ret) const { ScriptRequest rq(this); JS::RootedValue rval(rq.cx); if (!Eval(code, &rval)) return false; return FromJSVal(rq, rval, ret); } #endif // INCLUDED_SCRIPTINTERFACE Index: ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp =================================================================== --- ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 24982) +++ ps/trunk/source/simulation2/scripting/JSInterface_Simulation.cpp (revision 24983) @@ -1,220 +1,224 @@ /* Copyright (C) 2021 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_Simulation.h" #include "graphics/GameView.h" #include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Pyrogenesis.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpGuiInterface.h" #include "simulation2/components/ICmpObstruction.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/helpers/Geometry.h" #include "simulation2/helpers/Selection.h" #include "simulation2/Simulation2.h" #include "simulation2/system/Entity.h" #include #include -JS::Value JSI_Simulation::GuiInterfaceCall(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue data) +namespace JSI_Simulation +{ +JS::Value GuiInterfaceCall(const ScriptInterface& scriptInterface, const std::wstring& name, JS::HandleValue data) { if (!g_Game) return JS::UndefinedValue(); CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpGuiInterface(*sim, SYSTEM_ENTITY); if (!cmpGuiInterface) return JS::UndefinedValue(); ScriptRequest rqSim(sim->GetScriptInterface()); - JS::RootedValue arg(rqSim.cx, sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), data)); + JS::RootedValue arg(rqSim.cx, sim->GetScriptInterface().CloneValueFromOtherCompartment(scriptInterface, data)); JS::RootedValue ret(rqSim.cx); cmpGuiInterface->ScriptCall(g_Game->GetViewedPlayerID(), name, arg, &ret); - return pCmptPrivate->pScriptInterface->CloneValueFromOtherCompartment(sim->GetScriptInterface(), ret); + return scriptInterface.CloneValueFromOtherCompartment(sim->GetScriptInterface(), ret); } -void JSI_Simulation::PostNetworkCommand(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue cmd) +void PostNetworkCommand(const ScriptInterface& scriptInterface, JS::HandleValue cmd) { if (!g_Game) return; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr cmpCommandQueue(*sim, SYSTEM_ENTITY); if (!cmpCommandQueue) return; ScriptRequest rqSim(sim->GetScriptInterface()); JS::RootedValue cmd2(rqSim.cx, - sim->GetScriptInterface().CloneValueFromOtherCompartment(*(pCmptPrivate->pScriptInterface), cmd)); + sim->GetScriptInterface().CloneValueFromOtherCompartment(scriptInterface, cmd)); cmpCommandQueue->PostNetworkCommand(cmd2); } -void JSI_Simulation::DumpSimState(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +void DumpSimState() { OsPath path = psLogDir()/"sim_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); g_Game->GetSimulation2()->DumpDebugState(file); } -entity_id_t JSI_Simulation::PickEntityAtPoint(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int x, int y) +entity_id_t PickEntityAtPoint(int x, int y) { return EntitySelection::PickEntityAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetViewedPlayerID(), false); } -std::vector JSI_Simulation::PickPlayerEntitiesInRect(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int x0, int y0, int x1, int y1, int player) +std::vector PickPlayerEntitiesInRect(int x0, int y0, int x1, int y1, int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player, false); } -std::vector JSI_Simulation::PickPlayerEntitiesOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int player) +std::vector PickPlayerEntitiesOnScreen(int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, player, false); } -std::vector JSI_Simulation::PickNonGaiaEntitiesOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::vector PickNonGaiaEntitiesOnScreen() { return EntitySelection::PickNonGaiaEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres, false); } -std::vector JSI_Simulation::GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) +std::vector GetEntitiesWithStaticObstructionOnScreen() { struct StaticObstructionFilter { bool operator()(IComponent* cmp) { ICmpObstruction* cmpObstruction = static_cast(cmp); return cmpObstruction->GetObstructionType() == ICmpObstruction::STATIC; } }; return EntitySelection::GetEntitiesWithComponentInRect(*g_Game->GetSimulation2(), IID_Obstruction, *g_Game->GetView()->GetCamera(), 0, 0, g_xres, g_yres); } -JS::Value JSI_Simulation::GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CmptPrivate* pCmptPrivate, entity_pos_t x, entity_pos_t z) +JS::Value GetEdgesOfStaticObstructionsOnScreenNearTo(const ScriptInterface& scriptInterface, entity_pos_t x, entity_pos_t z) { if (!g_Game) return JS::UndefinedValue(); CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); - ScriptRequest rq(pCmptPrivate->pScriptInterface); + ScriptRequest rq(scriptInterface); JS::RootedValue edgeList(rq.cx); ScriptInterface::CreateArray(rq, &edgeList); int edgeListIndex = 0; float distanceThreshold = 10.0f; CFG_GET_VAL("gui.session.snaptoedgesdistancethreshold", distanceThreshold); CFixedVector2D entityPos(x, z); - std::vector entities = GetEntitiesWithStaticObstructionOnScreen(pCmptPrivate); + std::vector entities = GetEntitiesWithStaticObstructionOnScreen(); for (entity_id_t entity : entities) { CmpPtr cmpObstruction(sim->GetSimContext(), entity); if (!cmpObstruction) continue; CmpPtr cmpPosition(sim->GetSimContext(), entity); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CFixedVector2D halfSize = cmpObstruction->GetStaticSize() / 2; if (halfSize.X.IsZero() || halfSize.Y.IsZero() || std::max(halfSize.X, halfSize.Y) <= fixed::FromInt(2)) continue; std::array corners = { CFixedVector2D(-halfSize.X, -halfSize.Y), CFixedVector2D(-halfSize.X, halfSize.Y), halfSize, CFixedVector2D(halfSize.X, -halfSize.Y) }; const fixed angle = cmpPosition->GetRotation().Y; for (CFixedVector2D& corner : corners) corner = corner.Rotate(angle) + cmpPosition->GetPosition2D(); for (size_t i = 0; i < corners.size(); ++i) { JS::RootedValue edge(rq.cx); const CFixedVector2D& corner = corners[i]; const CFixedVector2D& nextCorner = corners[(i + 1) % corners.size()]; const fixed distanceToEdge = Geometry::DistanceToSegment(entityPos, corner, nextCorner); if (distanceToEdge.ToFloat() > distanceThreshold) continue; CFixedVector2D normal = -(nextCorner - corner).Perpendicular(); normal.Normalize(); ScriptInterface::CreateObject( rq, &edge, "begin", corner, "end", nextCorner, "angle", angle, "normal", normal, "order", "cw"); - pCmptPrivate->pScriptInterface->SetPropertyInt(edgeList, edgeListIndex++, edge); + scriptInterface.SetPropertyInt(edgeList, edgeListIndex++, edge); } } return edgeList; } -std::vector JSI_Simulation::PickSimilarPlayerEntities(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations) +std::vector PickSimilarPlayerEntities(const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations) { return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetViewedPlayerID(), includeOffScreen, matchRank, false, allowFoundations); } -JS::Value JSI_Simulation::GetAIs(ScriptInterface::CmptPrivate* pCmptPrivate) +JS::Value GetAIs(const ScriptInterface& scriptInterface) { - return ICmpAIManager::GetAIs(*(pCmptPrivate->pScriptInterface)); + return ICmpAIManager::GetAIs(scriptInterface); } -void JSI_Simulation::SetBoundingBoxDebugOverlay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool enabled) +void SetBoundingBoxDebugOverlay(bool enabled) { ICmpSelectable::ms_EnableDebugOverlays = enabled; } -void JSI_Simulation::RegisterScriptFunctions(const ScriptInterface& scriptInterface) +void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("GuiInterfaceCall"); - scriptInterface.RegisterFunction("PostNetworkCommand"); - scriptInterface.RegisterFunction("DumpSimState"); - scriptInterface.RegisterFunction("GetAIs"); - scriptInterface.RegisterFunction("PickEntityAtPoint"); - scriptInterface.RegisterFunction, int, int, int, int, int, &PickPlayerEntitiesInRect>("PickPlayerEntitiesInRect"); - scriptInterface.RegisterFunction, int, &PickPlayerEntitiesOnScreen>("PickPlayerEntitiesOnScreen"); - scriptInterface.RegisterFunction, &PickNonGaiaEntitiesOnScreen>("PickNonGaiaEntitiesOnScreen"); - scriptInterface.RegisterFunction, &GetEntitiesWithStaticObstructionOnScreen>("GetEntitiesWithStaticObstructionOnScreen"); - scriptInterface.RegisterFunction("GetEdgesOfStaticObstructionsOnScreenNearTo"); - scriptInterface.RegisterFunction, std::string, bool, bool, bool, &PickSimilarPlayerEntities>("PickSimilarPlayerEntities"); - scriptInterface.RegisterFunction("SetBoundingBoxDebugOverlay"); + ScriptFunction::Register<&GuiInterfaceCall>(rq, "GuiInterfaceCall"); + ScriptFunction::Register<&PostNetworkCommand>(rq, "PostNetworkCommand"); + ScriptFunction::Register<&DumpSimState>(rq, "DumpSimState"); + ScriptFunction::Register<&GetAIs>(rq, "GetAIs"); + ScriptFunction::Register<&PickEntityAtPoint>(rq, "PickEntityAtPoint"); + ScriptFunction::Register<&PickPlayerEntitiesInRect>(rq, "PickPlayerEntitiesInRect"); + ScriptFunction::Register<&PickPlayerEntitiesOnScreen>(rq, "PickPlayerEntitiesOnScreen"); + ScriptFunction::Register<&PickNonGaiaEntitiesOnScreen>(rq, "PickNonGaiaEntitiesOnScreen"); + ScriptFunction::Register<&GetEntitiesWithStaticObstructionOnScreen>(rq, "GetEntitiesWithStaticObstructionOnScreen"); + ScriptFunction::Register<&GetEdgesOfStaticObstructionsOnScreenNearTo>(rq, "GetEdgesOfStaticObstructionsOnScreenNearTo"); + ScriptFunction::Register<&PickSimilarPlayerEntities>(rq, "PickSimilarPlayerEntities"); + ScriptFunction::Register<&SetBoundingBoxDebugOverlay>(rq, "SetBoundingBoxDebugOverlay"); +} } Index: ps/trunk/source/soundmanager/scripting/JSInterface_Sound.h =================================================================== --- ps/trunk/source/soundmanager/scripting/JSInterface_Sound.h (revision 24982) +++ ps/trunk/source/soundmanager/scripting/JSInterface_Sound.h (revision 24983) @@ -1,29 +1,29 @@ /* Copyright (C) 2018 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_SOUNDSCRIPTINTERFACE #define INCLUDED_SOUNDSCRIPTINTERFACE -class ScriptInterface; +class ScriptRequest; namespace JSI_Sound { - void RegisterScriptFunctions(const ScriptInterface& scriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_SOUNDSCRIPTINTERFACE Index: ps/trunk/source/soundmanager/scripting/JSInterface_Sound.cpp =================================================================== --- ps/trunk/source/soundmanager/scripting/JSInterface_Sound.cpp (revision 24982) +++ ps/trunk/source/soundmanager/scripting/JSInterface_Sound.cpp (revision 24983) @@ -1,153 +1,154 @@ /* 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_Sound.h" #include "lib/config2.h" #include "lib/utf8.h" #include "maths/Vector3D.h" #include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "soundmanager/SoundManager.h" #include namespace JSI_Sound { #if CONFIG2_AUDIO - void StartMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) + void StartMusic() { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetMusicEnabled(true); } - void StopMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) + void StopMusic() { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetMusicEnabled(false); } - void ClearPlaylist(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) + void ClearPlaylist() { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->ClearPlayListItems(); } - void AddPlaylistItem(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename) + void AddPlaylistItem(const std::wstring& filename) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->AddPlayListItem(VfsPath(filename)); } - void StartPlaylist(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool looping) + void StartPlaylist(bool looping) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->StartPlayList(looping ); } - void PlayMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename, bool looping) + void PlayMusic(const std::wstring& filename, bool looping) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->PlayAsMusic(filename, looping); } - void PlayUISound(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename, bool looping) + void PlayUISound(const std::wstring& filename, bool looping) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->PlayAsUI(filename, looping); } - void PlayAmbientSound(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename, bool looping) + void PlayAmbientSound(const std::wstring& filename, bool looping) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->PlayAsAmbient(filename, looping); } - bool MusicPlaying(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) + bool MusicPlaying() { return true; } - void SetMasterGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float gain) + void SetMasterGain(float gain) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetMasterGain(gain); } - void SetMusicGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float gain) + void SetMusicGain(float gain) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetMusicGain(gain); } - void SetAmbientGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float gain) + void SetAmbientGain(float gain) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetAmbientGain(gain); } - void SetActionGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float gain) + void SetActionGain(float gain) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetActionGain(gain); } - void SetUIGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float gain) + void SetUIGain(float gain) { if (CSoundManager* sndManager = (CSoundManager*)g_SoundManager) sndManager->SetUIGain(gain); } #else - bool MusicPlaying(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate) ){ return false; } - void PlayAmbientSound(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& UNUSED(filename), bool UNUSED(looping) ){} - void PlayUISound(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& UNUSED(filename), bool UNUSED(looping) ) {} - void PlayMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& UNUSED(filename), bool UNUSED(looping) ) {} - void StartPlaylist(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), bool UNUSED(looping) ){} - void AddPlaylistItem(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& UNUSED(filename) ){} - void ClearPlaylist(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate) ){} - void StopMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate) ){} - void StartMusic(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate) ){} - void SetMasterGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float UNUSED(gain)){} - void SetMusicGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float UNUSED(gain)){} - void SetAmbientGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float UNUSED(gain)){} - void SetActionGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float UNUSED(gain)){} - void SetUIGain(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float UNUSED(gain)){} + bool MusicPlaying( ){ return false; } + void PlayAmbientSound(const std::wstring& UNUSED(filename), bool UNUSED(looping) ){} + void PlayUISound(const std::wstring& UNUSED(filename), bool UNUSED(looping) ) {} + void PlayMusic(const std::wstring& UNUSED(filename), bool UNUSED(looping) ) {} + void StartPlaylist(bool UNUSED(looping) ){} + void AddPlaylistItem(const std::wstring& UNUSED(filename) ){} + void ClearPlaylist( ){} + void StopMusic( ){} + void StartMusic( ){} + void SetMasterGain(float UNUSED(gain)){} + void SetMusicGain(float UNUSED(gain)){} + void SetAmbientGain(float UNUSED(gain)){} + void SetActionGain(float UNUSED(gain)){} + void SetUIGain(float UNUSED(gain)){} #endif - void RegisterScriptFunctions(const ScriptInterface& scriptInterface) + void RegisterScriptFunctions(const ScriptRequest& rq) { - scriptInterface.RegisterFunction("StartMusic"); - scriptInterface.RegisterFunction("StopMusic"); - scriptInterface.RegisterFunction("ClearPlaylist"); - scriptInterface.RegisterFunction("AddPlaylistItem"); - scriptInterface.RegisterFunction("StartPlaylist"); - scriptInterface.RegisterFunction("PlayMusic"); - scriptInterface.RegisterFunction("PlayUISound"); - scriptInterface.RegisterFunction("PlayAmbientSound"); - scriptInterface.RegisterFunction("MusicPlaying"); - scriptInterface.RegisterFunction("SetMasterGain"); - scriptInterface.RegisterFunction("SetMusicGain"); - scriptInterface.RegisterFunction("SetAmbientGain"); - scriptInterface.RegisterFunction("SetActionGain"); - scriptInterface.RegisterFunction("SetUIGain"); + ScriptFunction::Register<&StartMusic>(rq, "StartMusic"); + ScriptFunction::Register<&StopMusic>(rq, "StopMusic"); + ScriptFunction::Register<&ClearPlaylist>(rq, "ClearPlaylist"); + ScriptFunction::Register<&AddPlaylistItem>(rq, "AddPlaylistItem"); + ScriptFunction::Register<&StartPlaylist>(rq, "StartPlaylist"); + ScriptFunction::Register<&PlayMusic>(rq, "PlayMusic"); + ScriptFunction::Register<&PlayUISound>(rq, "PlayUISound"); + ScriptFunction::Register<&PlayAmbientSound>(rq, "PlayAmbientSound"); + ScriptFunction::Register<&MusicPlaying>(rq, "MusicPlaying"); + ScriptFunction::Register<&SetMasterGain>(rq, "SetMasterGain"); + ScriptFunction::Register<&SetMusicGain>(rq, "SetMusicGain"); + ScriptFunction::Register<&SetAmbientGain>(rq, "SetAmbientGain"); + ScriptFunction::Register<&SetActionGain>(rq, "SetActionGain"); + ScriptFunction::Register<&SetUIGain>(rq, "SetUIGain"); } } Index: ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h =================================================================== --- ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h (revision 24982) +++ ps/trunk/source/simulation2/scripting/JSInterface_Simulation.h (revision 24983) @@ -1,43 +1,28 @@ /* 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_JSI_SIMULATION #define INCLUDED_JSI_SIMULATION -#include "scriptinterface/ScriptInterface.h" -#include "simulation2/helpers/Position.h" -#include "simulation2/system/Entity.h" +class ScriptRequest; namespace JSI_Simulation { - JS::Value GuiInterfaceCall(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& name, JS::HandleValue data); - void PostNetworkCommand(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue cmd); - entity_id_t PickEntityAtPoint(ScriptInterface::CmptPrivate* pCmptPrivate, int x, int y); - void DumpSimState(ScriptInterface::CmptPrivate* pCmptPrivate); - std::vector PickPlayerEntitiesInRect(ScriptInterface::CmptPrivate* pCmptPrivate, int x0, int y0, int x1, int y1, int player); - std::vector PickPlayerEntitiesOnScreen(ScriptInterface::CmptPrivate* pCmptPrivate, int player); - std::vector PickNonGaiaEntitiesOnScreen(ScriptInterface::CmptPrivate* pCmptPrivate); - std::vector GetEntitiesWithStaticObstructionOnScreen(ScriptInterface::CmptPrivate* pCmptPrivate); - JS::Value GetEdgesOfStaticObstructionsOnScreenNearTo(ScriptInterface::CmptPrivate* pCmptPrivate, entity_pos_t x, entity_pos_t z); - std::vector PickSimilarPlayerEntities(ScriptInterface::CmptPrivate* pCmptPrivate, const std::string& templateName, bool includeOffScreen, bool matchRank, bool allowFoundations); - JS::Value GetAIs(ScriptInterface::CmptPrivate* pCmptPrivate); - void SetBoundingBoxDebugOverlay(ScriptInterface::CmptPrivate* pCmptPrivate, bool enabled); - - void RegisterScriptFunctions(const ScriptInterface& ScriptInterface); + void RegisterScriptFunctions(const ScriptRequest& rq); } #endif // INCLUDED_JSI_SIMULATION