diff --git a/book/03-sidecar.md b/book/03-sidecar.md index 5a8e05d..d8db916 100644 --- a/book/03-sidecar.md +++ b/book/03-sidecar.md @@ -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 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 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 [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 +[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