@rpgm-tools/neo-angband-core 0.12.0 → 0.14.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 CHANGED
@@ -32,7 +32,7 @@ import { Rng, ENGINE_VERSION, PARITY_BASELINE } from "@rpgm-tools/neo-angband-co
32
32
  const rng = new Rng(1234);
33
33
  console.log(rng.damroll(3, 6), rng.damroll(3, 6), rng.damroll(3, 6)); // 9 13 8
34
34
 
35
- console.log(ENGINE_VERSION, PARITY_BASELINE); // 0.12.0 4.2.6
35
+ console.log(ENGINE_VERSION, PARITY_BASELINE); // 0.14.0 4.2.6
36
36
  ```
37
37
 
38
38
  Two entry points:
@@ -184,27 +184,67 @@ export interface ModHooks {
184
184
  /**
185
185
  * Fold several mods' contributions into the single ModHooks core holds.
186
186
  *
187
- * Order is LOAD order, and what "later wins" means differs per hook because the
188
- * hooks differ in kind - which is precisely why this is one written-down function
189
- * and not a generic merge:
187
+ * Order is LOAD order, and THE LATER MOD WINS EVERY DISAGREEMENT. That is the
188
+ * rule the mod manager's own row promises the player ("Move later (loads last,
189
+ * wins conflicts)") and the rule every other composition layer follows, so this
190
+ * one obeys it too - see the note below for the two folds that look like
191
+ * exceptions and are not.
190
192
  *
193
+ * - LAST-HANDLER hooks (walkBlockedByDiggable) are asked in REVERSE load order
194
+ * and stop at the first non-null, so the last mod to have an opinion is the
195
+ * one whose handling takes effect and no earlier mod can double-spend the
196
+ * energy it already paid out.
197
+ * - ORDERING hooks (objectListTiebreak) chain the same way round: the last
198
+ * mod's comparator is the primary key and earlier ones break the ties it
199
+ * leaves, which is a valid total order and is "later wins" for a comparator.
200
+ * - TRANSFORM hooks (messageText) compose in load order, each seeing the
201
+ * previous one's output - so the last mod still speaks last and has the final
202
+ * say over the text that reaches the player.
191
203
  * - VETO hooks (levelGenerated, artifactCommit, historyAdd) are conjunctive:
192
- * every contributor must agree, and the first refusal decides. This is the
193
- * only safe fold - a mod that vetoes a duplicate artifact must not be
194
- * overruled by a later mod that merely has no opinion.
195
- * - TRANSFORM hooks (messageText) compose in order, each seeing the previous
196
- * one's output.
197
- * - FIRST-HANDLER hooks (walkBlockedByDiggable) stop at the first non-null, so
198
- * an earlier mod's handling wins and a later one cannot double-spend energy.
204
+ * every contributor runs and any refusal decides.
199
205
  * - ANY hooks (saveNoiseScent) are disjunctive: one mod asking for the data is
200
206
  * enough, because the data is additive and a second mod cannot object.
201
- * - ORDERING hooks (objectListTiebreak) stop at the first non-zero answer, the
202
- * same way a lexicographic comparator chains.
207
+ *
208
+ * WHY THE LAST TWO ARE NOT EXCEPTIONS. "Later wins" answers the question "two
209
+ * mods disagree about one thing - whose answer is used?", and a veto hook is not
210
+ * asking that question. `true` from historyAdd means "I have nothing to say
211
+ * about this entry", not "I insist it be written": the hook is called once per
212
+ * entry, and two mods suppressing two different things are not in conflict at
213
+ * all. Resolving it last-wins would mean a later mod's silence cancelled an
214
+ * earlier mod's rule, breaking BOTH mods to satisfy a consistency nobody asked
215
+ * for. Same for saveNoiseScent, where `false` is "I do not need this" and the
216
+ * data is additive. The consistency that matters is that no mod's opinion is
217
+ * ever DISCARDED in favour of an earlier one, and these two discard nothing.
203
218
  *
204
219
  * Returns undefined when nothing contributed, so the caller can leave the field
205
220
  * absent rather than storing an empty object - keeping "no mod loaded" and "a
206
221
  * mod loaded that touches nothing" indistinguishable from core's side.
207
222
  */
223
+ /**
224
+ * How composeModHooks folds several mods' contributions to one hook.
225
+ *
226
+ * A subset of the vocabulary the pack tooling uses for every composition layer
227
+ * (mod-sdk's `Fold`), so the host can hand these values straight to the conflict
228
+ * report and the compiler checks the two agree.
229
+ */
230
+ export type ModHookFold = "all-must-agree" | "chained" | "last-answer" | "any-yes";
231
+ /**
232
+ * WHICH FOLD EACH HOOK USES, next to the function that implements it.
233
+ *
234
+ * The conflict report needs this to tell the player whether two mods touching
235
+ * one hook COMBINE or whether one of them is being silently ignored. Every fold
236
+ * here obeys "the later mod wins"; what differs is whether there is anything for
237
+ * a winner to win, and only `last-answer` leaves a contribution unrun. That
238
+ * distinction is the whole reason the report names the fold instead of printing
239
+ * one sentence about load order.
240
+ *
241
+ * It lives in core, beside composeModHooks, because a second copy in the host is
242
+ * the shape that drifts: the host is where the report is rendered, so a hook
243
+ * added here and forgotten there would be described by whatever the table
244
+ * happened to default to. Keyed by `keyof ModHooks`, so adding a member to the
245
+ * interface without adding it here does not compile.
246
+ */
247
+ export declare const MOD_HOOK_FOLDS: Readonly<Record<keyof ModHooks, ModHookFold>>;
208
248
  /**
209
249
  * A mod's hook function threw. Reported to whoever guarded it (guardModHooks).
210
250
  *
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/mod/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;;;;;;;;;;;;;OAgBG;IACH,qBAAqB,CAAC,EAAE,CACtB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAC9B,IAAI,EAAE,OAAO,KACV,MAAM,GAAG,IAAI,CAAC;IAEnB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,CACnB,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAC/C,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,KAC5C,MAAM,CAAC;IAEZ;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3D;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,KAAK,OAAO,CAAC;IAEpE;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC;IAE/G;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;IAE/B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,QAAQ,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GACrC,QAAQ,CAmEV;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,SAAS,QAAQ,EAAE,GACjC,QAAQ,GAAG,SAAS,CAkEtB"}
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/mod/hooks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB;;;;;;;;;;;;;;;;OAgBG;IACH,qBAAqB,CAAC,EAAE,CACtB,KAAK,EAAE,SAAS,EAChB,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,EAC9B,IAAI,EAAE,OAAO,KACV,MAAM,GAAG,IAAI,CAAC;IAEnB;;;;;;;;;OASG;IACH,kBAAkB,CAAC,EAAE,CACnB,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,EAC/C,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,KAC5C,MAAM,CAAC;IAEZ;;;;;;;;;;;;OAYG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;IAE3D;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,KAAK,OAAO,CAAC;IAEpE;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC;IAE/G;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC;IAE/B;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC;CACvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH;;;;;;GAMG;AACH,MAAM,MAAM,WAAW,GACnB,gBAAgB,GAChB,SAAS,GACT,aAAa,GACb,SAAS,CAAC;AAEd;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,QAAQ,EAAE,WAAW,CAAC,CAQxE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC3B,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,MAAM,QAAQ,CAAC;IAC9B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,QAAQ,EACf,OAAO,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,GACrC,QAAQ,CAmEV;AAED,wBAAgB,eAAe,CAC7B,aAAa,EAAE,SAAS,QAAQ,EAAE,GACjC,QAAQ,GAAG,SAAS,CA8EtB"}
package/dist/mod/hooks.js CHANGED
@@ -69,6 +69,31 @@
69
69
  * break by reaching for a global; the test suite pins it by running generation
70
70
  * with a hook installed and asserting the level is bit-identical.
71
71
  */
72
+ /**
73
+ * WHICH FOLD EACH HOOK USES, next to the function that implements it.
74
+ *
75
+ * The conflict report needs this to tell the player whether two mods touching
76
+ * one hook COMBINE or whether one of them is being silently ignored. Every fold
77
+ * here obeys "the later mod wins"; what differs is whether there is anything for
78
+ * a winner to win, and only `last-answer` leaves a contribution unrun. That
79
+ * distinction is the whole reason the report names the fold instead of printing
80
+ * one sentence about load order.
81
+ *
82
+ * It lives in core, beside composeModHooks, because a second copy in the host is
83
+ * the shape that drifts: the host is where the report is rendered, so a hook
84
+ * added here and forgotten there would be described by whatever the table
85
+ * happened to default to. Keyed by `keyof ModHooks`, so adding a member to the
86
+ * interface without adding it here does not compile.
87
+ */
88
+ export const MOD_HOOK_FOLDS = {
89
+ walkBlockedByDiggable: "last-answer",
90
+ objectListTiebreak: "last-answer",
91
+ levelGenerated: "all-must-agree",
92
+ artifactCommit: "all-must-agree",
93
+ historyAdd: "all-must-agree",
94
+ saveNoiseScent: "any-yes",
95
+ messageText: "chained",
96
+ };
72
97
  /**
73
98
  * ONE mod's contribution, with every hook wrapped so a throw cannot escape into
74
99
  * the middle of a turn.
@@ -164,9 +189,18 @@ export function composeModHooks(contributions) {
164
189
  if (list.length === 0)
165
190
  return undefined;
166
191
  const out = {};
167
- const walk = list.map((c) => c.walkBlockedByDiggable).filter(isFn);
192
+ /* REVERSE load order for both of the last-answer folds, so the mod the player
193
+ * moved to the bottom of the list is the one that gets asked first and
194
+ * therefore the one that decides. Reversed here, once, rather than by walking
195
+ * the array backwards at call time: the reversal is a property of the FOLD, and
196
+ * a `for (let i = n - 1; ...)` in the hot path is the kind of detail a later
197
+ * edit quietly straightens out. */
198
+ const walk = list.map((c) => c.walkBlockedByDiggable).filter(isFn).reverse();
168
199
  if (walk.length > 0) {
169
200
  out.walkBlockedByDiggable = (state, grid, deps) => {
201
+ /* Declining is contractually free of observable effect (see the member's
202
+ * doc), so asking a mod that then declines costs nothing - which is what
203
+ * makes it safe to ask them in any order at all. */
170
204
  for (const fn of walk) {
171
205
  const energy = fn(state, grid, deps);
172
206
  if (energy !== null)
@@ -175,9 +209,12 @@ export function composeModHooks(contributions) {
175
209
  return null;
176
210
  };
177
211
  }
178
- const tiebreak = list.map((c) => c.objectListTiebreak).filter(isFn);
212
+ const tiebreak = list.map((c) => c.objectListTiebreak).filter(isFn).reverse();
179
213
  if (tiebreak.length > 0) {
180
214
  out.objectListTiebreak = (a, b) => {
215
+ /* The last mod's comparator is the primary sort key and the earlier ones
216
+ * only break the ties it leaves equal - a lexicographic chain, still a
217
+ * consistent total order, and "the later mod wins" for a comparator. */
181
218
  for (const fn of tiebreak) {
182
219
  const r = fn(a, b);
183
220
  if (r !== 0)
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/mod/hooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/mod/hooks.ts"],"names":[],"mappings":"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"}
@@ -128,7 +128,7 @@ export type OrphanStore = Record<string, OrphanEntry[]>;
128
128
  * core pack outright, and versionMap's versions are only ever used to key
129
129
  * quarantined MOD entities. An existing save keeps whatever it recorded.
130
130
  */
131
- export declare const CORE_PACK_VERSION = "0.12.0";
131
+ export declare const CORE_PACK_VERSION = "0.14.0";
132
132
  /** A core-only manifest: the base game with no mods, deterministic. */
133
133
  export declare function coreOnlyManifest(): SaveManifest;
134
134
  /**
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/mod/vocabulary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"vocabulary.js","sourceRoot":"","sources":["../../src/mod/vocabulary.ts"],"names":[],"mappings":"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"}
package/dist/version.d.ts CHANGED
@@ -21,5 +21,5 @@ export declare const PARITY_BASELINE = "4.2.6";
21
21
  * released tag is iterated takes a MINOR bump, because a published tag is pinned
22
22
  * by digest in a catalogue and must never be moved.
23
23
  */
24
- export declare const ENGINE_VERSION = "0.12.0";
24
+ export declare const ENGINE_VERSION = "0.14.0";
25
25
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -21,5 +21,5 @@ export const PARITY_BASELINE = "4.2.6";
21
21
  * released tag is iterated takes a MINOR bump, because a published tag is pinned
22
22
  * by digest in a catalogue and must never be moved.
23
23
  */
24
- export const ENGINE_VERSION = "0.12.0";
24
+ export const ENGINE_VERSION = "0.14.0";
25
25
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgm-tools/neo-angband-core",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Headless Neo Angband game engine: rules, world, entities, effects, generation",
5
5
  "license": "GPL-2.0-only",
6
6
  "author": "neostryder (RPGM Tools)",
package/src/mod/hooks.ts CHANGED
@@ -1,402 +1,468 @@
1
- /**
2
- * The behaviour seam: the closed set of points where a mod may change what the
3
- * ENGINE does, as opposed to what content it runs on.
4
- *
5
- * WHY THIS EXISTS
6
- *
7
- * Content mods are already fully expressible - a pack contributes records and
8
- * composeContentPacks merges them, and core never learns a mod was involved.
9
- * Behaviour is different. A fix like "don't log a duplicate unique kill" is not a
10
- * record; it is a decision taken inside a function, and no amount of data can
11
- * reach inside that function from outside. So the first cut of the bundled
12
- * bug-fixes / QoL mods did the only thing that was available: the FIX LIVED IN
13
- * CORE behind `if (modRuleEnabled(state, "bugfix.x"))`.
14
- *
15
- * That was wrong, and neostryder named exactly why (2026-07-29): "the whole point of
16
- * making them mods was to exclude them from the core game." A flag-gated fix is
17
- * not excluded from core. It ships in core, it is tested in core, and core
18
- * carries the string "bugfix.uniqueKillHistory" - so core knows the mod's name.
19
- * Deleting the mod folder would not delete one line of it.
20
- *
21
- * WHAT THIS CHANGES, AND WHAT IT HONESTLY DOES NOT
22
- *
23
- * After this, core contains no mod's fix and no mod's name: no `bugfix.*` or
24
- * `qol.*` literal, no staircase repair, no message rewriter. Each fix body lives
25
- * in its mod's own folder and arrives as that mod's code.
26
- *
27
- * What core still contains - and cannot stop containing - is the SEAM ITSELF:
28
- * one named, documented extension point per behaviour a mod may override. That
29
- * is not a residue of the bundled mods; it is the price of behaviour modding at
30
- * all, and it is the same bargain SKSE, Forge and every other real modding
31
- * system strikes. The difference from a flag read is the difference that matters:
32
- * a seam is generic and available to ANY mod, and it holds no opinion about what
33
- * should plug into it, whereas `if (modRuleEnabled(state, "bugfix.stairsReachable"))`
34
- * is core implementing one specific mod.
35
- *
36
- * SHAPE
37
- *
38
- * A plain interface of optional functions rather than a string-keyed registry,
39
- * for three reasons:
40
- *
41
- * - It is typed end to end. A registry keyed by strings pushes every payload
42
- * through `unknown` and moves the errors to runtime.
43
- * - "A disabled mod's patches DO NOT EXIST" becomes literally true, not merely
44
- * false-y: with no mod loaded the field is ABSENT, the optional call is not
45
- * made, and the faithful path is the only path that was ever compiled into
46
- * the branch. No flag map is consulted because there is no flag map.
47
- * - It costs nothing. `state.modHooks?.historyAdd?.(...)` on an absent object is
48
- * one undefined check, not a map lookup per call.
49
- *
50
- * COMPOSITION IS THE HOST'S JOB
51
- *
52
- * Two enabled mods may both want the same hook. Core deliberately holds ONE
53
- * ModHooks and knows nothing about mod identity, ordering or enablement - the
54
- * host (web/cli) collects each enabled mod's contributions in load order and
55
- * folds them into a single object (see composeModHooks). This is the same
56
- * layering as content: core consumes a composed result and never the pack list.
57
- *
58
- * A hook is arbitrary third-party code running inside a turn, so it can throw.
59
- * guardModHooks wraps one mod's contribution and turns a throw into that hook's
60
- * neutral answer - see its comment for why that is the least-bad of the three
61
- * options, and for the half of the job it deliberately leaves to the host.
62
- *
63
- * DETERMINISM
64
- *
65
- * Two of these run inside the generation and object pipelines, where an extra
66
- * RNG draw does not merely change a value, it desynchronises every draw after
67
- * it - so a seed would stop meaning the same level. Those hooks are documented
68
- * RNG-FREE and are given no rng to draw from. That is a contract a mod can still
69
- * break by reaching for a global; the test suite pins it by running generation
70
- * with a hook installed and asserting the level is bit-identical.
71
- */
72
-
73
- import type { GameState } from "../game/context.js";
74
-
75
- /**
76
- * A mod's behaviour contributions. Every member is optional; an absent member
77
- * means "this mod does not touch that point" and core takes its faithful path.
78
- *
79
- * Each entry documents the ONE core call site it serves, because a hook whose
80
- * call site is not written down is a hook nobody can verify is still wired -
81
- * exactly the failure the call-site census exists to catch.
82
- */
83
- export interface ModHooks {
84
- /**
85
- * A walk into a grid the player could tunnel through (cave-cmd.ts,
86
- * movementAutoDig's call site).
87
- *
88
- * Return the energy the move should cost, or null to decline - and declining
89
- * MUST be free of observable effect, because faithful core's behaviour here is
90
- * to bump into the wall without drawing any RNG. A hook that rolls a dig check
91
- * and then returns null has already moved the RNG stream.
92
- *
93
- * `deps` is the live CaveCmdDeps the session built, passed through because the
94
- * work a mod would want to do here (tunnelAux - one real dig attempt with the
95
- * upstream roll and payouts) needs dependencies only the session can construct.
96
- * Typed as unknown so this seam does not drag the cave-command types into every
97
- * consumer of ModHooks; a mod casts it back to CaveCmdDeps, which core exports.
98
- *
99
- * Serves: the QoL mod's "auto-dig on walk".
100
- */
101
- walkBlockedByDiggable?: (
102
- state: GameState,
103
- grid: { y: number; x: number },
104
- deps: unknown,
105
- ) => number | null;
106
-
107
- /**
108
- * Two floor-list entries that compared exactly equal, including on distance
109
- * (obj-list.ts, the comparator's final tiebreak).
110
- *
111
- * Return a negative/positive number to order them, or 0 to leave them equal
112
- * (which is what faithful core does - it keeps collect order via a stable
113
- * sort). Must be a consistent total order or the sort is meaningless.
114
- *
115
- * Serves: the bug-fixes mod's strict object-list ordering (#4664).
116
- */
117
- objectListTiebreak?: (
118
- a: { readonly dy: number; readonly dx: number },
119
- b: { readonly dy: number; readonly dx: number },
120
- ) => number;
121
-
122
- /**
123
- * A finished, otherwise-accepted level, before cave_generate returns it
124
- * (generate.ts, the accept branch).
125
- *
126
- * Return false to REJECT the level, which makes cave_generate re-roll exactly
127
- * as it does for a monster-maximum overflow. May mutate the level to repair it
128
- * and then return true.
129
- *
130
- * RNG-FREE: no rng is passed and none may be reached. A draw here shifts every
131
- * subsequent draw and a seed stops reproducing its dungeon.
132
- *
133
- * Serves: the bug-fixes mod's reachable-staircase repair.
134
- */
135
- levelGenerated?: (gen: unknown, quest: boolean) => boolean;
136
-
137
- /**
138
- * An object about to be committed as an artifact, when it ALREADY carries one
139
- * (obj/make.ts, the `obj.artifact` branch).
140
- *
141
- * Return false to refuse the commit; the caller then clears the artifact and
142
- * reports failure. Faithful core commits it unconditionally.
143
- *
144
- * RNG-FREE, for the same reason as levelGenerated: this runs inside object
145
- * creation, on the main stream.
146
- *
147
- * Serves: the bug-fixes mod's duplicate-artifact guard (#4510).
148
- */
149
- artifactCommit?: (aidx: number, alreadyCreated: boolean) => boolean;
150
-
151
- /**
152
- * A character-history entry about to be written (session/game.ts and any
153
- * other historyAdd call site that passes its context).
154
- *
155
- * Return false to suppress the entry. Faithful core writes every entry it
156
- * reaches, duplicates included.
157
- *
158
- * Serves: the bug-fixes mod's no-duplicate-unique-kill fix (#4245).
159
- */
160
- historyAdd?: (entry: { readonly what: string; readonly type: number; readonly duplicate: boolean }) => boolean;
161
-
162
- /**
163
- * Whether the noise and scent heatmaps belong in the save (session/save.ts,
164
- * snapshotSquares' argument).
165
- *
166
- * Return true to persist them. Faithful core omits them, which is the upstream
167
- * behaviour and the upstream bug.
168
- *
169
- * Serves: the bug-fixes mod's noise/scent persistence (#4605).
170
- */
171
- saveNoiseScent?: () => boolean;
172
-
173
- /**
174
- * Player-visible message text, on its way to the message line (the host's
175
- * message sink).
176
- *
177
- * Return the text to show. Faithful core shows what it was given, warts and
178
- * all. A hook here can only RESTATE a message - it must never change what a
179
- * message means, or the port would be showing text upstream never wrote and no
180
- * census could see it.
181
- *
182
- * Serves: the bug-fixes mod's cosmetic string corrections.
183
- */
184
- messageText?: (raw: string) => string;
185
- }
186
-
187
- /**
188
- * Fold several mods' contributions into the single ModHooks core holds.
189
- *
190
- * Order is LOAD order, and what "later wins" means differs per hook because the
191
- * hooks differ in kind - which is precisely why this is one written-down function
192
- * and not a generic merge:
193
- *
194
- * - VETO hooks (levelGenerated, artifactCommit, historyAdd) are conjunctive:
195
- * every contributor must agree, and the first refusal decides. This is the
196
- * only safe fold - a mod that vetoes a duplicate artifact must not be
197
- * overruled by a later mod that merely has no opinion.
198
- * - TRANSFORM hooks (messageText) compose in order, each seeing the previous
199
- * one's output.
200
- * - FIRST-HANDLER hooks (walkBlockedByDiggable) stop at the first non-null, so
201
- * an earlier mod's handling wins and a later one cannot double-spend energy.
202
- * - ANY hooks (saveNoiseScent) are disjunctive: one mod asking for the data is
203
- * enough, because the data is additive and a second mod cannot object.
204
- * - ORDERING hooks (objectListTiebreak) stop at the first non-zero answer, the
205
- * same way a lexicographic comparator chains.
206
- *
207
- * Returns undefined when nothing contributed, so the caller can leave the field
208
- * absent rather than storing an empty object - keeping "no mod loaded" and "a
209
- * mod loaded that touches nothing" indistinguishable from core's side.
210
- */
211
- /**
212
- * A mod's hook function threw. Reported to whoever guarded it (guardModHooks).
213
- *
214
- * Carries no mod id, because core does not know one - the host wraps ONE mod's
215
- * contribution at a time and already holds the id in the closure it passes in.
216
- */
217
- export interface ModHookFault {
218
- /** Which extension point threw, by its ModHooks member name. */
219
- readonly hook: keyof ModHooks;
220
- /** Whatever the mod threw, unwrapped and uninterpreted. */
221
- readonly error: unknown;
222
- }
223
-
224
- /**
225
- * ONE mod's contribution, with every hook wrapped so a throw cannot escape into
226
- * the middle of a turn.
227
- *
228
- * WHY THIS IS NOT "CATCH AND CARRY ON REGARDLESS". An uncaught throw here lands
229
- * in the middle of core - halfway through a move, a generation pass or an object
230
- * roll - and unwinds the rest of the turn. It does not undo what the mod already
231
- * did before it threw, so the state is no LESS inconsistent for the abort; it is
232
- * more, because the turn's remaining bookkeeping never ran. And it reaches the
233
- * host as a bare exception from a function the host did not know a mod was
234
- * inside, so the player gets a frozen screen and no name.
235
- *
236
- * So a throwing hook is treated as a mod that DECLINED to answer at that point,
237
- * and the neutral answer is per-hook - it has to be, because "no opinion" is
238
- * `true` for a veto, `null` for a handler, `0` for a comparator and the input
239
- * itself for a transform. Those values live HERE, next to the fold rule in
240
- * composeModHooks that already spells out each hook's kind, rather than in the
241
- * host: two copies of "what does this hook mean by nothing" is the pair that
242
- * drifts.
243
- *
244
- * WHAT THIS DOES NOT DO, deliberately: it does not make the session safe. The
245
- * mod ran arbitrary code against live state and stopped partway. Neutralising
246
- * the hook keeps the game on its feet long enough to say so; it is the HOST's
247
- * job to treat the fault as terminal for the session - stop writing saves and
248
- * tell the player to reload (packages/web/src/mod-taint.ts). A guard without
249
- * that second half would be the worst outcome of the three: quietly wrong.
250
- *
251
- * LATCHED PER HOOK. Once a hook has thrown it is not called again for the rest
252
- * of the session. A hook that throws on a bad monster will throw on the next
253
- * one too, and an exception per call turns one fault into a storm of them; more
254
- * importantly, "sometimes the mod's rule applies and sometimes it doesn't" is
255
- * harder to reason about than "it stopped applying at the point it broke". The
256
- * mod's other hooks are untouched - the latch is per (mod, hook), not per mod.
257
- */
258
- export function guardModHooks(
259
- hooks: ModHooks,
260
- onFault: (fault: ModHookFault) => void,
261
- ): ModHooks {
262
- const out: ModHooks = {};
263
- const stopped = new Set<keyof ModHooks>();
264
-
265
- function guard<T>(hook: keyof ModHooks, run: () => T, neutral: T): T {
266
- if (stopped.has(hook)) return neutral;
267
- try {
268
- return run();
269
- } catch (error) {
270
- stopped.add(hook);
271
- onFault({ hook, error });
272
- return neutral;
273
- }
274
- }
275
-
276
- const walk = hooks.walkBlockedByDiggable;
277
- if (walk) {
278
- /* null is DECLINE, so core bumps the wall as it would with no mod loaded. */
279
- out.walkBlockedByDiggable = (state, grid, deps): number | null =>
280
- guard("walkBlockedByDiggable", () => walk(state, grid, deps), null);
281
- }
282
-
283
- const tiebreak = hooks.objectListTiebreak;
284
- if (tiebreak) {
285
- /* 0 is "still equal", which is exactly faithful core's answer. */
286
- out.objectListTiebreak = (a, b): number =>
287
- guard("objectListTiebreak", () => tiebreak(a, b), 0);
288
- }
289
-
290
- const level = hooks.levelGenerated;
291
- if (level) {
292
- /* ACCEPT. Rejecting on a throw would re-roll the level, and cave_generate
293
- * would then re-roll it again on the next throw, and so on until it gives
294
- * up - one broken hook would make the game unable to reach any level. */
295
- out.levelGenerated = (gen, quest): boolean =>
296
- guard("levelGenerated", () => level(gen, quest), true);
297
- }
298
-
299
- const artifact = hooks.artifactCommit;
300
- if (artifact) {
301
- /* COMMIT, which is what core does unconditionally without the hook. */
302
- out.artifactCommit = (aidx, alreadyCreated): boolean =>
303
- guard("artifactCommit", () => artifact(aidx, alreadyCreated), true);
304
- }
305
-
306
- const history = hooks.historyAdd;
307
- if (history) {
308
- /* WRITE the entry: faithful core writes every entry it reaches. Suppressing
309
- * on a throw would delete history the player earned. */
310
- out.historyAdd = (entry): boolean =>
311
- guard("historyAdd", () => history(entry), true);
312
- }
313
-
314
- const noise = hooks.saveNoiseScent;
315
- if (noise) {
316
- /* OMIT them, the upstream behaviour. */
317
- out.saveNoiseScent = (): boolean => guard("saveNoiseScent", () => noise(), false);
318
- }
319
-
320
- const text = hooks.messageText;
321
- if (text) {
322
- /* The raw message, unrestated - never an empty string, which would silently
323
- * eat a message the player needed to read. */
324
- out.messageText = (raw): string => guard("messageText", () => text(raw), raw);
325
- }
326
-
327
- return out;
328
- }
329
-
330
- export function composeModHooks(
331
- contributions: readonly ModHooks[],
332
- ): ModHooks | undefined {
333
- const list = contributions.filter((c) => Object.keys(c).length > 0);
334
- if (list.length === 0) return undefined;
335
-
336
- const out: ModHooks = {};
337
-
338
- const walk = list.map((c) => c.walkBlockedByDiggable).filter(isFn);
339
- if (walk.length > 0) {
340
- out.walkBlockedByDiggable = (state, grid, deps): number | null => {
341
- for (const fn of walk) {
342
- const energy = fn(state, grid, deps);
343
- if (energy !== null) return energy;
344
- }
345
- return null;
346
- };
347
- }
348
-
349
- const tiebreak = list.map((c) => c.objectListTiebreak).filter(isFn);
350
- if (tiebreak.length > 0) {
351
- out.objectListTiebreak = (a, b): number => {
352
- for (const fn of tiebreak) {
353
- const r = fn(a, b);
354
- if (r !== 0) return r;
355
- }
356
- return 0;
357
- };
358
- }
359
-
360
- const level = list.map((c) => c.levelGenerated).filter(isFn);
361
- if (level.length > 0) {
362
- out.levelGenerated = (gen, quest): boolean => {
363
- /* Every contributor runs even after one has repaired the level, because a
364
- * second mod's invariant is not satisfied by the first mod's repair. The
365
- * first REFUSAL short-circuits, since the level is being thrown away. */
366
- for (const fn of level) if (!fn(gen, quest)) return false;
367
- return true;
368
- };
369
- }
370
-
371
- const artifact = list.map((c) => c.artifactCommit).filter(isFn);
372
- if (artifact.length > 0) {
373
- out.artifactCommit = (aidx, alreadyCreated): boolean => {
374
- for (const fn of artifact) if (!fn(aidx, alreadyCreated)) return false;
375
- return true;
376
- };
377
- }
378
-
379
- const history = list.map((c) => c.historyAdd).filter(isFn);
380
- if (history.length > 0) {
381
- out.historyAdd = (entry): boolean => {
382
- for (const fn of history) if (!fn(entry)) return false;
383
- return true;
384
- };
385
- }
386
-
387
- const noise = list.map((c) => c.saveNoiseScent).filter(isFn);
388
- if (noise.length > 0) {
389
- out.saveNoiseScent = (): boolean => noise.some((fn) => fn());
390
- }
391
-
392
- const text = list.map((c) => c.messageText).filter(isFn);
393
- if (text.length > 0) {
394
- out.messageText = (raw): string => text.reduce((s, fn) => fn(s), raw);
395
- }
396
-
397
- return out;
398
- }
399
-
400
- function isFn<T>(v: T | undefined): v is T {
401
- return typeof v === "function";
402
- }
1
+ /**
2
+ * The behaviour seam: the closed set of points where a mod may change what the
3
+ * ENGINE does, as opposed to what content it runs on.
4
+ *
5
+ * WHY THIS EXISTS
6
+ *
7
+ * Content mods are already fully expressible - a pack contributes records and
8
+ * composeContentPacks merges them, and core never learns a mod was involved.
9
+ * Behaviour is different. A fix like "don't log a duplicate unique kill" is not a
10
+ * record; it is a decision taken inside a function, and no amount of data can
11
+ * reach inside that function from outside. So the first cut of the bundled
12
+ * bug-fixes / QoL mods did the only thing that was available: the FIX LIVED IN
13
+ * CORE behind `if (modRuleEnabled(state, "bugfix.x"))`.
14
+ *
15
+ * That was wrong, and neostryder named exactly why (2026-07-29): "the whole point of
16
+ * making them mods was to exclude them from the core game." A flag-gated fix is
17
+ * not excluded from core. It ships in core, it is tested in core, and core
18
+ * carries the string "bugfix.uniqueKillHistory" - so core knows the mod's name.
19
+ * Deleting the mod folder would not delete one line of it.
20
+ *
21
+ * WHAT THIS CHANGES, AND WHAT IT HONESTLY DOES NOT
22
+ *
23
+ * After this, core contains no mod's fix and no mod's name: no `bugfix.*` or
24
+ * `qol.*` literal, no staircase repair, no message rewriter. Each fix body lives
25
+ * in its mod's own folder and arrives as that mod's code.
26
+ *
27
+ * What core still contains - and cannot stop containing - is the SEAM ITSELF:
28
+ * one named, documented extension point per behaviour a mod may override. That
29
+ * is not a residue of the bundled mods; it is the price of behaviour modding at
30
+ * all, and it is the same bargain SKSE, Forge and every other real modding
31
+ * system strikes. The difference from a flag read is the difference that matters:
32
+ * a seam is generic and available to ANY mod, and it holds no opinion about what
33
+ * should plug into it, whereas `if (modRuleEnabled(state, "bugfix.stairsReachable"))`
34
+ * is core implementing one specific mod.
35
+ *
36
+ * SHAPE
37
+ *
38
+ * A plain interface of optional functions rather than a string-keyed registry,
39
+ * for three reasons:
40
+ *
41
+ * - It is typed end to end. A registry keyed by strings pushes every payload
42
+ * through `unknown` and moves the errors to runtime.
43
+ * - "A disabled mod's patches DO NOT EXIST" becomes literally true, not merely
44
+ * false-y: with no mod loaded the field is ABSENT, the optional call is not
45
+ * made, and the faithful path is the only path that was ever compiled into
46
+ * the branch. No flag map is consulted because there is no flag map.
47
+ * - It costs nothing. `state.modHooks?.historyAdd?.(...)` on an absent object is
48
+ * one undefined check, not a map lookup per call.
49
+ *
50
+ * COMPOSITION IS THE HOST'S JOB
51
+ *
52
+ * Two enabled mods may both want the same hook. Core deliberately holds ONE
53
+ * ModHooks and knows nothing about mod identity, ordering or enablement - the
54
+ * host (web/cli) collects each enabled mod's contributions in load order and
55
+ * folds them into a single object (see composeModHooks). This is the same
56
+ * layering as content: core consumes a composed result and never the pack list.
57
+ *
58
+ * A hook is arbitrary third-party code running inside a turn, so it can throw.
59
+ * guardModHooks wraps one mod's contribution and turns a throw into that hook's
60
+ * neutral answer - see its comment for why that is the least-bad of the three
61
+ * options, and for the half of the job it deliberately leaves to the host.
62
+ *
63
+ * DETERMINISM
64
+ *
65
+ * Two of these run inside the generation and object pipelines, where an extra
66
+ * RNG draw does not merely change a value, it desynchronises every draw after
67
+ * it - so a seed would stop meaning the same level. Those hooks are documented
68
+ * RNG-FREE and are given no rng to draw from. That is a contract a mod can still
69
+ * break by reaching for a global; the test suite pins it by running generation
70
+ * with a hook installed and asserting the level is bit-identical.
71
+ */
72
+
73
+ import type { GameState } from "../game/context.js";
74
+
75
+ /**
76
+ * A mod's behaviour contributions. Every member is optional; an absent member
77
+ * means "this mod does not touch that point" and core takes its faithful path.
78
+ *
79
+ * Each entry documents the ONE core call site it serves, because a hook whose
80
+ * call site is not written down is a hook nobody can verify is still wired -
81
+ * exactly the failure the call-site census exists to catch.
82
+ */
83
+ export interface ModHooks {
84
+ /**
85
+ * A walk into a grid the player could tunnel through (cave-cmd.ts,
86
+ * movementAutoDig's call site).
87
+ *
88
+ * Return the energy the move should cost, or null to decline - and declining
89
+ * MUST be free of observable effect, because faithful core's behaviour here is
90
+ * to bump into the wall without drawing any RNG. A hook that rolls a dig check
91
+ * and then returns null has already moved the RNG stream.
92
+ *
93
+ * `deps` is the live CaveCmdDeps the session built, passed through because the
94
+ * work a mod would want to do here (tunnelAux - one real dig attempt with the
95
+ * upstream roll and payouts) needs dependencies only the session can construct.
96
+ * Typed as unknown so this seam does not drag the cave-command types into every
97
+ * consumer of ModHooks; a mod casts it back to CaveCmdDeps, which core exports.
98
+ *
99
+ * Serves: the QoL mod's "auto-dig on walk".
100
+ */
101
+ walkBlockedByDiggable?: (
102
+ state: GameState,
103
+ grid: { y: number; x: number },
104
+ deps: unknown,
105
+ ) => number | null;
106
+
107
+ /**
108
+ * Two floor-list entries that compared exactly equal, including on distance
109
+ * (obj-list.ts, the comparator's final tiebreak).
110
+ *
111
+ * Return a negative/positive number to order them, or 0 to leave them equal
112
+ * (which is what faithful core does - it keeps collect order via a stable
113
+ * sort). Must be a consistent total order or the sort is meaningless.
114
+ *
115
+ * Serves: the bug-fixes mod's strict object-list ordering (#4664).
116
+ */
117
+ objectListTiebreak?: (
118
+ a: { readonly dy: number; readonly dx: number },
119
+ b: { readonly dy: number; readonly dx: number },
120
+ ) => number;
121
+
122
+ /**
123
+ * A finished, otherwise-accepted level, before cave_generate returns it
124
+ * (generate.ts, the accept branch).
125
+ *
126
+ * Return false to REJECT the level, which makes cave_generate re-roll exactly
127
+ * as it does for a monster-maximum overflow. May mutate the level to repair it
128
+ * and then return true.
129
+ *
130
+ * RNG-FREE: no rng is passed and none may be reached. A draw here shifts every
131
+ * subsequent draw and a seed stops reproducing its dungeon.
132
+ *
133
+ * Serves: the bug-fixes mod's reachable-staircase repair.
134
+ */
135
+ levelGenerated?: (gen: unknown, quest: boolean) => boolean;
136
+
137
+ /**
138
+ * An object about to be committed as an artifact, when it ALREADY carries one
139
+ * (obj/make.ts, the `obj.artifact` branch).
140
+ *
141
+ * Return false to refuse the commit; the caller then clears the artifact and
142
+ * reports failure. Faithful core commits it unconditionally.
143
+ *
144
+ * RNG-FREE, for the same reason as levelGenerated: this runs inside object
145
+ * creation, on the main stream.
146
+ *
147
+ * Serves: the bug-fixes mod's duplicate-artifact guard (#4510).
148
+ */
149
+ artifactCommit?: (aidx: number, alreadyCreated: boolean) => boolean;
150
+
151
+ /**
152
+ * A character-history entry about to be written (session/game.ts and any
153
+ * other historyAdd call site that passes its context).
154
+ *
155
+ * Return false to suppress the entry. Faithful core writes every entry it
156
+ * reaches, duplicates included.
157
+ *
158
+ * Serves: the bug-fixes mod's no-duplicate-unique-kill fix (#4245).
159
+ */
160
+ historyAdd?: (entry: { readonly what: string; readonly type: number; readonly duplicate: boolean }) => boolean;
161
+
162
+ /**
163
+ * Whether the noise and scent heatmaps belong in the save (session/save.ts,
164
+ * snapshotSquares' argument).
165
+ *
166
+ * Return true to persist them. Faithful core omits them, which is the upstream
167
+ * behaviour and the upstream bug.
168
+ *
169
+ * Serves: the bug-fixes mod's noise/scent persistence (#4605).
170
+ */
171
+ saveNoiseScent?: () => boolean;
172
+
173
+ /**
174
+ * Player-visible message text, on its way to the message line (the host's
175
+ * message sink).
176
+ *
177
+ * Return the text to show. Faithful core shows what it was given, warts and
178
+ * all. A hook here can only RESTATE a message - it must never change what a
179
+ * message means, or the port would be showing text upstream never wrote and no
180
+ * census could see it.
181
+ *
182
+ * Serves: the bug-fixes mod's cosmetic string corrections.
183
+ */
184
+ messageText?: (raw: string) => string;
185
+ }
186
+
187
+ /**
188
+ * Fold several mods' contributions into the single ModHooks core holds.
189
+ *
190
+ * Order is LOAD order, and THE LATER MOD WINS EVERY DISAGREEMENT. That is the
191
+ * rule the mod manager's own row promises the player ("Move later (loads last,
192
+ * wins conflicts)") and the rule every other composition layer follows, so this
193
+ * one obeys it too - see the note below for the two folds that look like
194
+ * exceptions and are not.
195
+ *
196
+ * - LAST-HANDLER hooks (walkBlockedByDiggable) are asked in REVERSE load order
197
+ * and stop at the first non-null, so the last mod to have an opinion is the
198
+ * one whose handling takes effect and no earlier mod can double-spend the
199
+ * energy it already paid out.
200
+ * - ORDERING hooks (objectListTiebreak) chain the same way round: the last
201
+ * mod's comparator is the primary key and earlier ones break the ties it
202
+ * leaves, which is a valid total order and is "later wins" for a comparator.
203
+ * - TRANSFORM hooks (messageText) compose in load order, each seeing the
204
+ * previous one's output - so the last mod still speaks last and has the final
205
+ * say over the text that reaches the player.
206
+ * - VETO hooks (levelGenerated, artifactCommit, historyAdd) are conjunctive:
207
+ * every contributor runs and any refusal decides.
208
+ * - ANY hooks (saveNoiseScent) are disjunctive: one mod asking for the data is
209
+ * enough, because the data is additive and a second mod cannot object.
210
+ *
211
+ * WHY THE LAST TWO ARE NOT EXCEPTIONS. "Later wins" answers the question "two
212
+ * mods disagree about one thing - whose answer is used?", and a veto hook is not
213
+ * asking that question. `true` from historyAdd means "I have nothing to say
214
+ * about this entry", not "I insist it be written": the hook is called once per
215
+ * entry, and two mods suppressing two different things are not in conflict at
216
+ * all. Resolving it last-wins would mean a later mod's silence cancelled an
217
+ * earlier mod's rule, breaking BOTH mods to satisfy a consistency nobody asked
218
+ * for. Same for saveNoiseScent, where `false` is "I do not need this" and the
219
+ * data is additive. The consistency that matters is that no mod's opinion is
220
+ * ever DISCARDED in favour of an earlier one, and these two discard nothing.
221
+ *
222
+ * Returns undefined when nothing contributed, so the caller can leave the field
223
+ * absent rather than storing an empty object - keeping "no mod loaded" and "a
224
+ * mod loaded that touches nothing" indistinguishable from core's side.
225
+ */
226
+ /**
227
+ * How composeModHooks folds several mods' contributions to one hook.
228
+ *
229
+ * A subset of the vocabulary the pack tooling uses for every composition layer
230
+ * (mod-sdk's `Fold`), so the host can hand these values straight to the conflict
231
+ * report and the compiler checks the two agree.
232
+ */
233
+ export type ModHookFold =
234
+ | "all-must-agree" // every contributor runs; the first refusal decides
235
+ | "chained" // each sees the previous one's output, so the last one speaks last
236
+ | "last-answer" // the LAST contributor with an opinion decides; earlier ones are not asked
237
+ | "any-yes"; // one contributor asking for it is enough
238
+
239
+ /**
240
+ * WHICH FOLD EACH HOOK USES, next to the function that implements it.
241
+ *
242
+ * The conflict report needs this to tell the player whether two mods touching
243
+ * one hook COMBINE or whether one of them is being silently ignored. Every fold
244
+ * here obeys "the later mod wins"; what differs is whether there is anything for
245
+ * a winner to win, and only `last-answer` leaves a contribution unrun. That
246
+ * distinction is the whole reason the report names the fold instead of printing
247
+ * one sentence about load order.
248
+ *
249
+ * It lives in core, beside composeModHooks, because a second copy in the host is
250
+ * the shape that drifts: the host is where the report is rendered, so a hook
251
+ * added here and forgotten there would be described by whatever the table
252
+ * happened to default to. Keyed by `keyof ModHooks`, so adding a member to the
253
+ * interface without adding it here does not compile.
254
+ */
255
+ export const MOD_HOOK_FOLDS: Readonly<Record<keyof ModHooks, ModHookFold>> = {
256
+ walkBlockedByDiggable: "last-answer",
257
+ objectListTiebreak: "last-answer",
258
+ levelGenerated: "all-must-agree",
259
+ artifactCommit: "all-must-agree",
260
+ historyAdd: "all-must-agree",
261
+ saveNoiseScent: "any-yes",
262
+ messageText: "chained",
263
+ };
264
+
265
+ /**
266
+ * A mod's hook function threw. Reported to whoever guarded it (guardModHooks).
267
+ *
268
+ * Carries no mod id, because core does not know one - the host wraps ONE mod's
269
+ * contribution at a time and already holds the id in the closure it passes in.
270
+ */
271
+ export interface ModHookFault {
272
+ /** Which extension point threw, by its ModHooks member name. */
273
+ readonly hook: keyof ModHooks;
274
+ /** Whatever the mod threw, unwrapped and uninterpreted. */
275
+ readonly error: unknown;
276
+ }
277
+
278
+ /**
279
+ * ONE mod's contribution, with every hook wrapped so a throw cannot escape into
280
+ * the middle of a turn.
281
+ *
282
+ * WHY THIS IS NOT "CATCH AND CARRY ON REGARDLESS". An uncaught throw here lands
283
+ * in the middle of core - halfway through a move, a generation pass or an object
284
+ * roll - and unwinds the rest of the turn. It does not undo what the mod already
285
+ * did before it threw, so the state is no LESS inconsistent for the abort; it is
286
+ * more, because the turn's remaining bookkeeping never ran. And it reaches the
287
+ * host as a bare exception from a function the host did not know a mod was
288
+ * inside, so the player gets a frozen screen and no name.
289
+ *
290
+ * So a throwing hook is treated as a mod that DECLINED to answer at that point,
291
+ * and the neutral answer is per-hook - it has to be, because "no opinion" is
292
+ * `true` for a veto, `null` for a handler, `0` for a comparator and the input
293
+ * itself for a transform. Those values live HERE, next to the fold rule in
294
+ * composeModHooks that already spells out each hook's kind, rather than in the
295
+ * host: two copies of "what does this hook mean by nothing" is the pair that
296
+ * drifts.
297
+ *
298
+ * WHAT THIS DOES NOT DO, deliberately: it does not make the session safe. The
299
+ * mod ran arbitrary code against live state and stopped partway. Neutralising
300
+ * the hook keeps the game on its feet long enough to say so; it is the HOST's
301
+ * job to treat the fault as terminal for the session - stop writing saves and
302
+ * tell the player to reload (packages/web/src/mod-taint.ts). A guard without
303
+ * that second half would be the worst outcome of the three: quietly wrong.
304
+ *
305
+ * LATCHED PER HOOK. Once a hook has thrown it is not called again for the rest
306
+ * of the session. A hook that throws on a bad monster will throw on the next
307
+ * one too, and an exception per call turns one fault into a storm of them; more
308
+ * importantly, "sometimes the mod's rule applies and sometimes it doesn't" is
309
+ * harder to reason about than "it stopped applying at the point it broke". The
310
+ * mod's other hooks are untouched - the latch is per (mod, hook), not per mod.
311
+ */
312
+ export function guardModHooks(
313
+ hooks: ModHooks,
314
+ onFault: (fault: ModHookFault) => void,
315
+ ): ModHooks {
316
+ const out: ModHooks = {};
317
+ const stopped = new Set<keyof ModHooks>();
318
+
319
+ function guard<T>(hook: keyof ModHooks, run: () => T, neutral: T): T {
320
+ if (stopped.has(hook)) return neutral;
321
+ try {
322
+ return run();
323
+ } catch (error) {
324
+ stopped.add(hook);
325
+ onFault({ hook, error });
326
+ return neutral;
327
+ }
328
+ }
329
+
330
+ const walk = hooks.walkBlockedByDiggable;
331
+ if (walk) {
332
+ /* null is DECLINE, so core bumps the wall as it would with no mod loaded. */
333
+ out.walkBlockedByDiggable = (state, grid, deps): number | null =>
334
+ guard("walkBlockedByDiggable", () => walk(state, grid, deps), null);
335
+ }
336
+
337
+ const tiebreak = hooks.objectListTiebreak;
338
+ if (tiebreak) {
339
+ /* 0 is "still equal", which is exactly faithful core's answer. */
340
+ out.objectListTiebreak = (a, b): number =>
341
+ guard("objectListTiebreak", () => tiebreak(a, b), 0);
342
+ }
343
+
344
+ const level = hooks.levelGenerated;
345
+ if (level) {
346
+ /* ACCEPT. Rejecting on a throw would re-roll the level, and cave_generate
347
+ * would then re-roll it again on the next throw, and so on until it gives
348
+ * up - one broken hook would make the game unable to reach any level. */
349
+ out.levelGenerated = (gen, quest): boolean =>
350
+ guard("levelGenerated", () => level(gen, quest), true);
351
+ }
352
+
353
+ const artifact = hooks.artifactCommit;
354
+ if (artifact) {
355
+ /* COMMIT, which is what core does unconditionally without the hook. */
356
+ out.artifactCommit = (aidx, alreadyCreated): boolean =>
357
+ guard("artifactCommit", () => artifact(aidx, alreadyCreated), true);
358
+ }
359
+
360
+ const history = hooks.historyAdd;
361
+ if (history) {
362
+ /* WRITE the entry: faithful core writes every entry it reaches. Suppressing
363
+ * on a throw would delete history the player earned. */
364
+ out.historyAdd = (entry): boolean =>
365
+ guard("historyAdd", () => history(entry), true);
366
+ }
367
+
368
+ const noise = hooks.saveNoiseScent;
369
+ if (noise) {
370
+ /* OMIT them, the upstream behaviour. */
371
+ out.saveNoiseScent = (): boolean => guard("saveNoiseScent", () => noise(), false);
372
+ }
373
+
374
+ const text = hooks.messageText;
375
+ if (text) {
376
+ /* The raw message, unrestated - never an empty string, which would silently
377
+ * eat a message the player needed to read. */
378
+ out.messageText = (raw): string => guard("messageText", () => text(raw), raw);
379
+ }
380
+
381
+ return out;
382
+ }
383
+
384
+ export function composeModHooks(
385
+ contributions: readonly ModHooks[],
386
+ ): ModHooks | undefined {
387
+ const list = contributions.filter((c) => Object.keys(c).length > 0);
388
+ if (list.length === 0) return undefined;
389
+
390
+ const out: ModHooks = {};
391
+
392
+ /* REVERSE load order for both of the last-answer folds, so the mod the player
393
+ * moved to the bottom of the list is the one that gets asked first and
394
+ * therefore the one that decides. Reversed here, once, rather than by walking
395
+ * the array backwards at call time: the reversal is a property of the FOLD, and
396
+ * a `for (let i = n - 1; ...)` in the hot path is the kind of detail a later
397
+ * edit quietly straightens out. */
398
+ const walk = list.map((c) => c.walkBlockedByDiggable).filter(isFn).reverse();
399
+ if (walk.length > 0) {
400
+ out.walkBlockedByDiggable = (state, grid, deps): number | null => {
401
+ /* Declining is contractually free of observable effect (see the member's
402
+ * doc), so asking a mod that then declines costs nothing - which is what
403
+ * makes it safe to ask them in any order at all. */
404
+ for (const fn of walk) {
405
+ const energy = fn(state, grid, deps);
406
+ if (energy !== null) return energy;
407
+ }
408
+ return null;
409
+ };
410
+ }
411
+
412
+ const tiebreak = list.map((c) => c.objectListTiebreak).filter(isFn).reverse();
413
+ if (tiebreak.length > 0) {
414
+ out.objectListTiebreak = (a, b): number => {
415
+ /* The last mod's comparator is the primary sort key and the earlier ones
416
+ * only break the ties it leaves equal - a lexicographic chain, still a
417
+ * consistent total order, and "the later mod wins" for a comparator. */
418
+ for (const fn of tiebreak) {
419
+ const r = fn(a, b);
420
+ if (r !== 0) return r;
421
+ }
422
+ return 0;
423
+ };
424
+ }
425
+
426
+ const level = list.map((c) => c.levelGenerated).filter(isFn);
427
+ if (level.length > 0) {
428
+ out.levelGenerated = (gen, quest): boolean => {
429
+ /* Every contributor runs even after one has repaired the level, because a
430
+ * second mod's invariant is not satisfied by the first mod's repair. The
431
+ * first REFUSAL short-circuits, since the level is being thrown away. */
432
+ for (const fn of level) if (!fn(gen, quest)) return false;
433
+ return true;
434
+ };
435
+ }
436
+
437
+ const artifact = list.map((c) => c.artifactCommit).filter(isFn);
438
+ if (artifact.length > 0) {
439
+ out.artifactCommit = (aidx, alreadyCreated): boolean => {
440
+ for (const fn of artifact) if (!fn(aidx, alreadyCreated)) return false;
441
+ return true;
442
+ };
443
+ }
444
+
445
+ const history = list.map((c) => c.historyAdd).filter(isFn);
446
+ if (history.length > 0) {
447
+ out.historyAdd = (entry): boolean => {
448
+ for (const fn of history) if (!fn(entry)) return false;
449
+ return true;
450
+ };
451
+ }
452
+
453
+ const noise = list.map((c) => c.saveNoiseScent).filter(isFn);
454
+ if (noise.length > 0) {
455
+ out.saveNoiseScent = (): boolean => noise.some((fn) => fn());
456
+ }
457
+
458
+ const text = list.map((c) => c.messageText).filter(isFn);
459
+ if (text.length > 0) {
460
+ out.messageText = (raw): string => text.reduce((s, fn) => fn(s), raw);
461
+ }
462
+
463
+ return out;
464
+ }
465
+
466
+ function isFn<T>(v: T | undefined): v is T {
467
+ return typeof v === "function";
468
+ }
Binary file
package/src/version.ts CHANGED
@@ -23,4 +23,4 @@ export const PARITY_BASELINE = "4.2.6";
23
23
  * released tag is iterated takes a MINOR bump, because a published tag is pinned
24
24
  * by digest in a catalogue and must never be moved.
25
25
  */
26
- export const ENGINE_VERSION = "0.12.0";
26
+ export const ENGINE_VERSION = "0.14.0";