From 678a33c10008880e7e3e9bfb98de446a2da16a7e Mon Sep 17 00:00:00 2001 From: trompetin17 Date: Thu, 29 May 2025 21:55:50 -0500 Subject: [PATCH] Dap Interface with Spidermonkey debug Spidermonkey provide a mechanics to debug all comportaments and real from a different place with JS code this allow us to reuse the current scriptinterface but addind the new Debugger object definition only for debugging without change any code from other place like GUI & simulation. Debugger Adapter Interface is a protocol that commons IDE implement to being able for debugging, the concept is to provide sockets connections with c++ but the Dap implementation in JS that allow us to extend for more Request / Events that DAP provide. Because Dap Interface its implemented with JS we need to handle message in the main thread so we are calling in the main loop before GUI messages --- binaries/data/config/default.cfg | 4 + build/premake/extern_libs5.lua | 8 + build/premake/premake5.lua | 18 + source/dapinterface/DapInterface.cpp | 448 ++++++++++++++++++ source/dapinterface/DapInterface.h | 84 ++++ source/dapinterface/tests/test_DapInterface.h | 58 +++ source/lib/config2.h | 6 +- source/main.cpp | 40 ++ 8 files changed, 665 insertions(+), 1 deletion(-) create mode 100644 source/dapinterface/DapInterface.cpp create mode 100644 source/dapinterface/DapInterface.h create mode 100644 source/dapinterface/tests/test_DapInterface.h diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index 772258d7b6..15bc6a8b3c 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -567,6 +567,10 @@ netwarnings = "true" ; Show warnings if the network connection is b [rlinterface] address = "127.0.0.1:6000" +[dapinterface] +address = "127.0.0.1" +port = 9229 + [sound] mastergain = 0.9 musicgain = 0.2 diff --git a/build/premake/extern_libs5.lua b/build/premake/extern_libs5.lua index 3baabd7ef4..14d3c03390 100644 --- a/build/premake/extern_libs5.lua +++ b/build/premake/extern_libs5.lua @@ -772,6 +772,14 @@ extern_lib_defs = { end end, }, + sockets = { + link_settings = function() + add_default_links({ + win_names = { "ws2_32" }, + no_delayload = 1, + }) + end, + } } diff --git a/build/premake/premake5.lua b/build/premake/premake5.lua index a182971c14..73ca736bba 100644 --- a/build/premake/premake5.lua +++ b/build/premake/premake5.lua @@ -56,6 +56,7 @@ newoption { category = "Pyrogenesis", trigger = "with-system-nvtt", description newoption { category = "Pyrogenesis", trigger = "with-valgrind", description = "Enable Valgrind support (non-Windows only)" } newoption { category = "Pyrogenesis", trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" } newoption { category = "Pyrogenesis", trigger = "without-atlas", description = "Disable Atlas scenario/map editor and ActorEditor" } +newoption { category = "Pyrogenesis", trigger = "without-dap-interface", description = "Disable Dap interface project" } newoption { category = "Pyrogenesis", trigger = "without-lobby", description = "Disable the use of gloox and the multiplayer lobby" } newoption { category = "Pyrogenesis", trigger = "without-miniupnpc", description = "Disable use of miniupnpc for port forwarding" } newoption { category = "Pyrogenesis", trigger = "without-nvtt", description = "Disable use of NVTT" } @@ -281,6 +282,10 @@ function project_set_build_flags() defines { "CONFIG2_MINIUPNPC=0" } end + if _OPTIONS["without-dap-interface"] then + defines { "CONFIG2_DAP_INTERFACE=0" } + end + -- various platform-specific build flags if os.istarget("windows") then @@ -709,6 +714,19 @@ function setup_all_libs () } setup_static_lib_project("rlinterface", source_dirs, extern_libs, { no_pch = 1 }) + if not _OPTIONS["without-dap-interface"] then + source_dirs = { + "dapinterface", + } + extern_libs = { + "boost", -- dragged in via simulation.h and scriptinterface.h + "fmt", + "spidermonkey", + "sockets" + } + setup_static_lib_project("dapinterface", source_dirs, extern_libs, { no_pch = 1 }) + end + source_dirs = { "third_party/tinygettext/src", } diff --git a/source/dapinterface/DapInterface.cpp b/source/dapinterface/DapInterface.cpp new file mode 100644 index 0000000000..bee1487757 --- /dev/null +++ b/source/dapinterface/DapInterface.cpp @@ -0,0 +1,448 @@ +/* Copyright (C) 2025 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/precompiled.h" + +#include "dapinterface/DapInterface.h" + +#include "fmt/format.h" +#include "js/Debug.h" +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "scriptinterface/FunctionWrapper.h" +#include "scriptinterface/JSON.h" +#include "scriptinterface/ModuleLoader.h" +#include "scriptinterface/Object.h" +#include "scriptinterface/ScriptExtraHeaders.h" +#include "scriptinterface/ScriptRequest.h" + +#include + + +#if OS_WIN +#include +#include +#else +#include +#include +#include +#include +#endif +#include +#include + +namespace DAP +{ + class Interface::SocketHandler { + public: + SocketHandler(Interface* callbackData) + : m_CallbackData(callbackData) + { + ENSURE(m_CallbackData && "Callback data must not be null"); + } + + ~SocketHandler() + { + m_Running = false; + CloseSocket(); + if (m_ServerThread.joinable()) + m_ServerThread.join(); + } + NONCOPYABLE(SocketHandler); + + bool BindAndListen(const std::string server_address, int port) + { +#if OS_WIN + WSADATA wsaData; + if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) + { + LOGERROR("Failed to start WSA"); + return false; + } +#endif + + m_ServerSocket = socket(AF_INET, SOCK_STREAM, 0); +#if OS_WIN + if (m_ServerSocket == INVALID_SOCKET) +#else + if (m_ServerSocket < 0) +#endif + { + LOGERROR("Failed to create socket"); + return false; + } + +#if OS_WIN + const int i{1}; + setsockopt(m_ServerSocket, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&i), sizeof(i)); +#else + const int i{1}; + setsockopt(m_ServerSocket, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&i), sizeof(i)); +#endif + sockaddr_in serverAddr{}; + serverAddr.sin_family = AF_INET; + serverAddr.sin_port = htons(port); + + if (server_address.empty()) + serverAddr.sin_addr.s_addr = INADDR_ANY; + else + { + serverAddr.sin_addr.s_addr = inet_addr(server_address.c_str()); + if (!inet_pton(AF_INET, server_address.c_str(), &serverAddr.sin_addr)) + { + LOGERROR("Invalid server address: %s", server_address.c_str()); + CloseSocket(); + return false; + } + } + + if (bind(m_ServerSocket, reinterpret_cast(&serverAddr), sizeof(serverAddr)) == -1) + { + LOGERROR("Failed to bind socket"); + CloseSocket(); + return false; + } + + if (listen(m_ServerSocket, 1) == -1) + { + LOGERROR("Failed to listen on socket"); + return false; + } + + return true; + } + + void StartServerThread() + { + m_ServerThread = std::thread(&Interface::SocketHandler::AcceptConnections, this); + } + + void AcceptConnections() + { + while (m_Running) + { + sockaddr_in clientAddr{}; + +#if OS_WIN + int clientAddrLen{sizeof(clientAddr)}; + m_ClientSocket = accept(m_ServerSocket, reinterpret_cast(&clientAddr), &clientAddrLen); + if (m_ClientSocket == INVALID_SOCKET) +#else + socklen_t clientAddrLen = sizeof(clientAddr); + m_ClientSocket = accept(m_ServerSocket, reinterpret_cast(&clientAddr), reinterpret_cast(&clientAddrLen)); + if (m_ClientSocket < 0) +#endif + { + if (m_Running) + LOGERROR("Failed to accept connection"); + else + LOGMESSAGE("Server stopped accepting connections"); + continue; + } + + LOGMESSAGE("Accepted connection from %s", inet_ntoa(clientAddr.sin_addr)); + std::lock_guard lock{m_ConnectionLock}; + this->HandleClient(); + LOGMESSAGE("Client Disconnected"); + } + } + + void HandleClient() + { + char buffer[1024] = {0}; + std::string message{}; + while (true) + { + // TODO: Validation if need more than one recv. + const int bytesRead{static_cast(recv(m_ClientSocket, buffer, sizeof(buffer), 0))}; + if (bytesRead <= 0) + break; + message.append(buffer, static_cast(bytesRead)); + LOGMESSAGE("Received message: %s", message.c_str()); + + while (!message.empty()) + { + // Parse Content-Length get DAP Body. + const std::size_t pos{message.find("Content-Length: ")}; + if (pos == std::string::npos) + { + LOGERROR("Invalid DAP message"); + break; + } + + const std::size_t endPos{message.find("\r\n\r\n", pos)}; + if (endPos == std::string::npos) + { + LOGERROR("Invalid DAP message"); + break; + } + + const std::size_t length{static_cast(std::stoi(message.substr(pos + 16, endPos - pos - 16)))}; + + // Should wait for more data?. + if (endPos + 4 + length > message.size()) + break; + + const std::string dapMessage{message.substr(endPos + 4, length)}; + + const std::string response{this->m_CallbackData->SendDapMessage(dapMessage)}; + + if (response.empty()) + { + LOGERROR("Failed to process message"); + break; + } + + // Return DAP message with protocol headers. + const std::string dapResponse{"Content-Length: " + std::to_string(response.size()) + "\r\n\r\n" + response}; + LOGMESSAGE("Sending response to client: %s", dapResponse.c_str()); + send(m_ClientSocket, dapResponse.c_str(), dapResponse.size(), 0); + + // Validate if there more messages. + message = message.substr(endPos + 4 + length); + } + } + + #if OS_WIN + closesocket(m_ClientSocket); + m_ClientSocket = INVALID_SOCKET; + #else + close(m_ClientSocket); + m_ClientSocket = -1; + #endif + } + + void SendToClient(const std::string& message) + { +#if OS_WIN + ENSURE(m_ClientSocket != INVALID_SOCKET && "Client socket is not connected"); +#else + ENSURE(m_ClientSocket != -1 && "Client socket is not connected"); +#endif + send(m_ClientSocket, message.c_str(), message.size(), 0); + } + + void CloseSocket() + { +#if OS_WIN + if (m_ClientSocket != INVALID_SOCKET) + { + closesocket(m_ClientSocket); + m_ClientSocket = INVALID_SOCKET; + } + if (m_ServerSocket != INVALID_SOCKET) + { + closesocket(m_ServerSocket); + m_ServerSocket = INVALID_SOCKET; + } + WSACleanup(); +#else + if (m_ClientSocket != -1) + { + close(m_ClientSocket); + m_ClientSocket = -1; + } + if (m_ServerSocket != -1) + { +#if OS_LINUX + shutdown(m_ServerSocket, SHUT_RDWR); +#endif + close(m_ServerSocket); + m_ServerSocket = -1; + } +#endif + } + + private: +#if OS_WIN + SOCKET m_ServerSocket{INVALID_SOCKET}; + SOCKET m_ClientSocket{INVALID_SOCKET}; +#else + int m_ServerSocket{-1}; + int m_ClientSocket{-1}; +#endif + + std::thread m_ServerThread; + std::mutex m_ConnectionLock; + Interface* m_CallbackData{nullptr}; + bool m_Running{true}; + }; + + Interface::Interface(const std::string serverAddress, int port, ScriptContext& scriptContext) + : m_SocketImpl{std::make_unique(this)}, + m_ModuleValue{scriptContext.GetGeneralJSContext()} + { + LOGMESSAGERENDER("Starting DAP interface server"); + + if (!m_SocketImpl->BindAndListen(serverAddress, port)) + throw DapInterfaceException{fmt::format("Failed to bind and listen on port {}", port)}; + + const VfsPath fntPath{L"tools/dap/entry.js"}; + if (!VfsFileExists(fntPath)) + throw DapInterfaceNoJSDebuggerException{ fmt::format("DAP entry script not found at {}", fntPath.string8().c_str())}; + + m_ScriptInterface = std::make_unique("Engine", "Debugger", scriptContext, [](const VfsPath& path) { + return path.string8().find("tools/dap/") == 0; + }); + m_ScriptInterface->SetCallbackData(this); + + ScriptRequest rq(m_ScriptInterface.get()); + if (!JS_DefineDebuggerObject(rq.cx, rq.glob)) + { + ScriptException::CatchPending(rq); + throw DapInterfaceNoJSDebuggerException{"Failed to define debugger object"}; + } + + // Register methods. + constexpr ScriptFunction::ObjectGetter Getter{&ScriptInterface::ObjectFromCBData}; + ScriptFunction::Register<&DAP::Interface::WaitForMessage, Getter>(rq, "WaitForMessage"); + ScriptFunction::Register<&DAP::Interface::EndWaitingForMessage, Getter>(rq, "EndWaitingForMessage"); + + auto result{m_ScriptInterface->GetModuleLoader().LoadModule(rq, fntPath)}; + + scriptContext.RunJobs(); + + auto& future{*result.begin()}; + JS::RootedObject ns{rq.cx, future.Get()}; + m_ModuleValue = {rq.cx, JS::ObjectValue(*ns)}; + + // Validate that message handler function is defined in the script. + if (!this->isJSHandlerDefined()) + throw DapInterfaceNoJSDebuggerException{"Message handler function not defined in the script"}; + + // Now its time to start the server thread. + m_SocketImpl->StartServerThread(); + } + + Interface::~Interface() = default; + + bool Interface::isJSHandlerDefined() + { + ScriptRequest rq{m_ScriptInterface.get()}; + JS::RootedValue handler{rq.cx}; + + if (!Script::GetProperty(rq, m_ModuleValue, "handleMessage", &handler)) + { + ScriptException::CatchPending(rq); + return false; + } + + if (!handler.isObject()) + return false; + + JS::RootedObject obj{rq.cx, &handler.toObject()}; + return JS_ObjectIsFunction(obj); + } + + std::string Interface::SendDapMessage(const std::string& message) + { + std::unique_lock msgLock{m_MsgLock}; + ENSURE(m_DapRequest.empty()); + m_DapRequest = std::move(message); + m_MsgApplied.notify_all(); + + m_MsgApplied.wait(msgLock, [this] { return m_DapRequest.empty(); }); + return m_DapResponse; + } + + std::string Interface::OnMessage(const std::string& message) + { + ScriptRequest rq{m_ScriptInterface.get()}; + + JS::RootedValue msg{rq.cx}; + if (!Script::ParseJSON(rq, message, &msg)) + { + LOGERROR("Failed to parse JSON message"); + return ""; + } + + JS::RootedValue rval{rq.cx}; + if (!ScriptFunction::Call(rq, m_ModuleValue, "handleMessage", &rval, msg)) + { + LOGERROR("Failed to call message handler"); + return ""; + } + + return Script::StringifyJSON(rq, &rval, false); + } + + void Interface::SendEventToClient() + { + ScriptRequest rq{m_ScriptInterface.get()}; + JS::RootedValue global{rq.cx, rq.globalValue()}; + JS::RootedValue rval{rq.cx}; + + while (true) + { + if (!ScriptFunction::Call(rq, m_ModuleValue, "sendEventToClient", &rval)) + { + LOGERROR("Failed to call sendEventToClient"); + return; + } + + // Nothing to send. + if (rval.isUndefined() || rval.isNull()) + return; + + const std::string ret{Script::StringifyJSON(rq, &rval, false)}; + + // Send to socket client. + const std::string dapResponse{"Content-Length: " + std::to_string(ret.size()) + "\r\n\r\n" + ret}; + LOGMESSAGE("Sending event to client: %s", ret.c_str()); + m_SocketImpl->SendToClient(dapResponse); + } + } + + void Interface::TryHandleMessage() + { + std::lock_guard waitingLock{m_WaitingLock}; + if (!m_DapRequest.empty()) + { + m_DapResponse = OnMessage(m_DapRequest); + m_DapRequest.clear(); + m_MsgApplied.notify_all(); + } + + // Handle Events from the script. + SendEventToClient(); + } + + void Interface::WaitForMessage() + { + std::unique_lock waitingLock{m_MsgLock}; + m_IsWaiting = true; + // Handle Events from the script. + do + { + SendEventToClient(); + + m_MsgApplied.wait(waitingLock, [this] { return !m_DapRequest.empty(); }); + + m_DapResponse = OnMessage(m_DapRequest); + m_DapRequest.clear(); + m_MsgApplied.notify_all(); + } while (m_IsWaiting); + } + + void Interface::EndWaitingForMessage() + { + if (!m_IsWaiting) + return; + std::lock_guard waitingLock{m_WaitingLock}; + m_IsWaiting = false; + } +} diff --git a/source/dapinterface/DapInterface.h b/source/dapinterface/DapInterface.h new file mode 100644 index 0000000000..0e2929782b --- /dev/null +++ b/source/dapinterface/DapInterface.h @@ -0,0 +1,84 @@ +/* Copyright (C) 2025 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_DAPINTERFACE +#define INCLUDED_DAPINTERFACE + +#include "scriptinterface/ScriptContext.h" +#include "scriptinterface/ScriptInterface.h" +#include "ps/GameSetup/Paths.h" + +#include +#include +#include +#include +#include +#include + +namespace DAP +{ + // DapInterfaceException + class DapInterfaceException : public std::runtime_error + { + public: + explicit DapInterfaceException(const std::string& message) + : std::runtime_error(message) {} + }; + + class DapInterfaceNoJSDebuggerException : public DapInterfaceException + { + public: + explicit DapInterfaceNoJSDebuggerException(const std::string& message) + : DapInterfaceException(message) {} + }; + + class Interface + { + public: + Interface(const std::string server_address, int port, ScriptContext& scriptContext); + ~Interface(); + NONCOPYABLE(Interface); + + void TryHandleMessage(); + + void WaitForMessage(); + void EndWaitingForMessage(); + + private: + class SocketHandler; + + bool isJSHandlerDefined(); + std::string SendDapMessage(const std::string& message); + std::string OnMessage(const std::string& message); + void SendEventToClient(); + + std::unique_ptr m_SocketImpl; + std::unique_ptr m_ScriptInterface; + + std::string m_DapRequest; + std::string m_DapResponse; + + std::mutex m_MsgLock; + std::mutex m_WaitingLock; + std::condition_variable m_MsgApplied; + + bool m_IsWaiting{false}; + JS::RootedValue m_ModuleValue; + }; +} + +#endif // !INCLUDED_DAPINTERFACE diff --git a/source/dapinterface/tests/test_DapInterface.h b/source/dapinterface/tests/test_DapInterface.h new file mode 100644 index 0000000000..20c2e9f294 --- /dev/null +++ b/source/dapinterface/tests/test_DapInterface.h @@ -0,0 +1,58 @@ +/* Copyright (C) 2025 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "lib/self_test.h" + +#include "dapinterface/DapInterface.h" +#include "ps/Filesystem.h" + +class TestDapInterface : public CxxTest::TestSuite +{ +public: + void setUp() + { + g_VFS = CreateVfs(); + } + + void tearDown() + { + g_VFS.reset(); + } + + void test_dap_interface() + { + TestLogger logger; + // Test invalid address + TS_ASSERT_THROWS_EQUALS((DAP::Interface{"invalid_address", 1234, *g_ScriptContext}), const DAP::DapInterfaceException& e, e.what(), fmt::format("Failed to bind and listen on port 1234")); + + const std::string address{"127.0.0.1"}; + const int port{1234}; + // Test no tools/dap/ directory + TS_ASSERT_THROWS_EQUALS((DAP::Interface{address, port, *g_ScriptContext}), const DAP::DapInterfaceNoJSDebuggerException& e, e.what(), "DAP entry script not found at tools/dap/entry.js"); + + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST)) + // Test localhost address + TS_ASSERT_THROWS_NOTHING((DAP::Interface{address, port, *g_ScriptContext })); + + // Test two instances of DAP::Interface + DAP::Interface dap1{address, port, *g_ScriptContext}; + TS_ASSERT_THROWS_EQUALS((DAP::Interface{address, port, *g_ScriptContext}), const DAP::DapInterfaceException& e, e.what(), fmt::format("Failed to bind and listen on port {}", port)); + + // Test send a message without clients + TS_ASSERT_THROWS_NOTHING(dap1.TryHandleMessage()); + } +}; diff --git a/source/lib/config2.h b/source/lib/config2.h index 69c02adf48..ca6a4dd80c 100644 --- a/source/lib/config2.h +++ b/source/lib/config2.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2021 Wildfire Games. +/* Copyright (C) 2025 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the @@ -32,6 +32,10 @@ // configuration choices, so rebuilding them all is acceptable. // use config.h when settings must apply to the entire project. +#ifndef CONFIG2_DAP_INTERFACE +# define CONFIG2_DAP_INTERFACE 1 +#endif // !CONFIG2_DAP_INTERFACE + // allow use of RDTSC for raw tick counts (otherwise, the slower but // more reliable on MP systems wall-clock will be used). #ifndef CONFIG2_TIMER_ALLOW_RDTSC diff --git a/source/main.cpp b/source/main.cpp index 2527be2e90..00192572ed 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -84,6 +84,7 @@ that of Atlas depending on commandline parameters. #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" +#include "dapinterface/DapInterface.h" #if OS_UNIX #include @@ -362,7 +363,11 @@ static void RendererIncrementalLoad() while (more && timer_Time() - startTime < maxTime); } +#if CONFIG2_DAP_INTERFACE +static void Frame(RL::Interface* rlInterface, const int fixedFrameFrequency, DAP::Interface* dapInterface) +#else static void Frame(RL::Interface* rlInterface, const int fixedFrameFrequency) +#endif // CONFIG2_DAP_INTERFACE { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); @@ -426,6 +431,11 @@ static void Frame(RL::Interface* rlInterface, const int fixedFrameFrequency) if (g_NetClient) g_NetClient->Poll(); +#if CONFIG2_DAP_INTERFACE + if (dapInterface) + dapInterface->TryHandleMessage(); +#endif // CONFIG2_DAP_INTERFACE + std::optional completionCommand{g_GUI->TickObjects()}; if (completionCommand.has_value()) g_Shutdown = completionCommand.value() ? ShutdownType::RestartAsAtlas : ShutdownType::Quit; @@ -505,6 +515,23 @@ static std::optional CreateRLInterface(const CmdLineArgs& args) return std::make_optional(server_address.c_str()); } +#if CONFIG2_DAP_INTERFACE +static std::optional CreateDAPInterface(const CmdLineArgs& args) +{ + if (!args.Has("dap-interface")) + return std::nullopt; + + const std::string server_address{args.Get("dapinterface.address").empty() ? + g_ConfigDB.Get("dapinterface.address", std::string{}) : args.Get("dapinterface.address")}; + + const int port{args.Get("dapinterface.port").empty() ? + g_ConfigDB.Get("dapinterface.port", int{}) : args.Get("dapinterface.port").ToInt()}; + + debug_printf("DAP interface listening on %s:%d\n", server_address.c_str(), port); + return std::make_optional(server_address, port, *g_ScriptContext); +} +#endif // CONFIG2_DAP_INTERFACE + // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(const PS::span argv) @@ -677,6 +704,10 @@ static void RunGameOrAtlas(const PS::span argv) g_Mods.UpdateAvailableMods(modInterface); } +#if CONFIG2_DAP_INTERFACE + std::optional dapInterface{CreateDAPInterface(args)}; +#endif // CONFIG2_DAP_INTERFACE + std::optional guiScriptInterface; if (isVisual) @@ -702,7 +733,11 @@ static void RunGameOrAtlas(const PS::span argv) while (g_Shutdown == ShutdownType::None) { if (isVisual) +#if CONFIG2_DAP_INTERFACE + Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency, dapInterface ? &*dapInterface : nullptr); +#else Frame(rlInterface ? &*rlInterface : nullptr, fixedFrameFrequency); +#endif else if(rlInterface) rlInterface->TryApplyMessage(); else @@ -717,6 +752,11 @@ static void RunGameOrAtlas(const PS::span argv) ShutdownNetworkAndUI(); guiScriptInterface.reset(); + +#if CONFIG2_DAP_INTERFACE + dapInterface.reset(); +#endif // CONFIG2_DAP_INTERFACE + ShutdownConfigAndSubsequent(); MainControllerShutdown();