diff --git a/res/scripts/stdlib.lua b/res/scripts/stdlib.lua index b08e8c02..3f732e07 100644 --- a/res/scripts/stdlib.lua +++ b/res/scripts/stdlib.lua @@ -587,6 +587,7 @@ function __process_post_runnables() __vc_named_coroutines[name] = nil end + debug.pull_events() network.__process_events() block.__process_register_events() block.__perform_ticks(time.delta()) diff --git a/res/scripts/stdmin.lua b/res/scripts/stdmin.lua index 26d5d813..df5c7ce2 100644 --- a/res/scripts/stdmin.lua +++ b/res/scripts/stdmin.lua @@ -1,3 +1,144 @@ +local breakpoints = {} +local dbg_steps_mode = false +local dbg_step_into_func = false +local hook_lock = false +local current_func +local current_func_stack_size + +local _debug_getinfo = debug.getinfo +local _debug_getlocal = debug.getlocal +local __pause = debug.pause +local __error = error +local __sethook = debug.sethook + +-- 'return' hook not called for some functions +-- todo: speedup +local function calc_stack_size() + local s = debug.traceback("", 2) + local count = 0 + for i in s:gmatch("\n") do + count = count + 1 + end + return count +end + +local is_debugging = debug.is_debugging() +if is_debugging then + __sethook(function (e, line) + if e == "return" then + local info = _debug_getinfo(2) + if info.func == current_func then + current_func = nil + end + end + if dbg_steps_mode and not hook_lock then + hook_lock = true + + if not dbg_step_into_func then + local func = _debug_getinfo(2).func + if func ~= current_func then + return + end + if current_func_stack_size ~= calc_stack_size() then + return + end + end + current_func = func + __pause("step") + debug.pull_events() + end + hook_lock = false + local bps = breakpoints[line] + if not bps then + return + end + local source = _debug_getinfo(2).source + if not bps[source] then + return + end + current_func = _debug_getinfo(2).func + current_func_stack_size = calc_stack_size() + __pause("breakpoint") + debug.pull_events() + end, "lr") +end + +local DBG_EVENT_SET_BREAKPOINT = 1 +local DBG_EVENT_RM_BREAKPOINT = 2 +local DBG_EVENT_STEP = 3 +local DBG_EVENT_STEP_INTO_FUNCTION = 4 +local DBG_EVENT_RESUME = 5 +local DBG_EVENT_GET_VALUE = 6 +local __pull_events = debug.__pull_events +local __sendvalue = debug.__sendvalue +debug.__pull_events = nil +debug.__sendvalue = nil + +function debug.pull_events() + if not is_debugging then + return + end + if not debug.is_debugging() then + is_debugging = false + __sethook() + end + local events = __pull_events() + if not events then + return + end + for i, event in ipairs(events) do + if event[1] == DBG_EVENT_SET_BREAKPOINT then + debug.set_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_RM_BREAKPOINT then + debug.remove_breakpoint(event[2], event[3]) + elseif event[1] == DBG_EVENT_STEP then + dbg_steps_mode = true + dbg_step_into_func = false + elseif event[1] == DBG_EVENT_STEP_INTO_FUNCTION then + dbg_steps_mode = true + dbg_step_into_func = true + elseif event[1] == DBG_EVENT_RESUME then + dbg_steps_mode = false + dbg_step_into_func = false + elseif event[1] == DBG_EVENT_GET_VALUE then + local _, value = _debug_getlocal(event[2] + 3, event[3]) + for _, key in ipairs(event[4]) do + if value == nil then + value = "error: index nil value" + break + end + value = value[key] + end + __sendvalue(value, event[2], event[3], event[4]) + __pause() + end + end +end + +function debug.set_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + bps = {} + breakpoints[line] = bps + end + bps[source] = true +end + +function debug.remove_breakpoint(source, line) + local bps = breakpoints[line] + if not bps then + return + end + bps[source] = nil +end + +function error(message, level) + if is_debugging then + __pause("exception", message) + end + __error(message, level) +end + -- Lua has no parallelizm, also _set_data does not call any lua functions so -- may be reused one global ffi buffer per lua_State local canvas_ffi_buffer @@ -473,8 +614,6 @@ function file.readlines(path) return lines end -local _debug_getinfo = debug.getinfo - function debug.count_frames() local frames = 1 while true do diff --git a/src/devtools/DebuggingServer.cpp b/src/devtools/DebuggingServer.cpp new file mode 100644 index 00000000..09a2b368 --- /dev/null +++ b/src/devtools/DebuggingServer.cpp @@ -0,0 +1,315 @@ +#include "DebuggingServer.hpp" + +#include "engine/Engine.hpp" +#include "network/Network.hpp" +#include "debug/Logger.hpp" +#include "coders/json.hpp" + +using namespace devtools; + +static debug::Logger logger("debug-server"); + +ClientConnection::~ClientConnection() { + if (auto connection = dynamic_cast( + network.getConnection(this->connection, true) + )) { + connection->close(); + } +} + +bool ClientConnection::initiate(network::ReadableConnection* connection) { + if (connection->available() < 8) { + return false; + } + char buffer[8] {}; + char expected[8] {}; + std::memcpy(expected, VCDBG_MAGIC, sizeof(VCDBG_MAGIC)); + expected[6] = VCDBG_VERSION >> 8; + expected[7] = VCDBG_VERSION & 0xFF; + connection->recv(buffer, sizeof(VCDBG_MAGIC)); + + connection->send(expected, sizeof(VCDBG_MAGIC)); + if (std::memcmp(expected, buffer, sizeof(VCDBG_MAGIC)) == 0) { + initiated = true; + return false; + } else { + connection->close(true); + return true; + } +} + +std::string ClientConnection::read() { + auto connection = dynamic_cast( + network.getConnection(this->connection, true) + ); + if (connection == nullptr) { + return ""; + } + if (!initiated) { + if (initiate(connection)) { + return ""; + } + } + if (messageLength == 0) { + if (connection->available() >= sizeof(int32_t)) { + int32_t length = 0; + connection->recv(reinterpret_cast(&length), sizeof(int32_t)); + if (length <= 0) { + logger.error() << "invalid message length " << length; + } else { + logger.info() << "message length " << length; + messageLength = length; + } + } + } else if (connection->available() >= messageLength) { + std::string string(messageLength, 0); + connection->recv(string.data(), messageLength); + messageLength = 0; + return string; + } + return ""; +} + +void ClientConnection::send(const dv::value& object) { + auto connection = dynamic_cast( + network.getConnection(this->connection, true) + ); + if (connection == nullptr) { + return; + } + auto message = json::stringify(object, false); + int32_t length = message.length(); + connection->send(reinterpret_cast(&length), sizeof(int32_t)); + connection->send(message.data(), length); +} + +void ClientConnection::sendResponse(const std::string& type) { + send(dv::object({{"type", type}})); +} + +bool ClientConnection::alive() const { + return network.getConnection(this->connection, true) != nullptr; +} + +static network::Server& create_tcp_server( + DebuggingServer& dbgServer, Engine& engine, int port +) { + auto& network = engine.getNetwork(); + u64id_t serverId = network.openTcpServer( + port, + [&network, &dbgServer](u64id_t sid, u64id_t id) { + auto& connection = dynamic_cast( + *network.getConnection(id, true) + ); + connection.setPrivate(true); + logger.info() << "connected client " << id << ": " + << connection.getAddress() << ":" + << connection.getPort(); + dbgServer.setClient(id); + } + ); + auto& server = *network.getServer(serverId, true); + server.setPrivate(true); + + auto& tcpServer = dynamic_cast(server); + tcpServer.setMaxClientsConnected(1); + + logger.info() << "tcp debugging server open at port " << server.getPort(); + + return tcpServer; +} + +static network::Server& create_server( + DebuggingServer& dbgServer, Engine& engine, const std::string& serverString +) { + logger.info() << "starting debugging server"; + + size_t sepPos = serverString.find(':'); + if (sepPos == std::string::npos) { + throw std::runtime_error("invalid debugging server configuration string"); + } + auto transport = serverString.substr(0, sepPos); + if (transport == "tcp") { + int port; + try { + port = std::stoi(serverString.substr(sepPos + 1)); + } catch (const std::exception& err) { + throw std::runtime_error("invalid tcp port"); + } + return create_tcp_server(dbgServer, engine, port); + } else { + throw std::runtime_error( + "unsupported debugging server transport '" + transport + "'" + ); + } +} + +DebuggingServer::DebuggingServer( + Engine& engine, const std::string& serverString +) + : engine(engine), + server(create_server(*this, engine, serverString)), + connection(nullptr) { +} + +DebuggingServer::~DebuggingServer() { + logger.info() << "stopping debugging server"; + server.close(); +} + + +bool DebuggingServer::update() { + if (connection == nullptr) { + return false; + } + std::string message = connection->read(); + if (message.empty()) { + if (!connection->alive()) { + bool status = performCommand(disconnectAction, dv::object()); + connection.reset(); + return status; + } + return false; + } + logger.debug() << "received: " << message; + try { + auto obj = json::parse(message); + if (!obj.has("type")) { + logger.error() << "missing message type"; + return false; + } + const auto& type = obj["type"].asString(); + if (performCommand(type, obj)) { + connection->sendResponse("resumed"); + return true; + } + } catch (const std::runtime_error& err) { + logger.error() << "could not to parse message: " << err.what(); + } + return false; +} + +bool DebuggingServer::performCommand( + const std::string& type, const dv::value& map +) { + if (!connectionEstablished && type == "connect") { + map.at("disconnect-action").get(disconnectAction); + connectionEstablished = true; + logger.info() << "client connection established"; + connection->sendResponse("success"); + } + if (!connectionEstablished) { + return false; + } + if (type == "terminate") { + engine.quit(); + connection->sendResponse("success"); + } else if (type == "detach") { + connection->sendResponse("success"); + connection.reset(); + engine.detachDebugger(); + return false; + } else if (type == "set-breakpoint" || type == "remove-breakpoint") { + if (!map.has("source") || !map.has("line")) + return false; + breakpointEvents.push_back(DebuggingEvent { + type[0] == 's' + ? DebuggingEventType::SET_BREAKPOINT + : DebuggingEventType::REMOVE_BREAKPOINT, + BreakpointEventDto { + map["source"].asString(), + static_cast(map["line"].asInteger()), + } + }); + } else if (type == "step" || type == "step-into-function") { + breakpointEvents.push_back(DebuggingEvent { + type == "step" + ? DebuggingEventType::STEP + : DebuggingEventType::STEP_INTO_FUNCTION, + SignalEventDto {} + }); + return true; + } else if (type == "resume") { + breakpointEvents.push_back(DebuggingEvent { + DebuggingEventType::RESUME, SignalEventDto {}}); + return true; + } else if (type == "get-value") { + if (!map.has("frame") || !map.has("local") || !map.has("path")) + return false; + + int frame = map["frame"].asInteger(); + int localIndex = map["local"].asInteger(); + + ValuePath path; + for (const auto& segment : map["path"]) { + if (segment.isString()) { + path.emplace_back(segment.asString()); + } else { + path.emplace_back(static_cast(segment.asInteger())); + } + } + breakpointEvents.push_back(DebuggingEvent { + DebuggingEventType::GET_VALUE, GetValueEventDto { + frame, localIndex, std::move(path) + } + }); + return true; + } else { + logger.error() << "unsupported command '" << type << "'"; + } + return false; +} + +void DebuggingServer::pause( + std::string&& reason, std::string&& message, dv::value&& stackTrace +) { + if (connection == nullptr) { + return; + } + auto response = dv::object({{"type", std::string("paused")}}); + if (!reason.empty()) { + response["reason"] = std::move(reason); + } + if (!message.empty()) { + response["message"] = std::move(message); + } + if (stackTrace != nullptr) { + response["stack"] = std::move(stackTrace); + } + connection->send(std::move(response)); + engine.startPauseLoop(); +} + +void DebuggingServer::sendValue( + dv::value&& value, int frame, int local, ValuePath&& path +) { + auto pathValue = dv::list(); + for (const auto& segment : path) { + if (auto string = std::get_if(&segment)) { + pathValue.add(*string); + } else { + pathValue.add(std::get(segment)); + } + } + connection->send(dv::object({ + {"type", std::string("value")}, + {"frame", frame}, + {"local", local}, + {"path", std::move(pathValue)}, + {"value", std::move(value)}, + })); +} + +void DebuggingServer::setClient(u64id_t client) { + connection = + std::make_unique(engine.getNetwork(), client); + connectionEstablished = false; +} + +std::vector DebuggingServer::pullEvents() { + return std::move(breakpointEvents); +} + +void DebuggingServer::setDisconnectAction(const std::string& action) { + disconnectAction = action; +} diff --git a/src/devtools/DebuggingServer.hpp b/src/devtools/DebuggingServer.hpp new file mode 100644 index 00000000..b02229c6 --- /dev/null +++ b/src/devtools/DebuggingServer.hpp @@ -0,0 +1,106 @@ +#pragma once + +#include +#include +#include +#include + +#include "typedefs.hpp" + +namespace network { + class Server; + class Connection; + class ReadableConnection; + class Network; +} + +namespace dv { + class value; +} + +class Engine; + +namespace devtools { + inline constexpr const char VCDBG_MAGIC[8] = "vc-dbg\0"; + inline constexpr int VCDBG_VERSION = 1; + + class ClientConnection { + public: + ClientConnection(network::Network& network, u64id_t connection) + : network(network), connection(connection) { + } + ~ClientConnection(); + + std::string read(); + void send(const dv::value& message); + void sendResponse(const std::string& type); + + bool alive() const; + private: + network::Network& network; + size_t messageLength = 0; + u64id_t connection; + bool initiated = false; + + bool initiate(network::ReadableConnection* connection); + }; + + enum class DebuggingEventType { + SET_BREAKPOINT = 1, + REMOVE_BREAKPOINT, + STEP, + STEP_INTO_FUNCTION, + RESUME, + GET_VALUE, + }; + + struct BreakpointEventDto { + std::string source; + int line; + }; + + struct SignalEventDto { + }; + + using ValuePath = std::vector>; + + struct GetValueEventDto { + int frame; + int localIndex; + ValuePath path; + }; + + struct DebuggingEvent { + DebuggingEventType type; + std::variant data; + }; + + class DebuggingServer { + public: + DebuggingServer(Engine& engine, const std::string& serverString); + ~DebuggingServer(); + + bool update(); + void pause( + std::string&& reason, std::string&& message, dv::value&& stackTrace + ); + + void sendValue(dv::value&& value, int frame, int local, ValuePath&& path); + + void setClient(u64id_t client); + std::vector pullEvents(); + + void setDisconnectAction(const std::string& action); + private: + Engine& engine; + network::Server& server; + std::unique_ptr connection; + bool connectionEstablished = false; + std::vector breakpointEvents; + std::string disconnectAction = "resume"; + + bool performCommand( + const std::string& type, const dv::value& map + ); + }; +} diff --git a/src/engine/Engine.cpp b/src/engine/Engine.cpp index d2a4eb20..a977f354 100644 --- a/src/engine/Engine.cpp +++ b/src/engine/Engine.cpp @@ -14,6 +14,7 @@ #include "coders/commons.hpp" #include "devtools/Editor.hpp" #include "devtools/Project.hpp" +#include "devtools/DebuggingServer.hpp" #include "content/ContentControl.hpp" #include "core_defs.hpp" #include "io/io.hpp" @@ -115,6 +116,9 @@ void Engine::initializeClient() { if (ENGINE_DEBUG_BUILD) { title += " [debug]"; } + if (debuggingServer) { + title = "[debugging] " + title; + } auto [window, input] = Window::initialize(&settings.display, title); if (!window || !input){ throw initialize_error("could not initialize window"); @@ -173,6 +177,18 @@ void Engine::initialize(CoreParameters coreParameters) { cmd = std::make_unique(); network = network::Network::create(settings.network); + if (!params.debugServerString.empty()) { + try { + debuggingServer = std::make_unique( + *this, params.debugServerString + ); + } catch (const std::runtime_error& err) { + throw initialize_error( + "debugging server error: " + std::string(err.what()) + ); + } + } + if (!params.scriptFile.empty()) { paths.setScriptFolder(params.scriptFile.parent_path()); } @@ -266,6 +282,14 @@ void Engine::postUpdate() { network->update(); postRunnables.run(); scripting::process_post_runnables(); + + if (debuggingServer) { + debuggingServer->update(); + } +} + +void Engine::detachDebugger() { + debuggingServer.reset(); } void Engine::updateFrontend() { @@ -287,6 +311,30 @@ void Engine::nextFrame() { input->pollEvents(); } +void Engine::startPauseLoop() { + bool initialCursorLocked = false; + if (!isHeadless()) { + initialCursorLocked = input->isCursorLocked(); + if (initialCursorLocked) { + input->toggleCursor(); + } + } + while (!isQuitSignal() && debuggingServer) { + network->update(); + if (debuggingServer->update()) { + break; + } + if (isHeadless()) { + platform::sleep(1.0 / params.tps * 1000); + } else { + nextFrame(); + } + } + if (initialCursorLocked) { + input->toggleCursor(); + } +} + void Engine::renderFrame() { screen->draw(time.getDelta()); @@ -299,7 +347,11 @@ void Engine::saveSettings() { io::write_string(EnginePaths::SETTINGS_FILE, toml::stringify(*settingsHandler)); if (!params.headless) { logger.info() << "saving bindings"; - io::write_string(EnginePaths::CONTROLS_FILE, input->getBindings().write()); + if (input) { + io::write_string( + EnginePaths::CONTROLS_FILE, input->getBindings().write() + ); + } } } @@ -318,6 +370,7 @@ void Engine::close() { logger.info() << "gui finished"; } audio::close(); + debuggingServer.reset(); network.reset(); clearKeepedObjects(); project.reset(); diff --git a/src/engine/Engine.hpp b/src/engine/Engine.hpp index 1f9438c0..67acc498 100644 --- a/src/engine/Engine.hpp +++ b/src/engine/Engine.hpp @@ -36,6 +36,7 @@ namespace network { namespace devtools { class Editor; + class DebuggingServer; } class initialize_error : public std::runtime_error { @@ -50,6 +51,7 @@ struct CoreParameters { std::filesystem::path userFolder = "."; std::filesystem::path scriptFile; std::filesystem::path projectFolder; + std::string debugServerString = "tcp:9030"; int tps = 20; }; @@ -72,6 +74,7 @@ class Engine : public util::ObjectsKeeper { std::unique_ptr input; std::unique_ptr gui; std::unique_ptr editor; + std::unique_ptr debuggingServer; PostRunnables postRunnables; Time time; OnWorldOpen levelConsumer; @@ -105,6 +108,7 @@ public: void updateFrontend(); void renderFrame(); void nextFrame(); + void startPauseLoop(); /// @brief Set screen (scene). /// nullptr may be used to delete previous screen before creating new one, @@ -182,4 +186,10 @@ public: const Project& getProject() { return *project; } + + devtools::DebuggingServer* getDebuggingServer() { + return debuggingServer.get(); + } + + void detachDebugger(); }; diff --git a/src/logic/scripting/lua/lua_extensions.cpp b/src/logic/scripting/lua/lua_extensions.cpp index b9c25fc6..27b47fae 100644 --- a/src/logic/scripting/lua/lua_extensions.cpp +++ b/src/logic/scripting/lua/lua_extensions.cpp @@ -1,11 +1,15 @@ #include #include #include +#include #include "libs/api_lua.hpp" #include "debug/Logger.hpp" +#include "engine/Engine.hpp" +#include "devtools/DebuggingServer.hpp" #include "logic/scripting/scripting.hpp" +using namespace devtools; using namespace scripting; static debug::Logger logger("lua-debug"); @@ -159,6 +163,223 @@ static int l_math_normal_random(lua::State* L) { return lua::pushnumber(L, randomFloats(generator)); } +constexpr inline int MAX_SHORT_STRING_LEN = 50; + +static std::string get_short_value(lua::State* L, int idx, int type) { + switch (type) { + case LUA_TNIL: + return "nil"; + case LUA_TBOOLEAN: + return lua::toboolean(L, idx) ? "true" : "false"; + case LUA_TNUMBER: { + std::stringstream ss; + ss << lua::tonumber(L, idx); + return ss.str(); + } + case LUA_TSTRING: { + const char* str = lua::tostring(L, idx); + if (strlen(str) > MAX_SHORT_STRING_LEN) { + return std::string(str, MAX_SHORT_STRING_LEN); + } else { + return str; + } + } + case LUA_TTABLE: + return "{...}"; + case LUA_TFUNCTION: { + std::stringstream ss; + ss << "function: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); + return ss.str(); + } + case LUA_TUSERDATA: { + std::stringstream ss; + ss << "userdata: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); + return ss.str(); + } + case LUA_TTHREAD: { + std::stringstream ss; + ss << "thread: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); + return ss.str(); + } + default: { + std::stringstream ss; + ss << "cdata: 0x" << std::hex + << reinterpret_cast(lua::topointer(L, idx)); + return ss.str(); + } + } +} + +static dv::value collect_locals(lua::State* L, lua_Debug& frame) { + auto locals = dv::list(); + + int localIndex = 1; + const char* name; + while ((name = lua_getlocal(L, &frame, localIndex++))) { + if (name[0] == '(') { + lua::pop(L); + continue; + } + auto local = dv::object(); + local["name"] = name; + local["index"] = localIndex - 1; + + int type = lua::type(L, -1); + local["type"] = lua::type_name(L, type); + local["short"] = get_short_value(L, -1, type); + locals.add(std::move(local)); + lua::pop(L); + } + return locals; +} + +static dv::value create_stack_trace(lua::State* L, int initFrame = 2) { + auto entriesList = dv::list(); + + lua_Debug frame; + int level = initFrame; + + while (lua_getstack(L, level, &frame)) { + auto entry = dv::object(); + if (lua_getinfo(L, "nSlf", &frame) == 0) { + level++; + entriesList.add(std::move(entry)); + continue; + } + if (frame.name) { + entry["function"] = frame.name; + } + if (frame.source) { + const char* src = + (frame.source[0] == '@') ? frame.source + 1 : frame.source; + entry["source"] = src; + entry["line"] = frame.currentline; + } + entry["what"] = frame.what; + entry["locals"] = collect_locals(L, frame); + entriesList.add(std::move(entry)); + level++; + } + return entriesList; +} + +static int l_debug_pause(lua::State* L) { + if (auto server = engine->getDebuggingServer()) { + std::string reason; + std::string message; + if (lua::isstring(L, 1)) { + reason = lua::tolstring(L, 1); + } + if (lua::isstring(L, 2)) { + message = lua::tolstring(L, 2); + } + server->pause( + std::move(reason), std::move(message), create_stack_trace(L) + ); + } + return 0; +} + +static int l_debug_sendvalue(lua::State* L) { + auto server = engine->getDebuggingServer(); + if (!server) { + return 0; + } + int frame = lua::tointeger(L, 2); + int local = lua::tointeger(L, 3); + + ValuePath path; + int pathSectors = lua::objlen(L, 4); + for (int i = 0; i < pathSectors; i++) { + lua::rawgeti(L, i + 1, 4); + if (lua::isstring(L, -1)) { + path.emplace_back(lua::tostring(L, -1)); + } else { + path.emplace_back(static_cast(lua::tointeger(L, -1))); + } + lua::pop(L); + } + + dv::value value = nullptr; + if (lua::istable(L, 1)) { + auto table = dv::object(); + + lua::pushnil(L); + while (lua::next(L, 1)) { + auto key = lua::tolstring(L, -2); + + int type = lua::type(L, -1); + table[std::string(key)] = dv::object({ + {"type", std::string(lua::type_name(L, type))}, + {"short", get_short_value(L, -1, type)}, + }); + lua::pop(L); + } + lua::pop(L); + value = std::move(table); + } else { + value = lua::tovalue(L, 1); + } + + server->sendValue(std::move(value), frame, local, std::move(path)); + return 0; +} + +static int l_debug_pull_events(lua::State* L) { + auto server = engine->getDebuggingServer(); + if (!server) { + return 0; + } + auto events = server->pullEvents(); + if (events.empty()) { + return 0; + } + lua::createtable(L, events.size(), 0); + for (int i = 0; i < events.size(); i++) { + const auto& event = events[i]; + lua::createtable(L, 3, 0); + + lua::pushinteger(L, static_cast(event.type)); + lua::rawseti(L, 1); + + if (auto dto = std::get_if(&event.data)) { + lua::pushstring(L, dto->source); + lua::rawseti(L, 2); + + lua::pushinteger(L, dto->line); + lua::rawseti(L, 3); + } else if (auto dto = std::get_if(&event.data)) { + lua::pushinteger(L, dto->frame); + lua::rawseti(L, 2); + + lua::pushinteger(L, dto->localIndex); + lua::rawseti(L, 3); + + lua::createtable(L, dto->path.size(), 0); + for (int i = 0; i < dto->path.size(); i++) { + const auto& segment = dto->path[i]; + if (auto string = std::get_if(&segment)) { + lua::pushstring(L, *string); + } else { + lua::pushinteger(L, std::get(segment)); + } + lua::rawseti(L, i + 1); + } + lua::rawseti(L, 4); + } + + lua::rawseti(L, i + 1); + } + return 1; +} + +static int l_debug_is_debugging(lua::State* L) { + return lua::pushboolean(L, engine->getDebuggingServer() != nullptr); +} + void initialize_libs_extends(lua::State* L) { if (lua::getglobal(L, "debug")) { lua::pushcfunction(L, lua::wrap); @@ -173,6 +394,18 @@ void initialize_libs_extends(lua::State* L) { lua::pushcfunction(L, lua::wrap); lua::setfield(L, "print"); + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "pause"); + + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__pull_events"); + + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "__sendvalue"); + + lua::pushcfunction(L, lua::wrap); + lua::setfield(L, "is_debugging"); + lua::pop(L); } if (lua::getglobal(L, "math")) { diff --git a/src/logic/scripting/lua/lua_util.hpp b/src/logic/scripting/lua/lua_util.hpp index d7aed664..a8fb246e 100644 --- a/src/logic/scripting/lua/lua_util.hpp +++ b/src/logic/scripting/lua/lua_util.hpp @@ -249,7 +249,7 @@ namespace lua { inline lua::Number tonumber(lua::State* L, int idx) { #ifndef NDEBUG if (lua_type(L, idx) != LUA_TNUMBER && !lua_isnoneornil(L, idx)) { - throw std::runtime_error("integer expected"); + throw std::runtime_error("number expected"); } #endif return lua_tonumber(L, idx); @@ -617,7 +617,9 @@ namespace lua { void remove_environment(lua::State*, int id); inline void close(lua::State* L) { - lua_close(L); + if (L) { + lua_close(L); + } } inline void addfunc( diff --git a/src/logic/scripting/lua/lua_wrapper.hpp b/src/logic/scripting/lua/lua_wrapper.hpp index f82f5bd4..df130af1 100644 --- a/src/logic/scripting/lua/lua_wrapper.hpp +++ b/src/logic/scripting/lua/lua_wrapper.hpp @@ -50,8 +50,8 @@ namespace lua { inline int type(lua::State* L, int idx) { return lua_type(L, idx); } - inline const char* type_name(lua::State* L, int idx) { - return lua_typename(L, idx); + inline const char* type_name(lua::State* L, int tp) { + return lua_typename(L, tp); } inline int rawget(lua::State* L, int idx = -2) { lua_rawget(L, idx); diff --git a/src/main.cpp b/src/main.cpp index 40e9665a..256818c3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,7 +25,7 @@ int main(int argc, char** argv) { } std::signal(SIGTERM, sigterm_handler); - debug::Logger::init(coreParameters.userFolder.string()+"/latest.log"); + debug::Logger::init(coreParameters.userFolder.string() + "/latest.log"); platform::configure_encoding(); auto& engine = Engine::getInstance(); @@ -33,7 +33,8 @@ int main(int argc, char** argv) { engine.initialize(std::move(coreParameters)); engine.run(); } catch (const initialize_error& err) { - logger.error() << "could not to initialize engine\n" << err.what(); + logger.error() << err.what(); + logger.error() << "could not to initialize engine"; } #if defined(NDEBUG) and defined(_WIN32) catch (const std::exception& err) { diff --git a/src/network/Network.cpp b/src/network/Network.cpp index 04c8dbec..be3f8d33 100644 --- a/src/network/Network.cpp +++ b/src/network/Network.cpp @@ -164,15 +164,16 @@ void Network::update() { } ++socketiter; } - auto serveriter = servers.begin(); - while (serveriter != servers.end()) { - auto server = serveriter->second.get(); - if (!server->isOpen()) { - serveriter = servers.erase(serveriter); - continue; - } - ++serveriter; + } + auto serveriter = servers.begin(); + while (serveriter != servers.end()) { + auto server = serveriter->second.get(); + if (!server->isOpen()) { + serveriter = servers.erase(serveriter); + continue; } + server->update(); + ++serveriter; } } diff --git a/src/network/Network.hpp b/src/network/Network.hpp index a6c2fca2..1110c750 100644 --- a/src/network/Network.hpp +++ b/src/network/Network.hpp @@ -3,13 +3,12 @@ #include "commons.hpp" namespace network { - class TcpConnection : public Connection { + class TcpConnection : public ReadableConnection { public: ~TcpConnection() override = default; virtual void connect(runnable callback) = 0; - virtual int recv(char* buffer, size_t length) = 0; - virtual int available() = 0; + virtual void setNoDelay(bool noDelay) = 0; [[nodiscard]] virtual bool isNoDelay() const = 0; @@ -37,6 +36,8 @@ namespace network { [[nodiscard]] TransportType getTransportType() const noexcept override { return TransportType::TCP; } + + virtual void setMaxClientsConnected(int count) = 0; }; class UdpServer : public Server { diff --git a/src/network/Sockets.cpp b/src/network/Sockets.cpp index 5152028b..c1b6285f 100644 --- a/src/network/Sockets.cpp +++ b/src/network/Sockets.cpp @@ -10,6 +10,7 @@ #ifdef _WIN32 #include +#define SHUT_RDWR SD_BOTH #else #include #include @@ -231,7 +232,7 @@ public: readBatch.clear(); if (state != ConnectionState::CLOSED) { - shutdown(descriptor, 2); + shutdown(descriptor, SHUT_RDWR); closesocket(descriptor); } } @@ -304,6 +305,7 @@ class SocketTcpServer : public TcpServer { bool open = true; std::unique_ptr thread = nullptr; int port; + int maxConnected = -1; public: SocketTcpServer(u64id_t id, Network* network, SOCKET descriptor, int port) : id(id), network(network), descriptor(descriptor), port(port) {} @@ -312,6 +314,22 @@ public: closeSocket(); } + void setMaxClientsConnected(int count) override { + maxConnected = count; + } + + void update() override { + std::vector clients; + for (u64id_t cid : this->clients) { + if (auto client = network->getConnection(cid, true)) { + if (client->getState() != ConnectionState::CLOSED) { + clients.emplace_back(cid); + } + } + } + std::swap(clients, this->clients); + } + void startListen(ConnectCallback handler) override { thread = std::make_unique([this, handler]() { while (open) { @@ -328,6 +346,11 @@ public: close(); break; } + if (maxConnected >= 0 && clients.size() >= maxConnected) { + logger.info() << "refused connection attempt from " << to_string(address); + closesocket(clientDescriptor); + continue; + } logger.info() << "client connected: " << to_string(address); auto socket = std::make_shared( clientDescriptor, address @@ -575,6 +598,8 @@ public: SocketUdpServer::close(); } + void update() override {} + void startListen(ServerDatagramCallback handler) override { callback = std::move(handler); diff --git a/src/network/commons.hpp b/src/network/commons.hpp index 83881c52..150bde25 100644 --- a/src/network/commons.hpp +++ b/src/network/commons.hpp @@ -75,9 +75,17 @@ namespace network { bool isprivate = false; }; + class ReadableConnection : public Connection { + public: + virtual int recv(char* buffer, size_t length) = 0; + virtual int available() = 0; + }; + class Server { public: virtual ~Server() = default; + + virtual void update() = 0; virtual void close() = 0; virtual bool isOpen() = 0; [[nodiscard]] virtual TransportType getTransportType() const noexcept = 0; diff --git a/src/util/command_line.cpp b/src/util/command_line.cpp index 6d84ab28..3691b77b 100644 --- a/src/util/command_line.cpp +++ b/src/util/command_line.cpp @@ -70,11 +70,15 @@ static bool perform_keyword( std::cout << ENGINE_VERSION_STRING << std::endl; return false; }, "", "display the engine version."), + ArgC("--dbg-server", [¶ms, &reader]() -> bool { + params.debugServerString = reader.next(); + return true; + }, "", "open debugging server where is {transport}:{port}"), ArgC("--help", []() -> bool { std::cout << "VoxelCore v" << ENGINE_VERSION_STRING << "\n\n"; std::cout << "Command-line arguments:\n"; for (auto& a : argumentsCommandline) { - std::cout << std::setw(20) << std::left << (a.keyword + " " + a.args); + std::cout << std::setw(24) << std::left << (a.keyword + " " + a.args); std::cout << "- " << a.help << std::endl; } std::cout << std::endl;