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