@rpgm-tools/neo-angband-mod-sdk 1.3.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,11 +7,13 @@ The short answer, and the target the rest of this document explains:
7
7
 
8
8
  > A mod that is pure data should survive engine releases without being
9
9
  > republished. A mod that ships code should survive patch and minor releases,
10
- > and get a release's warning before an ABI change strands it.
10
+ > and get a release's warning before an ABI change strands it. That ABI includes
11
+ > a named subset of `ctx.core`; the rest of that namespace is an escape hatch.
11
12
 
12
13
  Written down on 2026-08-02, after measuring how the four gates actually behaved.
13
14
  Three of the four were stricter than they needed to be, and the fourth did not
14
- exist.
15
+ exist. The fourth is `ctx.core`. A named subset of that namespace is part of the
16
+ promise; the rest of it is an escape hatch.
15
17
 
16
18
  ## The four things that can strand a mod
17
19
 
@@ -20,7 +22,7 @@ exist.
20
22
  | 1 | `engine` | a semver range over `ENGINE_VERSION` | **warns** for data, **refuses** code |
21
23
  | 2 | `modApi` | the plugin ABI, an integer | refuses outside the accepted window |
22
24
  | 3 | a patch target | one `patches` / `fieldPatches` / `removes` ref | **skips that op**, keeps the mod |
23
- | 4 | `ctx.core` | any of the engine's ~1950 exports | nothing. See below. |
25
+ | 4 | `ctx.core` | a guaranteed name disappearing; any other export a plugin actually calls | the loader still does nothing at load time; a guaranteed name cannot be removed from the engine without an alias or a two-release ABI bump. See below. |
24
26
 
25
27
  ### 1. `engine` is a label on data and a gate on code
26
28
 
@@ -231,10 +233,10 @@ instead of `values`, say) took down the whole pack, or every installed mod when
231
233
  the raw error named none of them; it is now refused the same way a missing
232
234
  patch target already was.
233
235
 
234
- ### 4. The handed-in namespaces are not covered by any of the above, and that is the honest gap
236
+ ### 4. `ctx.core` is handed over whole; a named subset of it is guaranteed
235
237
 
236
238
  `ModPluginContext.core` is the **live core module namespace** - the whole engine,
237
- around 1,950 runtime exports, deliberately not a curated slice (decision 18, and
239
+ 1,974 runtime exports, deliberately not a curated slice (decision 18, and
238
240
  because a curated list is the thing that drifts). The count is not maintained by
239
241
  hand: `packages/core/mod-api-surface.json` is the recorded surface and
240
242
  `mod-core-surface.test.ts` fails in BOTH directions against it, so a removal and
@@ -247,12 +249,60 @@ A core function can be renamed without touching any of that, so the one number a
247
249
  mod author checks says nothing about the surface they spend all their time
248
250
  calling.
249
251
 
250
- What exists now is not a fence but a **ratchet**:
252
+ That width is why the whole namespace cannot carry the compatibility promise: a
253
+ ban on removing any of 1,974 names would freeze the port. First-party plugins
254
+ call 23 of those names at runtime, and those 23 are the part of `ctx.core` the
255
+ promise covers.
256
+
257
+ | Export | `typeof` | Called by |
258
+ |---|---|---|
259
+ | `DDGRID` | object | feature-restoration |
260
+ | `FEAT` | object | bug-fixes, borg |
261
+ | `MON_RACE_FLAG_ENTRIES` | object | borg |
262
+ | `MON_SPELL_ENTRIES` | object | borg |
263
+ | `OptionState` | function | qol |
264
+ | `REST_COMPLETE` | number | borg |
265
+ | `RSF` | object | borg |
266
+ | `Rng` | function | borg |
267
+ | `TV` | object | borg |
268
+ | `describeLookGrid` | function | qol |
269
+ | `gearObjectForUse` | function | feature-restoration |
270
+ | `knownPile` | function | qol |
271
+ | `loc` | function | bug-fixes |
272
+ | `movementTunnelTest` | function | qol |
273
+ | `parseTilePrefs` | function | upstream-catchup |
274
+ | `placeStairs` | function | bug-fixes |
275
+ | `playerConfuseDir` | function | feature-restoration |
276
+ | `setPrefErrorPolicy` | function | qol |
277
+ | `squareIsEmpty` | function | bug-fixes |
278
+ | `squareIsNoStairs` | function | bug-fixes |
279
+ | `squareIsVisibleTrap` | function | qol |
280
+ | `squareNumWallsAdjacent` | function | bug-fixes |
281
+ | `tunnelAux` | function | qol |
282
+
283
+ qol 7, bug-fixes 6, borg 7, feature-restoration 3, upstream-catchup 1. linoleum
284
+ and forge call none: linoleum reaches the game through `host.tiles` and
285
+ `ctx.registries`; forge holds `ctx.core` and does not read a name from it.
286
+
287
+ `packages/core/mod-core-guaranteed.json` is the list, and
288
+ `mod-core-guaranteed.test.ts` fails if any of those names is missing from the
289
+ live namespace a plugin receives, or if its `typeof` has changed. Removing one
290
+ is a compatibility break: keep the old name as an alias, or take the two-release
291
+ `modApi` path. Updating `mod-api-surface.json` is not enough.
292
+
293
+ **Everything else on `ctx.core` is the escape hatch.** A plugin may call it. A
294
+ rename or removal of an unguaranteed name is recorded in the table below and can
295
+ ship in the same release. Prefer a seam (`ModHooks`, `ctx.registries`, a
296
+ capability-gated facade) when one exists. Reaching past those into an
297
+ unguaranteed name is coupling to that engine release.
298
+
299
+ What exists for the rest of the namespace is not a fence but a **ratchet**:
251
300
  `packages/core/mod-api-surface.json` records every runtime export, and
252
301
  `mod-core-surface.test.ts` fails when the set changes in either direction.
253
302
 
254
- - A **removal or rename** fails CI with the names, and the fix is either to keep
255
- the old name as an alias or to record the break here and take it knowingly.
303
+ - A **removal or rename** of an unguaranteed name fails CI with the names, and
304
+ the fix is either to keep the old name as an alias or to record the break here
305
+ and take it knowingly. A guaranteed name cannot take that second path.
256
306
  - An **addition** also fails, with a one-line fix
257
307
  (`node tools/api-surface.mjs --update`). That is not pedantry: a baseline that
258
308
  tolerated additions would go stale, an export added in one release and removed
@@ -262,9 +312,10 @@ What exists now is not a fence but a **ratchet**:
262
312
  #### There are two such namespaces now, watched the same way
263
313
 
264
314
  `ctx.authoring` is the mod SDK's public barrel, 94 runtime exports, handed over
265
- whole on exactly the terms `ctx.core` is and for exactly the same reason: a
266
- curated subset of an authoring API is a second list to maintain, and the first
267
- thing that happens to a curated list is that it lags the function somebody needs.
315
+ whole for the same reason `ctx.core` is: a curated subset of an authoring API is
316
+ a second list to maintain, and the first thing that happens to a curated list is
317
+ that it lags the function somebody needs. It is ratchet-only. It is not part of
318
+ the named `ctx.core` subset above.
268
319
 
269
320
  Until it was handed to a plugin, a rename inside the SDK was caught by `tsc -b`
270
321
  over this repository, because every consumer of it was in the repository. That
@@ -293,6 +344,9 @@ here for discoverability rather than as a warning.
293
344
  | unreleased (2026-08-22) | `ctx.composedRecords` | Every content record the running game was composed from, as JSON, keyed by pack-file stem with no extension. The UNBOUND twin of `ctx.registries`, and the shape the authoring functions above accept: `registries.monsters.races` is bound and carries neither the JSON key names nor the fields that bound to nothing, so a peer table cannot be built from it. Mod-added records are in it on the same terms as core's, each carrying its provenance. Absent during content composition, for the same reason `registries` is. |
294
345
  | unreleased (2026-08-22) | `ctx.reloadGame` | The game's own mod-change reload, so a mod that installed something can apply it: every plugin's `uninstall()` runs, the autoplayer hands the keyboard back, the live character is written down, and the session resumes that character instead of landing on the title screen. Behind `mod:install` rather than a capability of its own, because content composes at load and an install a mod cannot follow with a reload leaves the player holding something the process will never load. Not a permission to reload - a plugin reaches `location` with no grant - it is the four steps a mod cannot take for itself. |
295
346
  | unreleased (2026-08-22) | `ctx.installMod(...).lines` | A field on both arms of the existing install outcome: the wording the Mods screen itself prints for that same outcome, including one row per unmet requirement and the author's advice under them. Added because a mod built in the game must fail a requirement in the same words a downloaded mod fails in; `problem` is unchanged and remains one whole sentence. |
347
+ | unreleased (2026-09-01) | `ModRegistryHost.menus.addAction` | A capability-gated additive companion to `menus.register`: a plugin declaring `registry:menu` can add one labelled, namespaced callback row to `core:game-menu`. Existing menu transformers keep their behaviour, and an existing plugin neither calls nor observes the new method, so this is a backward-compatible API addition and does not move `MOD_API_VERSION`. |
348
+ | unreleased (2026-09-05) | `ctx.keymaps.entries`, `.rebind`, and `.remove` | The existing `keymap:write` facade now records an owner for each binding it creates. A mod can enumerate, replace, and remove only its own bindings in the active keyset; it cannot learn, overwrite, or remove player bindings or another mod's. Player keymap edits clear a claim, and the mod-change teardown removes claims that remain, so a disabled mod has no surviving keyboard binding. Existing `bind` remains free-trigger-only, so this is a backward-compatible API addition and does not move `MOD_API_VERSION`. |
349
+ | unreleased (2026-09-02) | API-2 Worker ABI | `modApi: 2` plus `runtime: "worker"` and a mod-relative `workerEntry` selects a dedicated module Worker booted by host code rather than importing `plugin.js` into the renderer. Its independently versioned protocol starts with immutable init data, logs, async preferences, own-asset bytes, bag migration, semantic commands, render-coalesced state and display snapshots, and host-rendered declarative panels. The intended final surface also adds versioned read snapshots, host-cached policies for synchronous paths, async hook decisions with validated patches, declarative registry/content and tile declarations, cached frontend/HUD/region display lists, semantic keymap/debug/wizard/storage operations, and mediated mod lifecycle actions. API-1 is temporarily trusted compatibility code, not a permanent dual-API arrangement; the planned end state is full API-2 cutover for every shipped mod. |
296
350
 
297
351
  #### Removals taken knowingly
298
352
 
@@ -428,12 +482,15 @@ Nothing else about `UiEntryConfig` moved, and a plugin that only calls
428
482
  unaffected: every one of those gained an OPTIONAL trailing registry argument and
429
483
  behaves exactly as before when it is omitted.
430
484
 
431
- **This does not make `ctx.core` stable.** It makes breaking it visible to the
432
- person breaking it, in the repository where it happens, before it reaches a
433
- player's browser. The remaining pressure valve is `ModHooks`, which is a closed
434
- interface of eight members that the bug-fixes mod alone needed six of - if
435
- authors keep reaching past it into `ctx.core`, that is the signal to grow the
436
- seam, not to fence the namespace.
485
+ **The rest of `ctx.core` is not frozen.** The ratchet makes a break visible in
486
+ this repository before it reaches a player's browser. Nested keys on a
487
+ guaranteed object (`FEAT.MORE`, `TV.SWORD`, `RSF.BR_FIRE`) follow the same
488
+ shape-change recording as the rest of the engine: the promise is the top-level
489
+ name and its `typeof`, not every property behind it. The remaining pressure
490
+ valve is `ModHooks`, which is a closed interface of eight members that the
491
+ bug-fixes mod alone needed six of - if authors keep reaching past it into
492
+ unguaranteed `ctx.core` names, that is the signal to grow the seam or to add a
493
+ name to the guaranteed subset, not to fence the namespace.
437
494
 
438
495
  ## What is *not* a compatibility mechanism
439
496
 
@@ -92,6 +92,15 @@ deletion - even of cosmetics - violates the "nothing a player earned vanishes
92
92
  without a trace" guarantee. The prompt is per-save and one-time so it never
93
93
  nags; declining leaves everything quarantined and reversible.
94
94
 
95
+ BUILT. The prompt is put once the game screen is live, keeps first, and
96
+ counts what a purge would destroy; dismissing it keeps, because the question
97
+ has been asked and keeping is the only answer that destroys nothing. The
98
+ answer is recorded in `SavedGame.orphansAcknowledged` and forced to disk
99
+ immediately, so neither answer is re-asked and a purge cannot be undone by a
100
+ crash. `orphanPromptDue` and `purgeOrphans`
101
+ (`packages/core/src/mod/orphan-stash.ts`) are the whole of the engine side;
102
+ purging is the only write in the orphan path.
103
+
95
104
  ### When a mod's content leaves the game (RATIFIED, decision 19)
96
105
 
97
106
  Quarantine is the storage mechanism; these are the player-facing
@@ -108,19 +117,35 @@ uninstalled but SHADOWED - a later mod in the load order `removes` or
108
117
  content as normal on the next descent; no half-loaded mod geometry is
109
118
  ever walked.
110
119
  - Stranded items. Items whose definition came from the missing mod are
111
- not dropped and not deleted. They are moved into the player's HOME
112
- (the game's existing persistent stash) as inert entries - visible,
113
- labelled with their origin mod, but not equippable, usable, or
114
- sellable while the mod is absent. Reinstalling the mod (same major
115
- version) reactivates them in place. This uses the same
116
- `orphans:<id>@<version>` store; the home is just where the player sees
117
- and reclaims them.
120
+ not dropped and not deleted. They are frozen in the
121
+ `orphans:<id>@<version>` store as inert entries - listed, labelled with
122
+ their origin mod, and not equippable, usable or sellable while the mod
123
+ is absent. Reinstalling the mod puts each one back in the slot it came
124
+ out of: an item that was worn returns to that equipment slot when the
125
+ slot is still free, and one that was carried returns to the pack.
126
+ BUILT, and built AS THE STORE rather than as home stock: the frozen
127
+ entry records the gear handle and the equipment slots it was
128
+ quarantined out of (`packages/core/src/mod/save-blocks.ts`), which is
129
+ what makes the exact restore possible. It occupies no home slot, no
130
+ pack slot and no weight while it waits, so it costs the player nothing
131
+ that a home slot would have cost, and the stash view below is where it
132
+ is seen and reclaimed.
118
133
  - The stash view. A dedicated, always-reachable screen lists everything
119
134
  currently quarantined - by uninstall OR by another mod's override -
120
135
  grouped by the mod that owns it, showing what it is, why it is inert
121
- ("frost uninstalled" / "shadowed by bigmonsters"), and what would
122
- restore it ("reinstall frost >=1.0" / "move bigmonsters below frost").
136
+ ("frost is not installed" / "frost is installed but switched off"), and
137
+ what would restore it ("install frost again" / "turn frost back on").
123
138
  Nothing a player earned ever vanishes without a trace they can find.
139
+ BUILT: Mods -> "Set aside by a missing mod", screen id
140
+ `core:mod-orphans` (`packages/web/src/mod-orphans.ts`), over the
141
+ read-only model in `packages/core/src/mod/orphan-stash.ts`. The row
142
+ carries the count, and a load that newly freezes something says so on
143
+ the message line naming what it was. Quarantine is keyed on namespace
144
+ PRESENCE, so the screen derives the reason from what the host can see
145
+ now rather than from a reason the store records; the shadowed case
146
+ reaches the same screen through the same route as soon as quarantine
147
+ produces it, because a shadowed entity's own namespace is present and
148
+ the screen already words that state.
124
149
 
125
150
  These recoveries are graceful degradation, not gameplay rollback: they
126
151
  preserve what the player has against a tooling change, and do not let the
@@ -270,17 +295,20 @@ documented, not hidden. The desktop build could clone directly, since it has a
270
295
  real filesystem and a real process to run git in. Either way the installer
271
296
  consumes the same pack format.
272
297
 
273
- ### From a marketplace (future release)
274
-
275
- The marketplace is a delivery layer over the same pipeline, not a
276
- separate system. It serves pre-validated, pre-packaged `.ngpack` bundles
277
- (the pack directory, zipped, with the manifest and a signed content
278
- hash) from its own host, and adds browse / search / screenshots /
279
- ratings in-app. Building it later is cheap because the bundle
280
- format and the installer are built now, with "git" and "marketplace" as two
281
- sources feeding one installer. [PROPOSED] The in-app browser is a view
282
- onto that source; actually building the marketplace backend is a future
283
- release, as noted.
298
+ ### From a marketplace - DECLINED
299
+
300
+ This section previously proposed a from-scratch, self-hosted marketplace
301
+ serving pre-validated, pre-packaged `.ngpack` bundles (the pack directory,
302
+ zipped, with the manifest and a signed content hash), with browse / search /
303
+ screenshots / ratings in-app. It was never built past this design. Nexus
304
+ Mods integration (tracked separately - see the mod-distribution issues)
305
+ supersedes it: Nexus already provides hosting, browse/search, screenshots
306
+ and ratings at a scale a self-hosted marketplace would take years to reach,
307
+ and building a second, competing one afterward would be duplicate
308
+ infrastructure with a much smaller audience. The manifest fields this
309
+ proposal anticipated (`description`, `screenshots`, `changelog`, `author`,
310
+ `license`, and a hash-based integrity record) were not wasted - they are
311
+ exactly what a Nexus-origin install preview reuses.
284
312
 
285
313
  ### Updating and uninstalling
286
314
 
@@ -639,8 +667,8 @@ pointing at the fix, not a silent divergence.
639
667
  Save determinism mode (core-governed ratchet). The label is not just cosmetic:
640
668
  every save carries a determinism mode that CORE owns and enforces regardless
641
669
  of which mods are loaded. A save starts DETERMINISTIC; the first time a
642
- determinism-affecting mod is enabled on it, core flips it to NONDETERMINISTIC
643
- seamlessly and IRREVERSIBLY. Removing the mod later does not restore
670
+ determinism-affecting mod is enabled on it, core flips it to NONDETERMINISTIC.
671
+ The change is IRREVERSIBLE. Removing the mod later does not restore
644
672
  deterministic mode - it is a one-way ratchet, so a deterministic (unmodded)
645
673
  save cannot be tainted by a mod and then "cleansed" to reclaim its
646
674
  reproducibility/anti-scum guarantees. Mods can trigger the flip but can never
@@ -743,13 +771,15 @@ UI next, marketplace last:
743
771
  capability consent, profiles), plus the one auto-sort button the
744
772
  2026-08-01 amendment above adds. Bulk reordering, staging, collections
745
773
  and update watching stay the external manager's job.
746
- 3. Future release: the marketplace backend and in-app browser, plus the
747
- externally-authored enabled-set/order file and a Vortex/MO2 extension
748
- over the shared on-disk format.
774
+ 3. Future release: Nexus Mods integration (a real second pinned origin,
775
+ not the from-scratch marketplace this item once named - see "From a
776
+ marketplace" above), the externally-authored enabled-set/order file, and
777
+ a Vortex/MO2 extension over the shared on-disk format.
749
778
 
750
- A `neo-pack` CLI (validate + bundle, a sibling of `neo-linoleum`) ships
751
- alongside so authors can check a pack in CI before publishing, and the
752
- repo carries sample mods that CI installs and runs.
779
+ Authors check a pack in CI with `neo-angband-mod-check` (the same rules
780
+ the game enforces at install) and produce a distributable `plugin.js`
781
+ with `neo-angband-mod-build`. Both ship in `@rpgm-tools/neo-angband-mod-sdk`.
782
+ The repo carries sample mods that CI installs and runs.
753
783
 
754
784
  ---
755
785
 
@@ -766,10 +796,17 @@ repo carries sample mods that CI installs and runs.
766
796
  6. Pre-migration snapshot as operational safety, reconciled with the
767
797
  no-save-scum rule. [DECIDED]
768
798
  7. Uninstall recovery: stranded characters return to town, mod items are
769
- quarantined to the player's home and reactivate on reinstall, and a
770
- stash view surfaces everything quarantined or shadowed. [DECIDED]
799
+ quarantined and reactivate in the slot they came out of on reinstall,
800
+ and a stash view surfaces everything quarantined or shadowed. [DECIDED;
801
+ the stash view and the item surfacing are BUILT, see "When a mod's
802
+ content leaves the game" above. The item half is the orphans store
803
+ rather than home stock, because the store records the gear handle and
804
+ equipment slots an item was taken from and the home has no way to carry
805
+ those; the effect a home entry was for - visible, labelled, inert,
806
+ costing no slot - is what the stash view provides. The stranded-location
807
+ half is not built.]
771
808
  8. Orphan policy: quarantine by default with a one-time per-save keep/purge
772
- prompt (keep default); no auto-purge. [DECIDED 2026-07-14]
809
+ prompt (keep default); no auto-purge. [DECIDED 2026-07-14; BUILT]
773
810
  9. Integrate with Vortex and the other popular mod managers, and split the
774
811
  labour with them: the game keeps rudimentary management (enable/disable,
775
812
  per-patch opt-out, a one-step order nudge, conflict report, profiles)
package/docs/MOD_REACH.md CHANGED
@@ -1318,7 +1318,7 @@ pack was already surfaced in the mod manager by `discoverContentModManifests`
1318
1318
  (`pack.ts`). The bytes were reachable; only the registration was not.
1319
1319
 
1320
1320
  The non-mod escape hatch is `?tiles=<base-url>` + `?graf=<id>`
1321
- (`tiles.ts:178-187`, `packages/web/src/main.ts:1005-1006`), which also unlocks the full catalog
1321
+ (`tiles.ts:178-187`, `packages/web/src/main.ts:1005-1006`), which also exposes the full catalog
1322
1322
  (`tile-catalog.ts:109`). That is a user/URL affordance, not a mod path, and it
1323
1323
  cannot add a grafID.
1324
1324
 
@@ -1453,7 +1453,7 @@ used, and what a failed check costs.
1453
1453
 
1454
1454
  ## (d) The gap list
1455
1455
 
1456
- Ranked by how much of "the whole game can be made over" each one unlocks.
1456
+ Ranked by how much of "the whole game can be made over" each one makes moddable.
1457
1457
 
1458
1458
  | # | Capability | Today | What would have to exist |
1459
1459
  | --- | --- | --- | --- |
@@ -1481,6 +1481,23 @@ Ranked by how much of "the whole game can be made over" each one unlocks.
1481
1481
 
1482
1482
  ---
1483
1483
 
1484
+ ### Gap 21 current correction, 2026-09-06
1485
+
1486
+ The row above contains historical measurements. The current code-derived record
1487
+ is docs/design/PRESENTATION_OWNER_SEAM.md. Its counts replace the old estimates:
1488
+ the HUD has 3 separately replaceable regions, the terminal has 33 full-screen
1489
+ erase paths plus its 1 compositor repaint, and 40 semantic ScreenView ids are
1490
+ modelled. The full-screen erase ratchet derives 31 region-declared paths, 2
1491
+ pending prompt paths, and 34 including the compositor in
1492
+ packages/web/src/main-regions.test.ts:430 through
1493
+ packages/web/src/main-regions.test.ts:433.
1494
+
1495
+ The presentation owner seam is live for the HUD, menus, and ScreenView paths.
1496
+ Registry:menu still changes rows and now composes with menu presentation rather
1497
+ than replacing it. The two pending prompt paths and screens sharing the
1498
+ core:text fallback remain incomplete. The design records the exact surface list,
1499
+ the host guarantees, and the compatibility fallback rule.
1500
+
1484
1501
  ## The goal: every dispatch point in this document becomes moddable
1485
1502
 
1486
1503
  **A correction, recorded 2026-08-08.** An earlier revision of this page argued
package/docs/MOD_SEAMS.md CHANGED
@@ -610,6 +610,13 @@ Six things about it, and the first two are the ones that will surprise you:
610
610
  action bar does with its buttons. `modal: true` takes the pointer, takes the
611
611
  focus on mount, and gets `role="dialog"`; a plain panel takes neither and gets
612
612
  `role="group"`.
613
+ - **The host fits the container to the visual viewport.** On a phone, opening
614
+ the virtual keyboard can leave the layout viewport at its former height while
615
+ shrinking the visible rectangle. The host follows `visualViewport` for the
616
+ panel and the canvas, including its offset, so a modal panel does not extend
617
+ behind the keyboard. A panel that puts fields near its lower edge should still
618
+ call `element.scrollIntoView({ block: "center" })` when a field receives focus:
619
+ a browser may reserve more of the visible rectangle for its own input chrome.
613
620
  - **The keyboard is decided per keystroke, by where the caret is.** The game's
614
621
  front end has ONE keydown registration - `window`, capture phase, installed at
615
622
  import - and every modal handler behind it calls `stopImmediatePropagation`. So
package/docs/PLUGINS.md CHANGED
@@ -161,7 +161,7 @@ What `ctx` carries:
161
161
  | `api` | the ABI version the **host** implements |
162
162
  | `engine` | the engine version, if you want to adapt rather than refuse |
163
163
  | `flags` | **your** resolved rule toggles: `choices[flag] ?? rule.default`, sliced to the rules your own manifest declares |
164
- | `core` | the live engine namespace: core's entire public API |
164
+ | `core` | the live engine namespace: core's entire public API. A named subset is guaranteed; the rest is an escape hatch. See [MOD_COMPATIBILITY.md](MOD_COMPATIBILITY.md) |
165
165
  | `state` | the live `GameState`. Handed to `register`, `migrateBag`, `controller` and every display seam; **never** to `hooks`, because the host composes hooks before a game exists |
166
166
  | `assetUrl` | `(path) => Promise<string \| null>`, a URL for one of *your* files |
167
167
  | `data` | your own record files, parsed, keyed without `.json` |
@@ -1392,8 +1392,13 @@ absent paths leave the exact glyph sink active.
1392
1392
 
1393
1393
  The same is true of `UiInput` in `packages/web/src/input-door.ts`. It is the
1394
1394
  single device-neutral route by which keyboard and keymap input reaches screens;
1395
- its direction carries an analog vector and angle. It does not grant a plugin a
1396
- binding registry in this phase. Stored player keymaps are evaluated first when
1395
+ its direction carries an analog vector and angle. A plugin that declares and is
1396
+ granted `keymap:write` receives `ctx.keymaps` during a live game. `bind()` claims
1397
+ only a free trigger; `entries()` lists only bindings the calling mod owns; and
1398
+ `rebind()` and `remove()` can change only those owned bindings. A plugin cannot
1399
+ read, overwrite, or remove the player's bindings or another mod's bindings. A
1400
+ player change clears the mod claim, and bindings still owned by a departing mod
1401
+ are removed during host teardown. Stored player keymaps are evaluated first when
1397
1402
  the root owns input; score pages, modals, and run interruption retain their
1398
1403
  existing literal-key gates, so a mod must not use injected input to outrank the
1399
1404
  player's chosen mapping or an active screen.
@@ -1421,7 +1426,7 @@ written down, and the table below is a reading of it:
1421
1426
  | `registry:rune` | what a RUNE is: the unit of object knowledge. `rune.desc` (the recall line), `.name` (the display decoration), `.knows` / `.learn` (the knowledge pair, handed the player so YOUR mod keeps the store, since core never grew a slot for it), `.objectHas` (whether an item carries it) and `.modMessage` (the "You feel stronger!" line, keyed on the modifier). Plus `.contribute`, which is how your rune gets into the list every consumer enumerates, and without it the six tables above are handlers nothing ever calls |
1422
1427
  | `registry:vocab` | declare genuinely new vocabulary (flags, stats, mod-coined kinds) and store per-entity values |
1423
1428
  | `registry:message` | message TYPES: `messages.define(name, sound?)` coins one and returns its number, `.lookup(name)` finds an existing one, `.types()` lists what has been added, `.addSounds(...)` attaches sounds. Adding a `message_type.json` RECORD needs no capability, the same way adding an item does; this is the code half, for a type your own plugin raises |
1424
- | `registry:menu` | rewrite one stable menu id's semantic rows. `menus.handlerFor(id)` returns the earlier transformer, so a later mod wraps it before calling `menus.register(id, ...)`; a throw or a non-row-array result is reported against that mod and leaves the original menu usable |
1429
+ | `registry:menu` | rewrite one stable menu id's semantic rows. `menus.handlerFor(id)` returns the earlier transformer, so a later mod wraps it before calling `menus.register(id, ...)`; a throw or a non-row-array result is reported against that mod and leaves the original menu usable. `menus.addAction("core:game-menu", action, label, handler)` adds a namespaced, runnable row to the Escape Game menu; it is the door for a mod-owned callback rather than a rewrite of a core action |
1425
1430
  | `registry:tiles` | supply tiles for content the loaded tile pack does not draw, which in practice means content a mod added. `tiles.register(filler)` installs one filler per mod; every registered filler runs, in load order, after the pack's own prefs and every mod's. It can only write where NOTHING is assigned, so it cannot repaint the tile set even by mistake and two mods cannot fight over an index. The same seam also grew a repaint-in-place door (`fill.transform`, mirror and/or a palette remap over an existing tile) and a player-cell door (`tiles.player`, asked once per frame what the character's own cell should show). See [Filling tiles](#filling-tiles) and [Repainting a tile, and drawing the player's own cell](#repainting-a-tile-and-drawing-the-players-own-cell) |
1426
1431
 
1427
1432
  A facade you did not declare throws when you touch it, even if the player
@@ -1547,6 +1552,31 @@ register(host) {
1547
1552
  }
1548
1553
  ```
1549
1554
 
1555
+ That transformer changes presentation only: a row it invents has no core action
1556
+ behind it. To add a **runnable** row of your own, declare `registry:menu` and
1557
+ use `addAction` instead. The host namespaces `action` by your mod id, appends
1558
+ the row to the Escape Game menu, and calls only your handler when the player
1559
+ selects it. The handler begins in the selection gesture, so it may open a
1560
+ browser folder picker.
1561
+
1562
+ ```js
1563
+ register(host, ctx) {
1564
+ host.menus.addAction(
1565
+ "core:game-menu",
1566
+ "choose-backup-folder",
1567
+ "Choose backup folder...",
1568
+ async () => {
1569
+ const name = await ctx.backupFolder?.choose();
1570
+ ctx.log(name ? `Using backup folder: ${name}` : "Backup folder unchanged.");
1571
+ },
1572
+ );
1573
+ }
1574
+ ```
1575
+
1576
+ `core:game-menu` is the only action location in this API version. A callback
1577
+ that throws is reported against its mod and returns the player to the Game menu;
1578
+ it never falls through to a similarly positioned core row.
1579
+
1550
1580
  Plugin code runs **in process, synchronously**, with the same access to the rng,
1551
1581
  the chunk, the player and the monster that core has, because a deep override
1552
1582
  cannot cross an async, isolated Worker boundary. So it is trusted code, exactly as
package/docs/README.md CHANGED
@@ -72,9 +72,9 @@ write down at all.
72
72
  | **Replace the front end**: draw the world yourself, any way you like | **Complete** | gap 9 |
73
73
  | Know **where you may draw**: named regions, in grid cells and CSS pixels, on every frame | **Complete** | gap 9 |
74
74
  | Drive the game programmatically: an autoplayer, a bot, a test harness | **Complete** | `BORG.md`, `ModPlugin.controller` |
75
- | Menus: add, reorder, retitle and re-tag **rows** (`registry:menu`). How a menu is **presented** is not reachable | **WIP** | gap 21 |
76
- | **Replace the UI**: the HUD, the status line, the message area, menu presentation, the ~50 full screens | **Not yet** | gap 21 |
77
- | Own a HUD region (messages, vitals, status) with core drawing the ones you do not claim, or **create a region of your own**: ordered, overlapping, transparent, composited, and owning pointer input on the cells it paints | **WIP** | gap 21 |
75
+ | Menus: add, reorder, retitle and re-tag **rows** (registry:menu), then replace the presentation of a supported question | **WIP** | gap 21, PRESENTATION_OWNER_SEAM.md |
76
+ | **Replace the UI**: the HUD, status line, message area, menu presentation, and full screens | **WIP** | gap 21, PRESENTATION_OWNER_SEAM.md |
77
+ | Own a HUD region (messages, vitals, status) with core drawing the ones you do not claim, or **create a region of your own**: ordered, overlapping, transparent, composited, and owning pointer input on the cells it paints | **WIP** | gap 21, PRESENTATION_OWNER_SEAM.md |
78
78
  | Retitle, regroup, reorder, re-tag or rebind an existing web keypress command (`registry:menu`); the command's closure stays shell-private | **Complete** | `MOD_REACH.md` row 23 |
79
79
  | Rename one of your own rule flags or sections without losing the player's saved choice (`renamedRuleFlags`, `renamedSectionFlags`) | **Complete** | `AUTHORING.md` |
80
80
  | Rebind keys, or add a gamepad: `input-door.ts` is host infrastructure, not a seam | **Not yet** | `MOD_SEAMS.md` |
@@ -157,15 +157,17 @@ that has not been settled.
157
157
  Scope of record; no repository or code exists yet.
158
158
  - Coming as the engine lands them (P7 deliverables): handler registry
159
159
  catalog (effects, commands, room builders), the sandbox capability
160
- reference for scripted plugins, dialog/quest/shop cookbooks, the
161
- `neo-pack` validator/bundler, and publishing guidance.
160
+ reference for scripted plugins, dialog/quest/shop cookbooks, and
161
+ publishing guidance. Validate and bundle are not in that list:
162
+ `neo-angband-mod-check` and `neo-angband-mod-build` already ship in
163
+ `@rpgm-tools/neo-angband-mod-sdk`.
162
164
 
163
165
  ## The first-party mods
164
166
 
165
167
  Seven, **none of them bundled**, all OFF until enabled (see
166
168
  `DEFAULT_ENABLED_MODS` - an untouched install is the faithful base game with no mod
167
169
  loaded). Each lives in its own repository and arrives through the mod manager's
168
- *Install a mod...* row:
170
+ *Recommended mods...* row:
169
171
 
170
172
  | id | shape | where it lives | what it adds |
171
173
  | --- | --- | --- | --- |
@@ -446,8 +448,9 @@ version and are called out in release notes.
446
448
  **`MOD_COMPATIBILITY.md` is the page that answers "will my mod still work".**
447
449
  In one line: a data-only mod should survive engine releases without being
448
450
  republished, and a mod that ships code gets a release's warning before an ABI
449
- change strands it. That page has the mechanisms, the measurements behind them,
450
- and the one place the promise does not yet hold (`ctx.core`).
451
+ change strands it. That ABI includes a named subset of `ctx.core`; the rest of
452
+ that namespace is an escape hatch. That page has the mechanisms, the
453
+ measurements behind them, and the list of guaranteed names.
451
454
 
452
455
  ## Licensing for mod authors
453
456
 
@@ -55,6 +55,12 @@ The game shows it beside the mod's name, so a player can tell two mods of the sa
55
55
 
56
56
  Without it the mod is offered to every version of the game forever, including the one that changes the thing it depends on. With it, a player is told the mod is too old instead of watching it misbehave. This was advice until it was measured: every mod that had shipped declared one, and the mods that did not were the ones nothing had checked.
57
57
 
58
+ ### Request only capabilities the game knows
59
+
60
+ `capabilities-recognized`
61
+
62
+ A capability string is the consent surface a player reads and the gate the runtime actually opens. An unrecognized string, or a capability on a pack that cannot execute, is refused when the game loads the plugin - after the player has already installed it. This check is CapabilitySet.fromManifest, the same function the loader calls, so a typo that used to pass here and fail there cannot.
63
+
58
64
  ### Declare modApi if the mod ships plugin.js
59
65
 
60
66
  `plugin-declares-modapi`
@@ -9,7 +9,7 @@ toolchain, no account, and no copy of the game's source code.
9
9
 
10
10
  ---
11
11
 
12
- ## The whole mod
12
+ ## Mod files
13
13
 
14
14
  A mod is a folder. Make one anywhere you like, call it `my-first-mod`, and put
15
15
  two files in it:
@@ -98,7 +98,7 @@ folder...**, and point it at the folder *containing* `my-first-mod`. Then turn i
98
98
  on and reload as above. The game remembers the folder, so editing a file and
99
99
  reloading the page is your whole edit-and-test loop.
100
100
 
101
- ## What you should see
101
+ ## Check the result
102
102
 
103
103
  Buy a dagger in the **Weapon Smiths** (the `3` on the town map) - that is the
104
104
  shop that stocks daggers, not the General Store. It now costs around 300 gold
@@ -110,7 +110,7 @@ ordinary again. That is the part worth pausing on: your change is a layer over
110
110
  the base game, not an edit to it. Nothing on your machine was modified, and the
111
111
  game underneath is still the vanilla one.
112
112
 
113
- ## Try changing this
113
+ ## Variations to try
114
114
 
115
115
  - Make the dagger **lighter**: add `{ "op": "set", "path": "weight", "value": 4 }`.
116
116
  - Change a **monster** instead. Add a `monster.json` with
@@ -120,7 +120,7 @@ game underneath is still the vanilla one.
120
120
  reload. Read the error: that message is your main debugging tool, and it is
121
121
  worth seeing once while you already know what is wrong.
122
122
 
123
- ## The finished version
123
+ ## Sample mod
124
124
 
125
125
  `samples/tutorials/tutorial-01-tweak-a-value/` in this repository is exactly this mod.
126
126
  It is not a copy of the tutorial. It is a mod that gets loaded and checked