diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpWorldGetStats.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpWorldGetStats.lua index eb37e2e..c0fd43a 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpWorldGetStats.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/Ops/RDB_OpWorldGetStats.lua @@ -1,9 +1,10 @@ --- Registration is deferred to RDB_OpRegisterAll(), called from --- RDB_Bootstrap's onServerStarted, rather than run at file top-level: --- PZ's require() is best-effort (warns and continues rather than forcing a --- synchronous load), so RDB_OpRegistry may not be defined yet if this file's --- top-level code ran immediately at auto-load time. onServerStarted fires --- only after every file has auto-loaded, so it's safe there. +-- Registration is deferred to register(), called from RDB_Bootstrap's +-- registerOps() (itself called from onServerStarted), rather than run at +-- file top-level: PZ's require() is best-effort (warns and continues rather +-- than forcing a synchronous load), so RDB_OpRegistry may not be defined yet +-- if this file's top-level code ran immediately at auto-load time. +-- onServerStarted fires only after every file has auto-loaded, so it's safe +-- there. RDB_OpWorldGetStats = {} diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Idempotency.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Idempotency.lua index 01cb8e8..a814e4e 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Idempotency.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_Idempotency.lua @@ -1,7 +1,15 @@ -- Processed request-id -> cached response, so a repeated request id replays -- the original response instead of re-executing the op. -- Persisted via ModData so it survives restarts. +-- +-- Entries expire after RDB_Constants.IDEMPOTENCY_TTL_MS (60s by default): +-- a request id is only idempotent for retries within that window, not +-- forever. Without a TTL, a client that reuses a fixed id for routine +-- polling would get the first-ever response back on every call, since a +-- low-traffic server would rarely reach MAX_ENTRIES and trigger the +-- count-based eviction below. +require("RconDataBridge.RDB_Constants") require("RconDataBridge.RDB_Log") RDB_Idempotency = {} @@ -18,20 +26,35 @@ local function ensureStore() return store end +local function isExpired(entry) + return getTimestampMs() - (entry.ts or 0) >= RDB_Constants.IDEMPOTENCY_TTL_MS +end + function RDB_Idempotency.has(id) - return ensureStore()[id] ~= nil + local entry = ensureStore()[id] + return entry ~= nil and not isExpired(entry) end function RDB_Idempotency.get(id) local entry = ensureStore()[id] - if not entry then + if not entry or isExpired(entry) then return nil end return entry.response end +-- Sweeps expired entries first (the common case that keeps the store small +-- under normal traffic), then falls back to trimming the oldest entries if +-- still over MAX_ENTRIES (a backstop against a burst of distinct ids within +-- a single TTL window). local function prune(s) + for id, entry in pairs(s) do + if isExpired(entry) then + s[id] = nil + end + end + local n = 0 for _ in pairs(s) do n = n + 1 diff --git a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_WorldStats.lua b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_WorldStats.lua index f0f43e2..2430812 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_WorldStats.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/server/RconDataBridge/RDB_WorldStats.lua @@ -1,4 +1,4 @@ --- World telemetry. +-- World telemetry, sourced from zombie.GameTime (getGameTime()). require("RconDataBridge.RDB_Constants") require("RconDataBridge.RDB_Log") @@ -7,16 +7,30 @@ require("RconDataBridge.RDB_OptionsRegistry") RDB_WorldStats = {} -local cached = { playerCount = 0, generatedAt = 0 } +local cached = {} function RDB_WorldStats.collect() return cached end local function refresh() + local gt = getGameTime() + cached = { - playerCount = getNumActivePlayers(), - generatedAt = getTimestampMs(), + worldAgeHours = gt:getWorldAgeHours(), + worldAgeDays = gt:getWorldAgeDaysSinceBegin(), + nightsSurvived = gt:getNightsSurvived(), + date = { + year = gt:getYear(), + month = gt:getMonth(), + day = gt:getDay(), + hour = gt:getHour(), + minute = gt:getMinutes(), + }, + isNight = gt:isNight(), + isRaining = gt:isRainingToday(), + timeMultiplier = gt:getMultiplier(), + generatedAt = getTimestampMs(), } RDB_OptionsRegistry.set(RDB_Constants.OPT.WORLD_STATS, RDB_Json.encode(cached)) diff --git a/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Base64.lua b/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Base64.lua index 22f8e85..edaa01c 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Base64.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Base64.lua @@ -14,8 +14,6 @@ -- path) and read via showoptions, neither of which goes through the -- tokenizer, so they carry raw JSON unmolested. --- TODO: does output still base64 encode? otherwise the API is weird (base64 json in, json out) - RDB_Base64 = {} local ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" diff --git a/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Constants.lua b/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Constants.lua index 85e3220..7377467 100644 --- a/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Constants.lua +++ b/Contents/mods/RconDataBridge/common/media/lua/shared/RconDataBridge/RDB_Constants.lua @@ -24,6 +24,15 @@ RDB_Constants.MAX_PAYLOAD_BYTES = 4096 RDB_Constants.OPTION_MAX_LENGTH = 8192 RDB_Constants.POLL_EVERY_TICKS = 30 +-- How long a request id's cached response is replayed for on retry before +-- it's treated as expired and the op runs again. Deliberately short: a +-- client that reuses a fixed id for routine polling (e.g. "ws-poll" every +-- few minutes) should get a fresh answer each time, not the first-ever +-- response forever. A count-based cap alone (MAX_ENTRIES in +-- RDB_Idempotency) doesn't help here, it only evicts under high request +-- volume, never under low-and-slow polling, which is exactly this case. +RDB_Constants.IDEMPOTENCY_TTL_MS = 60 * 1000 + RDB_Constants.ERROR_CODES = { SCHEMA_ERROR = "SCHEMA_ERROR", PAYLOAD_TOO_LARGE = "PAYLOAD_TOO_LARGE",