Changeset View
Standalone View
source/rlinterface/RLInterface.cpp
- This file was added.
/* Copyright (C) 2020 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 "rlinterface/RLInterface.h" | |||||
#include <vector> | |||||
#include <regex> | |||||
#include <sstream> | |||||
#include <boost/algorithm/string.hpp> | |||||
#include "ps/CLogger.h" | |||||
#include "simulation2/system/LocalTurnManager.h" | |||||
#include "third_party/mongoose/mongoose.h" | |||||
#include "third_party/mongoose/mongoose.cpp" | |||||
std::string RLInterface::SendGameMessage(const GameMessage msg) | |||||
{ | |||||
std::unique_lock<std::mutex> msgLock(m_msgLock); // Rename these? | |||||
m_GameMessage = &msg; | |||||
m_msgApplied.wait(msgLock); | |||||
return m_GameState; | |||||
Stan: range based for loop ? | |||||
Not Done Inline ActionsI don't think it is possible since this is using the autogenerated cpp files from the protobuf (it uses commands->actions(i) rather than commands->actions[i] so the "array" of actions is actually a functions which deserializes the given action from an index). Otherwise, I would definitely prefer it! irishninja: I don't think it is possible since this is using the autogenerated cpp files from the protobuf… | |||||
Not Done Inline ActionsAh right, I always suggest it, then look after at the code XD Stan: Ah right, I always suggest it, then look //after //at the code XD | |||||
Done Inline Actionshaha, no worries :) irishninja: haha, no worries :) | |||||
} | |||||
std::string RLInterface::Step(const std::vector<Command> commands) | |||||
Done Inline ActionsI think we have a specific type for player ids player_id_t or something. Stan: I think we have a specific type for player ids player_id_t or something. | |||||
{ | |||||
std::lock_guard<std::mutex> lock(m_lock); | |||||
// Interactions with the game engine (g_Game) must be done in the main | |||||
// thread as there are specific checks for this. We will pass our commands | |||||
// to the main thread to be applied | |||||
GameMessage msg = { GameMessageType::Commands, commands }; | |||||
return SendGameMessage(msg); | |||||
} | |||||
std::string RLInterface::Reset(const ScenarioConfig* scenario) | |||||
{ | |||||
std::lock_guard<std::mutex> lock(m_lock); | |||||
m_ScenarioConfig = *scenario; | |||||
Done Inline ActionsNo braces for single line statements see https://trac.wildfiregames.com/wiki/Coding_Conventions Stan: No braces for single line statements see https://trac.wildfiregames.com/wiki/Coding_Conventions | |||||
Done Inline ActionsNot done :) Stan: Not done :) | |||||
Done Inline ActionsMy bad. I will fix this :) irishninja: My bad. I will fix this :) | |||||
struct GameMessage msg = { GameMessageType::Reset }; | |||||
return SendGameMessage(msg); | |||||
Not Done Inline Actionsconst method? Stan: const method? | |||||
Not Done Inline Actions? Stan: ? | |||||
Done Inline ActionsUnfortunately, GetTemplates cannot be const since it acquires m_lock :/ irishninja: Unfortunately, `GetTemplates` cannot be const since it acquires `m_lock` :/ | |||||
Not Done Inline ActionsYou could make the lock mutable. This is a common well defined pattern exactly for mutexes. See https://en.cppreference.com/w/cpp/language/cv wraitii: You could make the lock mutable. This is a common well defined pattern exactly for mutexes. See… | |||||
} | |||||
std::vector<std::string> RLInterface::GetTemplates(const std::vector<std::string> names) const | |||||
{ | |||||
std::lock_guard<std::mutex> lock(m_lock); | |||||
CSimulation2& simulation = *g_Game->GetSimulation2(); | |||||
CmpPtr<ICmpTemplateManager> cmpTemplateManager(simulation.GetSimContext().GetSystemEntity()); | |||||
Not Done Inline Actionsconst &? Stan: const &? | |||||
Done Inline Actionsconst ? Stan: const ? | |||||
std::vector<std::string> templates; | |||||
for (const std::string templateName : names) | |||||
{ | |||||
const CParamNode* node = cmpTemplateManager->GetTemplate(templateName); | |||||
if (node != nullptr) | |||||
Done Inline ActionsError ? Stan: Error ? | |||||
{ | |||||
std::string content = utf8_from_wstring(node->ToXML()); | |||||
templates.push_back(content); | |||||
} | |||||
Done Inline Actionsavoid auto when you can :) Stan: avoid auto when you can :) | |||||
} | |||||
return templates; | |||||
} | |||||
Done Inline Actionsrange based for loop ? Stan: range based for loop ? | |||||
static void* RLMgCallback(mg_event event, struct mg_connection *conn, const struct mg_request_info *request_info) | |||||
{ | |||||
RLInterface* interface = (RLInterface*)request_info->user_data; | |||||
ENSURE(interface); | |||||
void* handled = (void*)""; // arbitrary non-NULL pointer to indicate successful handling | |||||
Done Inline Actionsnullptr Stan: nullptr | |||||
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* 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) | |||||
{ | |||||
Done Inline ActionsDoxygen. Stan: Doxygen. | |||||
case MG_NEW_REQUEST: | |||||
{ | |||||
std::stringstream stream; | |||||
std::string uri = request_info->uri; | |||||
if (uri == "/reset") | |||||
{ | |||||
int bufSize = conn->buf_size; | |||||
char buf[bufSize]; | |||||
mg_read(conn, &buf[0], bufSize); | |||||
std::string content(buf); | |||||
std::string qs(request_info->query_string); | |||||
ScenarioConfig scenario; | |||||
scenario.saveReplay = qs.find("saveReplay") != std::string::npos; | |||||
Done Inline ActionsThis does not follow our conventions for switch, see https://trac.wildfiregames.com/wiki/Coding_Conventions wraitii: This does not follow our conventions for `switch`, see https://trac.wildfiregames. | |||||
scenario.playerID = 1; | |||||
Done Inline Actionswe have a weird convention for this. -1 indent. Stan: we have a weird convention for this. -1 indent. | |||||
const std::regex playerRegex(".*playerID=([0-9]+)"); | |||||
std::smatch match; | |||||
Done Inline Actionsbraces Stan: braces | |||||
if (std::regex_search(qs, match, playerRegex)) | |||||
{ | |||||
Done Inline ActionsMight need JSAutoRequest pagerq(pcx); @elexis might know. Stan: Might need JSAutoRequest pagerq(pcx); @elexis might know. | |||||
int playerID = std::stoi(match[1].str()); | |||||
scenario.playerID = playerID; | |||||
} | |||||
scenario.content = content; | |||||
std::string gameState = interface->Reset(&scenario); | |||||
stream << gameState.c_str(); | |||||
} | |||||
else if (uri == "/step") | |||||
{ | |||||
if (!interface->IsGameRunning()) { | |||||
mg_printf(conn, "%s", notRunningResponse); | |||||
Done Inline ActionsComments on top missing final . Stan: Comments on top missing final . | |||||
return handled; | |||||
} | |||||
int bufSize = conn->buf_size; | |||||
char buf[bufSize]; | |||||
mg_read(conn, &buf[0], bufSize); | |||||
std::string postData(buf); | |||||
std::vector<std::string> lines; | |||||
boost::split(lines, postData, boost::is_any_of("\n")); | |||||
Done Inline ActionsStan: Avoid eval calls, we have a way to make empty objects @elexis or @Itms will tell you more. | |||||
Done Inline Actionsconst & ? Stan: const & ? | |||||
std::vector<Command> commands; | |||||
for (const std::string line : lines) { | |||||
Command cmd; | |||||
const std::size_t splitPos = line.find(";"); | |||||
if (splitPos != std::string::npos) | |||||
{ | |||||
cmd.playerID = std::stoi(line.substr(0, splitPos)); | |||||
cmd.json_cmd = line.substr(splitPos + 1); | |||||
commands.push_back(cmd); | |||||
} | |||||
} | |||||
std::string gameState = interface->Step(commands); | |||||
stream << gameState.c_str(); | |||||
} | |||||
else if (uri == "/templates") | |||||
{ | |||||
if (!interface->IsGameRunning()) { | |||||
Done Inline Actions. Stan: . | |||||
mg_printf(conn, "%s", notRunningResponse); | |||||
return handled; | |||||
Not Done Inline Actionsthis is probably not useful, I needed it during debugging. wraitii: this is probably not useful, I needed it during debugging. | |||||
} | |||||
int bufSize = conn->buf_size; | |||||
char buf[bufSize]; | |||||
mg_read(conn, &buf[0], bufSize); | |||||
std::string postData(buf); | |||||
std::vector<std::string> templateNames; | |||||
boost::split(templateNames, postData, boost::is_any_of("\n")); | |||||
for (std::string templateStr : interface->GetTemplates(templateNames)) | |||||
{ | |||||
stream << templateStr.c_str() << "\n"; | |||||
Done Inline Actions. Stan: . | |||||
Not Done Inline Actionsno braces ? Itms: no braces ? | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
mg_printf(conn, "%s", header404); | |||||
return handled; | |||||
} | |||||
Done Inline ActionsI am guessing I should also remove the curly braces here, too. Right? irishninja: I am guessing I should also remove the curly braces here, too. Right? | |||||
Done Inline ActionsYup Stan: Yup | |||||
Done Inline Actionsuse cpp style casts like you did above. Stan: use cpp style casts like you did above. | |||||
mg_printf(conn, "%s", header200); | |||||
std::string str = stream.str(); | |||||
mg_write(conn, str.c_str(), str.length()); | |||||
return handled; | |||||
} | |||||
Done Inline Actionsnullptr here and below I guess Stan: nullptr here and below I guess | |||||
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; | |||||
Done Inline ActionsAutorequest here to. Stan: Autorequest here to. | |||||
case MG_INIT_SSL: | |||||
Not Done Inline Actionsno braces here either Itms: no braces here either | |||||
return nullptr; | |||||
Done Inline Actionssame here about auto. Stan: same here about auto. | |||||
default: | |||||
debug_warn(L"Invalid Mongoose event type"); | |||||
return nullptr; | |||||
Not Done Inline Actionsand there Itms: and there | |||||
} | |||||
}; | |||||
void RLInterface::EnableHTTP(char* server_address) | |||||
{ | |||||
LOGMESSAGERENDER("Starting RL interface HTTP server"); | |||||
// Ignore multiple enablings | |||||
if (m_MgContext) | |||||
return; | |||||
const char *options[] = { | |||||
"listening_ports", server_address, | |||||
"num_threads", "6", // enough for the browser's parallel connection limit | |||||
nullptr | |||||
}; | |||||
Done Inline Actionsnullptr Stan: nullptr | |||||
m_MgContext = mg_start(RLMgCallback, this, options); | |||||
ENSURE(m_MgContext); | |||||
} | |||||
bool RLInterface::TryGetGameMessage(GameMessage& msg) | |||||
{ | |||||
if (m_GameMessage != nullptr) { | |||||
msg = *m_GameMessage; | |||||
m_GameMessage = nullptr; | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
void RLInterface::TryApplyMessage() | |||||
{ | |||||
const bool nonVisual = !g_GUI; | |||||
const bool isGameStarted = g_Game && g_Game->IsGameStarted(); | |||||
if (m_NeedsGameState && isGameStarted) | |||||
{ | |||||
m_GameState = GetGameState(); | |||||
m_msgApplied.notify_one(); | |||||
m_msgLock.unlock(); | |||||
m_NeedsGameState = false; | |||||
} | |||||
if (m_msgLock.try_lock()) | |||||
{ | |||||
GameMessage msg; | |||||
if (TryGetGameMessage(msg)) { | |||||
switch (msg.type) | |||||
{ | |||||
case GameMessageType::Reset: | |||||
{ | |||||
if (isGameStarted) | |||||
EndGame(); | |||||
Not Done Inline ActionsMissing jsautorequest here and below? @wraitii might know more Stan: Missing jsautorequest here and below? @wraitii might know more | |||||
Not Done Inline ActionsProbably yes. wraitii: Probably yes. | |||||
g_Game = new CGame(m_ScenarioConfig.saveReplay); | |||||
ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); | |||||
JSContext* cx = scriptInterface.GetContext(); | |||||
JS::RootedValue attrs(cx); | |||||
scriptInterface.ParseJSON(m_ScenarioConfig.content, &attrs); | |||||
g_Game->SetPlayerID(m_ScenarioConfig.playerID); | |||||
g_Game->StartGame(&attrs, ""); | |||||
if (nonVisual) | |||||
{ | |||||
LDR_NonprogressiveLoad(); | |||||
ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); | |||||
m_GameState = GetGameState(); | |||||
m_msgApplied.notify_one(); | |||||
m_msgLock.unlock(); | |||||
} | |||||
else | |||||
{ | |||||
JS::RootedValue initData(cx); | |||||
scriptInterface.CreateObject(cx, &initData); | |||||
scriptInterface.SetProperty(initData, "attribs", attrs); | |||||
JS::RootedValue playerAssignments(cx); | |||||
scriptInterface.CreateObject(cx, &playerAssignments); | |||||
scriptInterface.SetProperty(initData, "playerAssignments", playerAssignments); | |||||
Done Inline ActionsJSAutoRequest Itms: `JSAutoRequest` | |||||
g_GUI->SwitchPage(L"page_loading.xml", &scriptInterface, initData); | |||||
m_NeedsGameState = true; | |||||
} | |||||
} | |||||
break; | |||||
case GameMessageType::Commands: | |||||
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); | |||||
CLocalTurnManager* turnMgr = static_cast<CLocalTurnManager*>(g_Game->GetTurnManager()); | |||||
for (Command command : msg.commands) | |||||
{ | |||||
JSContext* cx = scriptInterface.GetContext(); | |||||
JS::RootedValue commandJSON(cx); | |||||
scriptInterface.ParseJSON(command.json_cmd, &commandJSON); | |||||
turnMgr->PostCommand(command.playerID, commandJSON); | |||||
} | |||||
const double deltaRealTime = DEFAULT_TURN_LENGTH_SP; | |||||
if (nonVisual) | |||||
{ | |||||
const double deltaSimTime = deltaRealTime * g_Game->GetSimRate(); | |||||
size_t maxTurns = static_cast<size_t>(g_Game->GetSimRate()); | |||||
g_Game->GetTurnManager()->Update(deltaSimTime, maxTurns); | |||||
} | |||||
else | |||||
g_Game->Update(deltaRealTime); | |||||
Not Done Inline ActionsWhy did you add braces to the body of this case? They seem unneeded with the current code. Itms: Why did you add braces to the body of this `case`? They seem unneeded with the current code. | |||||
Done Inline ActionsWithout the braces, I am getting redeclaration errors and a "jump to case label" error. Is there a better way I should be handling this? irishninja: Without the braces, I am getting redeclaration errors and a "jump to case label" error. Is… | |||||
Done Inline ActionsOh sorry I always forget that the compiler doesn't understand that break avoids the collision. The code is good then. Just for the style, maybe put the break; inside the braces, and add the same braces to the next case, in case new cases are added in the future. Itms: Oh sorry I always forget that the compiler doesn't understand that `break` avoids the collision. | |||||
m_GameState = GetGameState(); | |||||
m_msgApplied.notify_one(); | |||||
m_msgLock.unlock(); | |||||
break; | |||||
} | |||||
} else | |||||
m_msgLock.unlock(); | |||||
Done Inline Actionsno braces Stan: no braces | |||||
} | |||||
Done Inline ActionsJSAutoRequest Itms: `JSAutoRequest` | |||||
} | |||||
std::string RLInterface::GetGameState() | |||||
{ | |||||
const ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); | |||||
const CSimContext simContext = g_Game->GetSimulation2()->GetSimContext(); | |||||
CmpPtr<ICmpAIInterface> cmpAIInterface(simContext.GetSystemEntity()); | |||||
JSContext* cx = scriptInterface.GetContext(); | |||||
JS::RootedValue state(cx); | |||||
Not Done Inline ActionsDepending on the AI interface for this isn't great. You can probably mimic the function from C++ however, by calling Simulation2's GetEntitiesWithInterface, and then making GUIInterfaceCalls using the ComponentManager. wraitii: Depending on the AI interface for this isn't great.
You can probably mimic the function from… | |||||
Done Inline ActionsWould this approach still work when running headlessly? irishninja: Would this approach still work when running headlessly? | |||||
Not Done Inline ActionsI believe it will, yes. Why do you think it might not work? Perhaps there's something more I can explain about how our simulation works :) wraitii: I believe it will, yes.
Why do you think it might not work? Perhaps there's something more I… | |||||
Not Done Inline ActionsI had assumed that GUIInterfaceCalls required a GUI to be present. I don't fully understand how it all fits together just yet :/ After looking at the code a little more, I see an example where script calls are made from the GuiInterface for getting the replay metadata. Is this what you mean by GuiInterfaceCalls? It also looks like that example uses CmpPtr rather than GetEntitiesWithInterface and I don't quite understand how these are meant to be combined. I had been getting the AI interface but it seems here that I need entities with the AI interface (since I am going to be calling "GetFullRepresentation") but they also need to have the GuiInterface (since I need to make ScriptCalls). I am sure I am misunderstanding but if you could provide some guidance or clarity, it would be much appreciated :) As an example of what I have been playing around with. The following code fails as expected but might help illustrate how I understand the suggestion (incorrect but maybe it will be helpful): CSimulation2* sim = g_Game->GetSimulation2(); CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_GuiInterface); entity_id_t ent = ents.front().first; const CSimContext simContext = g_Game->GetSimulation2()->GetSimContext(); CmpPtr<ICmpGuiInterface> cmpGuiInterface(simContext, ent); ScriptInterface& scriptInterface = sim->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue arg(cx); JS::RootedValue state(cx); cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetFullRepresentation", arg, &state); // This fails as the fn is only defined for the AI interface. Switching to use the AIInterface fails to compile since "ScriptCall" is not defined for ICmpAIInterface. return scriptInterface.StringifyJSON(&state, false); irishninja: I had assumed that GUIInterfaceCalls required a GUI to be present. I don't fully understand how… | |||||
Not Done Inline Actions
Indeed it doesn't. When you call something in the GuiInterface, you're calling the simulation. Components can also be "system" or local to an entity. The GuiInterface is a "system" component. Your (understandable) mistake was thinking that each entity has a GuiInterface like they have an AIProxy. AIInterface is also a system component, by the way. CmpPtr is just a C++ wrapper to a component pointer. So to recap, what you need to do is something like this (compilation not guaranteed): CSimulation2* sim = g_Game->GetSimulation2(); // Here I fetch a list of all entities in the game with an IID_AIProxy. CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_AIProxy); // Query the GUI Interface of the system. CmpPtr<ICmpGuiInterface> cmpGuiInterface(sim, SYSTEM_ENTITY); ScriptInterface& scriptInterface = sim->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); for (entity_id_t ent : ents) { // this is per-entity CmpPtr<ICmpAIProxy> cmpAIProxy(sim, ent); std::string test = cmpAIProxy->GetFullRepresentation(); } cmpGuiInterface->ScriptCall(INVALID_PLAYER, L"GetSimulationState", arg, &state); return scriptInterface.StringifyJSON(&state, false); The thing is this still relies on AIProxy, so it's kind of the same as using AIInterface... And it need s aC++ AI Proxy component :/ So to be honest I'm not sure it's much better than what you have now, after all. wraitii: > I had assumed that GUIInterfaceCalls required a GUI to be present. I don't fully understand… | |||||
cmpAIInterface->GetFullRepresentation(&state, true); | |||||
return scriptInterface.StringifyJSON(&state, false); | |||||
} | |||||
bool RLInterface::IsGameRunning() | |||||
{ | |||||
return !!g_Game; | |||||
} | |||||
Done Inline ActionsJSAutoRequest Itms: `JSAutoRequest` |
range based for loop ?