diff --git a/CHANGELOG.md b/CHANGELOG.md index b66aca0..d81ea79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` 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. diff --git a/README.md b/README.md index c0e88ba..388ab16 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 +``` + +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. + +```bash +python tools/argus_edicts.py --make-cfg 45 --repeat 4 # dumps at 45, 90, 135, 180s +python tools/argus_edicts.py runs/.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 diff --git a/tools/argus_mcp/README.md b/tools/argus_mcp/README.md index 82d7176..da51825 100644 --- a/tools/argus_mcp/README.md +++ b/tools/argus_mcp/README.md @@ -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 ` 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 pick u | w <> a <> h <> r <> @@ -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_.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 ` / `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. |