Use cpp-httplib instead of mongoose

Use cpp-httplib for Profiler2 and RLInterface instead of mongoose.

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
This commit is contained in:
Ralph Sennhauser 2024-10-24 19:58:16 +02:00
parent 7e575aa855
commit ba4ef61c15
No known key found for this signature in database
10 changed files with 307 additions and 334 deletions

View file

@ -710,6 +710,7 @@ function setup_all_libs ()
"network",
}
extern_libs = {
"cpp_httplib",
"spidermonkey",
"enet",
"sdl",
@ -730,6 +731,7 @@ function setup_all_libs ()
"boost", -- dragged in via simulation.h and scriptinterface.h
"fmt",
"spidermonkey",
"cpp_httplib",
}
setup_static_lib_project("rlinterface", source_dirs, extern_libs, { no_pch = 1 })
@ -872,6 +874,7 @@ function setup_all_libs ()
"libpng",
"fmt",
"freetype",
"cpp_httplib",
}
@ -1108,6 +1111,7 @@ used_extern_libs = {
"libxml2",
"boost",
"cpp_httplib",
"cxxtest",
"comsuppw",
"enet",

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -36,6 +36,8 @@
typedef intptr_t ssize_t;
// prevent wxWidgets from (incompatibly) redefining it
#define HAVE_SSIZE_T
// prevent cpp-httplib from (incompatibly) redefining it
#define _SSIZE_T_DEFINED
// VC9 defines off_t as long, but we need 64-bit file offsets even in
// 32-bit builds. to avoid conflicts, we have to define _OFF_T_DEFINED,

View file

@ -537,7 +537,7 @@ static std::optional<RL::Interface> CreateRLInterface(const CmdLineArgs& args)
g_ConfigDB.Get("rlinterface.address", std::string{}) : args.Get("rl-interface")};
debug_printf("RL interface listening on %s\n", server_address.c_str());
return std::make_optional<RL::Interface>(server_address.c_str());
return std::make_optional<RL::Interface>(server_address);
}
#if CONFIG2_DAP_INTERFACE

View file

@ -0,0 +1,62 @@
/* Copyright (C) 2026 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 <http://www.gnu.org/licenses/>.
*/
#include "precompiled.h"
#include "HttpServer.h"
#include "ps/Future.h"
#include "ps/TaskManager.h"
#include <httplib.h>
#include <queue>
namespace
{
class TaskQueueAdapter : public httplib::TaskQueue
{
public:
bool enqueue(std::function<void()> fn) override
{
// Remove finished
while (!m_Futures.empty() && m_Futures.front().IsDone())
{
m_Futures.front().Get();
m_Futures.pop();
}
m_Futures.push({g_TaskManager, fn});
return true;
}
void shutdown() override
{
m_Futures = {};
}
private:
std::queue<Future<void>> m_Futures;
};
} // namespace
namespace PS::Net
{
std::unique_ptr<httplib::Server> createHttpServer() {
auto server = std::make_unique<httplib::Server>();
server->new_task_queue = [] { return new TaskQueueAdapter(); };
return server;
}
} // namespace PS::Net

View file

@ -0,0 +1,30 @@
/* Copyright (C) 2026 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 <http://www.gnu.org/licenses/>.
*/
#ifndef HTTPSEVER_H
#define HTTPSEVER_H
#include <memory>
namespace httplib { class Server; }
namespace PS::Net
{
std::unique_ptr<httplib::Server> createHttpServer();
}
#endif

View file

@ -381,6 +381,7 @@ void ShutdownNetworkAndUI()
g_RenderingOptions.ClearHooks();
g_Profiler2.ShutDownHTTP();
g_Profiler2.ShutdownGPU();
if (hasRenderer)

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -28,6 +28,7 @@
#include "lib/code_generation.h"
#include "lib/os_path.h"
#include "lib/path.h"
#include "network/HttpServer.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/ConfigDB.h"
@ -35,13 +36,13 @@
#include "ps/Profiler2GPU.h"
#include "ps/Pyrogenesis.h"
#include "ps/TaskManager.h"
#include "third_party/mongoose/mongoose.h"
#include <algorithm>
#include <cstdio>
#include <fmt/format.h>
#include <fstream>
#include <functional>
#include <httplib.h>
#include <iomanip>
#include <map>
#include <set>
@ -64,7 +65,7 @@ const u8 CProfiler2::RESYNC_MAGIC[8] = {0x11, 0x22, 0x33, 0x44, 0xf4, 0x93, 0xbe
thread_local CProfiler2::ThreadStorage* CProfiler2::m_CurrentStorage = nullptr;
CProfiler2::CProfiler2() :
m_Initialised(false), m_FrameNumber(0), m_MgContext(NULL), m_GPU(NULL)
m_Initialised{false}, m_FrameNumber{0}, m_HttpServer{nullptr}, m_HttpServerThread{}, m_GPU{nullptr}
{
}
@ -74,103 +75,6 @@ CProfiler2::~CProfiler2()
Shutdown();
}
/**
* Mongoose callback. Run in an arbitrary thread (possibly concurrently with other requests).
*/
static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info)
{
CProfiler2* profiler = (CProfiler2*)request_info->user_data;
ENSURE(profiler);
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" // TODO: not great for security
"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* header400 =
"HTTP/1.1 400 Bad Request\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
"Invalid request";
switch (event)
{
case MG_NEW_REQUEST:
{
std::stringstream stream;
std::string uri = request_info->uri;
if (uri == "/download")
Future{g_TaskManager, std::bind_front(&CProfiler2::SaveToFile, profiler)}.Get();
else if (uri == "/overview")
{
Future{g_TaskManager,
std::bind_front(&CProfiler2::ConstructJSONOverview, profiler, std::ref(stream))}.Get();
}
else if (uri == "/query")
{
if (!request_info->query_string)
{
mg_printf(conn, "%s (no query string)", header400);
return handled;
}
// Identify the requested thread
char buf[256];
int len = mg_get_var(request_info->query_string, strlen(request_info->query_string), "thread", buf, ARRAY_SIZE(buf));
if (len < 0)
{
mg_printf(conn, "%s (no 'thread')", header400);
return handled;
}
std::string thread(buf);
const char* err = Future{g_TaskManager,
std::bind_front(&CProfiler2::ConstructJSONResponse, profiler, std::ref(stream),
std::ref(thread))}.Get();
if (err)
{
mg_printf(conn, "%s (%s)", header400, err);
return handled;
}
}
else
{
mg_printf(conn, "%s", header404);
return handled;
}
mg_printf(conn, "%s", header200);
std::string str = stream.str();
mg_write(conn, str.c_str(), str.length());
return handled;
}
case MG_HTTP_ERROR:
return NULL;
case MG_EVENT_LOG:
// Called by Mongoose's cry()
LOGERROR("Mongoose error: %s", request_info->log_message);
return NULL;
case MG_INIT_SSL:
return NULL;
default:
debug_warn(L"Invalid Mongoose event type");
return NULL;
}
};
void CProfiler2::Initialise()
{
ENSURE(!m_Initialised);
@ -190,23 +94,53 @@ void CProfiler2::EnableHTTP()
ENSURE(m_Initialised);
LOGMESSAGERENDER("Starting profiler2 HTTP server");
std::lock_guard lock{m_Mutex};
// Ignore multiple enablings
if (m_MgContext)
if (m_HttpServer)
return;
using namespace std::literals;
const std::string listeningPort{CConfigDB::GetIfInitialised("profiler2.server.port", "8000"s)};
const std::string listeningServer{CConfigDB::GetIfInitialised("profiler2.server", "127.0.0.1"s)};
const std::string numThreads{CConfigDB::GetIfInitialised("profiler2.server.threads", "6"s)};
m_HttpServer = PS::Net::createHttpServer();
std::string listening_ports = fmt::format("{0}:{1}", listeningServer, listeningPort);
const char* options[] = {
"listening_ports", listening_ports.c_str(),
"num_threads", numThreads.c_str(),
nullptr
};
m_MgContext = mg_start(MgCallback, this, options);
ENSURE(m_MgContext);
m_HttpServer->Get("/download", [this](const httplib::Request &, httplib::Response &) {
SaveToFile();
});
m_HttpServer->Get("/overview", [this](const httplib::Request &, httplib::Response &res) {
std::stringstream stream;
ConstructJSONOverview(stream);
res.set_content(stream.str(), "application/json");
});
m_HttpServer->Get("/query", [this](const httplib::Request &req, httplib::Response &res) {
if (!req.has_param("thread"))
{
res.set_content("Request \"query\" needs parameter \"thread\"", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
std::string thread = req.get_param_value("thread");
std::stringstream stream;
ConstructJSONResponse(stream, thread);
res.set_content(stream.str(), "application/json");
});
m_HttpServer->set_post_routing_handler([](const httplib::Request&, httplib::Response& res) {
// TODO: Not ideal for security reasons
res.set_header("Access-Control-Allow-Origin", "*");
});
m_HttpServerThread = std::thread([this](){
using namespace std::literals;
const int listeningPort{CConfigDB::GetIfInitialised("profiler2.server.port", 8000)};
const std::string listeningServer{CConfigDB::GetIfInitialised("profiler2.server", "127.0.0.1"s)};
if (!m_HttpServer->listen(listeningServer, listeningPort))
{
LOGERROR("Failed to start http server");
}
});
}
void CProfiler2::EnableGPU()
@ -228,26 +162,34 @@ void CProfiler2::ShutdownGPU()
void CProfiler2::ShutDownHTTP()
{
LOGMESSAGERENDER("Shutting down profiler2 HTTP server");
if (m_MgContext)
{
mg_stop(m_MgContext);
m_MgContext = NULL;
}
std::lock_guard lock{m_Mutex};
if (!m_HttpServer)
return;
m_HttpServer->stop();
if(m_HttpServerThread.joinable())
m_HttpServerThread.join();
m_HttpServer.reset();
}
void CProfiler2::Toggle()
{
// TODO: Maybe we can open the browser to the profiler page automatically
if (m_GPU && m_MgContext)
LOGMESSAGERENDER("Toggle profiler http");
if (m_GPU && m_HttpServer)
{
ShutdownGPU();
ShutDownHTTP();
}
else if (!m_GPU && !m_MgContext)
else if (!m_GPU && !m_HttpServer)
{
EnableGPU();
EnableHTTP();
}
else
{
LOGMESSAGERENDER("Toggle profile bad state!");
}
}
void CProfiler2::Shutdown()
@ -255,12 +197,7 @@ void CProfiler2::Shutdown()
ENSURE(m_Initialised);
ENSURE(!m_GPU); // must shutdown GPU before profiler
if (m_MgContext)
{
mg_stop(m_MgContext);
m_MgContext = NULL;
}
ENSURE(!m_HttpServer); // must shutdown HTTP server before profiler
// the destructor is not called for the main thread
// we have to call it manually to avoid memory leaks

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -90,11 +90,12 @@
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
class CProfiler2GPU;
namespace Renderer::Backend { class IDeviceCommandContext; }
struct mg_context;
namespace httplib { class Server; }
// Note: Lots of functions are defined inline, to hypothetically
// minimise performance overhead.
@ -388,7 +389,8 @@ private:
int m_FrameNumber;
mg_context* m_MgContext;
std::unique_ptr<httplib::Server> m_HttpServer;
std::thread m_HttpServerThread;
CProfiler2GPU* m_GPU;

View file

@ -24,6 +24,7 @@
#include "gui/GUIManager.h"
#include "lib/debug.h"
#include "lib/types.h"
#include "network/HttpServer.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Errors.h"
@ -31,6 +32,7 @@
#include "ps/GameSetup/GameSetup.h"
#include "ps/Loader.h"
#include "scriptinterface/JSON.h"
#include "ps/TaskManager.h"
#include "scriptinterface/Object.h"
#include "scriptinterface/ScriptInterface.h"
#include "scriptinterface/ScriptRequest.h"
@ -42,32 +44,149 @@
#include "simulation2/system/TurnManager.h"
#include <cstdlib>
#include <fmt/format.h>
#include <httplib.h>
#include <js/RootingAPI.h>
#include <js/TypeDecls.h>
#include <js/Value.h>
#include <queue>
#include <sstream>
#include <stdexcept>
#include <string_view>
#include <utility>
namespace RL
{
Interface::Interface(const char* server_address)
Interface::Interface(std::string const serverAddress)
{
LOGMESSAGERENDER("Starting RL interface HTTP server");
m_HttpServer = PS::Net::createHttpServer();
const char *options[] = {
"listening_ports", server_address,
"num_threads", "1",
nullptr
};
m_Context = mg_start(MgCallback, this, options);
if (!m_Context)
throw SetupError{};
m_HttpServer->Post("/reset", [this](const httplib::Request &req, httplib::Response &res) {
if(req.body.empty())
{
res.set_content("No POST data found.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
ScenarioConfig scenario;
scenario.saveReplay = req.has_param("saveReplay");
scenario.playerID = 1;
if (req.has_param("playerID"))
{
scenario.playerID = std::stoi(req.get_param_value("playerID"));
}
scenario.content = req.body;
const std::string gameState = Reset(std::move(scenario));
res.set_content(gameState, "text/plain");
});
m_HttpServer->Post("/step", [this](const httplib::Request &req, httplib::Response &res) {
if (!IsGameRunning())
{
res.set_content("Game not running. Please create a scenario first.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
std::stringstream postStream(req.body);
std::string line;
std::vector<GameCommand> commands;
while (std::getline(postStream, line, '\n'))
{
const std::size_t splitPos = line.find(";");
if (splitPos == std::string::npos)
continue;
GameCommand cmd;
cmd.playerID = std::stoi(line.substr(0, splitPos));
cmd.json_cmd = line.substr(splitPos + 1);
commands.push_back(std::move(cmd));
}
const std::string gameState = Step(std::move(commands));
if (gameState.empty())
{
res.set_content("Game not running. Please create a scenario first.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
else
res.set_content(gameState, "text/plain");
});
m_HttpServer->Post("/evaluate", [this](const httplib::Request &req, httplib::Response &res) {
if (!IsGameRunning())
{
res.set_content("Game not running. Please create a scenario first.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
if (req.body.empty())
{
res.set_content("No POST data found.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
std::string code{req.body};
const std::string codeResult = Evaluate(std::move(code));
if (codeResult.empty())
{
res.set_content("Game not running. Please create a scenario first.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
else
res.set_content(codeResult, "text/plain");
});
m_HttpServer->Get("/templates", [this](const httplib::Request &req, httplib::Response &res) {
if (!IsGameRunning()) {
res.set_content("Game not running. Please create a scenario first.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
if (req.body.empty())
{
res.set_content("No POST data found.", "text/plain");
res.status = httplib::StatusCode::BadRequest_400;
return;
}
std::stringstream postStream(req.body);
std::string line;
std::vector<std::string> templateNames;
while (std::getline(postStream, line, '\n'))
templateNames.push_back(line);
std::stringstream stream;
for (std::string templateStr : GetTemplates(templateNames))
stream << templateStr.c_str() << "\n";
res.set_content(stream.str(), "text/plain");
});
std::size_t sepIndex = serverAddress.find(":");
if (sepIndex == std::string::npos)
{
throw std::invalid_argument{fmt::format("Invalid server address for RL interface '{}'", serverAddress)};
}
std::string address = serverAddress.substr(0, sepIndex);
int port = std::stoi(serverAddress.substr(sepIndex + 1, serverAddress.length() - sepIndex - 1));
m_HttpServerThread = std::thread([this, address, port](){
if (!m_HttpServer->listen(address, port))
{
LOGERROR("Failed to start http server");
}
});
}
Interface::~Interface()
{
mg_stop(m_Context);
Interface::~Interface() {
m_HttpServer->stop();
if(m_HttpServerThread.joinable()) {
m_HttpServerThread.join();
}
}
// Interactions with the game engine (g_Game) must be done in the main
@ -120,188 +239,6 @@ std::vector<std::string> Interface::GetTemplates(const std::vector<std::string>&
return templates;
}
void* Interface::MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info)
{
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;
const std::string uri = request_info->uri;
if (uri == "/reset")
{
std::string data = GetRequestContent(conn);
if (data.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
ScenarioConfig scenario;
const char *queryString = request_info->query_string;
const std::string_view qs(queryString ? queryString : "");
scenario.saveReplay = qs.find("saveReplay") != std::string_view::npos;
char playerID[1];
const int len = mg_get_var(queryString, qs.length(), "playerID", playerID, 1);
scenario.playerID = len == -1 ? 1 : std::stoi(playerID);
scenario.content = std::move(data);
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;
}
std::string data = GetRequestContent(conn);
std::stringstream postStream(data);
std::string line;
std::vector<GameCommand> commands;
while (std::getline(postStream, line, '\n'))
{
const std::size_t splitPos = line.find(";");
if (splitPos == std::string::npos)
continue;
GameCommand cmd;
cmd.playerID = std::stoi(line.substr(0, splitPos));
cmd.json_cmd = line.substr(splitPos + 1);
commands.push_back(std::move(cmd));
}
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 == "/evaluate")
{
if (!interface->IsGameRunning())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
std::string code = GetRequestContent(conn);
if (code.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
const std::string codeResult = interface->Evaluate(std::move(code));
if (codeResult.empty())
{
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
else
stream << codeResult.c_str();
}
else if (uri == "/templates")
{
if (!interface->IsGameRunning()) {
mg_printf(conn, "%s", notRunningResponse);
return handled;
}
const std::string data = GetRequestContent(conn);
if (data.empty())
{
mg_printf(conn, "%s", noPostData);
return handled;
}
std::stringstream postStream(data);
std::string line;
std::vector<std::string> 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);
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;
}
}
std::string Interface::GetRequestContent(struct mg_connection *conn)
{
const static std::string NO_CONTENT;
const char* val = mg_get_header(conn, "Content-Length");
if (!val)
{
return NO_CONTENT;
}
const int contentSize = std::atoi(val);
std::string content(contentSize, ' ');
mg_read(conn, content.data(), contentSize);
return content;
}
bool Interface::TryGetGameMessage(GameMessage& msg)
{
if (m_GameMessage.type != GameMessageType::None)

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify
@ -20,15 +20,15 @@
#include "lib/code_annotation.h"
#include "simulation2/helpers/Player.h"
#include "third_party/mongoose/mongoose.h"
#include <condition_variable>
#include <exception>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
struct mg_context;
namespace httplib { class Server; }
namespace RL
{
@ -93,7 +93,7 @@ class Interface
{
NONCOPYABLE(Interface);
public:
Interface(const char* server_address);
Interface(std::string const serverAddress);
~Interface();
/**
@ -103,9 +103,6 @@ public:
void TryApplyMessage();
private:
static void* MgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info);
static std::string GetRequestContent(struct mg_connection *conn);
/**
* Process commands, update the simulation by one turn.
* @return the gamestate after processing commands.
@ -170,7 +167,8 @@ private:
std::condition_variable m_MsgApplied;
std::string m_Code;
mg_context* m_Context;
std::unique_ptr<httplib::Server> m_HttpServer;
std::thread m_HttpServerThread;
};
}