@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.
- package/README.md +31 -1
- package/bin/neo-angband-mod-check.mjs +2 -0
- package/dist/capabilities.d.ts +23 -1
- package/dist/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +32 -3
- package/dist/capabilities.js.map +1 -1
- package/dist/manifest.d.ts +16 -2
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -1
- package/dist/standards.d.ts +7 -4
- package/dist/standards.d.ts.map +1 -1
- package/dist/standards.js +43 -5
- package/dist/standards.js.map +1 -1
- package/docs/AUTHORING.md +10 -8
- package/docs/MOD_COMPATIBILITY.md +74 -17
- package/docs/MOD_LIFECYCLE.md +68 -31
- package/docs/MOD_REACH.md +19 -2
- package/docs/MOD_SEAMS.md +7 -0
- package/docs/PLUGINS.md +34 -4
- package/docs/README.md +11 -8
- package/docs/REQUIREMENTS.md +6 -0
- package/docs/tutorials/01-tweak-a-value.md +4 -4
- package/docs/tutorials/02-add-an-item.md +21 -21
- package/docs/tutorials/03-add-a-monster.md +23 -23
- package/docs/tutorials/04-change-a-spell.md +5 -5
- package/docs/tutorials/05-hook-behaviour.md +8 -8
- package/docs/tutorials/06-add-an-option.md +7 -7
- package/docs/tutorials/07-add-an-artifact.md +13 -13
- package/docs/tutorials/README.md +21 -18
- package/package.json +1 -1
- package/src/capabilities.ts +35 -5
- package/src/manifest.ts +43 -2
- package/src/standards.ts +41 -5
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
255
|
-
the old name as an alias or to record the break here
|
|
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
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
**
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
package/docs/MOD_LIFECYCLE.md
CHANGED
|
@@ -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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
and
|
|
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
|
|
122
|
-
restore it ("
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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:
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
1396
|
-
|
|
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** (
|
|
76
|
-
| **Replace the UI**: the HUD,
|
|
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,
|
|
161
|
-
|
|
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
|
-
*
|
|
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
|
|
450
|
-
|
|
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
|
|
package/docs/REQUIREMENTS.md
CHANGED
|
@@ -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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
##
|
|
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
|