Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 24630) +++ ps/trunk/source/main.cpp (revision 24631) @@ -1,784 +1,742 @@ /* 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 . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/ModInstaller.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" -#include "rlinterface/RLInterface.cpp" +#include "rlinterface/RLInterface.h" #include "scriptinterface/ScriptEngine.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" #if OS_UNIX #include // geteuid #endif // OS_UNIX #if OS_MACOSX #include "lib/sysdep/os/osx/osx_atlas.h" #endif #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif #include #include extern CmdLineArgs g_args; extern CStrW g_UniqueLogPostfix; // Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) bool g_GameRestarted = false; // Determines the lifetime of the mainloop enum ShutdownType { // The application shall continue the main loop. None, // The process shall terminate as soon as possible. Quit, // The engine should be restarted in the same process, for instance to activate different mods. Restart, // Atlas should be started in the same process. RestartAsAtlas }; static ShutdownType g_Shutdown = ShutdownType::None; // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; bool IsQuitRequested() { return g_Shutdown == ShutdownType::Quit; } void QuitEngine() { g_Shutdown = ShutdownType::Quit; } void RestartEngine() { g_Shutdown = ShutdownType::Restart; } void StartAtlas() { g_Shutdown = ShutdownType::RestartAsAtlas; } // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: QuitEngine(); break; case SDL_DROPFILE: { char* dropped_filedir = ev->ev.drop.file; const Paths paths(g_args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); installer.Install(std::string(dropped_filedir), g_ScriptContext, true); SDL_free(dropped_filedir); if (installer.GetInstalledMods().empty()) LOGERROR("Failed to install mod %s", dropped_filedir); else { LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front()); RestartEngine(); } break; } case SDL_HOTKEYPRESS: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { QuitEngine(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { WriteBigScreenshot(L".bmp", 10); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { ScriptRequest rq(g_GUI->GetScriptInterface()); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(rq.cx); ScriptInterface::ToJSVal(rq, &tmpVal, ev); std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VSync) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (g_Shutdown != ShutdownType::None) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_RLInterface) g_RLInterface->TryApplyMessage(); if (g_Game && g_Game->IsGameStarted() && need_update) { if (!g_RLInterface) g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (g_SoundManager) g_SoundManager->IdleTask(); if (ShouldRender()) { Render(); { PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); ogl_WarnIfError(); } g_Renderer.OnSwapBuffers(); } g_Profiler.Frame(); g_GameRestarted = false; LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP); g_Profiler.Frame(); if (g_Game->IsGameFinished()) QuitEngine(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } static void StartRLInterface(CmdLineArgs args) { std::string server_address; CFG_GET_VAL("rlinterface.address", server_address); if (!args.Get("rl-interface").empty()) server_address = args.Get("rl-interface"); - g_RLInterface = new RLInterface(); - g_RLInterface->EnableHTTP(server_address.c_str()); + g_RLInterface = std::make_unique(server_address.c_str()); debug_printf("RL interface listening on %s\n", server_address.c_str()); } -static void RunRLServer(const bool isNonVisual, const std::vector modsToInstall, const CmdLineArgs args) -{ - int flags = INIT_MODS; - while (!Init(args, flags)) - { - flags &= ~INIT_MODS; - Shutdown(SHUTDOWN_FROM_CONFIG); - } - g_Shutdown = ShutdownType::None; - - std::vector installedMods; - if (!modsToInstall.empty()) - { - Paths paths(args); - CModInstaller installer(paths.UserData() / "mods", paths.Cache()); - - // Install the mods without deleting the pyromod files - for (const OsPath& modPath : modsToInstall) - installer.Install(modPath, g_ScriptContext, true); - - installedMods = installer.GetInstalledMods(); - } - - if (isNonVisual) - { - InitNonVisual(args); - StartRLInterface(args); - while (g_Shutdown == ShutdownType::None) - g_RLInterface->TryApplyMessage(); - QuitEngine(); - } - else - { - InitGraphics(args, 0, installedMods); - MainControllerInit(); - StartRLInterface(args); - while (g_Shutdown == ShutdownType::None) - Frame(); - } - - Shutdown(0); - MainControllerShutdown(); - CXeromyces::Terminate(); - delete g_RLInterface; -} - // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_args = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface")) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); + const bool isUsingRLInterface = args.Has("rl-interface"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } std::vector modsToInstall; for (const CStr& arg : args.GetArgsWithoutName()) { const OsPath modPath(arg); if (!CModInstaller::IsDefaultModExtension(modPath.Extension())) { debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str()); continue; } if (!FileExists(modPath)) { debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str()); continue; } if (DirectoryExists(modPath)) { debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str()); continue; } modsToInstall.emplace_back(std::move(modPath)); } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { if (!args.Has("mod")) { LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?"); CXeromyces::Terminate(); return; } Paths paths(args); g_VFS = CreateVfs(); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); MountMods(paths, GetMods(args, INIT_MODS)); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog"), !args.Has("hashtest-full") || args.Get("hashtest-full") == "true", args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true"); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); - if (args.Has("rl-interface")) - { - RunRLServer(isNonVisual, modsToInstall, args); - return; - } - // run the game int flags = INIT_MODS; do { g_Shutdown = ShutdownType::None; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptContext, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); + if (isUsingRLInterface) + StartRLInterface(args); + while (g_Shutdown == ShutdownType::None) - NonVisualFrame(); + { + if (isUsingRLInterface) + g_RLInterface->TryApplyMessage(); + else + NonVisualFrame(); + } } else { InitGraphics(args, 0, installedMods); MainControllerInit(); + if (isUsingRLInterface) + StartRLInterface(args); while (g_Shutdown == ShutdownType::None) Frame(); } // Do not install mods again in case of restart (typically from the mod selector) modsToInstall.clear(); Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (g_Shutdown == ShutdownType::Restart); #if OS_MACOSX if (g_Shutdown == ShutdownType::RestartAsAtlas) startNewAtlasProcess(); #else if (g_Shutdown == ShutdownType::RestartAsAtlas) ATLAS_RunIfOnCmdLine(args, true); #endif CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/ps/GameSetup/GameSetup.h =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.h (revision 24630) +++ ps/trunk/source/ps/GameSetup/GameSetup.h (revision 24631) @@ -1,103 +1,105 @@ /* 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_GAMESETUP #define INCLUDED_GAMESETUP // // GUI integration // // display progress / description in loading screen extern void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task); extern void Render(); extern bool ShouldRender(); /** * initialize global modules that are be needed before Init. * must be called from the very beginning of main. **/ extern void EarlyInit(); +extern void EndGame(); + enum InitFlags { // avoid setting a video mode / initializing OpenGL; assume that has // already been done and everything is ready for rendering. // needed by map editor because it creates its own window. INIT_HAVE_VMODE = 1, // skip initializing the in-game GUI. // needed by map editor because it uses its own GUI. INIT_NO_GUI = 2, // avoid setting display_error app hook // needed by map editor because it has its own wx error display INIT_HAVE_DISPLAY_ERROR = 4, // initialize the mod folders from command line parameters INIT_MODS = 8, // mount the public mod // needed by the map editor as "mod" does not provide everything it needs INIT_MODS_PUBLIC = 16 }; enum ShutdownFlags { // start shutdown from config down // needed for loading mods as specified in the config // without having to go through a full init-shutdown cycle SHUTDOWN_FROM_CONFIG = 1 }; /** * enable/disable rendering of the GUI (intended mainly for screenshots) */ extern void RenderGui(bool RenderingState); extern void RenderLogger(bool RenderingState); /** * enable/disable rendering of the cursor - this does not hide cursor, but reverts to OS style */ extern void RenderCursor(bool RenderingState); class CmdLineArgs; class Paths; extern const std::vector& GetMods(const CmdLineArgs& args, int flags); /** * Mounts all files of the given mods in the global VFS. * Make sure to call CacheEnabledModVersions after every call to this. */ extern void MountMods(const Paths& paths, const std::vector& mods); /** * Returns true if successful, false if mods changed and restart_engine was called. * In the latter case the caller should call Shutdown() with SHUTDOWN_FROM_CONFIG. */ extern bool Init(const CmdLineArgs& args, int flags); extern void InitInput(); extern void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods = std::vector()); extern void InitNonVisual(const CmdLineArgs& args); extern void Shutdown(int flags); extern void CancelLoad(const CStrW& message); extern bool InDevelopmentCopy(); #endif // INCLUDED_GAMESETUP Index: ps/trunk/source/ps/scripting/JSInterface_Game.cpp =================================================================== --- ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24630) +++ ps/trunk/source/ps/scripting/JSInterface_Game.cpp (revision 24631) @@ -1,186 +1,185 @@ /* Copyright (C) 2020 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "JSInterface_Game.h" #include "graphics/Terrain.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Replay.h" #include "ps/World.h" +#include "ps/GameSetup/GameSetup.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/system/TurnManager.h" #include "simulation2/Simulation2.h" #include "soundmanager/SoundManager.h" -extern void EndGame(); - bool JSI_Game::IsGameStarted(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { return g_Game; } void JSI_Game::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)) { EndGame(); } int JSI_Game::GetPlayerID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { if (!g_Game) return -1; return g_Game->GetPlayerID(); } void JSI_Game::SetPlayerID(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int id) { if (!g_Game) return; g_Game->SetPlayerID(id); } void JSI_Game::SetViewedPlayer(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), int id) { if (!g_Game) return; g_Game->SetViewedPlayerID(id); } float JSI_Game::GetSimRate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { return g_Game->GetSimRate(); } void JSI_Game::SetSimRate(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), float rate) { g_Game->SetSimRate(rate); } bool JSI_Game::IsPaused(ScriptInterface::CmptPrivate* pCmptPrivate) { 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) { 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->Pause(pause); #endif if (g_NetClient && sendMessage) g_NetClient->SendPausedMessage(pause); } bool JSI_Game::IsVisualReplay(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { if (!g_Game) return false; return g_Game->IsVisualReplay(); } std::wstring JSI_Game::GetCurrentReplayDirectory(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { 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) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void JSI_Game::RewindTimeWarp(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void JSI_Game::DumpTerrainMipmap(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate)) { VfsPath filename(L"screenshots/terrainmipmap.png"); g_Game->GetWorld()->GetTerrain()->GetHeightMipmap().DumpToDisk(filename); OsPath realPath; g_VFS->GetRealPath(filename, realPath); LOGMESSAGERENDER("Terrain mipmap written to '%s'", realPath.string8()); } void JSI_Game::RegisterScriptFunctions(const ScriptInterface& scriptInterface) { scriptInterface.RegisterFunction("IsGameStarted"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("EndGame"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("SetPlayerID"); scriptInterface.RegisterFunction("SetViewedPlayer"); scriptInterface.RegisterFunction("GetSimRate"); scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("IsPaused"); scriptInterface.RegisterFunction("SetPaused"); scriptInterface.RegisterFunction("IsVisualReplay"); scriptInterface.RegisterFunction("GetCurrentReplayDirectory"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); scriptInterface.RegisterFunction("DumpTerrainMipmap"); } Index: ps/trunk/source/rlinterface/RLInterface.cpp =================================================================== --- ps/trunk/source/rlinterface/RLInterface.cpp (revision 24630) +++ ps/trunk/source/rlinterface/RLInterface.cpp (revision 24631) @@ -1,388 +1,392 @@ /* 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 . */ // Pull in the headers from the default precompiled header, // even if rlinterface doesn't use precompiled headers. #include "lib/precompiled.h" #include "rlinterface/RLInterface.h" #include "gui/GUIManager.h" +#include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Loader.h" -#include "ps/CLogger.h" +#include "ps/GameSetup/GameSetup.h" +#include "simulation2/Simulation2.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpTemplateManager.h" -#include "simulation2/Simulation2.h" #include "simulation2/system/LocalTurnManager.h" -#include "third_party/mongoose/mongoose.h" +#include #include +#include // Globally accessible pointer to the RL Interface. -RLInterface* g_RLInterface = nullptr; +std::unique_ptr g_RLInterface; + +namespace RL +{ +Interface::Interface(const char* server_address) : m_GameMessage({GameMessageType::None}) +{ + LOGMESSAGERENDER("Starting RL interface HTTP server"); + + const char *options[] = { + "listening_ports", server_address, + "num_threads", "1", + nullptr + }; + mg_context* mgContext = mg_start(MgCallback, this, options); + ENSURE(mgContext); +} // Interactions with the game engine (g_Game) must be done in the main -// thread as there are specific checks for this. We will pass our commands -// to the main thread to be applied -std::string RLInterface::SendGameMessage(const GameMessage msg) -{ - std::unique_lock msgLock(m_msgLock); - m_GameMessage = &msg; - m_msgApplied.wait(msgLock); +// thread as there are specific checks for this. We will pass messages +// to the main thread to be applied (ie, "GameMessage"s). +std::string Interface::SendGameMessage(GameMessage&& msg) +{ + std::unique_lock msgLock(m_MsgLock); + ENSURE(m_GameMessage.type == GameMessageType::None); + m_GameMessage = std::move(msg); + m_MsgApplied.wait(msgLock, [this]() { return m_GameMessage.type == GameMessageType::None; }); return m_GameState; } -std::string RLInterface::Step(const std::vector commands) +std::string Interface::Step(std::vector&& commands) { - std::lock_guard lock(m_lock); - GameMessage msg = { GameMessageType::Commands, commands }; - return SendGameMessage(msg); + std::lock_guard lock(m_Lock); + return SendGameMessage({ GameMessageType::Commands, std::move(commands) }); } -std::string RLInterface::Reset(const ScenarioConfig* scenario) +std::string Interface::Reset(ScenarioConfig&& scenario) { - std::lock_guard lock(m_lock); - m_ScenarioConfig = *scenario; - struct GameMessage msg = { GameMessageType::Reset }; - return SendGameMessage(msg); + std::lock_guard lock(m_Lock); + m_ScenarioConfig = std::move(scenario); + return SendGameMessage({ GameMessageType::Reset }); } -std::vector RLInterface::GetTemplates(const std::vector names) const +std::vector Interface::GetTemplates(const std::vector& names) const { - std::lock_guard lock(m_lock); + std::lock_guard lock(m_Lock); CSimulation2& simulation = *g_Game->GetSimulation2(); CmpPtr cmpTemplateManager(simulation.GetSimContext().GetSystemEntity()); std::vector templates; for (const std::string& templateName : names) { const CParamNode* node = cmpTemplateManager->GetTemplate(templateName); if (node != nullptr) - { - std::string content = utf8_from_wstring(node->ToXML()); - templates.push_back(content); - } + templates.push_back(utf8_from_wstring(node->ToXML())); } return templates; } -static void* RLMgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) +void* Interface::MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) { - RLInterface* interface = (RLInterface*)request_info->user_data; + Interface* interface = (Interface*)request_info->user_data; ENSURE(interface); void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling const char* header200 = "HTTP/1.1 200 OK\r\n" "Access-Control-Allow-Origin: *\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n"; const char* header404 = "HTTP/1.1 404 Not Found\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Unrecognised URI"; const char* noPostData = "HTTP/1.1 400 Bad Request\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "No POST data found."; const char* notRunningResponse = "HTTP/1.1 400 Bad Request\r\n" "Content-Type: text/plain; charset=utf-8\r\n\r\n" "Game not running. Please create a scenario first."; switch (event) { case MG_NEW_REQUEST: { std::stringstream stream; - std::string uri = request_info->uri; + const std::string uri = request_info->uri; if (uri == "/reset") { const char* val = mg_get_header(conn, "Content-Length"); if (!val) { mg_printf(conn, "%s", noPostData); return handled; } ScenarioConfig scenario; - std::string qs(request_info->query_string); + const std::string qs(request_info->query_string); scenario.saveReplay = qs.find("saveReplay") != std::string::npos; scenario.playerID = 1; char playerID[1]; - int len = mg_get_var(request_info->query_string, qs.length(), "playerID", playerID, 1); + const int len = mg_get_var(request_info->query_string, qs.length(), "playerID", playerID, 1); if (len != -1) scenario.playerID = std::stoi(playerID); - int bufSize = std::atoi(val); - std::unique_ptr buf = std::unique_ptr(new char[bufSize]); + const int bufSize = std::atoi(val); + std::unique_ptr buf = std::unique_ptr(new char[bufSize]); mg_read(conn, buf.get(), bufSize); - std::string content(buf.get(), bufSize); + const std::string content(buf.get(), bufSize); scenario.content = content; - std::string gameState = interface->Reset(&scenario); + const std::string gameState = interface->Reset(std::move(scenario)); stream << gameState.c_str(); } else if (uri == "/step") { if (!interface->IsGameRunning()) { mg_printf(conn, "%s", notRunningResponse); return handled; } const char* val = mg_get_header(conn, "Content-Length"); if (!val) { mg_printf(conn, "%s", noPostData); return handled; } int bufSize = std::atoi(val); - std::unique_ptr buf = std::unique_ptr(new char[bufSize]); + std::unique_ptr buf = std::unique_ptr(new char[bufSize]); mg_read(conn, buf.get(), bufSize); - std::string postData(buf.get(), bufSize); + const std::string postData(buf.get(), bufSize); std::stringstream postStream(postData); std::string line; - std::vector commands; + std::vector commands; while (std::getline(postStream, line, '\n')) { - Command cmd; + GameCommand cmd; const std::size_t splitPos = line.find(";"); if (splitPos != std::string::npos) { cmd.playerID = std::stoi(line.substr(0, splitPos)); cmd.json_cmd = line.substr(splitPos + 1); commands.push_back(cmd); } } - std::string gameState = interface->Step(commands); + const std::string gameState = interface->Step(std::move(commands)); if (gameState.empty()) { mg_printf(conn, "%s", notRunningResponse); return handled; } else stream << gameState.c_str(); } else if (uri == "/templates") { if (!interface->IsGameRunning()) { mg_printf(conn, "%s", notRunningResponse); return handled; } const char* val = mg_get_header(conn, "Content-Length"); if (!val) { mg_printf(conn, "%s", noPostData); return handled; } - int bufSize = std::atoi(val); - std::unique_ptr buf = std::unique_ptr(new char[bufSize]); + const int bufSize = std::atoi(val); + std::unique_ptr buf = std::unique_ptr(new char[bufSize]); mg_read(conn, buf.get(), bufSize); - std::string postData(buf.get(), bufSize); + const std::string postData(buf.get(), bufSize); std::stringstream postStream(postData); std::string line; std::vector templateNames; while (std::getline(postStream, line, '\n')) templateNames.push_back(line); for (std::string templateStr : interface->GetTemplates(templateNames)) stream << templateStr.c_str() << "\n"; } else { mg_printf(conn, "%s", header404); return handled; } mg_printf(conn, "%s", header200); - std::string str = stream.str(); + const std::string str = stream.str(); mg_write(conn, str.c_str(), str.length()); return handled; } case MG_HTTP_ERROR: return nullptr; case MG_EVENT_LOG: // Called by Mongoose's cry() LOGERROR("Mongoose error: %s", request_info->log_message); return nullptr; case MG_INIT_SSL: return nullptr; default: debug_warn(L"Invalid Mongoose event type"); return nullptr; } }; -void RLInterface::EnableHTTP(const char* server_address) -{ - LOGMESSAGERENDER("Starting RL interface HTTP server"); - - // Ignore multiple enablings - if (m_MgContext) - return; - - const char *options[] = { - "listening_ports", server_address, - "num_threads", "6", // enough for the browser's parallel connection limit - nullptr - }; - m_MgContext = mg_start(RLMgCallback, this, options); - ENSURE(m_MgContext); -} - -bool RLInterface::TryGetGameMessage(GameMessage& msg) +bool Interface::TryGetGameMessage(GameMessage& msg) { - if (m_GameMessage != nullptr) { - msg = *m_GameMessage; - m_GameMessage = nullptr; + if (m_GameMessage.type != GameMessageType::None) + { + msg = m_GameMessage; + m_GameMessage = {GameMessageType::None}; return true; } return false; } -void RLInterface::TryApplyMessage() +void Interface::TryApplyMessage() { - const bool nonVisual = !g_GUI; const bool isGameStarted = g_Game && g_Game->IsGameStarted(); if (m_NeedsGameState && isGameStarted) { m_GameState = GetGameState(); - m_msgApplied.notify_one(); - m_msgLock.unlock(); + m_MsgApplied.notify_one(); + m_MsgLock.unlock(); m_NeedsGameState = false; } - if (m_msgLock.try_lock()) + if (!m_MsgLock.try_lock()) + return; + + GameMessage msg; + if (!TryGetGameMessage(msg)) { - GameMessage msg; - if (TryGetGameMessage(msg)) { - switch (msg.type) + m_MsgLock.unlock(); + return; + } + + ApplyMessage(msg); +} + +void Interface::ApplyMessage(const GameMessage& msg) +{ + const static std::string EMPTY_STATE; + const bool nonVisual = !g_GUI; + const bool isGameStarted = g_Game && g_Game->IsGameStarted(); + switch (msg.type) + { + case GameMessageType::Reset: + { + if (isGameStarted) + EndGame(); + + g_Game = new CGame(m_ScenarioConfig.saveReplay); + ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); + ScriptRequest rq(scriptInterface); + JS::RootedValue attrs(rq.cx); + scriptInterface.ParseJSON(m_ScenarioConfig.content, &attrs); + + g_Game->SetPlayerID(m_ScenarioConfig.playerID); + g_Game->StartGame(&attrs, ""); + + if (nonVisual) { - case GameMessageType::Reset: - { - if (isGameStarted) - EndGame(); + LDR_NonprogressiveLoad(); + ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); + m_GameState = GetGameState(); + m_MsgApplied.notify_one(); + m_MsgLock.unlock(); + } + else + { + JS::RootedValue initData(rq.cx); + scriptInterface.CreateObject(rq, &initData); + scriptInterface.SetProperty(initData, "attribs", attrs); + + JS::RootedValue playerAssignments(rq.cx); + scriptInterface.CreateObject(rq, &playerAssignments); + scriptInterface.SetProperty(initData, "playerAssignments", playerAssignments); - g_Game = new CGame(m_ScenarioConfig.saveReplay); - ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); - ScriptRequest rq(scriptInterface); - - JS::RootedValue attrs(rq.cx); - scriptInterface.ParseJSON(m_ScenarioConfig.content, &attrs); - - g_Game->SetPlayerID(m_ScenarioConfig.playerID); - g_Game->StartGame(&attrs, ""); - - if (nonVisual) - { - LDR_NonprogressiveLoad(); - ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); - m_GameState = GetGameState(); - m_msgApplied.notify_one(); - m_msgLock.unlock(); - } - else - { - JS::RootedValue initData(rq.cx); - scriptInterface.CreateObject(rq, &initData); - scriptInterface.SetProperty(initData, "attribs", attrs); - - JS::RootedValue playerAssignments(rq.cx); - scriptInterface.CreateObject(rq, &playerAssignments); - scriptInterface.SetProperty(initData, "playerAssignments", playerAssignments); - - g_GUI->SwitchPage(L"page_loading.xml", &scriptInterface, initData); - m_NeedsGameState = true; - } - break; - } + g_GUI->SwitchPage(L"page_loading.xml", &scriptInterface, initData); + m_NeedsGameState = true; + } + break; + } - case GameMessageType::Commands: - { - if (!g_Game) - { - m_GameState = EMPTY_STATE; - m_msgApplied.notify_one(); - m_msgLock.unlock(); - return; - } - - CLocalTurnManager* turnMgr = static_cast(g_Game->GetTurnManager()); - - const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); - ScriptRequest rq(scriptInterface); - for (Command command : msg.commands) - { - JS::RootedValue commandJSON(rq.cx); - scriptInterface.ParseJSON(command.json_cmd, &commandJSON); - turnMgr->PostCommand(command.playerID, commandJSON); - } - - const double deltaRealTime = DEFAULT_TURN_LENGTH_SP; - if (nonVisual) - { - const double deltaSimTime = deltaRealTime * g_Game->GetSimRate(); - size_t maxTurns = static_cast(g_Game->GetSimRate()); - g_Game->GetTurnManager()->Update(deltaSimTime, maxTurns); - } - else - g_Game->Update(deltaRealTime); - - m_GameState = GetGameState(); - m_msgApplied.notify_one(); - m_msgLock.unlock(); - break; - } + case GameMessageType::Commands: + { + if (!g_Game) + { + m_GameState = EMPTY_STATE; + m_MsgApplied.notify_one(); + m_MsgLock.unlock(); + return; } + const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); + CLocalTurnManager* turnMgr = static_cast(g_Game->GetTurnManager()); + + for (const GameCommand& command : msg.commands) + { + ScriptRequest rq(scriptInterface); + JS::RootedValue commandJSON(rq.cx); + scriptInterface.ParseJSON(command.json_cmd, &commandJSON); + turnMgr->PostCommand(command.playerID, commandJSON); + } + + const u32 deltaRealTime = DEFAULT_TURN_LENGTH_SP; + if (nonVisual) + { + const double deltaSimTime = deltaRealTime * g_Game->GetSimRate(); + const size_t maxTurns = static_cast(g_Game->GetSimRate()); + g_Game->GetTurnManager()->Update(deltaSimTime, maxTurns); + } + else + g_Game->Update(deltaRealTime); + + m_GameState = GetGameState(); + m_MsgApplied.notify_one(); + m_MsgLock.unlock(); + break; } - else - m_msgLock.unlock(); } } -std::string RLInterface::GetGameState() +std::string Interface::GetGameState() const { const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); - ScriptRequest rq(scriptInterface); - const CSimContext simContext = g_Game->GetSimulation2()->GetSimContext(); CmpPtr cmpAIInterface(simContext.GetSystemEntity()); + ScriptRequest rq(scriptInterface); JS::RootedValue state(rq.cx); cmpAIInterface->GetFullRepresentation(&state, true); return scriptInterface.StringifyJSON(&state, false); } -bool RLInterface::IsGameRunning() +bool Interface::IsGameRunning() const { - return !!g_Game; + return g_Game != nullptr; +} } Index: ps/trunk/source/rlinterface/RLInterface.h =================================================================== --- ps/trunk/source/rlinterface/RLInterface.h (revision 24630) +++ ps/trunk/source/rlinterface/RLInterface.h (revision 24631) @@ -1,77 +1,158 @@ /* 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_RLINTERFACE #define INCLUDED_RLINTERFACE #include "simulation2/helpers/Player.h" +#include "third_party/mongoose/mongoose.h" #include #include -#include #include -struct ScenarioConfig { +namespace RL +{ +struct ScenarioConfig +{ bool saveReplay; player_id_t playerID; std::string content; }; -struct Command { + +struct GameCommand +{ int playerID; std::string json_cmd; }; -enum GameMessageType { Reset, Commands }; -struct GameMessage { - GameMessageType type; - std::vector commands; +enum class GameMessageType +{ + None, + Reset, + Commands, }; -extern void EndGame(); - -struct mg_context; -const static std::string EMPTY_STATE; - -class RLInterface +/** + * Holds messages from the RL client to the game. + */ +struct GameMessage { + GameMessageType type; + std::vector commands; +}; - public: - - std::string Step(const std::vector commands); - std::string Reset(const ScenarioConfig* scenario); - std::vector GetTemplates(const std::vector names) const; - - void EnableHTTP(const char* server_address); - std::string SendGameMessage(const GameMessage msg); - bool TryGetGameMessage(GameMessage& msg); - void TryApplyMessage(); - std::string GetGameState(); - bool IsGameRunning(); - - private: - mg_context* m_MgContext = nullptr; - const GameMessage* m_GameMessage = nullptr; - std::string m_GameState; - bool m_NeedsGameState = false; - mutable std::mutex m_lock; - std::mutex m_msgLock; - std::condition_variable m_msgApplied; - ScenarioConfig m_ScenarioConfig; +/** + * Implements an interface providing fundamental capabilities required for reinforcement + * learning (over HTTP). + * + * This consists of enabling an external script to configure the scenario (via Reset) and + * then step the game engine manually and apply player actions (via Step). The interface + * also supports querying unit templates to provide information about max health and other + * potentially relevant game state information. + * + * See source/tools/rlclient/ for the external client code. + * + * The HTTP server is threaded. + * Flow of data (with the interface active): + * 0. The game/main thread calls TryApplyMessage() + * - If no messages are pending, GOTO 0 (the simulation is not advanced). + * 1. TryApplyMessage locks m_MsgLock, pulls the message, processes it, advances the simulation, and sets m_GameState. + * 2. TryApplyMessage notifies the RL thread that it can carry on and unlocks m_MsgLock. The main thread carries on frame rendering and goes back to 0. + * 3. The RL thread locks m_MsgLock, reads m_GameState, unlocks m_MsgLock, and sends the gamestate as HTTP Response to the RL client. + * 4. The client processes the response and ultimately sends a new HTTP message to the RL Interface. + * 5. The RL thread locks m_MsgLock, pushes the message, and starts waiting on the game/main thread to notify it (step 2). + * - GOTO 0. + */ +class Interface +{ + NONCOPYABLE(Interface); +public: + Interface(const char* server_address); + + /** + * Non-blocking call to process any pending messages from the RL client. + * Updates m_GameState to the gamestate after messages have been processed. + */ + void TryApplyMessage(); + +private: + static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info); + + /** + * Process commands, update the simulation by one turn. + * @return the gamestate after processing commands. + */ + std::string Step(std::vector&& commands); + + /** + * Reset the game state according to scenario, cleaning up existing games if required. + * @return the gamestate after resetting. + */ + std::string Reset(ScenarioConfig&& scenario); + + /** + * @return template data for all templates of @param names. + */ + std::vector GetTemplates(const std::vector& names) const; + + /** + * @return true if a game is currently running. + */ + bool IsGameRunning() const; + + /** + * Internal helper. Move @param msg into m_GameMessage, wait until it has been processed by the main thread, + * and @return the gamestate after that message is processed. + * It is invalid to call this if m_GameMessage is not currently empty. + */ + std::string SendGameMessage(GameMessage&& msg); + + /** + * Internal helper. + * @return true if m_GameMessage is not empty, and updates @param msg, false otherwise (msg is then unchanged). + */ + bool TryGetGameMessage(GameMessage& msg); + + /** + * Process any pending messages from the RL client. + * Updates m_GameState to the gamestate after messages have been processed. + */ + void ApplyMessage(const GameMessage& msg); + + /** + * @return the full gamestate as a JSON strong. + * This uses the AI representation since it is readily available in the JS Engine. + */ + std::string GetGameState() const; + +private: + GameMessage m_GameMessage; + ScenarioConfig m_ScenarioConfig; + std::string m_GameState; + bool m_NeedsGameState = false; + + mutable std::mutex m_Lock; + std::mutex m_MsgLock; + std::condition_variable m_MsgApplied; }; -extern RLInterface* g_RLInterface; +} + +extern std::unique_ptr g_RLInterface; #endif // INCLUDED_RLINTERFACE Index: ps/trunk/source/scriptinterface/ScriptEngine.h =================================================================== --- ps/trunk/source/scriptinterface/ScriptEngine.h (revision 24630) +++ ps/trunk/source/scriptinterface/ScriptEngine.h (revision 24631) @@ -1,56 +1,58 @@ /* 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_SCRIPTENGINE #define INCLUDED_SCRIPTENGINE #include "ScriptTypes.h" #include "ps/Singleton.h" #include "js/Initialization.h" +#include + /** * A class using the RAII (Resource Acquisition Is Initialization) idiom to manage initialization * and shutdown of the SpiderMonkey script engine. It also keeps a count of active script contexts * in order to validate the following constraints: * 1. JS_Init must be called before any ScriptContexts are initialized * 2. JS_Shutdown must be called after all ScriptContexts have been destroyed */ class ScriptEngine : public Singleton { public: ScriptEngine() { ENSURE(m_Contexts.empty() && "JS_Init must be called before any contexts are created!"); JS_Init(); } ~ScriptEngine() { ENSURE(m_Contexts.empty() && "All contexts must be destroyed before calling JS_ShutDown!"); JS_ShutDown(); } void RegisterContext(const JSContext* cx) { m_Contexts.push_back(cx); } void UnRegisterContext(const JSContext* cx) { m_Contexts.remove(cx); } private: std::list m_Contexts; }; #endif // INCLUDED_SCRIPTENGINE