Files
PQS/docs/ARCHITECTURE.md
T

320 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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. **Display**`ctime` 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.php`** — **always 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.php`** — *referenced 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 `.exe`s, `.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.