mirror of
https://github.com/wheremyfoodat/Panda3DS.git
synced 2025-04-08 23:25:40 +12:00
Refactor http server
This commit is contained in:
parent
4a24a331da
commit
3a21661f45
5 changed files with 212 additions and 152 deletions
|
@ -63,6 +63,7 @@ class Emulator {
|
||||||
|
|
||||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
||||||
HttpServer httpServer;
|
HttpServer httpServer;
|
||||||
|
friend class HttpServer;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Keep the handle for the ROM here to reload when necessary and to prevent deleting it
|
// Keep the handle for the ROM here to reload when necessary and to prevent deleting it
|
||||||
|
@ -93,8 +94,4 @@ class Emulator {
|
||||||
bool loadELF(const std::filesystem::path& path);
|
bool loadELF(const std::filesystem::path& path);
|
||||||
bool loadELF(std::ifstream& file);
|
bool loadELF(std::ifstream& file);
|
||||||
void initGraphicsContext();
|
void initGraphicsContext();
|
||||||
|
|
||||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
|
||||||
void pollHttpServer();
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,34 +3,75 @@
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <condition_variable>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
#include "helpers.hpp"
|
#include "helpers.hpp"
|
||||||
|
|
||||||
enum class HttpAction { None, Screenshot, PressKey, ReleaseKey };
|
enum class HttpActionType { None, Screenshot, Key };
|
||||||
|
|
||||||
|
class Emulator;
|
||||||
|
namespace httplib {
|
||||||
|
class Server;
|
||||||
|
class Response;
|
||||||
|
} // namespace httplib
|
||||||
|
|
||||||
|
// Wrapper for httplib::Response that allows the HTTP server to wait for the response to be ready
|
||||||
|
struct DeferredResponseWrapper {
|
||||||
|
DeferredResponseWrapper(httplib::Response& response) : inner_response(response) {}
|
||||||
|
|
||||||
|
httplib::Response& inner_response;
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
bool ready = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions derive from this class and are used to communicate with the HTTP server
|
||||||
|
class HttpAction {
|
||||||
|
public:
|
||||||
|
HttpAction(HttpActionType type) : type(type) {}
|
||||||
|
virtual ~HttpAction() = default;
|
||||||
|
|
||||||
|
HttpActionType getType() const { return type; }
|
||||||
|
|
||||||
|
static std::unique_ptr<HttpAction> createScreenshotAction(DeferredResponseWrapper& response);
|
||||||
|
static std::unique_ptr<HttpAction> createKeyAction(uint32_t key, bool state);
|
||||||
|
|
||||||
|
private:
|
||||||
|
HttpActionType type;
|
||||||
|
};
|
||||||
|
|
||||||
struct HttpServer {
|
struct HttpServer {
|
||||||
|
HttpServer(Emulator* emulator);
|
||||||
|
~HttpServer();
|
||||||
|
|
||||||
|
void processActions();
|
||||||
|
|
||||||
|
private:
|
||||||
static constexpr const char* httpServerScreenshotPath = "screenshot.png";
|
static constexpr const char* httpServerScreenshotPath = "screenshot.png";
|
||||||
|
|
||||||
std::atomic_bool pendingAction = false;
|
Emulator* emulator;
|
||||||
HttpAction action = HttpAction::None;
|
|
||||||
std::mutex actionMutex = {};
|
|
||||||
u32 pendingKey = 0;
|
|
||||||
|
|
||||||
HttpServer();
|
std::unique_ptr<httplib::Server> server;
|
||||||
|
|
||||||
void startHttpServer();
|
std::thread httpServerThread;
|
||||||
std::string status();
|
std::queue<std::unique_ptr<HttpAction>> actionQueue;
|
||||||
|
std::mutex actionQueueMutex;
|
||||||
|
|
||||||
private:
|
std::map<std::string, u32> keyMap;
|
||||||
std::map<std::string, std::pair<u32, bool>> keyMap;
|
|
||||||
std::array<bool, 12> pressedKeys = {};
|
|
||||||
bool paused = false;
|
bool paused = false;
|
||||||
|
|
||||||
|
void startHttpServer();
|
||||||
|
void pushAction(std::unique_ptr<HttpAction> action);
|
||||||
|
std::string status();
|
||||||
u32 stringToKey(const std::string& key_name);
|
u32 stringToKey(const std::string& key_name);
|
||||||
bool getKeyState(const std::string& key_name);
|
|
||||||
void setKeyState(const std::string& key_name, bool state);
|
HttpServer(const HttpServer&) = delete;
|
||||||
|
HttpServer& operator=(const HttpServer&) = delete;
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // PANDA3DS_ENABLE_HTTP_SERVER
|
#endif // PANDA3DS_ENABLE_HTTP_SERVER
|
|
@ -90,6 +90,7 @@ class HIDService {
|
||||||
|
|
||||||
void pressKey(u32 mask) { newButtons |= mask; }
|
void pressKey(u32 mask) { newButtons |= mask; }
|
||||||
void releaseKey(u32 mask) { newButtons &= ~mask; }
|
void releaseKey(u32 mask) { newButtons &= ~mask; }
|
||||||
|
bool isPressed(u32 mask) { return (oldButtons & mask) != 0; }
|
||||||
|
|
||||||
u32 getOldButtons() { return oldButtons; }
|
u32 getOldButtons() { return oldButtons; }
|
||||||
s16 getCirclepadX() { return circlePadX; }
|
s16 getCirclepadX() { return circlePadX; }
|
||||||
|
|
|
@ -13,7 +13,11 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1;
|
||||||
|
|
||||||
Emulator::Emulator()
|
Emulator::Emulator()
|
||||||
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config),
|
: config(std::filesystem::current_path() / "config.toml"), kernel(cpu, memory, gpu), cpu(memory, kernel), gpu(memory, config),
|
||||||
memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID()) {
|
memory(cpu.getTicksRef()), cheats(memory, kernel.getServiceManager().getHID())
|
||||||
|
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
||||||
|
, httpServer(this)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) {
|
||||||
Helpers::panic("Failed to initialize SDL2");
|
Helpers::panic("Failed to initialize SDL2");
|
||||||
}
|
}
|
||||||
|
@ -100,10 +104,6 @@ void Emulator::step() {}
|
||||||
void Emulator::render() {}
|
void Emulator::render() {}
|
||||||
|
|
||||||
void Emulator::run() {
|
void Emulator::run() {
|
||||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
|
||||||
httpServer.startHttpServer();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
while (running) {
|
while (running) {
|
||||||
runFrame();
|
runFrame();
|
||||||
HIDService& hid = kernel.getServiceManager().getHID();
|
HIDService& hid = kernel.getServiceManager().getHID();
|
||||||
|
@ -337,7 +337,7 @@ void Emulator::run() {
|
||||||
void Emulator::runFrame() {
|
void Emulator::runFrame() {
|
||||||
if (romType != ROMType::None) {
|
if (romType != ROMType::None) {
|
||||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
||||||
pollHttpServer();
|
httpServer.processActions();
|
||||||
#endif
|
#endif
|
||||||
cpu.runFrame(); // Run 1 frame of instructions
|
cpu.runFrame(); // Run 1 frame of instructions
|
||||||
gpu.display(); // Display graphics
|
gpu.display(); // Display graphics
|
||||||
|
@ -448,37 +448,3 @@ bool Emulator::loadELF(std::ifstream& file) {
|
||||||
|
|
||||||
// Reset our graphics context and initialize the GPU's graphics context
|
// Reset our graphics context and initialize the GPU's graphics context
|
||||||
void Emulator::initGraphicsContext() { gpu.initGraphicsContext(); }
|
void Emulator::initGraphicsContext() { gpu.initGraphicsContext(); }
|
||||||
|
|
||||||
#ifdef PANDA3DS_ENABLE_HTTP_SERVER
|
|
||||||
void Emulator::pollHttpServer() {
|
|
||||||
std::scoped_lock lock(httpServer.actionMutex);
|
|
||||||
|
|
||||||
HIDService& hid = kernel.getServiceManager().getHID();
|
|
||||||
|
|
||||||
if (httpServer.pendingAction) {
|
|
||||||
switch (httpServer.action) {
|
|
||||||
case HttpAction::Screenshot: gpu.screenshot(HttpServer::httpServerScreenshotPath); break;
|
|
||||||
|
|
||||||
case HttpAction::PressKey:
|
|
||||||
if (httpServer.pendingKey != 0) {
|
|
||||||
hid.pressKey(httpServer.pendingKey);
|
|
||||||
httpServer.pendingKey = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpAction::ReleaseKey:
|
|
||||||
if (httpServer.pendingKey != 0) {
|
|
||||||
hid.releaseKey(httpServer.pendingKey);
|
|
||||||
httpServer.pendingKey = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case HttpAction::None: break;
|
|
||||||
}
|
|
||||||
|
|
||||||
httpServer.action = HttpAction::None;
|
|
||||||
httpServer.pendingAction = false;
|
|
||||||
httpServer.pendingAction.notify_all();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
|
@ -2,131 +2,186 @@
|
||||||
#include "httpserver.hpp"
|
#include "httpserver.hpp"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
|
#include "emulator.hpp"
|
||||||
#include "httplib.h"
|
#include "httplib.h"
|
||||||
#include "services/hid.hpp"
|
|
||||||
|
|
||||||
HttpServer::HttpServer() : keyMap(
|
class HttpActionScreenshot : public HttpAction {
|
||||||
{
|
public:
|
||||||
{"A", { HID::Keys::A, false } },
|
HttpActionScreenshot(DeferredResponseWrapper& response) : HttpAction(HttpActionType::Screenshot), response(response) {}
|
||||||
{"B", { HID::Keys::B, false } },
|
|
||||||
{"Select", { HID::Keys::Select, false } },
|
DeferredResponseWrapper& getResponse() { return response; }
|
||||||
{"Start", { HID::Keys::Start, false } },
|
|
||||||
{"Right", { HID::Keys::Right, false } },
|
private:
|
||||||
{"Left", { HID::Keys::Left, false } },
|
DeferredResponseWrapper& response;
|
||||||
{"Up", { HID::Keys::Up, false } },
|
};
|
||||||
{"Down", { HID::Keys::Down, false } },
|
|
||||||
{"R", { HID::Keys::R, false } },
|
class HttpActionKey : public HttpAction {
|
||||||
{"L", { HID::Keys::L, false } },
|
public:
|
||||||
{"X", { HID::Keys::X, false } },
|
HttpActionKey(uint32_t key, bool state) : HttpAction(HttpActionType::Key), key(key), state(state) {}
|
||||||
{"Y", { HID::Keys::Y, false } },
|
|
||||||
|
uint32_t getKey() const { return key; }
|
||||||
|
bool getState() const { return state; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t key;
|
||||||
|
bool state;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unique_ptr<HttpAction> HttpAction::createScreenshotAction(DeferredResponseWrapper& response) {
|
||||||
|
return std::make_unique<HttpActionScreenshot>(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<HttpAction> HttpAction::createKeyAction(uint32_t key, bool state) { return std::make_unique<HttpActionKey>(key, state); }
|
||||||
|
|
||||||
|
HttpServer::HttpServer(Emulator* emulator)
|
||||||
|
: emulator(emulator), server(std::make_unique<httplib::Server>()), keyMap({
|
||||||
|
{"A", {HID::Keys::A}},
|
||||||
|
{"B", {HID::Keys::B}},
|
||||||
|
{"Select", {HID::Keys::Select}},
|
||||||
|
{"Start", {HID::Keys::Start}},
|
||||||
|
{"Right", {HID::Keys::Right}},
|
||||||
|
{"Left", {HID::Keys::Left}},
|
||||||
|
{"Up", {HID::Keys::Up}},
|
||||||
|
{"Down", {HID::Keys::Down}},
|
||||||
|
{"R", {HID::Keys::R}},
|
||||||
|
{"L", {HID::Keys::L}},
|
||||||
|
{"X", {HID::Keys::X}},
|
||||||
|
{"Y", {HID::Keys::Y}},
|
||||||
|
}) {
|
||||||
|
httpServerThread = std::thread(&HttpServer::startHttpServer, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServer::~HttpServer() {
|
||||||
|
printf("Stopping http server...\n");
|
||||||
|
server->stop();
|
||||||
|
if (httpServerThread.joinable()) {
|
||||||
|
httpServerThread.join();
|
||||||
}
|
}
|
||||||
) {}
|
}
|
||||||
|
|
||||||
|
void HttpServer::pushAction(std::unique_ptr<HttpAction> action) {
|
||||||
|
std::scoped_lock lock(actionQueueMutex);
|
||||||
|
actionQueue.push(std::move(action));
|
||||||
|
}
|
||||||
|
|
||||||
void HttpServer::startHttpServer() {
|
void HttpServer::startHttpServer() {
|
||||||
std::thread http_thread([this]() {
|
server->Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); });
|
||||||
httplib::Server server;
|
|
||||||
|
|
||||||
server.Get("/ping", [](const httplib::Request&, httplib::Response& response) { response.set_content("pong", "text/plain"); });
|
server->Get("/screen", [this](const httplib::Request&, httplib::Response& response) {
|
||||||
|
DeferredResponseWrapper wrapper(response);
|
||||||
server.Get("/screen", [this](const httplib::Request&, httplib::Response& response) {
|
// Lock the mutex before pushing the action to ensure that the condition variable is not notified before we wait on it
|
||||||
{
|
std::unique_lock lock(wrapper.mutex);
|
||||||
std::scoped_lock lock(actionMutex);
|
pushAction(HttpAction::createScreenshotAction(wrapper));
|
||||||
pendingAction = true;
|
wrapper.cv.wait(lock, [&wrapper] { return wrapper.ready; });
|
||||||
action = HttpAction::Screenshot;
|
|
||||||
}
|
|
||||||
// wait until the screenshot is ready
|
|
||||||
pendingAction.wait(true);
|
|
||||||
std::ifstream image(httpServerScreenshotPath, std::ios::binary);
|
|
||||||
std::vector<char> buffer(std::istreambuf_iterator<char>(image), {});
|
|
||||||
response.set_content(buffer.data(), buffer.size(), "image/png");
|
|
||||||
});
|
|
||||||
|
|
||||||
server.Get("/input", [this](const httplib::Request& request, httplib::Response& response) {
|
|
||||||
bool ok = false;
|
|
||||||
for (auto& [keyStr, value] : request.params) {
|
|
||||||
auto key = stringToKey(keyStr);
|
|
||||||
printf("Param: %s\n", keyStr.c_str());
|
|
||||||
|
|
||||||
if (key != 0) {
|
|
||||||
std::scoped_lock lock(actionMutex);
|
|
||||||
pendingAction = true;
|
|
||||||
pendingKey = key;
|
|
||||||
ok = true;
|
|
||||||
if (value == "1") {
|
|
||||||
action = HttpAction::PressKey;
|
|
||||||
setKeyState(keyStr, true);
|
|
||||||
} else if (value == "0") {
|
|
||||||
action = HttpAction::ReleaseKey;
|
|
||||||
setKeyState(keyStr, false);
|
|
||||||
} else {
|
|
||||||
// Should not happen but just in case
|
|
||||||
pendingAction = false;
|
|
||||||
ok = false;
|
|
||||||
}
|
|
||||||
// Not supporting multiple keys at once for now (ever?)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ok) {
|
|
||||||
response.set_content("ok", "text/plain");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.Get("/step", [this](const httplib::Request&, httplib::Response& response) {
|
|
||||||
// TODO: implement /step
|
|
||||||
response.set_content("ok", "text/plain");
|
|
||||||
});
|
|
||||||
|
|
||||||
server.Get("/status", [this](const httplib::Request&, httplib::Response& response) {
|
|
||||||
response.set_content(status(), "text/plain");
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: ability to specify host and port
|
|
||||||
printf("Starting HTTP server on port 1234\n");
|
|
||||||
server.listen("localhost", 1234);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
http_thread.detach();
|
server->Get("/input", [this](const httplib::Request& request, httplib::Response& response) {
|
||||||
|
bool ok = false;
|
||||||
|
for (auto& [keyStr, value] : request.params) {
|
||||||
|
u32 key = stringToKey(keyStr);
|
||||||
|
|
||||||
|
if (key != 0) {
|
||||||
|
bool state = value == "1";
|
||||||
|
|
||||||
|
if (!state && value != "0") {
|
||||||
|
// Invalid state
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushAction(HttpAction::createKeyAction(key, state));
|
||||||
|
ok = true;
|
||||||
|
} else {
|
||||||
|
// Invalid key
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
response.set_content("ok", "text/plain");
|
||||||
|
} else {
|
||||||
|
response.set_content("error", "text/plain");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server->Get("/step", [this](const httplib::Request&, httplib::Response& response) {
|
||||||
|
// TODO: implement /step
|
||||||
|
response.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
server->Get("/status", [this](const httplib::Request&, httplib::Response& response) { response.set_content(status(), "text/plain"); });
|
||||||
|
|
||||||
|
// TODO: ability to specify host and port
|
||||||
|
printf("Starting HTTP server on port 1234\n");
|
||||||
|
server->listen("localhost", 1234);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string HttpServer::status() {
|
std::string HttpServer::status() {
|
||||||
|
HIDService& hid = emulator->kernel.getServiceManager().getHID();
|
||||||
|
|
||||||
std::stringstream stringStream;
|
std::stringstream stringStream;
|
||||||
|
|
||||||
stringStream << "Panda3DS\n";
|
stringStream << "Panda3DS\n";
|
||||||
stringStream << "Status: " << (paused ? "Paused" : "Running") << "\n";
|
stringStream << "Status: " << (paused ? "Paused" : "Running") << "\n";
|
||||||
|
|
||||||
for (auto& [keyStr, value] : keyMap) {
|
for (auto& [keyStr, value] : keyMap) {
|
||||||
stringStream << keyStr << ": " << value.second << "\n";
|
stringStream << keyStr << ": " << hid.isPressed(value) << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return stringStream.str();
|
return stringStream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void HttpServer::processActions() {
|
||||||
|
std::scoped_lock lock(actionQueueMutex);
|
||||||
|
|
||||||
|
HIDService& hid = emulator->kernel.getServiceManager().getHID();
|
||||||
|
|
||||||
|
while (!actionQueue.empty()) {
|
||||||
|
std::unique_ptr<HttpAction> action = std::move(actionQueue.front());
|
||||||
|
actionQueue.pop();
|
||||||
|
|
||||||
|
switch (action->getType()) {
|
||||||
|
case HttpActionType::Screenshot: {
|
||||||
|
HttpActionScreenshot* screenshotAction = static_cast<HttpActionScreenshot*>(action.get());
|
||||||
|
emulator->gpu.screenshot(httpServerScreenshotPath);
|
||||||
|
std::ifstream file(httpServerScreenshotPath, std::ios::binary);
|
||||||
|
std::vector<char> buffer(std::istreambuf_iterator<char>(file), {});
|
||||||
|
|
||||||
|
DeferredResponseWrapper& response = screenshotAction->getResponse();
|
||||||
|
response.inner_response.set_content(buffer.data(), buffer.size(), "image/png");
|
||||||
|
std::unique_lock<std::mutex> lock(response.mutex);
|
||||||
|
response.ready = true;
|
||||||
|
response.cv.notify_one();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case HttpActionType::Key: {
|
||||||
|
HttpActionKey* keyAction = static_cast<HttpActionKey*>(action.get());
|
||||||
|
if (keyAction->getState()) {
|
||||||
|
hid.pressKey(keyAction->getKey());
|
||||||
|
} else {
|
||||||
|
hid.releaseKey(keyAction->getKey());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
u32 HttpServer::stringToKey(const std::string& key_name) {
|
u32 HttpServer::stringToKey(const std::string& key_name) {
|
||||||
if (keyMap.find(key_name) != keyMap.end()) {
|
if (keyMap.find(key_name) != keyMap.end()) {
|
||||||
return keyMap[key_name].first;
|
return keyMap[key_name];
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpServer::getKeyState(const std::string& key_name) {
|
|
||||||
if (keyMap.find(key_name) != keyMap.end()) {
|
|
||||||
return keyMap[key_name].second;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HttpServer::setKeyState(const std::string& key_name, bool state) {
|
|
||||||
if (keyMap.find(key_name) != keyMap.end()) {
|
|
||||||
keyMap[key_name].second = state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // PANDA3DS_ENABLE_HTTP_SERVER
|
#endif // PANDA3DS_ENABLE_HTTP_SERVER
|
Loading…
Add table
Reference in a new issue