@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/semver.js ADDED
@@ -0,0 +1,212 @@
1
+ /**
2
+ * A small, dependency-free semver range matcher.
3
+ *
4
+ * This package bundles into a browser build, so it cannot pull in the
5
+ * `semver` npm package; this file implements just enough of the range
6
+ * grammar for pack manifests (MOD_LIFECYCLE.md section 3):
7
+ *
8
+ * - `*` or `x` (any case): matches any version.
9
+ * - an exact version, `1.2.3`: matches only that version.
10
+ * - a partial version used bare, `1.2` or `1`: matches any version with
11
+ * that prefix (`1.2` matches `1.2.0`..`1.2.x`; `1` matches `1.0.0`..`1.x.x`).
12
+ * - caret ranges, `^1.2.3`: compatible-with, following npm's rule that the
13
+ * leftmost nonzero component may not change (`^1.2.3` allows up to but
14
+ * not including `2.0.0`; `^0.2.3` allows up to but not including `0.3.0`;
15
+ * `^0.0.3` allows only `0.0.3`).
16
+ * - tilde ranges, `~1.2.3`: patch-level allowed (up to but not including
17
+ * `1.3.0`); `~1.2` is the same; `~1` allows up to but not including `2.0.0`.
18
+ * - comparator sets: `>=`, `>`, `<=`, `<`, `=`, combined with spaces and
19
+ * ANDed together, e.g. `>=1.0.0 <2.0.0`.
20
+ *
21
+ * Limitation (documented, not fixed): prerelease tags (`1.0.0-beta.2`) are
22
+ * compared naively as a single lexicographic string once the numeric
23
+ * major.minor.patch triple is equal, rather than the full dot-separated,
24
+ * numeric-vs-alphanumeric identifier comparison the semver spec defines.
25
+ * A version with no prerelease is always treated as newer than one with a
26
+ * prerelease at the same major.minor.patch, matching the spec; the ordering
27
+ * among different prerelease strings themselves does not. Pack authors who
28
+ * need exact prerelease ordering should not rely on it here.
29
+ */
30
+ export class SemverError extends Error {
31
+ }
32
+ const PARTIAL_RE = /^(\d+|[xX*])(?:\.(\d+|[xX*]))?(?:\.(\d+|[xX*]))?(?:-([0-9A-Za-z.-]+))?$/;
33
+ function isWildcardToken(s) {
34
+ return s === "x" || s === "X" || s === "*";
35
+ }
36
+ /** Parse a (possibly partial, possibly wildcarded) version-shaped string. */
37
+ function parsePartial(s) {
38
+ const match = PARTIAL_RE.exec(s);
39
+ if (!match) {
40
+ throw new SemverError(`malformed version: ${s}`);
41
+ }
42
+ const [, majorStr, minorStr, patchStr, prerelease] = match;
43
+ const major = majorStr === undefined || isWildcardToken(majorStr) ? null : Number(majorStr);
44
+ const minor = minorStr === undefined || isWildcardToken(minorStr) ? null : Number(minorStr);
45
+ const patch = patchStr === undefined || isWildcardToken(patchStr) ? null : Number(patchStr);
46
+ return { major, minor, patch, prerelease: prerelease ?? null };
47
+ }
48
+ /** Parse a full, exact version (all three components required); throws SemverError. */
49
+ function parseVersion(s) {
50
+ const p = parsePartial(s);
51
+ if (p.major === null || p.minor === null || p.patch === null) {
52
+ throw new SemverError(`expected a full major.minor.patch version, got: ${s}`);
53
+ }
54
+ return { major: p.major, minor: p.minor, patch: p.patch, prerelease: p.prerelease };
55
+ }
56
+ /** -1 if a < b, 0 if equal, 1 if a > b. See the prerelease limitation above. */
57
+ function compareVersions(a, b) {
58
+ if (a.major !== b.major)
59
+ return a.major - b.major < 0 ? -1 : 1;
60
+ if (a.minor !== b.minor)
61
+ return a.minor - b.minor < 0 ? -1 : 1;
62
+ if (a.patch !== b.patch)
63
+ return a.patch - b.patch < 0 ? -1 : 1;
64
+ if (a.prerelease === b.prerelease)
65
+ return 0;
66
+ if (a.prerelease === null)
67
+ return 1; // no prerelease outranks any prerelease
68
+ if (b.prerelease === null)
69
+ return -1;
70
+ return a.prerelease < b.prerelease ? -1 : 1;
71
+ }
72
+ /** Fill an absent minor/patch with 0, e.g. for comparator/caret/tilde bounds. */
73
+ function fullFromPartial(p, major) {
74
+ return {
75
+ major,
76
+ minor: p.minor ?? 0,
77
+ patch: p.patch ?? 0,
78
+ prerelease: p.prerelease,
79
+ };
80
+ }
81
+ function caretChecker(p, major) {
82
+ const lower = fullFromPartial(p, major);
83
+ let upper;
84
+ if (major > 0) {
85
+ upper = { major: major + 1, minor: 0, patch: 0, prerelease: null };
86
+ }
87
+ else if (lower.minor > 0) {
88
+ upper = { major: 0, minor: lower.minor + 1, patch: 0, prerelease: null };
89
+ }
90
+ else {
91
+ upper = { major: 0, minor: 0, patch: lower.patch + 1, prerelease: null };
92
+ }
93
+ return (v) => compareVersions(v, lower) >= 0 && compareVersions(v, upper) < 0;
94
+ }
95
+ function tildeChecker(p, major) {
96
+ const lower = fullFromPartial(p, major);
97
+ const upper = p.minor === null
98
+ ? { major: major + 1, minor: 0, patch: 0, prerelease: null }
99
+ : { major, minor: lower.minor + 1, patch: 0, prerelease: null };
100
+ return (v) => compareVersions(v, lower) >= 0 && compareVersions(v, upper) < 0;
101
+ }
102
+ function comparatorChecker(op, p, major) {
103
+ const bound = fullFromPartial(p, major);
104
+ switch (op) {
105
+ case ">=":
106
+ return (v) => compareVersions(v, bound) >= 0;
107
+ case ">":
108
+ return (v) => compareVersions(v, bound) > 0;
109
+ case "<=":
110
+ return (v) => compareVersions(v, bound) <= 0;
111
+ case "<":
112
+ return (v) => compareVersions(v, bound) < 0;
113
+ case "=":
114
+ return (v) => compareVersions(v, bound) === 0;
115
+ default:
116
+ // Unreachable: parseToken only dispatches here with a known operator.
117
+ throw new SemverError(`unknown comparator: ${op}`);
118
+ }
119
+ }
120
+ /** A bare token with no operator: an exact version, or a wildcard prefix. */
121
+ function bareChecker(p, major) {
122
+ if (p.minor === null) {
123
+ const lower = { major, minor: 0, patch: 0, prerelease: null };
124
+ const upper = { major: major + 1, minor: 0, patch: 0, prerelease: null };
125
+ return (v) => compareVersions(v, lower) >= 0 && compareVersions(v, upper) < 0;
126
+ }
127
+ if (p.patch === null) {
128
+ const lower = { major, minor: p.minor, patch: 0, prerelease: null };
129
+ const upper = {
130
+ major,
131
+ minor: p.minor + 1,
132
+ patch: 0,
133
+ prerelease: null,
134
+ };
135
+ return (v) => compareVersions(v, lower) >= 0 && compareVersions(v, upper) < 0;
136
+ }
137
+ const exact = { major, minor: p.minor, patch: p.patch, prerelease: p.prerelease };
138
+ return (v) => compareVersions(v, exact) === 0;
139
+ }
140
+ /** Parse one whitespace-delimited comparator token into a checker function. */
141
+ function parseToken(token) {
142
+ if (token === "*" || token.toLowerCase() === "x") {
143
+ return () => true;
144
+ }
145
+ let op = "";
146
+ let rest = token;
147
+ if (rest.startsWith(">=")) {
148
+ op = ">=";
149
+ rest = rest.slice(2);
150
+ }
151
+ else if (rest.startsWith("<=")) {
152
+ op = "<=";
153
+ rest = rest.slice(2);
154
+ }
155
+ else if (rest.startsWith("^")) {
156
+ op = "^";
157
+ rest = rest.slice(1);
158
+ }
159
+ else if (rest.startsWith("~")) {
160
+ op = "~";
161
+ rest = rest.slice(1);
162
+ }
163
+ else if (rest.startsWith(">")) {
164
+ op = ">";
165
+ rest = rest.slice(1);
166
+ }
167
+ else if (rest.startsWith("<")) {
168
+ op = "<";
169
+ rest = rest.slice(1);
170
+ }
171
+ else if (rest.startsWith("=")) {
172
+ op = "=";
173
+ rest = rest.slice(1);
174
+ }
175
+ const partial = parsePartial(rest);
176
+ if (partial.major === null) {
177
+ // A wildcard major combined with an operator ("^x", ">=x") degrades to
178
+ // "any version"; there is no meaningful bound to compute.
179
+ return () => true;
180
+ }
181
+ switch (op) {
182
+ case "^":
183
+ return caretChecker(partial, partial.major);
184
+ case "~":
185
+ return tildeChecker(partial, partial.major);
186
+ case ">=":
187
+ case ">":
188
+ case "<=":
189
+ case "<":
190
+ case "=":
191
+ return comparatorChecker(op, partial, partial.major);
192
+ default:
193
+ return bareChecker(partial, partial.major);
194
+ }
195
+ }
196
+ /**
197
+ * Does `version` satisfy `range`? Throws SemverError if either string is
198
+ * malformed (an unparseable version, or a range with an unparseable token).
199
+ */
200
+ export function satisfies(version, range) {
201
+ const trimmed = range.trim();
202
+ if (trimmed.length === 0) {
203
+ throw new SemverError("empty version range");
204
+ }
205
+ if (trimmed === "*" || trimmed.toLowerCase() === "x") {
206
+ return true;
207
+ }
208
+ const v = parseVersion(version);
209
+ const tokens = trimmed.split(/\s+/);
210
+ return tokens.every((token) => parseToken(token)(v));
211
+ }
212
+ //# sourceMappingURL=semver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semver.js","sourceRoot":"","sources":["../src/semver.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@rpgm-tools/neo-angband-mod-sdk",
3
+ "version": "0.10.0",
4
+ "description": "Pack schemas, validation, and tooling for Neo Angband mods (content packs, tile packs, scripted plugins)",
5
+ "license": "GPL-2.0-only",
6
+ "author": "neostryder (RPGM Tools)",
7
+ "homepage": "https://github.com/neostryder/neo-angband/blob/master/docs/MODS.md",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/neostryder/neo-angband.git",
11
+ "directory": "packages/mod-sdk"
12
+ },
13
+ "bugs": "https://github.com/neostryder/neo-angband/issues",
14
+ "keywords": [
15
+ "angband",
16
+ "neo-angband",
17
+ "modding",
18
+ "mod-sdk",
19
+ "roguelike"
20
+ ],
21
+ "type": "module",
22
+ "sideEffects": false,
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "main": "dist/index.js",
27
+ "types": "dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "default": "./dist/index.js"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "src",
38
+ "!**/*.test.ts",
39
+ "!**/*.test.js",
40
+ "!**/*.test.js.map",
41
+ "!**/*.test.d.ts",
42
+ "!**/*.test.d.ts.map",
43
+ "!**/*.tsbuildinfo",
44
+ "LICENSE.md",
45
+ "README.md"
46
+ ],
47
+ "//publishConfig": [
48
+ "provenance is deliberately absent - trusted publishing attaches it with no flag,",
49
+ "and the flag would break the one manual publish a new package needs. The long",
50
+ "version is in packages/core/package.json and docs/RELEASING.md step 4."
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "scripts": {
56
+ "build": "tsc"
57
+ }
58
+ }
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Capability model for scripted plugins (MOD_LIFECYCLE.md section 4, P7
3
+ * phase 5).
4
+ *
5
+ * Only `shape: plugin` packs may request capabilities; content and tile
6
+ * packs are validated data that cannot execute, so they request none
7
+ * (docs/MODS.md trust tiers). A plugin's `capabilities` list in its
8
+ * manifest is the consent surface: the installer shows each one in plain
9
+ * language, the user approves, and the runtime grants exactly that set -
10
+ * nothing a plugin did not ask for and the user did not see. The
11
+ * perceive/act facades (a later P7 phase) call `CapabilitySet.check()`
12
+ * before honoring a request; an ungranted capability throws a clear
13
+ * author-facing error rather than silently doing nothing or diverging.
14
+ *
15
+ * Vocabulary (four forms, MOD_LIFECYCLE section 4 / the frost example in
16
+ * section 2):
17
+ * - "command:add" - register commands on the act facade.
18
+ * - "event:<name>" - subscribe to an engine event, e.g.
19
+ * "event:turn-start".
20
+ * - "state:<domain>.read" - read one perceive-facade domain, e.g.
21
+ * "state:party.read"; or the wildcard
22
+ * "state:*.read" for any domain.
23
+ * - "network:<host>" - outbound network to one host; or "network:*"
24
+ * for any host. Not in the section-4 examples
25
+ * verbatim, but named there ("network access to
26
+ * api.example.com"); "*" is this module's
27
+ * extension for a plugin that genuinely needs
28
+ * unrestricted egress, and reads the same way
29
+ * the other wildcards do.
30
+ * - "registry:<domain>" - override a game SYSTEM registry from a TRUSTED
31
+ * in-process plugin (W2.2, core/mod/registry-host.ts):
32
+ * "registry:effect" | "registry:room" |
33
+ * "registry:command" | "registry:monster" |
34
+ * "registry:vocab"; or the wildcard "registry:*"
35
+ * for all of them. "registry:vocab" (W2.3) declares
36
+ * NEW vocabulary (flags/stats/any kind). Distinct
37
+ * from "command:add": that adds a command via the
38
+ * act facade, this replaces what a command DOES (and
39
+ * the effect/room/AI logic behind the game).
40
+ * (The named-core-rule flags the bundled qol /
41
+ * bug-fixes mods use, GameState.modRules, are a
42
+ * DECLARATIVE manifest field - PackManifest.rules -
43
+ * applied by the host, so they need no capability.)
44
+ *
45
+ * This module only surfaces `nondeterministic` from the manifest. The
46
+ * save's determinism ratchet itself - flipping a save from DETERMINISTIC to
47
+ * NONDETERMINISTIC the first time such a mod is enabled, once and
48
+ * irreversibly - lives in core/save (decisions 4/18/22), not here.
49
+ */
50
+
51
+ import { hasFacet, type PackManifest } from "./manifest.js";
52
+
53
+ export class CapabilityError extends Error {}
54
+
55
+ /** A capability string parsed into its structured form. */
56
+ export type ParsedCapability =
57
+ | { kind: "command"; action: "add" }
58
+ | { kind: "event"; name: string }
59
+ | { kind: "state"; domain: string; access: "read" }
60
+ | { kind: "network"; host: string }
61
+ | { kind: "registry"; domain: string };
62
+
63
+ const EVENT_RE = /^event:([a-z][a-z0-9-]*)$/;
64
+ const STATE_RE = /^state:(\*|[a-z][a-z0-9-]*)\.read$/;
65
+ const NETWORK_RE = /^network:(\*|[a-zA-Z0-9.-]+)$/;
66
+ /** The override domains ModRegistryHost gates, plus the "*" wildcard. */
67
+ const REGISTRY_RE = /^registry:(\*|effect|room|command|monster|vocab)$/;
68
+
69
+ /**
70
+ * Parse and validate a capability string against the vocabulary above,
71
+ * returning its structured form. Throws CapabilityError on anything
72
+ * malformed or outside the recognized patterns - an unknown capability is
73
+ * a hard error, not a silent no-op, since a typo'd request should fail
74
+ * loudly at install rather than quietly never matching a grant.
75
+ */
76
+ export function parseCapability(cap: string): ParsedCapability {
77
+ if (cap === "command:add") {
78
+ return { kind: "command", action: "add" };
79
+ }
80
+ const event = EVENT_RE.exec(cap);
81
+ if (event) {
82
+ return { kind: "event", name: event[1] as string };
83
+ }
84
+ const state = STATE_RE.exec(cap);
85
+ if (state) {
86
+ return { kind: "state", domain: state[1] as string, access: "read" };
87
+ }
88
+ const network = NETWORK_RE.exec(cap);
89
+ if (network) {
90
+ return { kind: "network", host: network[1] as string };
91
+ }
92
+ const registry = REGISTRY_RE.exec(cap);
93
+ if (registry) {
94
+ return { kind: "registry", domain: registry[1] as string };
95
+ }
96
+ throw new CapabilityError(`unrecognized capability: "${cap}"`);
97
+ }
98
+
99
+ /**
100
+ * True if `grant` covers `request`. Exact match for command/event, plus the
101
+ * two documented wildcards: a "state:*.read" grant covers a read of any
102
+ * domain, and a "network:*" grant covers egress to any host.
103
+ */
104
+ function grantCovers(grant: ParsedCapability, request: ParsedCapability): boolean {
105
+ switch (request.kind) {
106
+ case "command":
107
+ return grant.kind === "command";
108
+ case "event":
109
+ return grant.kind === "event" && grant.name === request.name;
110
+ case "state":
111
+ return (
112
+ grant.kind === "state" &&
113
+ grant.access === request.access &&
114
+ (grant.domain === "*" || grant.domain === request.domain)
115
+ );
116
+ case "network":
117
+ return (
118
+ grant.kind === "network" &&
119
+ (grant.host === "*" || grant.host === request.host)
120
+ );
121
+ case "registry":
122
+ return (
123
+ grant.kind === "registry" &&
124
+ (grant.domain === "*" || grant.domain === request.domain)
125
+ );
126
+ }
127
+ }
128
+
129
+ /**
130
+ * The capabilities a plugin was granted, built from its manifest. Facades
131
+ * hold one of these per loaded plugin and call `check()` before honoring
132
+ * any request that touches commands, events, state reads, or the network.
133
+ */
134
+ export class CapabilitySet {
135
+ private readonly grants: readonly ParsedCapability[];
136
+ private readonly nondeterministic: boolean;
137
+ private readonly affectsGameplay: boolean;
138
+
139
+ private constructor(
140
+ grants: readonly ParsedCapability[],
141
+ nondeterministic: boolean,
142
+ affectsGameplay: boolean,
143
+ ) {
144
+ this.grants = grants;
145
+ this.nondeterministic = nondeterministic;
146
+ this.affectsGameplay = affectsGameplay;
147
+ }
148
+
149
+ /**
150
+ * Build a CapabilitySet from a pack manifest. Only `shape: plugin` packs
151
+ * may request capabilities (MOD_LIFECYCLE section 4): a content or tile
152
+ * pack with a non-empty `capabilities` list throws CapabilityError, since
153
+ * that shape cannot execute and so has nothing to grant capabilities to -
154
+ * the request signals author confusion or an upstream validation bug,
155
+ * not something to silently ignore.
156
+ */
157
+ static fromManifest(manifest: PackManifest): CapabilitySet {
158
+ const requested = manifest.capabilities ?? [];
159
+ if (!hasFacet(manifest, "plugin") && requested.length > 0) {
160
+ throw new CapabilityError(
161
+ `pack ${manifest.id}: only shape "plugin" packs may request capabilities ` +
162
+ `(this pack is shape "${manifest.shape}")`,
163
+ );
164
+ }
165
+ const grants = requested.map((cap) => parseCapability(cap));
166
+ return new CapabilitySet(
167
+ grants,
168
+ manifest.nondeterministic ?? false,
169
+ manifest.affectsGameplay ?? false,
170
+ );
171
+ }
172
+
173
+ /**
174
+ * True if `cap` is covered by a grant in this set, honoring the
175
+ * "state:*.read" and "network:*" wildcards. Throws CapabilityError if
176
+ * `cap` itself is not a recognized capability string.
177
+ */
178
+ has(cap: string): boolean {
179
+ const request = parseCapability(cap);
180
+ return this.grants.some((grant) => grantCovers(grant, request));
181
+ }
182
+
183
+ /**
184
+ * Throws CapabilityError, naming the missing capability and how to fix
185
+ * it, unless `cap` is granted. This is the guard the perceive/act facades
186
+ * call before honoring a plugin's request - an author-facing error, not
187
+ * a silent divergence.
188
+ */
189
+ check(cap: string): void {
190
+ if (this.has(cap)) return;
191
+ throw new CapabilityError(
192
+ `this plugin needs capability "${cap}"; add it to pack.json capabilities`,
193
+ );
194
+ }
195
+
196
+ /** True if the manifest declared `nondeterministic: true` (section 4). */
197
+ isNondeterministic(): boolean {
198
+ return this.nondeterministic;
199
+ }
200
+
201
+ /** True if the manifest declared `affectsGameplay: true`. */
202
+ isAffectsGameplay(): boolean {
203
+ return this.affectsGameplay;
204
+ }
205
+ }
package/src/compose.ts ADDED
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Record composition: how a stack of packs becomes one game.
3
+ *
4
+ * Every record in the composed game is identified by a PackRef
5
+ * ("<owner-pack>:<slug>"). Packs may:
6
+ * - add records (they become the owner),
7
+ * - patch records owned by packs they declare as dependencies
8
+ * (deep merge: objects merge per key, arrays and scalars replace,
9
+ * an explicit null deletes the key),
10
+ * - replace such records wholesale, or
11
+ * - remove them.
12
+ *
13
+ * The base game is pack zero ("core") and gets no special treatment:
14
+ * a total conversion is just a pack that replaces or removes core
15
+ * records. Composition is deterministic given the resolved load order,
16
+ * and every record carries provenance (owner plus every pack that
17
+ * modified it) for savefiles and debugging.
18
+ */
19
+
20
+ import type { PackManifest, PackRef } from "./manifest.js";
21
+ import { packRef } from "./manifest.js";
22
+ import { applyFieldPatch } from "./patch.js";
23
+ import type { FieldPatch } from "./patch.js";
24
+
25
+ export type JsonValue =
26
+ | null
27
+ | boolean
28
+ | number
29
+ | string
30
+ | JsonValue[]
31
+ | { [key: string]: JsonValue };
32
+ export type JsonRecord = { [key: string]: JsonValue };
33
+
34
+ /** One pack's contribution to one record file (e.g. "monster"). */
35
+ export interface FileContribution {
36
+ /** New records; this pack becomes their owner. Each needs a name. */
37
+ records?: JsonRecord[];
38
+ /** Deep-merge patches onto records owned by declared dependencies. */
39
+ patches?: Record<string, JsonRecord>;
40
+ /** Wholesale replacements (owner and ref are preserved). */
41
+ replaces?: Record<string, JsonRecord>;
42
+ /** Refs to delete from the composed game. */
43
+ removes?: string[];
44
+ /**
45
+ * Field-level patches (see patch.ts): ordered field ops per target ref.
46
+ * composePacks applies these in load order after the coarse `patches`/
47
+ * `replaces` for the same pack (each pack's ops fold onto the running
48
+ * value, which is identical to composeFieldPatches over the ordered
49
+ * list). The pre-launch conflict report (P7 phase 6) reads the same data
50
+ * to find same-field collisions without the false-positive whole-record
51
+ * conflicts `patches` produces.
52
+ */
53
+ fieldPatches?: Record<string, FieldPatch>;
54
+ }
55
+
56
+ export interface PackContent {
57
+ manifest: PackManifest;
58
+ /** Contributions keyed by record file: "monster", "object", ... */
59
+ files: Record<string, FileContribution>;
60
+ }
61
+
62
+ export interface ComposedRecord {
63
+ ref: PackRef;
64
+ /** The pack that added the record. */
65
+ owner: string;
66
+ /** Every pack that patched or replaced it, in load order. */
67
+ modifiedBy: string[];
68
+ value: JsonRecord;
69
+ }
70
+
71
+ export class ComposeError extends Error {}
72
+
73
+ /** Deep merge per the pack patch rules. Returns a new object. */
74
+ export function mergePatch(base: JsonRecord, patch: JsonRecord): JsonRecord {
75
+ const out: JsonRecord = { ...base };
76
+ for (const [key, val] of Object.entries(patch)) {
77
+ if (val === null) {
78
+ delete out[key];
79
+ } else if (
80
+ typeof val === "object" &&
81
+ !Array.isArray(val) &&
82
+ typeof out[key] === "object" &&
83
+ out[key] !== null &&
84
+ !Array.isArray(out[key])
85
+ ) {
86
+ out[key] = mergePatch(out[key] as JsonRecord, val as JsonRecord);
87
+ } else {
88
+ out[key] = val;
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ function mayModify(m: PackManifest, ownerPack: string): boolean {
95
+ return ownerPack === m.id || (m.dependencies ?? {})[ownerPack] !== undefined;
96
+ }
97
+
98
+ function ownerOf(ref: string): string {
99
+ const at = ref.indexOf(":");
100
+ return at === -1 ? "" : ref.slice(0, at);
101
+ }
102
+
103
+ /**
104
+ * Compose packs (already in resolved load order) into per-file record
105
+ * maps. Iteration order of each map is deterministic: records appear
106
+ * in the order their owning packs added them.
107
+ */
108
+ export function composePacks(
109
+ packs: readonly PackContent[],
110
+ ): Map<string, Map<PackRef, ComposedRecord>> {
111
+ const game = new Map<string, Map<PackRef, ComposedRecord>>();
112
+
113
+ for (const pack of packs) {
114
+ const pid = pack.manifest.id;
115
+ for (const [file, contrib] of Object.entries(pack.files)) {
116
+ let table = game.get(file);
117
+ if (!table) {
118
+ table = new Map();
119
+ game.set(file, table);
120
+ }
121
+
122
+ for (const rec of contrib.records ?? []) {
123
+ const name = rec["name"];
124
+ if (typeof name !== "string" || name.length === 0) {
125
+ throw new ComposeError(`${pid}/${file}: record without a name`);
126
+ }
127
+ const ref = packRef(pid, name);
128
+ if (table.has(ref)) {
129
+ throw new ComposeError(`${pid}/${file}: duplicate record ${ref}`);
130
+ }
131
+ table.set(ref, { ref, owner: pid, modifiedBy: [], value: rec });
132
+ }
133
+
134
+ for (const kind of ["patches", "replaces"] as const) {
135
+ for (const [refStr, body] of Object.entries(contrib[kind] ?? {})) {
136
+ const ref = refStr as PackRef;
137
+ const existing = table.get(ref);
138
+ if (!existing) {
139
+ const verb = kind === "patches" ? "patch" : "replace";
140
+ throw new ComposeError(`${pid}/${file}: ${verb} target ${ref} does not exist`);
141
+ }
142
+ if (!mayModify(pack.manifest, ownerOf(ref))) {
143
+ throw new ComposeError(
144
+ `${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`,
145
+ );
146
+ }
147
+ existing.value =
148
+ kind === "patches" ? mergePatch(existing.value, body) : body;
149
+ existing.modifiedBy.push(pid);
150
+ }
151
+ }
152
+
153
+ for (const [refStr, ops] of Object.entries(contrib.fieldPatches ?? {})) {
154
+ const ref = refStr as PackRef;
155
+ const existing = table.get(ref);
156
+ if (!existing) {
157
+ throw new ComposeError(
158
+ `${pid}/${file}: fieldPatch target ${ref} does not exist`,
159
+ );
160
+ }
161
+ if (!mayModify(pack.manifest, ownerOf(ref))) {
162
+ throw new ComposeError(
163
+ `${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`,
164
+ );
165
+ }
166
+ existing.value = applyFieldPatch(existing.value, ops);
167
+ existing.modifiedBy.push(pid);
168
+ }
169
+
170
+ for (const refStr of contrib.removes ?? []) {
171
+ const ref = refStr as PackRef;
172
+ if (!table.has(ref)) {
173
+ throw new ComposeError(`${pid}/${file}: remove target ${ref} does not exist`);
174
+ }
175
+ if (!mayModify(pack.manifest, ownerOf(ref))) {
176
+ throw new ComposeError(
177
+ `${pid}/${file}: cannot remove ${ref} without declaring ${ownerOf(ref)} as a dependency`,
178
+ );
179
+ }
180
+ table.delete(ref);
181
+ }
182
+ }
183
+ }
184
+
185
+ return game;
186
+ }