feat(bridge): protocol 2 — the read path, and the first CI this repo has had #2
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/phase-3-read-path"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Fifteen hooks: presence, deaths, chat, gathering, raided structures, bans, reports, login attempts and the wipe. Every frame carries an envelope —
type,serverId,wipeId— built in one place, so nothing can emit a frame without one.Proven on live Oxide and Carbon 2.0.259.0 from the byte-identical file.
Three rules the code enforces structurally
voidand cannot answer. Areturn nulltoday is one edit fromreturn truetomorrow, and the edit looks harmless in a diff; a signature change does not.CanUserLoginis in the wave for what it observes.OnDispenserGatherfires on every swing at a tree. It accumulates into a per-player tally flushed once a minute as oneplayer.tallyframe — as a delta, so a dropped frame costs one interval instead of corrupting the series.wipeIdis derived here, from the save's creation time, because this is the only component that can read it.Two defects a live server found (second commit)
Initruns before the save is loaded. Invisible for two phases because a hot-reloaded plugin reads an already-loaded world and gets the right answer every time; it took a server that booted with the plugin — which is every real one — to showwipeId=nonebeside a save on disk.Unloadblocked the game's main thread for 2002 ms, reported by Carbon ashook 'Unload' took longer than 100msnext tolink thread did not stop cleanly. The link thread sits in a blockingTcpClient.Connect, which has no timeout and cannot be woken. A host that refuses answers instantly; a host that drops does not answer at all — every loopback test is the first kind. Now bounded, with a stop handle of its own (it cannot shareWake, which also means "the queue has something in it").After the fix: no slow-hook warning, no stranded thread.
The CI this repo did not have
No
.gitea/workflows/existed at all.scripts/checkPlugin.jsasks the three questions a compiler here cannot — this plugin is deployed as source and built by the framework against game assemblies that exist only on a Rust server:ExpectedHooks, sorg.hookscan report it;void, unless answering is written down inANSWERS_DELIBERATELY(currently empty);ProtocolVersionagrees withoverlay.toml.The void rule is inverted on purpose. A list of vetoable hooks would be maintained here against a catalogue in another repository, and the first one somebody forgot to add is the one that would pass.
Its own suite breaks it seven ways — including the failure that would make the other six meaningless: a method parser that silently matches nothing.
Spec:
docs/rust-link/PROTOCOL.md§8 (RunicGateway/docs#255).🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Fifteen hooks: presence, deaths, chat, gathering, raided structures, bans, reports, login attempts and the wipe. Every frame now carries an envelope — `type`, `serverId` and `wipeId` — built in one place so nothing can emit a frame without one. Three rules the code enforces structurally rather than by intention: • A read-path hook never vetoes. Four of these are documented as "returning a non-null value overrides default behavior", so every hook is declared `void` and cannot answer. `CanUserLogin` is in the wave for what it observes. • A hook that can fire more than once a second per player is a counter. `OnDispenserGather` fires on every swing at a tree; it accumulates into a per-player tally flushed once a minute as one `player.tally` frame, as a delta rather than a running total. • `wipeId` is derived here, from the save's creation time, because this is the only component that can read it. PROTOCOL.md §8.2 reverses protocol 1 on that point deliberately. `server.hello` becomes a board rather than a greeting, and `players.online` joins it; both are re-sent on connect and every 60 seconds, which is what makes a restarted sidecar repopulate itself without asking. `rg.hooks` reports which hooks have actually fired. Hooks bind by name and arity through reflection on both frameworks, so a rename by Facepunch and a name Carbon's catalogue omits present identically — as silence. This is the standing answer to both, and it outranks either catalogue because it is a measurement. The repository had no `.gitea/workflows/` at all. `scripts/checkPlugin.js` asks the three questions a compiler here cannot: every hook is in `ExpectedHooks`, so `rg.hooks` can see it; every hook is `void`, unless answering is a decision written down in `ANSWERS_DELIBERATELY`; and `ProtocolVersion` agrees with `overlay.toml`, which is what stops a bundle that will not compose. The void rule is inverted on purpose — a list of *vetoable* hooks would have to be maintained against a catalogue in another repository, and the first one somebody forgot to add is the one that would pass. Its own suite breaks it seven ways, including the failure that would make the other six meaningless: a method parser that silently matches nothing. Proven on a live Oxide server: compiled, loaded, the envelope correct, both ban hooks firing, and the boards repopulating a sidecar whose database had been deleted 0.3 seconds earlier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4