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
62 lines
2.5 KiB
YAML
62 lines
2.5 KiB
YAML
# Gate every pull request into `main`.
|
|
#
|
|
# This repository had no workflows at all — the same hole phase 2 found in
|
|
# Module-Rust, in the repo that ships the half running inside somebody's game
|
|
# server. It is the one component here that cannot be compiled by CI: the plugin
|
|
# is deployed as SOURCE and built by Oxide or Carbon against game assemblies that
|
|
# exist only on a Rust server, so a build job is not available at any price.
|
|
#
|
|
# What is available is a reader, and the mistakes worth reading for are the ones
|
|
# both frameworks make silent. Hooks bind by name and arity through reflection,
|
|
# with no compile-time check and no warning when a name matches nothing, so:
|
|
#
|
|
# • a hook that is not in `ExpectedHooks` is invisible to `rg.hooks`, which is
|
|
# the instrument this project relies on to answer "does this hook fire on
|
|
# this framework" (CARBON.md §6);
|
|
# • a hook that RETURNS something can cancel a death, swallow a player's
|
|
# gathered wood, or refuse a login (PROTOCOL.md §8.7);
|
|
# • a `ProtocolVersion` that disagrees with `overlay.toml` produces a bundle
|
|
# that will not compose, and the game link has no handshake to catch it.
|
|
#
|
|
# `scripts/checkPlugin.js` asks all three, dependency-free, and its own test
|
|
# suite breaks it seven ways — including the failure that would make every other
|
|
# case meaningless, a method parser that silently matches nothing.
|
|
#
|
|
# Enforcement (one-time, in the Gitea UI):
|
|
# Repository Settings → Branches → Branch Protection (rule for `main`)
|
|
# • Enable Status Check
|
|
# • Status check patterns: PR Checks / *
|
|
# Gitea only lists a context after it has reported once; the glob matches
|
|
# without the dropdown and keeps matching as jobs are added.
|
|
|
|
name: PR Checks
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main, edge]
|
|
|
|
concurrency:
|
|
group: pr-checks-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
plugin-checks:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- uses: actions/setup-node@v4
|
|
with:
|
|
node-version: 20
|
|
|
|
# No install step: the checks are dependency-free on purpose, which is also
|
|
# how a contributor runs them.
|
|
- name: Check the plugin's hooks, void rule and protocol declaration
|
|
run: node scripts/checkPlugin.js
|
|
|
|
# Named individually rather than `node --test scripts/`: directory mode is
|
|
# not portable across the Node versions this project runs on.
|
|
- name: Test the checker itself
|
|
run: node --test scripts/checkPlugin.test.js
|