@rpgm-tools/neo-angband-mod-sdk 0.10.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.
Files changed (53) hide show
  1. package/LICENSE.md +43 -0
  2. package/README.md +72 -0
  3. package/dist/capabilities.d.ts +116 -0
  4. package/dist/capabilities.d.ts.map +1 -0
  5. package/dist/capabilities.js +170 -0
  6. package/dist/capabilities.js.map +1 -0
  7. package/dist/compose.d.ts +71 -0
  8. package/dist/compose.d.ts.map +1 -0
  9. package/dist/compose.js +118 -0
  10. package/dist/compose.js.map +1 -0
  11. package/dist/conflicts.d.ts +78 -0
  12. package/dist/conflicts.d.ts.map +1 -0
  13. package/dist/conflicts.js +160 -0
  14. package/dist/conflicts.js.map +1 -0
  15. package/dist/index.d.ts +31 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +24 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/loader.d.ts +83 -0
  20. package/dist/loader.d.ts.map +1 -0
  21. package/dist/loader.js +314 -0
  22. package/dist/loader.js.map +1 -0
  23. package/dist/manifest.d.ts +261 -0
  24. package/dist/manifest.d.ts.map +1 -0
  25. package/dist/manifest.js +264 -0
  26. package/dist/manifest.js.map +1 -0
  27. package/dist/patch.d.ts +90 -0
  28. package/dist/patch.d.ts.map +1 -0
  29. package/dist/patch.js +195 -0
  30. package/dist/patch.js.map +1 -0
  31. package/dist/record-key.d.ts +99 -0
  32. package/dist/record-key.d.ts.map +1 -0
  33. package/dist/record-key.js +157 -0
  34. package/dist/record-key.js.map +1 -0
  35. package/dist/resolve.d.ts +42 -0
  36. package/dist/resolve.d.ts.map +1 -0
  37. package/dist/resolve.js +161 -0
  38. package/dist/resolve.js.map +1 -0
  39. package/dist/semver.d.ts +37 -0
  40. package/dist/semver.d.ts.map +1 -0
  41. package/dist/semver.js +212 -0
  42. package/dist/semver.js.map +1 -0
  43. package/package.json +58 -0
  44. package/src/capabilities.ts +205 -0
  45. package/src/compose.ts +186 -0
  46. package/src/conflicts.ts +242 -0
  47. package/src/index.ts +73 -0
  48. package/src/loader.ts +393 -0
  49. package/src/manifest.ts +523 -0
  50. package/src/patch.ts +257 -0
  51. package/src/record-key.ts +180 -0
  52. package/src/resolve.ts +175 -0
  53. package/src/semver.ts +231 -0
package/dist/loader.js ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Content-pack loading: turn a resolved set of packs into the merged per-file
3
+ * record arrays a host binds into the running game.
4
+ *
5
+ * This is the join MOD_INTEGRATION_PLAN.md (Wave 1, W1.1) calls for. Until now
6
+ * the composition engine (resolveLoadOrder + composePacks) had no runtime
7
+ * caller: the game bound a single hard-coded pack directly. This wraps the
8
+ * engine into one entry point so the base game and every mod flow through the
9
+ * same pipeline, and a mod's records / patches / replaces / removes /
10
+ * fieldPatches actually take effect.
11
+ *
12
+ * The host (web / cli) owns the glue: it reads packs off disk or bundle, calls
13
+ * composeContentPacks, assembles its GamePack from the result, and hands that
14
+ * to core bindCore. Core stays mod-sdk-agnostic (it only ever sees a merged
15
+ * pack), so the layering the audit relied on is preserved.
16
+ *
17
+ * TWO MERGE PHASES, AND WHY
18
+ *
19
+ * composePacks needs a unique string `name` on every added record, because that
20
+ * is the identity it builds refs from. Measured over the shipped core pack, 24
21
+ * of the 44 record files satisfy that and 20 do not (14 carry no string `name`
22
+ * at all; 6 carry names that slug to the same ref). So:
23
+ *
24
+ * 1. COMPOSED FILES (24) go through composePacks, which merges records
25
+ * per-record and appends a later pack's additions after an earlier pack's.
26
+ * 2. PASSTHROUGH FILES (20) keep whole-file semantics for `records` - the last
27
+ * provider in load order wins the file - because a mod that ships
28
+ * `constants.json` means "use mine", not "add a second constants record",
29
+ * and the host binds one. Per-record ops are then applied on top, in load
30
+ * order, against the winning array (applyPassthroughOps below), using the
31
+ * per-file identity declared in record-key.ts.
32
+ *
33
+ * NOTHING IS DROPPED IN SILENCE. This is the invariant the whole file exists to
34
+ * hold. Until 2026-07-29 phase 2 did not exist: a `patches` / `replaces` /
35
+ * `fieldPatches` / `removes` entry aimed at any of the 20 was stripped from the
36
+ * contribution before compose ever saw it, so a mod author shipped a valid op
37
+ * and got no effect and no message - the worst failure mode there is, because a
38
+ * feature that appears to work cannot be found by review. Now every per-record
39
+ * op either takes effect or produces a line in `ComposedContent.problems`
40
+ * naming the pack, the file, the op and the ref. Whole-file replacement of a
41
+ * passthrough file is reported the same way, because it silently discards
42
+ * whatever the previous provider (usually core) put there.
43
+ *
44
+ * `problems` is reported rather than thrown because the web host composes at
45
+ * module scope with no try (packages/web/src/pack.ts), so a throw here is a
46
+ * blank page rather than a message. It is the same shape of channel the pack
47
+ * readers already use (`problems: readonly string[]`), so a host concatenates it
48
+ * into the list it already shows.
49
+ */
50
+ import { slugify } from "./manifest.js";
51
+ import { resolveLoadOrder } from "./resolve.js";
52
+ import { composePacks, mergePatch } from "./compose.js";
53
+ import { applyFieldPatch } from "./patch.js";
54
+ import { keyDescription, keySpecFor, recordKey, RECORD_KEY_SPECS } from "./record-key.js";
55
+ function isNamedRecord(r) {
56
+ return (typeof r === "object" &&
57
+ r !== null &&
58
+ !Array.isArray(r) &&
59
+ typeof r.name === "string" &&
60
+ r.name.length > 0);
61
+ }
62
+ /**
63
+ * A pack's added records for one file are per-record composable only if they
64
+ * are all name-keyed and their refs (pack:slug(name)) do not collide - exactly
65
+ * the two conditions composePacks would otherwise throw on.
66
+ */
67
+ function recordsComposable(records) {
68
+ const slugs = new Set();
69
+ for (const r of records) {
70
+ if (!isNamedRecord(r))
71
+ return false;
72
+ const slug = slugify(r["name"]);
73
+ if (slugs.has(slug))
74
+ return false;
75
+ slugs.add(slug);
76
+ }
77
+ return true;
78
+ }
79
+ /** Reorder loaded packs into resolved load order (dependencies first). */
80
+ function orderPacks(packs) {
81
+ const ordered = resolveLoadOrder(packs.map((p) => p.manifest));
82
+ const byId = new Map(packs.map((p) => [p.manifest.id, p]));
83
+ return ordered.map((m) => byId.get(m.id));
84
+ }
85
+ /** The pack id a ref's "<owner>:<slug>" prefix names. */
86
+ function ownerOf(ref) {
87
+ const at = ref.indexOf(":");
88
+ return at === -1 ? "" : ref.slice(0, at);
89
+ }
90
+ /** compose.ts mayModify: a pack may only touch its own or a declared dep's records. */
91
+ function mayModify(m, ownerPack) {
92
+ return ownerPack === m.id || (m.dependencies ?? {})[ownerPack] !== undefined;
93
+ }
94
+ /** Every per-record op in one contribution, flattened, in apply order. */
95
+ function perRecordOps(contrib) {
96
+ const out = [];
97
+ for (const ref of Object.keys(contrib.patches ?? {}))
98
+ out.push({ kind: "patch", ref });
99
+ for (const ref of Object.keys(contrib.replaces ?? {}))
100
+ out.push({ kind: "replace", ref });
101
+ for (const ref of Object.keys(contrib.fieldPatches ?? {})) {
102
+ out.push({ kind: "fieldPatch", ref });
103
+ }
104
+ for (const ref of contrib.removes ?? [])
105
+ out.push({ kind: "remove", ref });
106
+ return out;
107
+ }
108
+ const OP_VERB = {
109
+ patch: "patches",
110
+ replace: "replaces",
111
+ fieldPatch: "fieldPatches",
112
+ remove: "removes",
113
+ };
114
+ /**
115
+ * Apply every pack's per-record ops to a PASSTHROUGH file's winning record
116
+ * array, in load order, and report every op that could not be honoured.
117
+ *
118
+ * Returns the (possibly new) record array; the input is never mutated, and when
119
+ * no pack contributes an op the input array is returned unchanged so routing the
120
+ * base game alone through this path stays a no-op by reference.
121
+ */
122
+ function applyPassthroughOps(file, records, ordered, providerId, problems) {
123
+ const hasOps = ordered.some((p) => p.files[file] !== undefined && perRecordOps(p.files[file]).length > 0);
124
+ if (!hasOps)
125
+ return records;
126
+ /* No declared identity (history: chart/next/roll/phrase are all values a mod
127
+ * would change). Every op is reported and none is applied - the one honest
128
+ * answer, because inventing a key here would mis-merge instead of dropping. */
129
+ if (RECORD_KEY_SPECS[file] === undefined && !recordsKeyedByName(records)) {
130
+ for (const pack of ordered) {
131
+ const contrib = pack.files[file];
132
+ if (!contrib)
133
+ continue;
134
+ for (const { kind, ref } of perRecordOps(contrib)) {
135
+ problems.push(`${pack.manifest.id}: ${file} ${OP_VERB[kind]} "${ref}", but ${file} records have no per-record identity, so only whole-file replacement can change them`);
136
+ }
137
+ }
138
+ return records;
139
+ }
140
+ const spec = keySpecFor(file);
141
+ const working = records.map((r) => typeof r === "object" && r !== null && !Array.isArray(r) ? r : null);
142
+ /* ref -> EVERY position claiming it. Kept as a list rather than resolved to a
143
+ * winner on insert, so "not found" and "claimed twice" are one lookup with two
144
+ * outcomes: there is nowhere for a first-claim-wins fallback to hide. A ref two
145
+ * records claim is unaddressable and reported; both records stay in the game. */
146
+ const claims = new Map();
147
+ records.forEach((record, i) => {
148
+ const key = recordKey(file, record, spec);
149
+ if (key === null)
150
+ return; // unkeyable record: stays in the game, not addressable
151
+ const ref = `${providerId}:${key}`;
152
+ const at = claims.get(ref);
153
+ if (at)
154
+ at.push(i);
155
+ else
156
+ claims.set(ref, [i]);
157
+ });
158
+ const removed = new Set();
159
+ const reject = (pid, kind, ref, why) => {
160
+ problems.push(`${pid}: ${file} ${OP_VERB[kind]} "${ref}", but ${why}`);
161
+ };
162
+ /** Resolve a ref to a live index, or report why it cannot be touched. */
163
+ const resolve = (pack, kind, ref) => {
164
+ const pid = pack.manifest.id;
165
+ const claimants = claims.get(ref) ?? [];
166
+ if (claimants.length > 1) {
167
+ reject(pid, kind, ref, `${claimants.length} ${file} records share that identity (${keyDescription(file)}), so it cannot be addressed - patch a record with a unique identity instead`);
168
+ return null;
169
+ }
170
+ const at = claimants[0];
171
+ if (at === undefined) {
172
+ reject(pid, kind, ref, `no such record exists in ${file} (identity is ${keyDescription(file)})`);
173
+ return null;
174
+ }
175
+ if (removed.has(at)) {
176
+ reject(pid, kind, ref, "an earlier pack already removed it");
177
+ return null;
178
+ }
179
+ if (!mayModify(pack.manifest, ownerOf(ref))) {
180
+ reject(pid, kind, ref, `${pid} does not declare ${ownerOf(ref)} as a dependency`);
181
+ return null;
182
+ }
183
+ return at;
184
+ };
185
+ for (const pack of ordered) {
186
+ const contrib = pack.files[file];
187
+ if (!contrib)
188
+ continue;
189
+ for (const [ref, body] of Object.entries(contrib.patches ?? {})) {
190
+ const at = resolve(pack, "patch", ref);
191
+ if (at === null)
192
+ continue;
193
+ working[at] = mergePatch(working[at], body);
194
+ }
195
+ for (const [ref, body] of Object.entries(contrib.replaces ?? {})) {
196
+ const at = resolve(pack, "replace", ref);
197
+ if (at === null)
198
+ continue;
199
+ working[at] = body;
200
+ }
201
+ for (const [ref, ops] of Object.entries(contrib.fieldPatches ?? {})) {
202
+ const at = resolve(pack, "fieldPatch", ref);
203
+ if (at === null)
204
+ continue;
205
+ working[at] = applyFieldPatch(working[at], ops);
206
+ }
207
+ for (const ref of contrib.removes ?? []) {
208
+ const at = resolve(pack, "remove", ref);
209
+ if (at === null)
210
+ continue;
211
+ removed.add(at);
212
+ }
213
+ }
214
+ const out = [];
215
+ working.forEach((r, i) => {
216
+ if (!removed.has(i))
217
+ out.push(r === null ? records[i] : r);
218
+ });
219
+ return out;
220
+ }
221
+ /** Whether a passthrough file's records are name-keyed after all (mod-only file). */
222
+ function recordsKeyedByName(records) {
223
+ return records.some((r) => isNamedRecord(r));
224
+ }
225
+ /**
226
+ * Compose a set of loaded packs into merged per-file record arrays. With a
227
+ * single pack (the base game alone) the output is record-identical to the
228
+ * input: every record object is preserved by reference and its order is
229
+ * unchanged, so routing the base game through this path is a no-op.
230
+ */
231
+ export function composeContentPacks(packs) {
232
+ const ordered = orderPacks(packs);
233
+ const problems = [];
234
+ const fileNames = new Set();
235
+ for (const p of ordered) {
236
+ for (const f of Object.keys(p.files))
237
+ fileNames.add(f);
238
+ }
239
+ // Classify each file: per-record composable, or whole-file passthrough.
240
+ const composable = new Set();
241
+ for (const f of fileNames) {
242
+ let ok = true;
243
+ for (const p of ordered) {
244
+ const contrib = p.files[f];
245
+ if (contrib?.records && !recordsComposable(contrib.records)) {
246
+ ok = false;
247
+ break;
248
+ }
249
+ }
250
+ if (ok)
251
+ composable.add(f);
252
+ }
253
+ const contents = ordered.map((p) => {
254
+ const files = {};
255
+ for (const [f, contrib] of Object.entries(p.files)) {
256
+ if (composable.has(f))
257
+ files[f] = contrib;
258
+ }
259
+ return { manifest: p.manifest, files };
260
+ });
261
+ const game = composePacks(contents);
262
+ const out = {};
263
+ for (const [file, table] of game) {
264
+ out[file] = [...table.values()].map((r) => r.value);
265
+ }
266
+ /* Passthrough files, in two phases (see the header): the last provider in load
267
+ * order wins the whole file, then every pack's per-record ops apply on top. */
268
+ for (const f of fileNames) {
269
+ if (composable.has(f))
270
+ continue;
271
+ let providerId = "";
272
+ for (const p of ordered) {
273
+ const contrib = p.files[f];
274
+ if (!contrib?.records)
275
+ continue;
276
+ if (providerId !== "") {
277
+ /* Whole-file replacement is destructive and used to be invisible: the
278
+ * previous provider's records simply vanished. Say so. */
279
+ problems.push(`${p.manifest.id}: ${f} replaces the whole file, discarding ${out[f].length} record(s) from ${providerId} - ${f} records are not name-keyed, so a whole file is the only thing that can be added to it`);
280
+ }
281
+ out[f] = [...contrib.records];
282
+ providerId = p.manifest.id;
283
+ }
284
+ if (providerId === "") {
285
+ /* SAFETY NET, not a live path. A file is only classified passthrough
286
+ * because some pack's `records` for it are not name-keyed, and such a pack
287
+ * is by definition a provider - so today providerId is never "" here. The
288
+ * equivalent case for a COMPOSABLE file (a mod patches a file nobody
289
+ * supplies records for) is loud already: composePacks throws
290
+ * "patch target ... does not exist". This branch is kept, cordoned and
291
+ * documented rather than deleted, because if the classification ever
292
+ * changes its absence would be a silent drop - the exact failure this file
293
+ * exists to prevent. Covered by loader.test.ts only through the composable
294
+ * counterpart. */
295
+ for (const p of ordered) {
296
+ const contrib = p.files[f];
297
+ if (!contrib)
298
+ continue;
299
+ for (const { kind, ref } of perRecordOps(contrib)) {
300
+ problems.push(`${p.manifest.id}: ${f} ${OP_VERB[kind]} "${ref}", but no pack supplies any ${f} records`);
301
+ }
302
+ }
303
+ continue;
304
+ }
305
+ out[f] = applyPassthroughOps(f, out[f], ordered, providerId, problems);
306
+ }
307
+ return {
308
+ records: out,
309
+ composedFiles: [...composable].sort(),
310
+ passthroughFiles: [...fileNames].filter((f) => !composable.has(f)).sort(),
311
+ problems,
312
+ };
313
+ }
314
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Pack manifests: identity, versioning, and dependencies.
3
+ *
4
+ * Every pack - the base game included - carries a manifest. Load order,
5
+ * record composition, and savefile provenance all key off it.
6
+ */
7
+ /** Pack identifiers are namespaced: "<pack>:<id>", e.g. "core:kobold". */
8
+ export type PackRef = `${string}:${string}`;
9
+ /** The three pack shapes (docs/MODS.md). */
10
+ export type PackShape = "content" | "tiles" | "plugin";
11
+ /** Every shape, for validation and for iterating the facet vocabulary. */
12
+ export declare const PACK_SHAPES: readonly PackShape[];
13
+ /**
14
+ * WHAT A PACK CONTRIBUTES, as a SET rather than one exclusive kind.
15
+ *
16
+ * `shape` was exclusive, and the two halves of the loader gated on opposite
17
+ * values: code loaded only for `shape: "plugin"` (web/src/mod-code.ts) and
18
+ * records composed only for `shape: "content"` (web/src/pack.ts). So the folder
19
+ * layout the plugin documentation promises -
20
+ *
21
+ * my-mod/ manifest.json plugin.js monster.json tiles/orc.png
22
+ *
23
+ * - could never work: declaring "plugin" dropped monster.json from composition,
24
+ * and declaring "content" refused the code. Each half had tests and each half
25
+ * passed; nothing asserted the two together. A mod that adds a monster AND gives
26
+ * it behaviour is the ordinary case, not an exotic one.
27
+ *
28
+ * `facets` is that set. `shape` stays REQUIRED and remains the pack's primary
29
+ * kind - it is what the manager displays and what every existing manifest
30
+ * already carries - and when `facets` is present it must CONTAIN `shape`, so the
31
+ * two fields cannot contradict each other. A hybrid declares:
32
+ *
33
+ * { "shape": "content", "facets": ["content", "plugin"] }
34
+ *
35
+ * The consent property is unchanged and is why `facets` is a declaration rather
36
+ * than something inferred from the folder's contents: shipping plugin.js without
37
+ * naming the `plugin` facet is still a REFUSAL, because running code must be
38
+ * something a mod states rather than something a file listing implies.
39
+ */
40
+ export declare function packFacets(manifest: Pick<PackManifest, "shape" | "facets">): ReadonlySet<PackShape>;
41
+ /** Whether a pack contributes `facet` (its shape, or any declared facet). */
42
+ export declare function hasFacet(manifest: Pick<PackManifest, "shape" | "facets">, facet: PackShape): boolean;
43
+ /**
44
+ * One player-toggleable "rule" a pack contributes: a flag name the pack owns,
45
+ * plus the human-facing label / description / default the in-app "Fixes &
46
+ * tweaks" menu renders.
47
+ *
48
+ * WHAT THIS USED TO BE, AND WHY IT IS NOT THAT ANY MORE. The first design made
49
+ * this a registry of CORE flags: the corrected behaviour lived in ported core as
50
+ * an off-by-default branch guarded by `if (modRuleEnabled(state, flag))`, the
51
+ * host applied the resolved choices to GameState.modRules, and no mod code ran.
52
+ * That design was deleted on 2026-07-29 because a flag-gated fix is not excluded
53
+ * from core - core shipped the fix body AND the mod's flag name, so deleting the
54
+ * mod folder would not have deleted a line of it. `modRuleEnabled` is GONE
55
+ * (packages/core/src/game/context.ts, where its removal is recorded), and
56
+ * `GameState.modRules` still exists but is OPAQUE to core: core stores it because
57
+ * a save has to record which patches a character was played with, and never
58
+ * branches on it (`context.ts`, the modRules doc comment).
59
+ *
60
+ * WHAT A RULE IS NOW: an input to the MOD's own code. Mods do run code. A mod
61
+ * that changes behaviour ships `hooks.ts` next to its manifest, default-exporting
62
+ * `(flags: Readonly<Record<string, boolean>>) => ModHooks`. The host discovers it
63
+ * (packages/web/src/mod-hooks.ts), calls it once per ENABLED mod in load order
64
+ * with only THAT mod's resolved flags (`choices[flag] ?? rule.default` for the
65
+ * rules its own manifest declares, so one mod cannot read another's toggles), and
66
+ * folds the results into the single ModHooks core holds via `composeModHooks`
67
+ * (packages/core/src/mod/hooks.ts). Each fix body lives in its mod's folder; what
68
+ * core contains is the generic seam, not any mod's name.
69
+ *
70
+ * A disabled mod's patches DO NOT EXIST rather than existing and reading false:
71
+ * its entry point is never called, it contributes no hook, composeModHooks
72
+ * returns undefined, and GameState.modHooks stays absent - so core runs the
73
+ * faithful 4.2.6 path, which is the only path compiled into the branch.
74
+ *
75
+ * A rules-only pack is still a plain `content` pack requesting no capabilities;
76
+ * `rules` remains pure declaration, and this manifest still holds no behaviour.
77
+ */
78
+ export interface PackRule {
79
+ /**
80
+ * The flag this rule toggles (e.g. "qol.autoDig"). Namespaced by convention to
81
+ * the owning pack, because the pack's own hooks.ts is what reads it; the host
82
+ * also records the resolved value on GameState.modRules as save state.
83
+ */
84
+ flag: string;
85
+ /** Short menu label (e.g. "Auto-dig"). */
86
+ title: string;
87
+ /** One- or two-line description shown under the toggle in the menu. */
88
+ description: string;
89
+ /** Whether the rule is ON by default when the mod is enabled. */
90
+ default: boolean;
91
+ }
92
+ /**
93
+ * A capability a scripted plugin requests (MOD_LIFECYCLE section 4). The
94
+ * runtime grants only what a `shape: plugin` pack declares and the user
95
+ * approves; content and tile packs request none. The vocabulary
96
+ * ("command:add", "event:turn-start", "state:*.read", "network:<host>", ...)
97
+ * is enforced by the capability model (P7 phase 5); the manifest only records
98
+ * the request, so any string is accepted here.
99
+ */
100
+ export type Capability = string;
101
+ /**
102
+ * One graphics mode a `tiles`-facet pack contributes.
103
+ *
104
+ * This was read loosely off the raw JSON for a long time and was NOT in the
105
+ * validated schema, which the moddability measurement recorded as a gap
106
+ * (docs/modding/MOD_REACH.md). The consequence was specific rather than
107
+ * theoretical: a typo in `grafID` or `path` produced no error anywhere - the entry
108
+ * was silently skipped, and a mod author saw a Graphics row that simply never
109
+ * appeared. Declaring it here means the manifest is refused at the edge, with the
110
+ * mod's id and the offending field named.
111
+ */
112
+ export interface PackTilePack {
113
+ /**
114
+ * The list.txt serial number this mode renders as. A `tilesheet` pack must claim
115
+ * one the core catalog already knows (it borrows that row's cell size, atlas
116
+ * filename and pref file); a `linoleum` pack carries its own metadata and may
117
+ * claim a new id - use >= 100 to stay clear of upstream's numbering.
118
+ */
119
+ grafID: number;
120
+ /**
121
+ * The pack's directory INSIDE THE MOD FOLDER (`original-tiles`,
122
+ * `tiles/my-set`), or absent for a pack that is the mod folder itself.
123
+ *
124
+ * Mod-relative, not a site path. A mod cannot know where a host serves it from,
125
+ * and two of the three sources serve it from nowhere: a folder the player picked
126
+ * has no URL for its files until their bytes are wrapped in a blob:, and a mod
127
+ * installed from a repository lives in IndexedDB. The host composes this with
128
+ * the mod's own asset resolver.
129
+ */
130
+ path?: string;
131
+ /**
132
+ * Which renderer draws it: `tilesheet` (or absent) for upstream's own scheme -
133
+ * one atlas PNG addressed by row/column - and `linoleum` for a loose pack, a
134
+ * directory of individually named PNGs. This is the PACK's renderer; the
135
+ * manifest's top-level `engine` is the game version the mod targets.
136
+ */
137
+ engine?: "tilesheet" | "linoleum";
138
+ /**
139
+ * The Graphics row's label. Required in effect for a mode the core catalog does
140
+ * not have, since there would be nothing to name the row; a pack re-skinning a
141
+ * catalogued mode may omit it and borrow that row's name.
142
+ */
143
+ menuname?: string;
144
+ }
145
+ export interface PackManifest {
146
+ /**
147
+ * The pack's namespace: lowercase kebab-case, unique among loaded
148
+ * packs. "core" is reserved for the base game.
149
+ */
150
+ id: string;
151
+ /** Human-readable title. */
152
+ name: string;
153
+ /** Semantic version of the pack itself. */
154
+ version: string;
155
+ /**
156
+ * The pack's primary kind, and what the mod manager displays. When `facets` is
157
+ * absent this is the pack's only facet.
158
+ */
159
+ shape: PackShape;
160
+ /**
161
+ * Everything this pack contributes, when it contributes more than one kind -
162
+ * a mod shipping both `plugin.js` and record JSON declares
163
+ * `["content", "plugin"]`. Must contain `shape`. See packFacets().
164
+ */
165
+ facets?: readonly PackShape[];
166
+ /**
167
+ * Engine version range the pack requires (semver range, e.g. ">=0.5.0
168
+ * <0.7.0"). A save refuses to load on an incompatible engine.
169
+ */
170
+ engine?: string;
171
+ /**
172
+ * Packs this one depends on, by id. A pack may only patch, replace,
173
+ * or remove records owned by packs it declares here. Values are
174
+ * version constraints; "*" accepts any version.
175
+ */
176
+ dependencies?: Record<string, string>;
177
+ /**
178
+ * Soft dependencies: if the named pack is present it loads first and may
179
+ * be modified, but its absence is not an error (MOD_LIFECYCLE section 2).
180
+ */
181
+ optionalDependencies?: Record<string, string>;
182
+ /** Load-order hints (MOD_LIFECYCLE section 3): follow / precede these ids. */
183
+ loadAfter?: string[];
184
+ loadBefore?: string[];
185
+ /**
186
+ * The pack's own save-block schema version. The engine hands a mod its
187
+ * old `mod:<id>` bag and asks it to migrate from this number on update.
188
+ */
189
+ saveSchema?: number;
190
+ /** Capabilities a `shape: plugin` pack requests (see Capability). */
191
+ capabilities?: Capability[];
192
+ /**
193
+ * The mod-plugin ABI version this pack's `plugin.js` was written against, and
194
+ * REQUIRED of any pack that ships one. Separate from `engine` on purpose: the
195
+ * engine version and the ABI a mod's code compiles against diverge immediately
196
+ * - a patch release changes the former and not the latter.
197
+ *
198
+ * Declared here, in the MANIFEST, rather than only inside plugin.js, so the
199
+ * host can refuse an incompatible plugin BEFORE importing it. A version check
200
+ * that lives inside the module can only run after the module's top-level code
201
+ * has already executed, which is the wrong order for player-supplied code.
202
+ *
203
+ * An exact integer, matched exactly, because the ABI is explicitly unstable
204
+ * until 1.0: every change to it bumps this number and every mod must
205
+ * republish. A semver range would imply a compatibility promise that does not
206
+ * exist yet.
207
+ */
208
+ modApi?: number;
209
+ /**
210
+ * Player-toggleable flags this pack owns (see PackRule). The bundled qol /
211
+ * bug-fixes mods use this to declare their fixes/tweaks for the in-app "Fixes
212
+ * & tweaks" menu; the host resolves (choice ?? default), hands each mod its own
213
+ * slice when it calls that mod's hooks.ts, and records the result on
214
+ * GameState.modRules as save state. Absent for a pack with nothing to toggle.
215
+ */
216
+ rules?: PackRule[];
217
+ /**
218
+ * Graphics modes this pack contributes (see PackTilePack). Only read for a pack
219
+ * with the `tiles` facet; a content pack that declares them contributes none.
220
+ */
221
+ tilePacks?: PackTilePack[];
222
+ /**
223
+ * Declares the pack deliberately nondeterministic (a wall-clock event, an
224
+ * external agent, live multiplayer). Trips the save's determinism ratchet
225
+ * once, irreversibly (MOD_LIFECYCLE section 4, decision 4/18).
226
+ */
227
+ nondeterministic?: boolean;
228
+ /** Declares a gameplay change that permanently makes an enabled save non-scoring. */
229
+ affectsGameplay?: boolean;
230
+ /**
231
+ * What the pack does, in the author's own words, for a human deciding whether
232
+ * to enable it. Prose, not a tagline: the in-app mod manager wraps it to fill
233
+ * the detail pane of the highlighted row, and a marketplace listing would show
234
+ * the same text. Absent is allowed but leaves a player with only the id/shape
235
+ * to go on.
236
+ */
237
+ description?: string;
238
+ /** Free-form author credit. */
239
+ author?: string;
240
+ /** SPDX license expression for the pack's own content. */
241
+ license?: string;
242
+ /** Source repository URL (installer provenance). */
243
+ repository?: string;
244
+ /** Path to the changelog within the pack. */
245
+ changelog?: string;
246
+ /** Paths to screenshot assets within the pack (marketplace preview). */
247
+ screenshots?: string[];
248
+ }
249
+ export declare class ManifestError extends Error {
250
+ }
251
+ /** Validate a parsed manifest object; throws ManifestError. */
252
+ export declare function validateManifest(value: unknown): PackManifest;
253
+ /**
254
+ * Slug a record name into the id segment of a PackRef: lowercase, runs
255
+ * of non-alphanumerics collapse to single hyphens ("Farmer Maggot" ->
256
+ * "farmer-maggot"). Stable: this is a savefile-visible identity.
257
+ */
258
+ export declare function slugify(name: string): string;
259
+ /** Build a namespaced record reference. */
260
+ export declare function packRef(packId: string, name: string): PackRef;
261
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"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"}