/* 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 .
*/
// Pull in the headers from the default precompiled header,
// even if rlinterface doesn't use precompiled headers.
#include "lib/precompiled.h"
#include "rlinterface/RLInterface.h"
#include "gui/GUIManager.h"
#include "lib/debug.h"
#include "lib/types.h"
#include "network/HttpServer.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Errors.h"
#include "ps/Game.h"
#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"
#include "simulation2/Simulation2.h"
#include "simulation2/components/ICmpAIInterface.h"
#include "simulation2/components/ICmpTemplateManager.h"
#include "simulation2/system/Component.h"
#include "simulation2/system/LocalTurnManager.h"
#include "simulation2/system/TurnManager.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace RL
{
Interface::Interface(std::string const serverAddress)
{
LOGMESSAGERENDER("Starting RL interface HTTP server");
m_HttpServer = PS::Net::createHttpServer();
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 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 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() {
m_HttpServer->stop();
if(m_HttpServerThread.joinable()) {
m_HttpServerThread.join();
}
}
// Interactions with the game engine (g_Game) must be done in the main
// thread as there are specific checks for this. We will pass messages
// to the main thread to be applied (ie, "GameMessage"s).
std::string Interface::SendGameMessage(GameMessage&& msg)
{
std::unique_lock msgLock(m_MsgLock);
ENSURE(m_GameMessage.type == GameMessageType::None);
m_GameMessage = std::move(msg);
m_MsgApplied.wait(msgLock, [this]() { return m_GameMessage.type == GameMessageType::None; });
return m_ReturnValue;
}
std::string Interface::Step(std::vector&& commands)
{
std::lock_guard lock(m_Lock);
return SendGameMessage({ GameMessageType::Commands, std::move(commands) });
}
std::string Interface::Reset(ScenarioConfig&& scenario)
{
std::lock_guard lock(m_Lock);
m_ScenarioConfig = std::move(scenario);
return SendGameMessage({ GameMessageType::Reset });
}
std::string Interface::Evaluate(std::string&& code)
{
std::lock_guard lock(m_Lock);
m_Code = std::move(code);
return SendGameMessage({ GameMessageType::Evaluate });
}
std::vector Interface::GetTemplates(const std::vector& names) const
{
std::lock_guard lock(m_Lock);
CSimulation2& simulation = *g_Game->GetSimulation2();
CmpPtr cmpTemplateManager(simulation.GetSimContext().GetSystemEntity());
std::vector templates;
for (const std::string& templateName : names)
{
const CParamNode* node = cmpTemplateManager->GetTemplate(templateName);
if (node != nullptr)
templates.push_back(node->ToXMLString());
}
return templates;
}
bool Interface::TryGetGameMessage(GameMessage& msg)
{
if (m_GameMessage.type != GameMessageType::None)
{
msg = m_GameMessage;
m_GameMessage = {GameMessageType::None};
return true;
}
return false;
}
void Interface::TryApplyMessage()
{
const bool isGameStarted = g_Game && g_Game->IsGameStarted();
if (m_NeedsGameState && isGameStarted)
{
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
m_NeedsGameState = false;
}
if (!m_MsgLock.try_lock())
return;
GameMessage msg;
if (!TryGetGameMessage(msg))
{
m_MsgLock.unlock();
return;
}
ApplyMessage(msg);
}
void Interface::ApplyMessage(const GameMessage& msg)
{
const static std::string EMPTY_STATE;
const bool nonVisual = !g_GUI;
const bool isGameStarted = g_Game && g_Game->IsGameStarted();
switch (msg.type)
{
case GameMessageType::Reset:
{
if (isGameStarted)
EndGame();
g_Game = new CGame(m_ScenarioConfig.saveReplay);
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue attrs(rq.cx);
Script::ParseJSON(rq, m_ScenarioConfig.content, &attrs);
g_Game->SetPlayerID(m_ScenarioConfig.playerID);
g_Game->StartGame(&attrs, "");
if (nonVisual)
{
PS::Loader::NonprogressiveLoad();
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK);
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
}
else
{
JS::RootedValue initData(rq.cx);
Script::CreateObject(rq, &initData);
Script::SetProperty(rq, initData, "attribs", attrs);
JS::RootedValue playerAssignments(rq.cx);
Script::CreateObject(rq, &playerAssignments);
Script::SetProperty(rq, initData, "playerAssignments", playerAssignments);
g_GUI->SwitchPage(L"page_loading.xml", &scriptInterface, initData);
m_NeedsGameState = true;
}
break;
}
case GameMessageType::Commands:
{
if (!g_Game)
{
m_ReturnValue = EMPTY_STATE;
m_MsgApplied.notify_one();
m_MsgLock.unlock();
return;
}
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
CLocalTurnManager* turnMgr = static_cast(g_Game->GetTurnManager());
for (const GameCommand& command : msg.commands)
{
ScriptRequest rq(scriptInterface);
JS::RootedValue commandJSON(rq.cx);
Script::ParseJSON(rq, command.json_cmd, &commandJSON);
turnMgr->PostCommand(command.playerID, commandJSON);
}
const u32 deltaRealTime = DEFAULT_TURN_LENGTH;
if (nonVisual)
{
const double deltaSimTime = deltaRealTime * g_Game->GetSimRate();
const size_t maxTurns = static_cast(g_Game->GetSimRate());
g_Game->GetTurnManager()->Update(deltaSimTime, maxTurns,
std::bind_front(&CGUIManager::SendEventToAll, g_GUI));
}
else
g_Game->Update(deltaRealTime);
m_ReturnValue = GetGameState();
m_MsgApplied.notify_one();
m_MsgLock.unlock();
break;
}
case GameMessageType::Evaluate:
{
if (!g_Game)
{
m_ReturnValue = EMPTY_STATE;
m_MsgApplied.notify_one();
m_MsgLock.unlock();
return;
}
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
ScriptRequest rq(scriptInterface);
JS::RootedValue ret(rq.cx);
scriptInterface.Eval(m_Code.c_str(), &ret);
m_ReturnValue = Script::StringifyJSON(rq, &ret, false);
m_MsgApplied.notify_one();
m_MsgLock.unlock();
break;
}
default:
break;
}
}
std::string Interface::GetGameState() const
{
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface();
const CSimContext simContext = g_Game->GetSimulation2()->GetSimContext();
CmpPtr cmpAIInterface(simContext.GetSystemEntity());
ScriptRequest rq(scriptInterface);
JS::RootedValue state(rq.cx);
cmpAIInterface->GetFullRepresentation(&state, true);
return Script::StringifyJSON(rq, &state, false);
}
bool Interface::IsGameRunning() const
{
return g_Game != nullptr;
}
}