Merge pull request 'docs(kit): pin core 1.17.0; teach 1.13.0 to 1.17.0 and one module per site' (#15) from docs/core-1.17.0 into main
Reviewed-on: #15
This commit is contained in:
@@ -135,7 +135,7 @@ whether it is a directory; a symlink answers no, and your module is skipped in
|
||||
complete silence. This is the single most common way a first install appears to do
|
||||
nothing at all.
|
||||
|
||||
Two more things that look like your module failing and are not:
|
||||
Three more things that look like your module failing and are not:
|
||||
|
||||
- If core is running in a container, your files have to be on the volume core sees
|
||||
— `MODULES_DIR` (`/app/modules` under the shipped Compose file), not the
|
||||
@@ -143,6 +143,14 @@ Two more things that look like your module failing and are not:
|
||||
- A core with a fresh database boots in **maintenance mode**, and public module
|
||||
pages sit behind the same maintenance gate core's own do. Your page will look
|
||||
broken while the site is not live yet.
|
||||
- **A site runs one game module.** If the modules directory already holds one, an
|
||||
Ultima Online or Rust site's for example, only the directory whose name sorts
|
||||
first starts. Every other is refused before any of its code is read, and its
|
||||
reason on Admin → Modules names both modules. `examplegame` sorts ahead of `rust`
|
||||
and `uo`, so on such a core yours starts and the site's own module does not. Try
|
||||
your module on a core of your own, with nothing else in its modules directory.
|
||||
This is [`MODULE_SYSTEM.md` §2.5][system]; the installer refuses a second module
|
||||
too, so the rule only surprises someone copying directories by hand.
|
||||
|
||||
## What you should see
|
||||
|
||||
@@ -270,6 +278,7 @@ what `ctx` hands you and why it is handed rather than imported, what each
|
||||
module must never do.
|
||||
|
||||
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
|
||||
[system]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_SYSTEM.md
|
||||
[issues]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit/issues
|
||||
[renamecheck]: ../scripts/checkRenameSites.js
|
||||
[acceptance]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/modules/kit-acceptance.md
|
||||
|
||||
@@ -147,9 +147,10 @@ statement of your dependencies, and it makes a test double for it — see
|
||||
|
||||
## What you register
|
||||
|
||||
Twelve calls — ten registrations and the two lifecycle hooks — all synchronous,
|
||||
all listed in [§2.4][api]. What is worth knowing is not their signatures but the
|
||||
model behind them.
|
||||
Nineteen calls, all synchronous and all listed in [§2.4][api]: fifteen
|
||||
registrations, the two lifecycle hooks, and the two identity-link hooks. Five of the
|
||||
registrations are the event system's, and [chapter 5](05-events.md) has those. What
|
||||
is worth knowing is not their signatures but the model behind them.
|
||||
|
||||
**Every call stages; nothing is committed until your whole module is known good.**
|
||||
The shape of a claim is checked at the call, so a malformed one throws with your
|
||||
@@ -505,6 +506,37 @@ warm-up it never finished, and handing it a half-built world to tear down is wor
|
||||
than not closing cleanly. A module with no hooks at all still reaches `started`:
|
||||
having nothing to warm up is not the same as never having started.
|
||||
|
||||
### When a player links a sign-in identity
|
||||
|
||||
If your game has its own idea of an account, a player's Steam id for instance, you
|
||||
may want to know when somebody links that identity to their site account
|
||||
(`MODULE_API_VERSION` 1.17.0). Linking stays core's. Core verifies the identity
|
||||
with its provider and writes the link; your module may read what core verified,
|
||||
refuse a link, and hear one, and nothing more.
|
||||
|
||||
```js
|
||||
api.onIdentityLinking(async ({ userId, provider, subject }) => ({ ok: true }))
|
||||
api.onIdentityLinked(async ({ userId, provider, subject, displayName }) => {})
|
||||
// and, from anywhere: ctx.identities.list(userId, provider)
|
||||
```
|
||||
|
||||
- **`onIdentityLinking` is asked before the link is written**, for every provider,
|
||||
so filter on `provider` yourself. Answer `{ ok: false, reason: 'in_use' }` when
|
||||
the identity already belongs to somebody else in your game; the person reads
|
||||
core's own *"already linked to another user"*. **It fails closed**: a throw, or no
|
||||
answer within 5 s, refuses the link. That is the right direction for a check whose
|
||||
whole job is to stop one game account landing on two people.
|
||||
- **Your `message` is logged, never shown.** Module text on the page would let
|
||||
anybody craft a link that prints a sentence of their choosing on the account
|
||||
screen.
|
||||
- **`onIdentityLinked` is told after the row exists**, and is awaited for 5 s so
|
||||
the page the person lands on already shows what you made of it. Its failure is
|
||||
logged and the link stands, so write it to tolerate a race: your own record may be
|
||||
taken between the check and this call, and "theirs stands" is the answer.
|
||||
- **There is no unlink hook.** Unlinking removes only the sign-in, and a record your
|
||||
module made from it is yours to keep or remove.
|
||||
- Neither hook can mint a session or change who is signed in.
|
||||
|
||||
## The schema fragment
|
||||
|
||||
`template/server/db/schema.sql` is your tables. It is replayed **in full, at every
|
||||
|
||||
@@ -89,6 +89,7 @@ api.registerEventLeases([{
|
||||
id: 'examplegame.rate.gather',
|
||||
label: 'Gather rate',
|
||||
type: 'float', min: 0.5, max: 5,
|
||||
example: 2, // optional, 1.13.0: the value box's placeholder
|
||||
maxDurationMs: 48 * 60 * 60 * 1000,
|
||||
|
||||
async read() { /* the live baseline */ },
|
||||
@@ -167,6 +168,13 @@ second code path is a dry run of the second path. Validate everything you can
|
||||
reach without writing, then stop. Answering `{ ok: true }` unconditionally makes
|
||||
the dry run worthless in the one situation it exists for.
|
||||
|
||||
**A dry run may also advise** (MODULE_API 1.16.0). `{ ok: true, detail: { warnings:
|
||||
["…"] } }` shows each line to the author as a warning on that step, and it never
|
||||
fails the dry run. Use it for what is true of the world now and may not be when the
|
||||
run starts: *"the spot is refused now: a player is within 50 m"*. Strings only, at
|
||||
most ten per step, each cut at 500 characters. What would make the step fail
|
||||
whenever it ran is not advice; answer that as a refusal.
|
||||
|
||||
**`progress` is read by strangers, so it answers for one reader** (MODULE_API 1.12.0).
|
||||
While a run is live, core asks it how its step stands and puts the answer on the
|
||||
event's public page and in the app.
|
||||
@@ -189,6 +197,48 @@ unreconstructable afterwards by anybody who did not write the action.
|
||||
another module) registered. A source that refuses degrades its field to free text
|
||||
with a warning and never blocks the form — so resolve from live data and return
|
||||
`[]` on failure, rather than defending with a hardcoded list that will be wrong.
|
||||
A row may carry a `note` (1.13.0), a sentence the editor shows under the field
|
||||
while that row is chosen. It is advice about the value; nothing on the save path
|
||||
reads it.
|
||||
|
||||
Two more things a param can say, both for the editor's sake:
|
||||
|
||||
- **`duration: 'minutes'`** (1.13.0), on an `int` or `float` that is a length of
|
||||
time. The editor then knows the step holds something for that long, and sees to
|
||||
it that the phase it sits in lasts at least as long. Declare it on every param
|
||||
that is a duration; the editor has no other way to tell.
|
||||
- **`multiple: true`** (1.14.0), with an optional `max` (1 to 100, 20 by default),
|
||||
on a `string` or `int`. The author picks several values, and `perform()` and
|
||||
`cost()` receive an array. A bare value arrives as a list of one, so a spec
|
||||
written by hand still works. Price the whole list in `cost()`: three kits given
|
||||
are three gives.
|
||||
|
||||
## Templates: a shortcut past the editor
|
||||
|
||||
A fifth declaration, `api.registerEventTemplates` (MODULE_API 1.13.0), gives an
|
||||
author a named starting point for a new event: "A kit weekend", built from a kit
|
||||
and two dates.
|
||||
|
||||
```js
|
||||
api.registerEventTemplates([{
|
||||
id: 'examplegame.beacon-night',
|
||||
label: 'Beacon night',
|
||||
description: 'Light the beacons at dusk, put them out at dawn.',
|
||||
inputs: [ /* params, as an action's are, at most 8 */ ],
|
||||
async build({ inputs }) { return { title, summary, spec: { phases } } },
|
||||
}])
|
||||
```
|
||||
|
||||
**A template is never a second way to make an event.** What `build()` returns is
|
||||
checked as a spec and opened as an ordinary unsaved draft, and the author saves it
|
||||
through the same route and the same checks as one they typed. So a template cannot
|
||||
make an event the save path would refuse, and nothing about it is stored: changing
|
||||
or removing one touches no event it ever made. When the inputs make no sense,
|
||||
answer `{ ok: false, error }` with a sentence for the author; anything you throw
|
||||
stays in the log.
|
||||
|
||||
Build one last, once your actions and leases are right. It is a convenience over
|
||||
them, never a replacement for them.
|
||||
|
||||
---
|
||||
|
||||
@@ -233,6 +283,13 @@ silence and the operator sees `"<action id> refused"`. Writing this chapter's
|
||||
template is how that was found — its first draft used `detail`, and every refusal
|
||||
it produced was anonymous.
|
||||
|
||||
**A retry may say when** (MODULE_API 1.15.0). Core retries a failed step after a
|
||||
flat minute. If your game knows better, as with a thing that is warming up for ten
|
||||
minutes, answer `{ ok: false, retry: true, retryAfterMs }`. Core clamps it to one
|
||||
to thirty minutes rather than refusing it, and it still counts as one attempt of
|
||||
the step's few: it changes *when*, never *how many*. So a hint is not a way to wait
|
||||
for something that may take an hour; refuse that, and say why in `error`.
|
||||
|
||||
## 2. Pass the idempotency key through, and put it on a command rather than a question
|
||||
|
||||
Core hands `perform()` an `idempotencyKey` derived from the step's identity — never
|
||||
@@ -315,6 +372,13 @@ time is up), call `ctx.events.expired({ kind, ref })` when it does. Core then ma
|
||||
row `expired`: finished as planned, never taken back at teardown. Left to `reconcile`,
|
||||
the same thing would turn up `orphaned` and read as something that went wrong.
|
||||
|
||||
**And report its deadline when you make it.** A resource in `perform()`'s answer may
|
||||
carry `until`, the instant the game will end it by itself (an ISO string or epoch
|
||||
milliseconds). After an outage, core's reconcile can run before the game's own "it
|
||||
ended" has reached it. A resource gone past its `until` is then filed `expired`, as
|
||||
`ctx.events.expired()` would have filed it, instead of `orphaned`. Without `until`,
|
||||
the same outage turns every timed thing your event made into an operator's alarm.
|
||||
|
||||
**You say WHEN to reconcile, because core cannot.** Core has no concept of the game
|
||||
being up — it sees `{ ok: false, retry: true }` and cannot tell a wedged sidecar
|
||||
from a game that rebooted and lost everything an event made. So it asks once, at
|
||||
@@ -379,6 +443,8 @@ Four things a second module's author reaches for and should not.
|
||||
Its absence is merely a lower standard rather than a broken promise.
|
||||
7. **`progress`**, if a visitor would want to watch the step happen. It is the only
|
||||
one an outsider reads, so build it once the ledger it counts from is right.
|
||||
8. **A template**, if authors keep building the same event by hand. It saves them
|
||||
typing and adds no power, so it comes last.
|
||||
|
||||
Then read your own `revert` again, and ask what it answers when the game is down.
|
||||
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
{
|
||||
"repo": "https://gitea.whitlocktech.com/RunicGateway/website.git",
|
||||
"branch": "main",
|
||||
"ref": "0a37a44164ab107584b918e656651452b50bd4f3",
|
||||
"ref": "6890a74c7c4ad5690509e8c163e477d7f7b399e7",
|
||||
"why": [
|
||||
"The core this kit is written against, pinned to a commit rather than a branch.",
|
||||
"",
|
||||
"2026-10-23: moved from 0a37a44 (1.12.0) to 6890a74 (1.17.0, website#220,",
|
||||
"the cutover that brought Steam sign-in and the Rust last fixes to `main`),",
|
||||
"past 1.13.0 to 1.16.0, which reached `main` together on 2026-10-22. The",
|
||||
"chapters were read again for all five versions:",
|
||||
"- 1.13.0: templates (chapter 5 gains a section), a param's duration, a",
|
||||
" lease's example, an option row's note;",
|
||||
"- 1.14.0: a param's multiple and max;",
|
||||
"- 1.15.0: a retry's retryAfterMs;",
|
||||
"- 1.16.0: a dry run's detail.warnings;",
|
||||
"- 1.17.0: onIdentityLinking / onIdentityLinked and ctx.identities.list",
|
||||
" (chapter 2 gains a section, and its call count is corrected to nineteen).",
|
||||
"Two core changes in the same cutover carried no version bump, and both are",
|
||||
"taught: a site runs ONE game module, which the loader now enforces at boot",
|
||||
"(chapter 1, the reader most likely to be surprised by it), and a resource's",
|
||||
"`until` is read at reconcile (chapter 5). The template declares none of the",
|
||||
"new members, so nothing it does changed meaning; module.json's coreApi",
|
||||
"becomes ^1.17.0 for the equality check. The real core run was repeated at",
|
||||
"this ref, with the template as the only module in the directory.",
|
||||
"",
|
||||
"2026-10-06: moved from 655fbf3f (1.10.0) to 0a37a44 (1.12.0, website#210),",
|
||||
"past 1.11.0 (website#209), which this pin had skipped. Chapter 5 was read again",
|
||||
"for both versions and gained two paragraphs:",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"id": "examplegame",
|
||||
"name": "Example Game",
|
||||
"version": "0.1.0",
|
||||
"coreApi": "^1.12.0",
|
||||
"coreApi": "^1.17.0",
|
||||
"server": "server/index.js",
|
||||
"client": { "entry": "client/dist/entry.js" },
|
||||
"schema": "server/db/schema.sql",
|
||||
|
||||
Reference in New Issue
Block a user