CityRP Documentation Repository

Problem statement

Last updated

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_ents cares about entities being destroyed, and a changelevel keeps the process and Lua state alive while wiping every entity — so a PID-based marker would newly break the case that os.time()-at-load happens to get right today. #3432 took this further: the load of sv_session.lua itself is the derivation point, with GM:Initialize the place to persist rather than derive.
  • cityrp.session.previous() is cut. It is listed below as API, but its only caller would be refund_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:Initialize boot 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_ents to use cityrp.session instead of its own PLUGIN.session/PLUGIN.lastSession/OnDBConnected bookkeeping, removing the duplicated, plugin-local reimplementation of the same concept.

Candidate use cases

  • refund_ents (gamemode/core/plugins/refund_ents/sv_init.lua) — replace PLUGIN.session/PLUGIN.lastSession with cityrp.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.