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();