From dd801de28db50cba98f05bcd3418ef09409de146 Mon Sep 17 00:00:00 2001 From: Overlord Date: Thu, 27 Aug 2026 11:57:02 +0200 Subject: [PATCH] Enhance request pipeline with safe operation dispatch, support for paused server states, and bridge status updates. --- .../RconDataBridge/Ops/RDB_OpBridgeStatus.lua | 13 ++++++- .../RconDataBridge/RDB_RequestPipeline.lua | 35 ++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua index 66a21e8..895ac9c 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpBridgeStatus.lua @@ -1,18 +1,29 @@ +---@diagnostic disable: need-check-nil + -- Registration deferred to register(), called from RDB_Bootstrap's -- onServerStarted, see RDB_OpWorldGetStats.lua. RDB_OpBridgeStatus = {} +-- Mirrors zombie.GameTime.isGamePaused()'s server-side branch exactly +-- (decompiled: `GameServer.Players.isEmpty() && ServerOptions.instance.pauseEmpty.getValue()`), +-- true precisely when RDB_RequestPipeline would be relying on Events.OnTickEvenPaused +-- instead of Events.OnTick to even see this request. +local function isPaused() + return ServerOptions.instance:getBoolean("PauseEmpty") and getOnlinePlayers():size() == 0 +end + function RDB_OpBridgeStatus.register() RDB_OpRegistry.register("get_bridge_status", { handler = function(_) local bootedAt = RDB_Bootstrap.getBootTimestamp() - local now = getTimestampMs() + local now = getTimestampMs() return true, { version = RDB_Constants.PROTOCOL_VERSION, bootedAt = bootedAt, uptimeSeconds = (now - bootedAt) / 1000, + paused = isPaused(), rateLimit = RDB_Security.getRateLimitStatus(), config = RDB_Config.getAll(), } diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua index 834e778..5ea442c 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_RequestPipeline.lua @@ -44,6 +44,19 @@ local function audit(id, op, ok, code) RDB_AuditLog.record({ id = id, op = op, ok = ok, code = code }) end +-- If an op handler throws (a bug, a bad deploy, a version mismatch between +-- files), surface it as a fast INTERNAL_ERROR response instead of silently +-- swallowing it. The raw Lua error is logged server-side only, never +-- sent to the client. +local function safeDispatch(op, args) + local ok, a, b, c = pcall(RDB_OpRegistry.dispatch, op, args) + if not ok then + RDB_Log.error("op '" .. tostring(op) .. "' handler threw: " .. tostring(a)) + return false, RDB_Constants.ERROR_CODES.INTERNAL_ERROR, "Unhandled error in operation handler." + end + return a, b, c +end + -- Handles one raw request string end to end. raw is base64: RCON's -- changeoption command strips every literal double-quote character from -- every argument (see RESEARCH_LOG.md), so a client can't send raw JSON -- @@ -109,7 +122,7 @@ local function processRequest(rawBase64) return end - local dispatchOk, dataOrCode, message = RDB_OpRegistry.dispatch(op, args) + local dispatchOk, dataOrCode, message = safeDispatch(op, args) local response if dispatchOk then response = buildOkResponse(id, dataOrCode) @@ -148,14 +161,28 @@ end local initialized = false --- Guarded: Events.OnServerStarted fires more than once per boot (see --- RESEARCH_LOG.md), and Events.OnTick.Add does not deduplicate identical --- listeners, so calling this twice would otherwise double-register onTick. +-- Guarded: Events.OnServerStarted fires more than once per boot, +-- and Events.OnTick.Add does not deduplicate identical listeners, +-- so calling this twice would otherwise double-register onTick. +-- +-- Registered on BOTH OnTick and OnTickEvenPaused: decompiling GameWindow's +-- main loop shows these two are mutually exclusive per real engine tick; +-- normal ticks fire OnTick (via IngameState's update path), while a tick +-- where the server is paused (PauseEmpty=true and no players connected; +-- see zombie.GameTime.isGamePaused) fires OnTickEvenPaused instead, never +-- both. Without the second registration, the entire bridge (every op, not +-- just one) goes completely inert on any server running PauseEmpty=true +-- whenever it's empty: no event ever calls onTick, so a request just sits +-- in RconDataBridge_Request forever with no response and no error, and a +-- client sees a silent timeout with nothing to diagnose. function RDB_RequestPipeline.init() if initialized then return end + initialized = true lastSeenRaw = RDB_OptionsRegistry.get(RDB_Constants.OPT.REQUEST) + Events.OnTick.Add(onTick) + Events.OnTickEvenPaused.Add(onTick) end