File size: 7,822 Bytes
9038fbf 71a2cf7 31f9605 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | # Radio/chat expansion states and substitution keys
Static pass, 2026-10-11. Reproduce with `python3 scripts/analyze_chat_expansion.py`; check with `python3 scripts/test_chat_expansion.py`. [Generated evidence](chat-expansion.json) contains verified qagame provenance, complete partitions for parser 186435, lookup 185212 and comparator 69386, plus bounded synthetic traces. No original executable or full QVM runs. Calls are recorded with zero placeholder returns; no formatted/copy/append output is produced. These findings extend [chat delivery](CHAT_DELIVERY.md).
## EXP-001: marker parsing and length gate
Parser 186435 checks a source length returned by helper 36038. A value **greater than 150** takes diagnostic `CG_UTParseRadio: radio string > MAX_MAPPING_STRING\n` through helper 2294 and leaves without the copy/clear path. Values 149/150 reach formatter 70386 with destination local 32, capacity **150**, template `%s` and the source pointer, then clear the source's first byte. Acceptance at 150 does not prove preservation of 150 bytes: capacity, formatting truncation and effective string length remain separate questions. The fixtures supply lengths; recorded copy and repeated length calls return zero, so the accepted fixture skips the character loop. It must not be interpreted as a real empty-input transformation.
Per-character slices at 186481–186676 establish the following states. Pending marker lives at local byte 182, key text at local 183, key length at local word 336; current byte is signed through SEX8.
- `$` (36) and `#` (35) both start a pending marker. An existing marker/key first gets formatter `%c%s` and append helper 36123, then marker is replaced and key cleared.
- While pending, ASCII letters `a`–`z` or `A`–`Z` are appended to the key with a NUL. Lookup 185212 receives the sender and **each growing prefix**, rather than waiting for a word delimiter.
- A nonzero lookup result is passed to append helper 36123 and clears the marker. A zero result retains it. Separate post-call fixtures supply both results explicitly; no lookup callee is executed by those parser slices.
- A nonletter while pending goes through `%c%s%c`, retaining marker/key/current-byte as formatter arguments, then clears the marker. A signed high byte 255 is forwarded as -1 in the character argument. Without a marker, the byte goes through `%c`.
- End-of-loop pending marker/key takes `%c%s` and append. No pending marker means no tail call.
Because lookup happens after each letter, a recognized short key can consume a prefix of a longer letter run. For example, the raw state machine permits matching `hp` before a later letter is inspected. Actual combined formatted output and buffer bounds require copy/length/append/lookup composition. A synthetic NUL per-character fixture verifies argument routing only; the real loop condition normally ends before its terminator.
## EXP-002: exact comparison keys and unresolved substitution bodies
All twelve key comparisons call helper **69386** with bound **99999**. Actual byte-loop traces establish ASCII case-insensitive lexicographic equality, terminated-string equality and rejection of a shorter/longer key. It folds lowercase ASCII to uppercase, reads signed bytes, and stops when its decrementing bound is exhausted. Comparison fixtures cover every exact/uppercase/shortened/extended key plus bounded prefix, embedded NUL and signed high-byte cases. This does not establish locale/Unicode matching.
| Key in comparison order | Static body evidence / remaining work |
| --- | --- |
| `health` | Four labels and dead override; bounded in EXP-003 |
| `hp` | Raw object field 684 formatted `%i`, then possible `0` override; bounded in EXP-003 |
| `clips` | Selected record word at offset 312 + 4*field144; high byte after mask/unsigned shift. Packing and item limits open |
| `bullets` | Same selected word, mask 65280 and signed shift 8. Packing semantics open |
| `weapon` | Low byte of selected word indexes stride 432 at base 4020; word interpreted by `%s`. Table/identity/bounds open |
| `location` | Helper 268637 requested with sender, shared buffer and capacity 150; complete location selection open |
| `gameitem` | Game types 7/8 choose `flag`/`bomb`; other path uses helper 36717 bit 1 for `coinbird`/`cat-bird`. Randomness and runtime modes open |
| `gametime` | Cvar/global arithmetic and `%01d:%02d` or `no timelimit`; full timing/wrap contract open |
| `roundtime` | Round-deadline arithmetic, game-type gates and `%01d:%02d` or `no roundtime`; full schedule open |
| `leader` | Helper 169397 and stride-880 object/record name access; return/no-leader handling open |
| `momo` | Buffer clear, optional referenced-object writes and zero return; bounded in EXP-003 |
| `crosshair` | Dead fallback or floating-point direction/trap trace/location path; vector, trace, ownership and geometry semantics open |
Lookup initializes a shared output buffer **35738436** through a capacity-150 formatting request for `unknown`. Recognized ordinary tails return that buffer pointer; unmatched keys take zero return. `momo` also returns zero after its side effect. Names in this table are exact supplied strings, not inferred command names. Static bodies are retained even where full evaluation is deferred.
## EXP-003: health boundaries and momo side effect
The `health` branch uses signed object field **684** thresholds: `<25` yields `near death`, `25–49` `badly wounded`, `50–74` `wounded`, and `>=75` `healthy`. Negative values follow the first branch. It then requests a replacement `dead` if sender-record offset **4324** is nonzero **or record offset 4 is >=3**. The `hp` branch first requests `%i` with field 684, then `0` under the same predicate. These are formatting requests; the formatter does not execute in the slices.
Thirty-six health fixtures cover -1/0/24/25/49/50/74/75/100 across four state/override combinations. Four hp fixtures preserve raw value 42 while selecting the override. The offset-4 test is **>=3 here**, unlike chat delivery's **==3** dead flag. Do not unify those predicates when porting without further evidence.
The `momo` match clears shared buffer byte zero and reads sender-record pointer **6168**. If nonzero, it writes global time **34992532 + 3500** to that referenced object's offset **84**, and **2** to offset **204**. Both NULL and nonzero pointer paths return **0**, with no callee calls in these bounded slices. A near-INT_MAX synthetic time fixture produces signed wrap in the evaluator; original interpreter/JIT conformance and timestamp lifecycle remain open. The referenced object's type, animation/UI meaning, reset/update ownership and who invokes this key are not assigned from the string alone.
## Remaining work and Rust dependencies
RE-269–271 record parser-state, key-comparison and health/momo milestones. RE-268 remains open for the complete expansion/output contract; RE-272 tracks full capacity-150 copy and append bounds, prefix consumption and buffer aliases, item/ammunition tables, timer arithmetic and cvar identities, location/leader/crosshair callees, momo object writers/consumers and original-runtime/platform fixtures. A Rust implementation will need owned-buffer semantics, exact marker/control-byte behavior and distinct field predicates, backed by completed output specifications before claiming parity.
The [ITEM-001–003 follow-up](CHAT_ITEMS_TIME.md) recovers bounded ammo/weapon/game-item/timer paths and thirty sampled name words. Full table, producer and timing/output contracts remain open under RE-272/276.
The [LOC-001–003 follow-up](CHAT_LOCATIONS.md) recovers leader scan/name paths, location cache/format boundaries and list-building/static spatial evidence. Full caller/output/geometry behavior remains open under RE-272/280.
|