Files
PQS/docs/ARCHITECTURE.md
T

17 KiB
Raw Blame History

PQS — Player Queueing System

Documentation generated from a full read of the codebase (2026-07-01).

Note: §7 (known issues) and §9 (improve-in-place plan) describe the code as it was before the 2026-07-01 improvement pass. See §10 for what has since been fixed and what remains.

1. What this is

PQS is a player queueing and mission-management system for a pod-based mech-combat simulator (Virtual World / BattleTech-style networked cockpits). It is deployed at large venues to register, queue, and cycle several thousand players through a fixed number of physical pods across a day.

The system runs entirely on an air-gapped LAN. There is no external internet dependency and DB credentials (pqs/pqs on localhost) are a formality of that closed network.

Branding strings in the code reference "FallOut Shelter" and "Cougar CON" (the operator / an event); the sim itself is referred to as FS (the game pods that consume getFS*.php).

2. Technology stack

Layer Technology
Server PHP (written for PHP 5.4/5.5, mysqli procedural API)
Database MySQL 5.15.5, MyISAM/InnoDB mix (pqs schema)
Frontend Static HTML + jQuery 1.x, AJAX polling via setInterval
Pod / kiosk hardware Raspberry Pi running omxplayer for intro/trainer videos, GPIO buttons (docs/omx_button.py, docs/get.py)
Time sync time.php shells out to sudo date -s to set box clock from a browser

There is no framework, no router, no build step, no dependency manager. Each .php file is a standalone page reached directly by URL. Shared logic lives in includes/.

3. Physical / deployment topology

                 ┌─────────────────────────────────────────┐
                 │      Air-gapped venue LAN (10.0.0.x)      │
                 │                                           │
  Players  ─────▶│  Registration kiosk  → callsign.php      │
                 │                                           │
  Staff    ─────▶│  Reg console         → registration.php  │
  Staff    ─────▶│  Game/ops console    → console.php        │
                 │                                           │
  Big TVs  ─────▶│  Queue displays      → combinedqueue.php  │
                 │                        smallcombinedqueue │
                 │                        genconqueue.php    │
                 │                                           │
  Console  ─────▶│  AutoHotkey scripts (NOT YET RECOVERED)   │
  operator PC    │    read getFSgame.php  (mission settings) │
                 │    read getFSplayers.php (roster/pods CSV)│
                 │    → keyboard-inject into the game console│
                 │      which then configures the FS pods    │
                 │                                           │
  Pi video  ────▶│  Poll  playFSTrainer.php  (missing file)  │
                 │         → triggers omxplayer intro video   │
                 └─────────────────────────────────────────┘
                              All → single PHP + MySQL host

4. Core domain model

The whole system revolves around missions (one pod launch) that fill up with players, then get committed (launched) one at a time.

Lifecycle of a mission

  1. Build — missions are pre-created (registration.php?build=N) or created on demand when a player registers and no open mission exists.
  2. Fill — players self-register at callsign.php (kiosk) or staff add them in registration.php. Each mission holds up to MissionSize players (= number of active pods).
  3. Lock — staff can lock a mission so no new players join (locked = 1).
  4. Commit — at console.php ("Commit Mission"), the lowest-numbered queued mission's players are:
    • copied into pqs_currentmission (who is in the pods right now),
    • archived into pqs_pastmissions (history, with ctime timestamp),
    • deleted from pqs_queue,
    • the mission row marked Completed = 1.
  5. Displayctime of the last commit + timepergame + timetoreset drives every "ETD" (estimated departure) and countdown on the display walls.

How the mission reaches the pods

PQS does not talk to the game/pods directly. The console operator runs a set of AutoHotkey scripts (currently not recovered — a gap to close) on the game-console PC. Those scripts read the plain-text output of getFSgame.php (mission settings) and getFSplayers.php (per-pod roster) and keyboard-inject those values into the game console UI, which in turn configures the physical pods. So the getFS*.php files are a data source scraped by AHK, not an endpoint the pods poll. Their output layout is dictated by whatever those AHK scripts expect — recovering the scripts is required to know the exact contract.

Group registration (squads)

callsign.php supports Group Leader / Group Member flows: a leader reserves N slots in one mission, creating placeholder rows LEADER-2, LEADER-3…; members later "claim" a placeholder by matching CallSign LIKE 'who-%'. Gated by the GroupAvalible config flag.

5. Database schema (docs/pqs.sql)

Table Purpose
pqs_queue Players currently waiting, keyed to a MissionID. Core working set.
pqs_mission One row per mission: size, #players, completed/locked flags, gametype, map, time.
pqs_currentmission Snapshot of who is in the pods for the mission that just launched.
pqs_pastmissions Append-only history (callsign, mech, mission, ctime). Drives all ETD math.
pqs_gameconfig Single-row global settings: pod count, time-per-game, weather, radar, friendly fire, etc.
pqs_pods Single-row 16 booleans: which physical pods (pod01..pod16) are active.
pqs_mechinfo Static catalog of ~63 mechs (class, tons, stats, roster #, image).
pqs_mapinfo Static catalog of maps and which game types each supports.
pqs_gametype Static catalog of the 10 game modes.
pqs_infopanel Single Updated flag used to trigger display-wall ad rotation refresh.

Notes:

  • pqs.sql is the canonical schema; dbbuild.sql is an older SQLyog dump with sample data.
  • The 16 pod booleans (pod01..pod16) are a fixed structure, not a limitation to normalize away: 16 is the max cockpits one game console can control, and each pod is toggled individually because the console addresses every position regardless of whether it is online/operational (see getFSplayers.php). MissionSize is the count of active pods; the booleans say which ones. pqs_gameconfig.numpods caches that count. console.php keeps these in sync on every config save.

6. File-by-file reference

Player-facing (kiosk)

  • callsign.php — the player registration kiosk. 1024×768 themed screen: enter callsign, pick class → mech, optional group role, submit. Inserts into pqs_queue, auto-creating a mission if none is open. Shows the assigned mission # and ETD, then returns to itself.

Staff-facing

  • registration.php — staff registration console. Add / edit / delete players, add one player to multiple future missions, build missions in bulk, edit a mission's game type & map, lock missions, search. Polls includes/getreg.php every 1s.
  • console.php — game-ops console. Sets global game configuration (time limits, pods active, weather/radar/damage rules, etc. → pqs_gameconfig
    • pqs_pods), locks and commits missions. Polls getconsole.php + mechpods.php every 2s.
  • history.php — paginated per-mission history viewer (prev/next), reads pqs_pastmissions joined to pqs_mission.
  • search.php — look up a callsign across queued missions with ETDs.
  • time.php — sets the server clock from the browser's JS time via sudo date -s (keeps the air-gapped box's clock correct without NTP).

Display walls (read-only, auto-refresh)

  • combinedqueue.php — main wall: next mission + groups 25 + current pods
    • clock + rotating info panel. 10s refresh.
  • smallcombinedqueue.php — compact version of the main wall (missions 15 + current pods + info panel), 5s refresh.
  • genconqueue.php — "extended" wall showing missions 615 (further-out queue). 30s refresh.
  • combinedqueue2.php — mid-range wall showing missions 610 + current pods
    • clock + info panel, 10s refresh.

Game-console integration (plain-text output, no HTML)

Consumed by the operator's AutoHotkey scripts (not the pods directly — see §4 "How the mission reaches the pods"). The AHK scripts are not yet recovered.

  • getFSgame.php — emits the next mission's settings as newline-delimited values (gametype, map, visibility, weather, … cameraship) for AHK to read.
  • getFSplayers.phpalways emits exactly 16 CSV lines (one per pod slot): active,callsign,rosterNum,team,unit — the pod loadout for the next mission. The line count is fixed at 16 by design: the game console addresses all 16 cockpit positions positionally whether or not each is online, so an inactive pod is sent as a zeroed line (0,0,0,0,0) rather than omitted. 16 is the maximum number of cockpits one game console can currently control.
  • playFSTrainer.phpreferenced by docs/get.py but missing from the repo; was polled by a Raspberry Pi to trigger the trainer video.

includes/ (AJAX fragments & shared code)

  • config.php — DB connect + load pqs_gameconfig/pqs_pods into $_SESSION.
  • getreg.php / getqueue.php / getconsole.php / getcurrent.php / getgroup.php — HTML fragments rendered into the consoles/walls via AJAX.
  • now.php — clock + "estimated time remaining" for current mission.
  • mechpods.php — echoes active pod count from session.
  • api.php — tiny AJAX endpoint: toggle a player's "merc" flag, poll the infopanel Updated flag.
  • infopanel.php / smallinfopanel.php — rotating advertisement/announcement slides (jQuery Cycle).
  • graph.php — GD-generated progress-bar PNG (appears unused by current pages).

docs/ (assets & operator scratch)

  • pqs.sql, dbbuild.sql — schema / seed dumps.
  • omx_button.py, get.py — Raspberry Pi video-kiosk helper scripts.
  • Clear Code.txt — the SQL to reset for a new event (truncate the 4 live tables).
  • readme screen adjust.txt, SQL_TCPip_Fixes.reg, installer .exes, .mp4 intro/roleplay videos, spare image sets.

7. Known issues, risks & technical debt

These matter most for the "improve vs. rewrite" decision.

Correctness / concurrency

  • No transactions or locking. Filling a mission is a read-numplayers → compare → UPDATE +1 sequence with no atomicity. With thousands of players and a kiosk plus staff console writing concurrently, race conditions can overfill a pod bay ("Containment Bay OVERLOADED" is literally a handled UI state). numplayers is a manually-maintained counter that can drift from the actual COUNT(*) in pqs_queue.
  • Pod state is spread across pqs_pods (which 16 are active), MissionSize (the count), and pqs_gameconfig.numpods (a cached count), reconciled by console.php. Note the fixed count of 16 is intentional — the hardware max per console — so this is a sync concern, not a "make pod count dynamic" concern.

Security (mitigated only by the air gap)

  • SQL injection is pervasive — nearly every query interpolates $_GET/ $_POST directly (e.g. console.php nummercs, api.php playerid, callsign fields). Safe only because the network is closed and trusted.
  • time.php passes a request value into exec("sudo date -s …") — command execution by design; would be dangerous on any reachable network.
  • api.php and parts of registration.php/console.php still emit DEBUG: output to the page.

Maintainability

  • Presentation, business logic, and SQL are fully interleaved in every file; connection boilerplate is copy-pasted dozens of times.
  • Deprecated mysql_error() calls remain alongside mysqli_* (would fatal on PHP 7+). The code targets an EOL PHP 5.x runtime.
  • 16 pods are hard-coded as pod01..pod16 everywhere (schema, console, FS output). This is intentional — 16 is the hardware max per game console and the console requires all 16 positions in a fixed order — but it does make the code verbose (16 near-identical branches in console.php and getFSplayers.php). A loop over a pod count of 16 would be cleaner without changing the contract.
  • Display refresh is fixed-interval full-fragment polling (no change detection except the one infopanel flag).
  • Inline <font> tags, table layouts, and absolute pixel sizes throughout.

Runtime assumptions

  • Timezone hard-coded to America/Chicago in most files.
  • URLs hard-coded to /pqs/… and to server IP via $_SERVER['SERVER_ADDR'].

8. Strengths worth preserving

  • The domain model is sound and battle-tested: mission → fill → lock → commit → history, with ETD projected from the last commit time. Any rewrite should keep this exact lifecycle.
  • It is operationally simple: drop PHP files on one box, point kiosks and TVs at URLs. No build, no services to orchestrate — valuable at a venue.
  • Feature set is complete for real events: group/squad reservations, per-mission game/map overrides, merc tracking, bulk mission build, history, live walls, and direct pod integration.

9. Improve vs. rewrite — starting point

If improving in place, the highest-value, lowest-risk fixes first:

  1. Make mission-fill atomic (transaction + SELECT … FOR UPDATE, or derive numplayers from COUNT(*)) to end overfill races.
  2. Parameterize all queries (mysqli prepared statements) and remove DEBUG: output.
  3. Centralize DB access into one include; replace remaining mysql_* calls.
  4. Make timezone configuration-driven; collapse the 16 repeated pod branches into a loop (keep the fixed 16-slot output contract — see below).

If rewriting from scratch, preserve section 4's lifecycle and section 5's tables as the spec. Treat getFSgame.php / getFSplayers.php output formats as a hard contract, but note the real contract is defined by the unrecovered AutoHotkey scripts that scrape them and keyboard-inject into the game console. Recovering those AHK scripts is a prerequisite to safely changing anything about how missions reach the pods — without them the exact expected output layout (field order, delimiters) can only be inferred from the PHP. One part of the contract is already known and must be kept: getFSplayers.php always returns exactly 16 lines in fixed pod order, inactive pods zeroed rather than omitted, because the game console addresses all 16 positions positionally.

10. Improvement work completed (2026-07-01)

An "improve in place" pass was executed against the plan in §9. All changes were tested on a local WSL2 Ubuntu stack (PHP 8.3 / MariaDB; see dev/).

New infrastructure

  • includes/db.php — one shared mysqli connection + parameterized query helpers (pqs_db, pqs_exec, pqs_rows, pqs_row, pqs_val); also the single source of truth for PQS_TIMEZONE. Targets PHP 7.4+ (mysqlnd).
  • includes/queue.php — all seat-taking domain logic, serialized by a GET_LOCK advisory lock and wrapped in transactions: pqs_enqueue_player, pqs_add_player_to_mission, pqs_add_player_to_missions, pqs_enqueue_group, pqs_claim_group_slot, pqs_remove_player.
  • dev/ — local test-stack setup + migrations/ (001: queue+mission → InnoDB; 002: currentmission+pastmissions → InnoDB).

§9 plan status

  1. Mission-fill races — DONE. Every writer (kiosk single/group, staff new/add-multiple, delete, commit) goes through the shared lock + a transaction; capacity is judged by real COUNT(*) and numplayers is rewritten from it, so it can't drift or overfill. Verified with 120140 concurrent/mixed writers: zero overfill, zero drift.
  2. Parameterize + strip DEBUG — DONE for all user-input paths. api.php (also un-broke the info-panel refresh, whose JSON the DEBUG text was corrupting), console.php, registration.php, history.php, search.php.
  3. Centralize DB / kill mysql_* — DONE. db.php; 11 mysql_error() landmines fixed; config.php unfetched-pqs_pods bug fixed.
  4. Timezone + pod loop — DONE. Timezone centralized in db.php; the 16 pod branches in console.php collapsed to loops (fixed 16 kept as the hardware contract).

Deliberately not changed

  • getFSgame.php / getFSplayers.php — injection-safe already; their output is the pod/AHK contract (see §4, ahk/README.md). Not refactored until the AHK scripts are recovered and the exact format is confirmed.
  • Internal display includes (getqueue/getconsole/getcurrent/getreg/ getgroup) — read-only with internally-derived mission IDs; left on their legacy connection (only getgroup's $_POST[group] was int-cast). Converting them to helpers is consistency-only.

Deployment prerequisites

  • Run migrations 001 and 002 against the pqs database.
  • The new code targets PHP 7.4+. If the venue box is still PHP 5.4, upgrading its PHP is the real prerequisite before deploying these changes.