overlay.toml gains runicnpc_api = 3, the release folds it into manifest.json,
and checkPlugin.js holds it equal to RunicNpcApiNeeded in the code. The
installer's bundle carries the latest RunicNPC release that answers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
world.zone gains flags, settings, enterMessage, leaveMessage, delivery,
format and dome (PROTOCOL §19.11). Flags are checked against the live
ZoneManager's own list, read through its API call ZoneFieldListRaw() (D211),
and refused bad-option rather than dropped; settings are an allowlist of
five with bounds. Both are kept on the registry entry and re-applied when
a zone is re-created.
The bridge says a zone's enter and leave messages itself (D193), as chat
or a popup (chat when PopupNotifications is missing), and stays quiet for
5 s after re-creating a zone.
RunicGatewayDomes.cs, a new helper, patches ZoneDomes so its API can be
called without a player, and raises OnRgDomesReady after ZoneDomes' own
start; the bridge puts its domes back only after it, so none is drawn
twice. Domes are removed before their zone at expiry and revert.
hello carries zoneManager { version, flags }, integrations.zoneDomes the
ZoneDomes and helper state, and hello is sent again when ZoneManager,
ZoneDomes or either helper loads or unloads. The release ships the new
helper beside the zone helper.
Not compiled or walked on a rig: the panel's file API was down on both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The bridge's half of PLAN_FIXES §6 step 2 (decisions D181-D185, docs#288).
- F13: `world.expired` carries the entity's kind as `what`. It used to write
it over the frame's own `kind`, so every expiry left filed as "zone".
- F1 (D159): `gathered` also counts OnDispenserBonus (the final-hit bonus),
OnCollectiblePickup (hemp, mushrooms, stones off the ground) and
OnGrowableGathered (farmed plants); all three are void and in ExpectedHooks.
- F3: taking down a building block you own, or one on a cupboard you are
authorised on, no longer counts toward `structures`. The frame still goes.
- F4: every remaining UserIDString is SteamIdOf(userID), so a player another
plugin spawned is never `steamId: null`.
- F8 (D184): `plugin.loaded` / `plugin.unloaded`, once the world is ready,
with the permissions that plugin added or removed (a diff of the registered
list, taken a tick after the hook).
- F12 (D181, D182): RunicGatewayZones.cs, a D168 helper shipped beside the
bridge, Harmony-postfixes ZoneManager's Zone.InitializeZone and, a tick
later, enters every connected player ZoneManager's own IsPositionInZone
places inside, through ZoneManager's own OnPlayerEnterZone. Without it (or
when it cannot find what it patches) the bridge scores its zones by
IsPositionInZone. Hello reports `zoneHelper: { state, version?, reason? }`.
- release.yml stages every helper beside the bridge and lists each .cs with
its sha256 in manifest.files; the installer and the egg place them all.
Not yet compiled: this is walked on both rigs before it merges.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Module-rust phase 18, step 3 of docs/modules/rust/PLAN.md §34.2.7.
The environment seam (D150): when the plugin writes its default config and
RUSTLINK_SERVER_ID holds a valid id (the site's ^[a-z0-9][a-z0-9-]{0,63}$),
the file's ServerId is that value. It is read nowhere else: after that write
the file is canonical and the site's lock applies. An invalid value is logged
and ignored. This is how the egg names a server; not a message change, so the
protocol stays at 12.
The release (D145, §34.2.1): release.yml reuses servuo-plugins' release engine
unchanged (tag-only, secrets preflighted, orphan tags recovered and swept, the
installer's bundle.yml dispatched at the end). The adapter re-runs
checkPlugin.js and packages runicgateway-rust-plugin-<version>.tar.gz with a
framework-neutral layout: RunicGateway.cs, its [Info] version stamped to the
release, and a manifest.json folding in overlay.toml.
overlay.toml gains min_carbon_version = "2.0.259", the build R19 was proven on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
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