# The player walk — proving the half of the read path a console cannot reach Protocol 2's catalogue divides cleanly in two, and the line is not about importance: it is about whether a hook can fire without somebody holding a mouse. Everything in the first half was proven from a console and a REST client while phase 3 was built — the boards, the wipe id, the envelope, bans, the server lifecycle. Everything below needs a **real player on a real server**, because the hooks carry a `BasePlayer`, a `HitInfo` or a chat line, and none of those three can be manufactured from a console without becoming a different test. This document is the walk that closes it. It takes about ten minutes, it is the same on Oxide and on Carbon, and it is written so that the answer is readable afterwards rather than watched live. --- ## Before you start 1. A rig running, with `RunicGateway.cs` loaded — `oxide.plugins` (or `c.plugins`) lists *Runic Gateway*, and `rg.link` answers `connected=True`. 2. A sidecar the rig can reach, with its store **empty** — that is what makes the event list at the end readable as a transcript of the walk and nothing else. 3. The sidecar's token to hand, for the reads at the bottom. Run this once, before you join: ``` rg.hooks ``` Every player hook should read **silent**. That is the baseline: the point of the walk is to move them, and starting from a run where some already fired proves less. --- ## The walk Do these in order. The order matters only in two places, noted where it does. | # | Do this | Fires | The frame should carry | |---|---|---|---| | 1 | **Join the server** | `CanUserLogin`, `OnUserApproved`, `OnPlayerConnected` | Three frames, in that order. The first two carry your **IP address** — check it is a real address and not the string `0`. `player.connected` carries your steam id and name | | 2 | **Wake up / spawn in** (click Respawn if you are dead) | `OnPlayerRespawned` | `player.respawned`, steam id only. It does **not** fire if you simply wake from sleeping — that is the hook's own documented behaviour, so no frame here is a pass, not a failure | | 3 | **Say something in chat**, then **say something in team chat** if you have a team | `OnPlayerChat` | Two `player.chat` frames, with `channel` reading `Global` and `Team`. The message must arrive whole — if it is truncated or the frame is missing, the flattener ate it | | 4 | **Chop a tree for about twenty seconds**, then **mine a node** | `OnDispenserGather` | **Nothing immediately.** This is the aggregate: one `player.tally` frame within 60 seconds, carrying `gathered` with `wood` and `stones`, summed. Seeing a frame per swing would be the bug | | 5 | **Kill an animal or a scientist** | `OnEntityDeath` | Again nothing immediately — `npcKills` on the next `player.tally`. No `player.death`: a chicken is not a killfeed entry | | 6 | **Die to the environment** — fall damage is easiest | `OnPlayerDeath` | `player.death` with `attackerType: "environment"`, a `grid` like `H7`, and **no** `attackerId`. Check the grid against the map: a wrong sign in the row arithmetic mirrors the whole map, and only a human with the map open can see that | | 7 | **Kill yourself** — `kill` in the F1 console | `OnPlayerDeath` | `attackerType: "self"`, no `attackerId` | | 8 | **If a second player is available**: kill each other once | `OnPlayerDeath` | `attackerType: "player"`, with `attackerId`, `attackerName`, a `weapon` shortname and a `distance` in metres. This is the killfeed's whole shape, and it is the one row phase 4's page is built from | | 9 | **Build a foundation, then destroy it yourself** | `OnEntityDeath` | `entity.destroyed` with `ownerId` (yours), `prefab`, `grid` and `attackerId`. Decay must **not** produce one of these — only a player breaking it | | 10 | **Disconnect** | `OnPlayerDisconnected` | `player.disconnected` with a `reason` and a **`sessionSec`** roughly equal to how long you were on. It also flushes your tally first, so any gathering since the last minute arrives immediately before it | Two ordering notes: step 4 must come before step 10 by at least a minute if you want to see the cadence flush rather than the disconnect flush, and step 1's three frames are the only place the order between hooks is itself part of the answer. --- ## Reading the result From the machine running the sidecar: ```bash TOKEN=… # [web].auth_token from sidecar.toml, or `--print-config` BASE=http://127.0.0.1:8090 # The whole walk, oldest first, as a transcript. curl -s -H "Authorization: Bearer $TOKEN" "$BASE/feed?since=0&limit=500" \ | python -m json.tool # Or one kind at a time. curl -s -H "Authorization: Bearer $TOKEN" "$BASE/events?kind=player.death&limit=20" ``` And from the game console: ``` rg.hooks ``` Every hook in the walk should now read **fired**, with a count. A hook still `silent` after the step that should have fired it is the finding — and on Carbon it is the specific question [`CARBON.md`](../modules/rust/CARBON.md) §6 asks, since Carbon's catalogue omits thirteen uMod names and nobody has yet checked whether they are renames or holes. --- ## What counts as a pass Not "frames arrived". Three things, and the third is the one worth slowing down for: 1. **Every hook in the table fired**, on both frameworks, from the same plugin file. 2. **Every frame carries the envelope** — `type`, `serverId` and `wipeId` on all of them ([`PROTOCOL.md`](PROTOCOL.md) §8.1). A player frame without a `wipeId` cannot be attributed to a wipe and its rollup is lost. 3. **The aggregates are aggregates.** `player.tally` is a delta since the last flush, so two minutes of chopping is two frames that sum to the total, not two frames each carrying the total. Getting this backwards makes every leaderboard roughly double, and it looks correct until somebody counts. Anything that disagrees with the table is a finding about the game or the framework rather than a mistake in the table — record it, the same way phases 0, 1 and 2 recorded theirs.