Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,114 @@ machine-local project brief; this is the distilled record. Lab
tooling (the Rust MCP server) versions independently; its own table
is in `tools/argus_mcp/README.md`.

## v4.09 (2026-09-05) - the co-op session, and two freezes named by the engine's own dump

A day driven by three human co-op sessions on e1m2. Each one produced a
defect, each defect produced a fix, and two of the fixes were built on
instruments the engine has carried since 1996 and the lab had never used.

**Navigation: waypoints past a ledge lip (#250).** navgen samples hull 1,
the player clip hull, whose floor reaches half a player width past every
real ledge. On e1m6 it seated n93 fourteen units out over a 304 unit drop
with lava at -128. The engine never agreed: `SV_movestep` calls
`SV_CheckBottom`, which point traces the box corners against the world, so
`walkmove` refuses the step and the hazard guard refuses it too. A bot
routed there can neither arrive nor give up, and a co-op companion spent a
whole session at that lip: 213 stalls and 215 jumps in 175 s. Section 4c
now runs the engine's own test with the same constants and order. e1m6
regenerated: 536 lip overhang samples dropped, 198 nodes to 188, worst
spawn reach 85 to 95 per cent, seats over a 96 unit void 32 to 0. Other
graphs are not regenerated; dry runs confirm the rule does not fragment
them (dm4 98 per cent, dm6 95, e1m1 99).

**An unroutable co-op objective stops hammering the router (#251).** The
`GOAL_FETCH` branch re-adopts its subject the moment `ar_goal` is cleared,
and the routefail handler clears `ar_goal` on every failure, so the pair
retried at think rate: 400 routefails in 175 s against four goal pushes.
The item shelf could not cover it, because `ar_failtime` is only stamped
when the failure comes from a different area and a bot hammering one
objective never leaves its patch. A routefail on the current fetch subject
now rests that bot 20 s, per bot rather than on the global shelf, with
`ar_goal` left clear so `Argus_PickGoal` runs and the bot shops meanwhile.
e1m6 routefails 400 and 377 to 8 and 9, coverage 6 and 9 cells to 23 and
22, busiest cell 57 per cent to under 10.

**Door contact is an overlap, not a radius (#253).** The handler decided it
was touching its opener when the opener's centre was within 40 units,
flattened in z, and then stood still to be touched. An opener is a brush.
e1m2's t120 door is fired by a trigger 110 wide and 6 thick, so a bot 38
units from that centre still had its box stopping 18 units short of the
volume. It froze waiting for a touch, and the engine only fires touch on
movement, so the touch could never arrive: the v3.43 lift statue in a new
place, five times and 35 s of a 157 s session. `Argus_TouchingBox` compares
`absmin`/`absmax` against the opener's, the same boxes `SV_LinkEdict` uses,
with 2 units of slack so a solid button pressed against still counts. On
e1m1, the only map whose botmatch drives the door path hard, freezes 6 and
6 became 2 and 4 and the worst freeze 13.4 s and 16.9 s became 8.2 s and
8.8 s, which is the give-up cap finally bounding it.

**A companion stops killing its team mate (#260).** Two sessions, two
deaths, two mechanisms. `Argus_Pain` fights back against "any live player,
human or bot" with no co-op test, and in co-op that branch can only ever
fire on a team mate, because monsters do not carry classname "player": the
tape shows both parties taking exactly 24 damage within one second of each
other. Separately, a bot with a monster for an enemy fires at its last
known position during the sight loss hold, and a human crossing that line
takes it. Two guards, both gated on `coop`: no retaliation against a team
mate, and a trace along the shot with fire held when a team mate is what it
reaches. Splash is not covered. Verified in play: the human shot Carmack
dead and he never shot back, four engage events all `engage monster`.
`Argus_Perceive` already returned before its player scan whenever co-op was
on, so a guard written there first was removed as dead code.

**A bot embedded in the world gets out (#262).** A bot whose origin is
inside solid cannot `walkmove` in any direction, so it stands still until
the match ends with no hold flag to explain it. `pointcontents` reads
hull 0, which is the test that separated a frozen bot from five moving ones
in the same dumps. Recovery follows the catchup warp: fog, `setorigin` to
the nearest nav node (seated at hull 1 standable origins by construction,
so a node is somewhere a player fits), zero velocity, clear the route
state. Nearest by distance only, deliberately not `Argus_NearestNode`,
because a traceline starting in solid returns fraction 1 and from in there
every node on the map looks visible. The first cut keyed on contents plus a
low instantaneous speed and teleported a healthy bot 192 units into the dm4
pit while it ran at 320 u/s; being inside solid AND not having moved is the
real signature, so it now needs three seconds without displacing 32 units.
Verified recovering a real one on dm4: embedded at t 33.3, warped out at
t 36.3, running at 306 u/s by t 37.2.

**A stalling bot stops deferring its own thinking (#263).** Stall recovery
pushes `ar_nextai` out 1.5 s so the bot wanders before re-planning, and it
re-armed that deferral on every stall. A bot stalling more often than every
1.5 s therefore pushes its own AI tick permanently out of reach: it never
re-picks a goal, never routes, and the wander meant to free it is the thing
keeping it blind. Romero on dm2 sat motionless for 65 s with `ar_goalstart`
frozen at 13.9 across two dumps 28 s apart while `ar_stalls` went 15 to 53.
Only defer if not already waiting. dm2 goals 78, 86, 89 became 100, 96, 91,
with every fix tape beating every control tape, and the worst freeze 22.3 s
became 8.2 s.

**Swim node selection (#248), and its correction.** `Argus_NearestNode`
prefers a node at or below a query point that is in water. The supporting
ladder was later found invalid: the "before" tape carried a stationary
netclient puppet as a second client, and in co-op the companion escorts its
team mate, so it was parked by the puppet rather than by the water. Matched
configurations put the arms at 52 against 55 cells. The logic hole is real
and the change does no measured harm, but the headline improvement was a
configuration artifact. Recorded so nobody cites it.

**Instruments.** `tools/argus_pointfile.py` writes the engine's `.pts`
overlay so the nav graph, its swim exits, or a tape's freeze cells can be
drawn in the world and walked to. `tools/argus_edicts.py` reads an `edicts`
dump, which walks the progs field definitions and therefore carries
`ar_node`, `ar_goal`, `ar_liftwait` and the rest; `edicts`, `edict <n>` and
`edictcount` joined the lab's tune whitelist so they can be injected into a
running match. Both freezes above were named by that dump in one command
each, where every earlier forensics session added a dprint and recompiled.
Rejected with measurements: `host_timescale` runs 2.9x but drops
engagements per game minute from 23.4 to 7.0, `sys_ticrate` does nothing,
and savegames are refused outright in multiplayer.

## v4.08 (2026-09-03) - navigation caching, item chain, and roster control

A performance, navigation accuracy, and player-experience milestone addressing bot goal selection latency, lift/button stand-pad precision, friendly collision clearance, and runtime bot management.
Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ argus/
| |-- argus_review.py # Tape review battery (summary, deaths, regions, rides)
| |-- argus_reach.py # Directed-reach audit of the shipped nav graphs
| |-- harvest_session.py # Stamps a play session's tape + demo into runs/
| |-- argus_pointfile.py # Nav overlays drawn in-world via the engine's pointfile
| |-- argus_edicts.py # Reads an `edicts` dump: live bot state, no recompile
| |-- pak_extract.py # Standalone id1 PAK archive reader / extractor
| |-- mdl_skins.py # Palette-remapped player MDL skin injector
| |-- setup_rig.sh # Automated headless Linux environment setup
Expand Down Expand Up @@ -427,6 +429,44 @@ feed. One physical caveat: demos are PVS-culled to the recording
client's view, so a bot across the map drops to a trickle - the
telemetry tape remains the full-map record and the A/B gates.

### Seeing the graph, and seeing inside a bot
Two instruments that need no engine changes, because the engine has
carried both since 1996.

**The nav graph in the world.** `pointfile` is the client command
mappers used to walk a leak out to the void, and its file format is
one `x y z` per line, so anything can write it. A top-down plot
cannot show a waypoint hanging over lava, or a door seat that stops
short of the trigger it waits on; standing in the level looking at
particles can.

```bash
python tools/argus_pointfile.py e1m2 --what swim # then type: pointfile

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Point the overlay at the installed game directory

For installations following the documented quick start, argus is copied into an arbitrary Quake installation, but this command uses the script's default output of <repo>/engine/argus/maps. The running engine therefore cannot find the generated .pts file unless the repository's engine directory happens to be its basedir. Pass and document --out <actual-game-dir>/maps, or derive the destination from the configured installation.

Useful? React with 👍 / 👎.

```

Modes are `nodes`, `links`, `swim`, `water`, and `tape` (the freeze
cells from a session log). Colour is `(-index & 15)` so it cycles and
cannot carry meaning, the particle pool bounds what shows at once
(`-particles 16384`), and the command is client side, so this needs a
listen game.

**Live bot state.** `ED_PrintEdicts` walks the progs field
definitions, so `edicts` dumps every entity with its non-default
fields, `ar_node` and `ar_goal` and `ar_liftwait` included. It is on
the lab's tune whitelist, so it can be injected into a running match,
and `--make-cfg` arms a delayed dump for `+exec` when there is no
injection channel.
Comment on lines +457 to +458

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Write the dump config into the active basedir

When the lab uses a configured ARGUS_BASEDIR outside <repo>/engine—including the repository's Linux setup, which uses tools/quake--make-cfg still hard-codes <repo>/engine/argus/edump.cfg. Consequently +exec edump.cfg cannot load the generated file, so this documented fallback produces no dump. The command needs an output/configured-basedir option, or the documentation must instruct operators to copy the file into the active game directory.

Useful? React with 👍 / 👎.


```bash
python tools/argus_edicts.py --make-cfg 45 --repeat 4 # dumps at 45, 90, 135, 180s

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Qualify the repeated-dump timing by server frame rate

These advertised timestamps only hold on the approximately 10 Hz dedicated lab server: --make-cfg implements each second as ten wait commands, and each wait delays one frame. In a listen session running near the documented 72 Hz, the four dumps occur roughly at 6, 12, 19, and 25 seconds instead of 45–180 seconds, potentially missing the freeze the operator intended to capture. Either calculate against the actual frame rate or state that this schedule is dedicated-lab-only.

Useful? React with 👍 / 👎.

python tools/argus_edicts.py runs/<tape>.log --dump 1 # read the one that landed
```

The dump is lossy under load: a 236 edict dump lost four headers to
the console, so counts are approximate and field values are not. A
free edict prints `FREE` and no fields, which is how the reader tells
a genuinely free slot from a header that never arrived.

---

## The lab MCP server and deploy wizard
Expand Down
22 changes: 21 additions & 1 deletion tools/argus_mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,26 @@ forensics predicted.

## The decision tape (ARGDBG)

### The edict dump

`tune command="edicts"` makes the server print every edict with all
its non-default fields, ours included: `ar_node`, `ar_goal`,
`ar_mode`, `ar_door`, `ar_liftwait`, `ar_failstreak`. That is the
state a forensics session used to add a dprint and recompile to read.
`edict <n>` prints one, `edictcount` just the totals. All three only
print, so they are as safe to inject as `status`.

`tools/argus_edicts.py` reads the result back: edict pressure against
the 600 ceiling, per-bot state ordered for "why is this stuck",
`--field` to compare one value across bots, `--dump N` when a log
holds several, and `--make-cfg SECS --repeat N` to arm dumps for
`+exec` when there is no injection channel. A stochastic freeze will
not sit still for one dump.

Caveat: the dump is lossy under load. A 236 edict dump lost four
`EDICT` headers to the console. Field values survive; counts do not,
and the reader says so.

Console `scratch1 1` (live via `tune command="scratch1 1"`; the
scratch cvars are vanilla's QC float pipe) makes every goal pick
print `ARGDBG <name> pick <class> u <utility> | w <> a <> h <> r <>
Expand Down Expand Up @@ -541,7 +561,7 @@ carries `scaled` and `scale_note` when that happens.
| `qc_read` | `ARGUS_ROOT` | Full source of one Argus function, with line numbers |
| `learn_hotspots` | `ARGUS_ROOT` | Fold stall/lava/hazard cells across logs. Writes `src/argus_nav_<map>.costs.json` for the next navgen; does not write QC. |
| `knobs` | nothing | Live cvars vs compile-time constants |
| `tune` | live match | Whitelisted console: skill, fraglimit, map. Unix stdin; Windows AttachConsole inject. |
| `tune` | live match | Whitelisted console: skill, fraglimit, map, scratch1-4, and the read-only `edicts` / `edict <n>` / `edictcount` dumps. Unix stdin; Windows AttachConsole inject. |
| `live_snapshot` | live or last match | Last ARGLOG row per bot. `since_line` for incremental. |
| `match_status` | live match | Running, pid, elapsed, log lines. `since_line` returns only new lines plus `next_line`. |
| `probe` | full lab | Prefer `experiment`. Compile + short match + lite brief, no A/B. Duration 10-120 s. |
Expand Down