@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.
- package/LICENSE.md +43 -0
- package/README.md +72 -0
- package/dist/capabilities.d.ts +116 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +170 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/compose.d.ts +71 -0
- package/dist/compose.d.ts.map +1 -0
- package/dist/compose.js +118 -0
- package/dist/compose.js.map +1 -0
- package/dist/conflicts.d.ts +78 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +160 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +83 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +314 -0
- package/dist/loader.js.map +1 -0
- package/dist/manifest.d.ts +261 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +264 -0
- package/dist/manifest.js.map +1 -0
- package/dist/patch.d.ts +90 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +195 -0
- package/dist/patch.js.map +1 -0
- package/dist/record-key.d.ts +99 -0
- package/dist/record-key.d.ts.map +1 -0
- package/dist/record-key.js +157 -0
- package/dist/record-key.js.map +1 -0
- package/dist/resolve.d.ts +42 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +161 -0
- package/dist/resolve.js.map +1 -0
- package/dist/semver.d.ts +37 -0
- package/dist/semver.d.ts.map +1 -0
- package/dist/semver.js +212 -0
- package/dist/semver.js.map +1 -0
- package/package.json +58 -0
- package/src/capabilities.ts +205 -0
- package/src/compose.ts +186 -0
- package/src/conflicts.ts +242 -0
- package/src/index.ts +73 -0
- package/src/loader.ts +393 -0
- package/src/manifest.ts +523 -0
- package/src/patch.ts +257 -0
- package/src/record-key.ts +180 -0
- package/src/resolve.ts +175 -0
- 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
|
+
}
|