Compare commits

...

4 Commits

Author SHA1 Message Date
e852e5574d Merge pull request 'docs(book): the game host already has the files your site wants (chapter 3 §2b)' (#12) from docs/asset-bridge-p9 into main
Reviewed-on: #12
2026-09-14 22:25:37 +00:00
ad37cade6e docs(book): the game host already has the files your site wants (chapter 3 §2b)
All checks were successful
PR Checks / prose (pull_request) Successful in 16s
PR Checks / template (pull_request) Successful in 36s
The integration kit's share of the Asset Bridge, and the whole of it: one section
in the sidecar chapter, teaching the pattern rather than re-specifying anything.
`docs/link/v8.md` stays normative and is linked out to, as every chapter does.

The problem is general even though our instance of it is not. Most games keep
content on the host that a website wants to show -- sprites, icons, portraits,
localisation tables, map definitions -- and the tempting answer is to make it the
operator's problem: export it on a desktop with a third-party tool, upload the
result, repeat after every patch. It works once and rots immediately.

The four design notes are the ones that cost us real time to learn: content rides
request/reply and never events (a sidecar that persists and broadcasts every
event would write megabytes of sprite into its store and fan it out to every
client); serve one at a time and put "busy" in the protocol so a caller treats it
as flow control; two stages, so the common case -- a restart that changed nothing
-- costs one small round trip; and version your DERIVATION separately from the
protocol, because improving how you read a file changes your bytes while the
file's hash stays put.

Plus the operational note that surprises people: do not import on boot.

Based on `main` rather than `edge` deliberately -- the kit's chapter 5 and the
§2a it follows are on main only, so this section has nowhere to sit on edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-14 13:09:57 -05:00
7f746aee3d Merge pull request 'chore(ci): pin the kit to the Event System core, and go green (Phase 16b cutover, 5 of 6)' (#11) from chore/events-cutover-repin into main
Reviewed-on: #11
2026-09-10 01:23:59 +00:00
5dc14fa626 chore(ci): pin the kit to the Event System core, and go green (Phase 16b)
All checks were successful
PR Checks / prose (pull_request) Successful in 7s
PR Checks / template (pull_request) Successful in 34s
`ci/core-ref.json` moves from 66bb3b9a (MODULE_API 1.9.0, the engagement
cutover) to 655fbf3f -- the commit 1.10.0 reached `main` on, website#199.

This closes a red `main` rather than only dating the book. Chapter 5 landed in
#10 declaring `coreApi ^1.10.0` while this file still named a 1.9.0 core, and
`checkCoreApi` asserts EQUALITY, so the repo has been red on that check since it
merged. That was deliberate and said so in the PR, but the red belongs to the
cutover window and not to the repo; this is the commit that was always going to
close it, and it could not be written until the events sha existed on `main`.
Same shape Teams phase 11 used.

A pin move is a RUN, not an edit -- the template job checks this number and
tests the template against fakes, and a fake accepts what core refuses. So the
template's real declarations went through core's real registries at this exact
ref: the budget, the option source, the lease and the event action were all
accepted, and `apply()` accepted the set. Nothing else here needed to move;
`template/module.json` has declared ^1.10.0 since #10.

Verified against a core at this ref:

  checkCoreApi         coreApi ^1.10.0 matches the pinned core's 1.10.0
  registry rig         all four declarations accepted, apply() accepted
  template server      82 pass, 0 fail          check:imports OK
  template client      20 pass, 0 fail          check:externals OK, build OK
  prose checks         checkLinks, checkRenameSites, checkChapterPaths all OK
  their own tests      10 pass, 11 pass

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-09 19:50:40 -05:00
2 changed files with 77 additions and 22 deletions

View File

@@ -135,6 +135,47 @@ clocks. And if the borrowed value lives in the game's own save file, the *hold*
must be persisted and the timer re-armed at load; a restart preserves the change must be persisted and the timer re-armed at load; a restart preserves the change
and destroys only the thing that would have undone it. and destroys only the thing that would have undone it.
## 2b. The game host already has the files your site wants
There is a third kind of traffic, and it is worth knowing about before you decide
your sidecar only ever forwards live state. Most games keep **content on the host**
that a website wants to show: sprites, icons, portraits, localisation tables, map
or spawn definitions. It is static, it is large, and it changes only when an
operator patches the game.
The tempting answer is to make the operator's problem: export it on a desktop with
some third-party tool, upload the result, repeat after every patch. It works once
and rots immediately, because nothing reminds anyone to redo it.
The better answer costs less than it sounds like: **the game host already has those
files, and you already have a channel to the game host.** Route them over it.
Four design notes, all learned the expensive way in `uo-link`'s protocol 8 (the
"asset bridge", [`v8.md`][v8]):
- **This is request/reply, never events.** A sidecar that persists and broadcasts
every event would write megabytes of sprite into its own store and fan it out to
every connected client. Content must ride the same correlated round-trip a query
uses — see [chapter 5](05-events.md) for the shape.
- **Serve one at a time, and say so in the protocol.** Decoding assets costs the
game host real memory. One in-flight request with an explicit "busy" answer is
simpler and safer than a queue, and a caller that treats busy as flow control
rather than failure gets a working import out of it.
- **Two stages: what exists, then what changed.** A cheap call that returns a list
with a hash per item and no content, then a second that fetches only the hashes
that moved. The common case — a restart that changed nothing — must cost one
small round trip, not a re-download of everything.
- **Version your *derivation*, separately from the protocol.** If you improve how
you read a file, the bytes you produce change while the source file's hash does
not. `uo-link` carries an `EXTRACTOR_VERSION` for exactly that, and a consumer
treats a change in it like a changed hash.
And one operational note, because it is the part that surprises people: **do not
import on boot.** A patch is an event the operator knows about and your website does
not. Re-reading hundreds of megabytes on every restart to discover that nothing
changed pays for the rare case forever; a button an operator presses after they
patch costs nothing and is honest about who knows what.
## 3. The wire is a versioned contract, not a build dependency ## 3. The wire is a versioned contract, not a build dependency
Your sidecar and your module ship separately, on different schedules, to hosts you Your sidecar and your module ship separately, on different schedules, to hosts you
@@ -246,4 +287,5 @@ it wrong takes the game down rather than the website.
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md [api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
[linkplan]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md [linkplan]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md
[linkint]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/INTEGRATION.md [linkint]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/INTEGRATION.md
[v8]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v8.md
[dryrun]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust-dryrun.md [dryrun]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/rust-dryrun.md

View File

@@ -1,14 +1,16 @@
{ {
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git", "repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
"branch": "main", "branch": "main",
"ref": "66bb3b9a3fad01112c06f32d931c9bae56d22de6", "ref": "655fbf3f69a6a1fd650ecbc81afd6cf9c2ad9f66",
"why": [ "why": [
"The core this kit is written against, pinned to a commit rather than a branch.", "The core this kit is written against, pinned to a commit rather than a branch.",
"This one is the engagement cutover, the commit MODULE_API_VERSION 1.9.0 reached", "This one is the EVENT SYSTEM cutover, the commit MODULE_API_VERSION 1.10.0",
"`main` on, and 1.9.0 is what template/module.json declares. It moved here from", "reached `main` on (website#199), and 1.10.0 is what template/module.json",
"1.6.0 (the Teams cutover) because engagement expanded the contract the book", "declares. It moved here from 66bb3b9a (1.9.0, the engagement cutover) because",
"teaches by three registrations and two calls: a module now declares what its", "the event contract expanded the book by a whole chapter: a module now declares",
"game can announce and never who is told.", "what its game can DO on request -- event actions, budget dimensions, leases and",
"option sources -- where every earlier chapter taught only a read path and a",
"thing to announce.",
"", "",
"Moving this pin is the moment someone re-reads the chapters: CI asserts the", "Moving this pin is the moment someone re-reads the chapters: CI asserts the",
"version template/module.json declares still equals this core's", "version template/module.json declares still equals this core's",
@@ -19,29 +21,40 @@
"own. Nothing goes red until someone moves the pin. Between cutovers the kit is", "own. Nothing goes red until someone moves the pin. Between cutovers the kit is",
"not wrong, it is DATED - and this file is where the date is written down.", "not wrong, it is DATED - and this file is where the date is written down.",
"", "",
"The mechanism earned its keep again here. Writing chapter 2's engagement", "This pin move is a REPAIR as well as a date. Chapter 5 landed (#10) declaring",
"section against 1.9.0 found that the seeded body a module ships is the one", "^1.10.0 while this file still named a 1.9.0 core, so `main` has been red on",
"thing registerEngagementSeeds does not validate - it checks that `blocks` is a", "checkCoreApi since it merged -- deliberately, and stated in that PR, but the",
"non-empty array and stops - so the template's own example body had a heading", "red belongs to the window and not to the repo. This is the commit that was",
"level of 2 where the block registry takes 'h2', and no block ids at all. It", "always going to close it, and it could not be written until the events sha",
"would have registered, seeded, and failed the first time an operator opened it.", "existed on `main`. Same shape Teams phase 11 used.",
"Caught by running the template's register() through core's real registry at",
"this ref, which is what a re-read is for; both the fix and the gap are now in",
"the chapter and beside the code.",
"", "",
"That gap is also why this file's own instruction is not enough on its own. The", "The mechanism earned its keep again here, and twice. Writing chapter 5 against",
"the event contract found that an idempotency key on a QUESTION makes every",
"later read permanently stale -- an at-most-once store answers a repeated key",
"with the ORIGINAL reply, so the template's second read of a value returned the",
"first read's answer for ever, and the module could not see a change it had just",
"made. It also found that a refusal's reason goes in `error`: core's classifier",
"reads no other name, so a refusal reported under `detail` reached an author as",
"a bare \"refused\". Neither was found by writing prose. Both were found by",
"running the template's real declarations through core's real registry and its",
"real envelopes through core's real dispatcher.",
"",
"That is also why this file's own instruction is not enough on its own. The",
"template job builds and tests the template against fakes and checks this", "template job builds and tests the template against fakes and checks this",
"number; it does not load the module into core. A declaration a fake accepts", "number; it does not load the module into core. A declaration a fake accepts",
"and core refuses would ship green, so a pin move is a run against a real core,", "and core refuses would ship green, so a pin move is a run against a real core,",
"not just an edit here.", "not just an edit here. It was run at THIS ref: core's real registries accepted",
"the template's budget, option source, lease and event action, and apply()",
"accepted the set.",
"", "",
"The branch said `edge` until 2026-08-12, when the module system cut over and", "The branch said `edge` until 2026-08-12, when the module system cut over and",
"that branch was deleted (MODULE_SYSTEM.md 2.9). Two later workstreams cut an", "that branch was deleted (MODULE_SYSTEM.md 2.9). Two later workstreams cut an",
"`edge` of their own and this pin skipped both: the kit is written against what", "`edge` of their own and this pin skipped both; the Event System cut a third,",
"shipped, never against what is in flight. Nothing in CI reads the branch field", "and this pin skipped that too until it reached `main`. The kit is written",
"- it clones the repo and checks out the sha - which is why a wrong label here", "against what shipped, never against what is in flight. Nothing in CI reads the",
"would sit unnoticed. It is for the person deciding whether a newer core is", "branch field - it clones the repo and checks out the sha - which is why a wrong",
"worth re-reading the book for.", "label here would sit unnoticed. It is for the person deciding whether a newer",
"core is worth re-reading the book for.",
"", "",
"Same convention as Module-uo's ci/core-ref.json, deliberately - one file, one", "Same convention as Module-uo's ci/core-ref.json, deliberately - one file, one",
"sha, reviewable in a diff." "sha, reviewable in a diff."