Problem statement
Last updated
May be stale
This page was last updated ago, on 2 September 2026. Check what it says against the code before relying on it.
This is the note that started the effort, moved here verbatim from todo/server-session-system.md
so it lives with the rest of the topic. It is the problem, not the design: charting corrected
one of its premises and cut one of its proposed API functions. Read
Charted decisions for what was actually settled, and
#3431 for the map.
Two corrections apply to the text below, both from the charting session:
- Where it asks for an identifier "provably tied to the real process lifetime", the predicate is actually world-load lifetime.
refund_entscares about entities being destroyed, and achangelevelkeeps the process and Lua state alive while wiping every entity — so a PID-based marker would newly break the case thatos.time()-at-load happens to get right today. #3432 took this further: the load ofsv_session.luaitself is the derivation point, withGM:Initializethe place to persist rather than derive.cityrp.session.previous()is cut. It is listed below as API, but its only caller would berefund_ents'lastSession, which on reading turns out to be a scoping window that silently drops contraband for anyone who misses two restarts — a latent bug, not a requirement.
Problem
There is no gamemode-wide concept of "the current server session" — i.e. a value that identifies one continuous run of the actual server process, and that reliably changes only when the process really restarts (not when Lua code is hot-reloaded).
The only thing close to this today is local to one plugin:
gamemodes/cityrp_2_by_limelightgaming/gamemode/core/plugins/refund_ents/sv_init.lua:21
PLUGIN.session = os.time()
This is set when the plugin file is loaded/parsed, not when the server process
actually boots. PLUGIN:Load() uses row.start_time == self.session as its sole
signal for "is this tracked entity still physically in the world (same session) or
was it wiped by a restart (older session, needs refunding)". Because the signal is
file-load time rather than process-boot time, any Lua reload that doesn't also restart
the engine process re-derives a new "session" while the actual world entities are
untouched — misclassifying live, still-owned entities as crash losses.
This is exactly the bug class behind
issue #106
("Contra Refund Bug (Exploit)"): a cross-player variant of it (missing owner_steamid filter,
letting session mixing hand out other players' contraband) was only fixed 2024-12-15 in
c4f7727a6, over five years after it was first reported. The underlying "what actually counts as a
new session" question has never been addressed at the root — refund_ents just rolled its own ad
hoc, plugin-local timestamp instead of asking a shared, authoritative source of truth for it,
because no such source of truth exists in the gamemode.
Any other system that needs "did the server actually restart since X" (crash/duplication guards, uptime displays, analytics, one-shot-per-boot migrations, etc.) is in the same position: it would have to invent its own local heuristic, with the same failure mode.
Intended fix
A small, gamemode-wide cityrp.session library, loaded early (before any plugin that
needs it), that is the single source of truth for "what session is this."
- On startup, generate/load a session identifier that is provably tied to the real
process lifetime, not to file reloads — e.g. write a boot marker (PID + timestamp, or
a monotonically increasing session id) that only gets (re)created via a genuine
GM:Initializeboot path, and confirm nothing in the reload/refresh tooling re-triggers that path without an actual process restart. - Persist the session to the DB (one row per boot: server, session id, start time), independent of any consumer plugin's own table.
- Expose a small API other systems can depend on instead of reinventing:
cityrp.session.id -- unique id for this boot
cityrp.session.startTime -- os.time() this session began
cityrp.session.previous() -- previous session's record, if any (for "last session" lookups)
cityrp.session.isCurrent(sessionId) -- true if sessionId matches this boot
- Migrate
refund_entsto usecityrp.sessioninstead of its ownPLUGIN.session/PLUGIN.lastSession/OnDBConnectedbookkeeping, removing the duplicated, plugin-local reimplementation of the same concept.
Candidate use cases
refund_ents(gamemode/core/plugins/refund_ents/sv_init.lua) — replacePLUGIN.session/PLUGIN.lastSessionwithcityrp.session, closing the reload-vs-restart ambiguity at the root instead of per-plugin.- Any future "was this entity/state lost to a crash or still legitimately active" check, so the same exploit shape doesn't get reintroduced elsewhere.
- Server uptime / "last restarted at" info for admin tooling or status displays.
- One-shot-per-boot migrations or cleanup tasks that should run once per real restart, not once per Lua reload.