@rpgm-tools/neo-angband-mod-sdk 0.13.0 → 0.15.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 +5 -2
- package/dist/compose.d.ts +60 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +65 -22
- package/dist/compose.js.map +1 -1
- package/dist/contested.d.ts +126 -0
- package/dist/contested.d.ts.map +1 -0
- package/dist/contested.js +131 -0
- package/dist/contested.js.map +1 -0
- package/dist/engine.d.ts +37 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +47 -4
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +44 -4
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +47 -9
- package/dist/loader.js.map +1 -1
- package/dist/manifest.d.ts +167 -0
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +173 -3
- package/dist/manifest.js.map +1 -1
- package/dist/sections.d.ts +91 -0
- package/dist/sections.d.ts.map +1 -0
- package/dist/sections.js +288 -0
- package/dist/sections.js.map +1 -0
- package/dist/sort.d.ts +120 -0
- package/dist/sort.d.ts.map +1 -0
- package/dist/sort.js +355 -0
- package/dist/sort.js.map +1 -0
- package/package.json +1 -1
- package/src/compose.ts +120 -31
- package/src/contested.ts +222 -0
- package/src/engine.ts +60 -5
- package/src/index.ts +34 -2
- package/src/loader.ts +607 -540
- package/src/manifest.ts +353 -4
- package/src/sections.ts +329 -0
- package/src/sort.ts +429 -0
package/README.md
CHANGED
|
@@ -21,9 +21,12 @@ compose records without pulling in a game.
|
|
|
21
21
|
| Area | Exports |
|
|
22
22
|
| --- | --- |
|
|
23
23
|
| Manifests | `validateManifest`, `PackManifest`, `PackShape`, `packFacets`, `hasFacet`, `packRef`, `slugify` |
|
|
24
|
-
| Load order | `resolveLoadOrder
|
|
24
|
+
| Load order | `resolveLoadOrder` (enforces), `satisfies` (the semver subset the manifests use) |
|
|
25
|
+
| Auto-sort | `sortModOrder` (proposes, and cannot fail), `collectSortEdges`, `SortPin`, `SortTier`, `PACK_GROUPS` |
|
|
26
|
+
| Sections | `resolveSectionState`, `expandSections`, `sectionFlag`, `PackSection`, `SECTION_BANDS` — the named parts of a mod |
|
|
27
|
+
| Compatibility | `PackCompat`, `COMPAT_CLAIMS` — what an author may claim about another mod (never binding) |
|
|
25
28
|
| Composition | `composePacks`, `composeContentPacks`, `mergePatch`, `applyFieldPatch`, `composeFieldPatches` |
|
|
26
|
-
| Conflicts | `computeConflictReport
|
|
29
|
+
| Conflicts | `computeConflictReport` (records), `contestedSlots` / `describeContested` (every other layer), `Fold`, `foldDiscards` |
|
|
27
30
|
| Record identity | `recordKey`, `keySpecFor`, `KEYED_RECORD_FILES`, `RECORD_KEY_SPECS` |
|
|
28
31
|
| Capabilities | `CapabilitySet`, `parseCapability` — what a scripted plugin is allowed to reach |
|
|
29
32
|
|
package/dist/compose.d.ts
CHANGED
|
@@ -44,6 +44,23 @@ export interface FileContribution {
|
|
|
44
44
|
* conflicts `patches` produces.
|
|
45
45
|
*/
|
|
46
46
|
fieldPatches?: Record<string, FieldPatch>;
|
|
47
|
+
/**
|
|
48
|
+
* Contributions attributed to a NAMED PART of this pack, keyed by a section id
|
|
49
|
+
* the manifest declares (see PackSection). Each value is an ordinary
|
|
50
|
+
* FileContribution, so a section contributes exactly what the pack itself can.
|
|
51
|
+
*
|
|
52
|
+
* Nested rather than a `section` key on each entry because `patches`,
|
|
53
|
+
* `replaces` and `fieldPatches` are all keyed BY REF - there is no room for a
|
|
54
|
+
* per-entry tag without changing three shapes, and every existing pack would
|
|
55
|
+
* have had to be rewritten. This way an unsectioned contribution stays exactly
|
|
56
|
+
* where it is and belongs to the pack's implicit default part.
|
|
57
|
+
*
|
|
58
|
+
* composePacks never sees this: expandSections (sections.ts) drops the
|
|
59
|
+
* disabled sections and flattens the rest into the pack list, in band order,
|
|
60
|
+
* BEFORE composition. So a switched-off section is absent rather than
|
|
61
|
+
* overridden - the same rule a disabled mod's hooks follow.
|
|
62
|
+
*/
|
|
63
|
+
sections?: Record<string, FileContribution>;
|
|
47
64
|
}
|
|
48
65
|
export interface PackContent {
|
|
49
66
|
manifest: PackManifest;
|
|
@@ -62,10 +79,52 @@ export declare class ComposeError extends Error {
|
|
|
62
79
|
}
|
|
63
80
|
/** Deep merge per the pack patch rules. Returns a new object. */
|
|
64
81
|
export declare function mergePatch(base: JsonRecord, patch: JsonRecord): JsonRecord;
|
|
82
|
+
/**
|
|
83
|
+
* How composePacks should react to a contribution it cannot honour.
|
|
84
|
+
*
|
|
85
|
+
* WHY THIS IS AN OPTION AND NOT A DECISION. The same event has two right answers
|
|
86
|
+
* depending on who is watching. A mod's BUILD should stop dead on a patch aimed
|
|
87
|
+
* at nothing, because the author is right there and a silent no-op is the worst
|
|
88
|
+
* thing you can hand them. A player's GAME should not: their mod was fine when it
|
|
89
|
+
* was published and the record it patches has since moved, and taking the whole
|
|
90
|
+
* mod away - or the whole game - is a punishment for the engine's change.
|
|
91
|
+
*
|
|
92
|
+
* So the throwing behaviour stays the default (every existing caller and the
|
|
93
|
+
* author-facing tooling keep it) and the host passes a reporter.
|
|
94
|
+
*/
|
|
95
|
+
export interface ComposePacksOptions {
|
|
96
|
+
/**
|
|
97
|
+
* Called instead of throwing, with the offending pack's id kept separate from
|
|
98
|
+
* the sentence so a host can put the line on that mod's own row. The
|
|
99
|
+
* contribution is then SKIPPED and composition continues.
|
|
100
|
+
*/
|
|
101
|
+
readonly onRefuse?: (packId: string, why: string) => void;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The tail every "no such record" refusal carries, in both merge phases.
|
|
105
|
+
*
|
|
106
|
+
* Said because the likeliest cause is not a typo. A pack that composed when it
|
|
107
|
+
* was published and does not now is usually pointing at a record the engine or
|
|
108
|
+
* another pack has since renamed, and an author reading "does not exist" about a
|
|
109
|
+
* ref they know they got right will go looking in the wrong place.
|
|
110
|
+
*/
|
|
111
|
+
export declare const RENAMED_HINT = " - it may have been renamed or removed by a newer version of the pack that owns it";
|
|
65
112
|
/**
|
|
66
113
|
* Compose packs (already in resolved load order) into per-file record
|
|
67
114
|
* maps. Iteration order of each map is deterministic: records appear
|
|
68
115
|
* in the order their owning packs added them.
|
|
116
|
+
*
|
|
117
|
+
* ONE BROKEN OP COSTS THAT OP, when `onRefuse` is supplied. Until 2026-08-02 the
|
|
118
|
+
* only behaviour was to throw, and the caller that mattered - composeContentPacks
|
|
119
|
+
* on the web host - sat under composeDroppingBroken, which answers a throw by
|
|
120
|
+
* removing the whole PACK. The result was an asymmetry nobody chose: the 20
|
|
121
|
+
* passthrough record files reported a missing ref and carried on, and the 24
|
|
122
|
+
* composable ones took the entire mod down for the same author mistake. A mod
|
|
123
|
+
* patching forty monsters lost all forty, plus its code and its rules, because
|
|
124
|
+
* one of the forty had been renamed in the engine.
|
|
125
|
+
*
|
|
126
|
+
* That is also the difference between an engine patch that costs mod authors a
|
|
127
|
+
* release and one that costs them nothing, which is the property this exists for.
|
|
69
128
|
*/
|
|
70
|
-
export declare function composePacks(packs: readonly PackContent[]): Map<string, Map<PackRef, ComposedRecord>>;
|
|
129
|
+
export declare function composePacks(packs: readonly PackContent[], options?: ComposePacksOptions): Map<string, Map<PackRef, ComposedRecord>>;
|
|
71
130
|
//# sourceMappingURL=compose.d.ts.map
|
package/dist/compose.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACjC,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,MAAM,MAAM,SAAS,GACjB,IAAI,GACJ,OAAO,GACP,MAAM,GACN,MAAM,GACN,SAAS,EAAE,GACX;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AACjC,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAEtD,mEAAmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACrC,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACtC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAC1C;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,YAAY,CAAC;IACvB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,OAAO,CAAC;IACb,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,KAAK,EAAE,UAAU,CAAC;CACnB;AAED,qBAAa,YAAa,SAAQ,KAAK;CAAG;AAE1C,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAkB1E;AAWD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3D;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,uFAC6D,CAAC;AAUvF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,WAAW,EAAE,EAC7B,OAAO,GAAE,mBAAwB,GAChC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC,CA8F3C"}
|
package/dist/compose.js
CHANGED
|
@@ -47,43 +47,94 @@ function ownerOf(ref) {
|
|
|
47
47
|
const at = ref.indexOf(":");
|
|
48
48
|
return at === -1 ? "" : ref.slice(0, at);
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The tail every "no such record" refusal carries, in both merge phases.
|
|
52
|
+
*
|
|
53
|
+
* Said because the likeliest cause is not a typo. A pack that composed when it
|
|
54
|
+
* was published and does not now is usually pointing at a record the engine or
|
|
55
|
+
* another pack has since renamed, and an author reading "does not exist" about a
|
|
56
|
+
* ref they know they got right will go looking in the wrong place.
|
|
57
|
+
*/
|
|
58
|
+
export const RENAMED_HINT = " - it may have been renamed or removed by a newer version of the pack that owns it";
|
|
59
|
+
/** How a refused op reads, matched to the passthrough path's wording. */
|
|
60
|
+
const REF_VERB = {
|
|
61
|
+
patches: "patches",
|
|
62
|
+
replaces: "replaces",
|
|
63
|
+
fieldPatches: "fieldPatches",
|
|
64
|
+
removes: "removes",
|
|
65
|
+
};
|
|
50
66
|
/**
|
|
51
67
|
* Compose packs (already in resolved load order) into per-file record
|
|
52
68
|
* maps. Iteration order of each map is deterministic: records appear
|
|
53
69
|
* in the order their owning packs added them.
|
|
70
|
+
*
|
|
71
|
+
* ONE BROKEN OP COSTS THAT OP, when `onRefuse` is supplied. Until 2026-08-02 the
|
|
72
|
+
* only behaviour was to throw, and the caller that mattered - composeContentPacks
|
|
73
|
+
* on the web host - sat under composeDroppingBroken, which answers a throw by
|
|
74
|
+
* removing the whole PACK. The result was an asymmetry nobody chose: the 20
|
|
75
|
+
* passthrough record files reported a missing ref and carried on, and the 24
|
|
76
|
+
* composable ones took the entire mod down for the same author mistake. A mod
|
|
77
|
+
* patching forty monsters lost all forty, plus its code and its rules, because
|
|
78
|
+
* one of the forty had been renamed in the engine.
|
|
79
|
+
*
|
|
80
|
+
* That is also the difference between an engine patch that costs mod authors a
|
|
81
|
+
* release and one that costs them nothing, which is the property this exists for.
|
|
54
82
|
*/
|
|
55
|
-
export function composePacks(packs) {
|
|
83
|
+
export function composePacks(packs, options = {}) {
|
|
56
84
|
const game = new Map();
|
|
85
|
+
const onRefuse = options.onRefuse;
|
|
57
86
|
for (const pack of packs) {
|
|
58
87
|
const pid = pack.manifest.id;
|
|
88
|
+
/** Report and skip, or throw when nobody is listening. Returns false either way. */
|
|
89
|
+
const refuse = (why, thrown) => {
|
|
90
|
+
if (!onRefuse)
|
|
91
|
+
throw new ComposeError(`${pid}/${thrown}`);
|
|
92
|
+
onRefuse(pid, why);
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
59
95
|
for (const [file, contrib] of Object.entries(pack.files)) {
|
|
60
96
|
let table = game.get(file);
|
|
61
97
|
if (!table) {
|
|
62
98
|
table = new Map();
|
|
63
99
|
game.set(file, table);
|
|
64
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Can `pid` touch `ref`, and does it exist? Reports the same two reasons
|
|
103
|
+
* the passthrough path reports, in the same words, so one mod's row does
|
|
104
|
+
* not read differently depending on which of the two merge phases its file
|
|
105
|
+
* happened to land in.
|
|
106
|
+
*/
|
|
107
|
+
const addressable = (kind, ref) => {
|
|
108
|
+
const verb = REF_VERB[kind];
|
|
109
|
+
if (!table.has(ref)) {
|
|
110
|
+
const noun = kind === "removes" ? "remove" : kind === "replaces" ? "replace" : kind === "patches" ? "patch" : "fieldPatch";
|
|
111
|
+
return refuse(`${file} ${verb} "${ref}", but no such record exists in ${file} (identity is the record's name)${RENAMED_HINT}`, `${file}: ${noun} target ${ref} does not exist`);
|
|
112
|
+
}
|
|
113
|
+
if (!mayModify(pack.manifest, ownerOf(ref))) {
|
|
114
|
+
const act = kind === "removes" ? "remove" : "modify";
|
|
115
|
+
return refuse(`${file} ${verb} "${ref}", but ${pid} does not declare ${ownerOf(ref)} as a dependency`, `${file}: cannot ${act} ${ref} without declaring ${ownerOf(ref)} as a dependency`);
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
65
119
|
for (const rec of contrib.records ?? []) {
|
|
66
120
|
const name = rec["name"];
|
|
67
121
|
if (typeof name !== "string" || name.length === 0) {
|
|
68
|
-
|
|
122
|
+
refuse(`${file} contributes a record with no "name", so nothing can address it and it was left out`, `${file}: record without a name`);
|
|
123
|
+
continue;
|
|
69
124
|
}
|
|
70
125
|
const ref = packRef(pid, name);
|
|
71
126
|
if (table.has(ref)) {
|
|
72
|
-
|
|
127
|
+
refuse(`${file} adds two records that both resolve to "${ref}", so the second was left out`, `${file}: duplicate record ${ref}`);
|
|
128
|
+
continue;
|
|
73
129
|
}
|
|
74
130
|
table.set(ref, { ref, owner: pid, modifiedBy: [], value: rec });
|
|
75
131
|
}
|
|
76
132
|
for (const kind of ["patches", "replaces"]) {
|
|
77
133
|
for (const [refStr, body] of Object.entries(contrib[kind] ?? {})) {
|
|
78
134
|
const ref = refStr;
|
|
135
|
+
if (!addressable(kind, ref))
|
|
136
|
+
continue;
|
|
79
137
|
const existing = table.get(ref);
|
|
80
|
-
if (!existing) {
|
|
81
|
-
const verb = kind === "patches" ? "patch" : "replace";
|
|
82
|
-
throw new ComposeError(`${pid}/${file}: ${verb} target ${ref} does not exist`);
|
|
83
|
-
}
|
|
84
|
-
if (!mayModify(pack.manifest, ownerOf(ref))) {
|
|
85
|
-
throw new ComposeError(`${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`);
|
|
86
|
-
}
|
|
87
138
|
existing.value =
|
|
88
139
|
kind === "patches" ? mergePatch(existing.value, body) : body;
|
|
89
140
|
existing.modifiedBy.push(pid);
|
|
@@ -91,24 +142,16 @@ export function composePacks(packs) {
|
|
|
91
142
|
}
|
|
92
143
|
for (const [refStr, ops] of Object.entries(contrib.fieldPatches ?? {})) {
|
|
93
144
|
const ref = refStr;
|
|
145
|
+
if (!addressable("fieldPatches", ref))
|
|
146
|
+
continue;
|
|
94
147
|
const existing = table.get(ref);
|
|
95
|
-
if (!existing) {
|
|
96
|
-
throw new ComposeError(`${pid}/${file}: fieldPatch target ${ref} does not exist`);
|
|
97
|
-
}
|
|
98
|
-
if (!mayModify(pack.manifest, ownerOf(ref))) {
|
|
99
|
-
throw new ComposeError(`${pid}/${file}: cannot modify ${ref} without declaring ${ownerOf(ref)} as a dependency`);
|
|
100
|
-
}
|
|
101
148
|
existing.value = applyFieldPatch(existing.value, ops);
|
|
102
149
|
existing.modifiedBy.push(pid);
|
|
103
150
|
}
|
|
104
151
|
for (const refStr of contrib.removes ?? []) {
|
|
105
152
|
const ref = refStr;
|
|
106
|
-
if (!
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
if (!mayModify(pack.manifest, ownerOf(ref))) {
|
|
110
|
-
throw new ComposeError(`${pid}/${file}: cannot remove ${ref} without declaring ${ownerOf(ref)} as a dependency`);
|
|
111
|
-
}
|
|
153
|
+
if (!addressable("removes", ref))
|
|
154
|
+
continue;
|
|
112
155
|
table.delete(ref);
|
|
113
156
|
}
|
|
114
157
|
}
|
package/dist/compose.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../src/compose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One conflict report over every composition layer.
|
|
3
|
+
*
|
|
4
|
+
* WHAT WAS WRONG. `computeConflictReport` (conflicts.ts) walks `pack.files`, so
|
|
5
|
+
* it sees CONTENT RECORDS and nothing else. Four other layers collided in
|
|
6
|
+
* complete silence:
|
|
7
|
+
*
|
|
8
|
+
* - GRAPHICS: two mods claiming one grafID. Nothing said so, and until
|
|
9
|
+
* 2026-08-01 the loser was the LATER mod, backwards from every other layer.
|
|
10
|
+
* - BEHAVIOUR: two mods contributing the same ModHooks member. For a
|
|
11
|
+
* last-answer hook the earlier mod's rule simply never runs, and until
|
|
12
|
+
* 2026-08-02 it was the LATER mod's that did not, backwards from the row the
|
|
13
|
+
* player reads.
|
|
14
|
+
* - RULES: two mods declaring the same flag string. `resolveModRules` is a flat
|
|
15
|
+
* last-wins namespace, so one mod silently reads the other's toggle.
|
|
16
|
+
* - CONTROLLER: two mods shipping an autoplayer. There is one slot; the second
|
|
17
|
+
* install wins and the first is not told.
|
|
18
|
+
*
|
|
19
|
+
* A report that sees one layer of five cannot be the substrate for author-
|
|
20
|
+
* declared conflict resolution, which is what this whole model needs it to be.
|
|
21
|
+
*
|
|
22
|
+
* WHY THE FOLD IS PART OF THE ANSWER. The layers do not resolve the same way and
|
|
23
|
+
* pretending they do would be the RimWorld trap - XML, then xpath, then C#, each
|
|
24
|
+
* with its own effective precedence, so "load order" quietly means three
|
|
25
|
+
* different things. Here the report NAMES the fold, because "these two combine"
|
|
26
|
+
* and "one of these is being ignored" are different news for the player and only
|
|
27
|
+
* one of them needs acting on.
|
|
28
|
+
*
|
|
29
|
+
* DERIVED, NOT DECLARED. Every claim is observed from what a mod actually
|
|
30
|
+
* contributes - the refs in its files, the hooks its factory returned, the
|
|
31
|
+
* grafIDs its manifest claims, the controller it handed over. A `touches` field
|
|
32
|
+
* in the manifest would have been easier and would drift the first time an
|
|
33
|
+
* author forgot to update it, which is the failure this report exists to catch.
|
|
34
|
+
*/
|
|
35
|
+
/** Which composition layer a contested slot belongs to. */
|
|
36
|
+
export type ContestedLayer = "record" | "graphics" | "behaviour" | "rule" | "controller";
|
|
37
|
+
/**
|
|
38
|
+
* How a layer resolves several claims on one slot.
|
|
39
|
+
*
|
|
40
|
+
* EVERY ONE OF THESE IS "THE LATER MOD WINS", which is the point: the mod
|
|
41
|
+
* manager promises the player one lever ("Move later (loads last, wins
|
|
42
|
+
* conflicts)"), and a lever that means five different things is not a lever. The
|
|
43
|
+
* folds differ only in whether there is anything for a winner to win.
|
|
44
|
+
*
|
|
45
|
+
* The three that DISCARD a claim - `last-wins`, `last-answer`, `single-slot` -
|
|
46
|
+
* are the ones worth a player's attention; the rest combine, and are reported so
|
|
47
|
+
* the picture is complete rather than because anything is wrong.
|
|
48
|
+
*/
|
|
49
|
+
export type Fold = "last-wins" | "last-answer" | "single-slot" | "all-must-agree" | "chained" | "any-yes";
|
|
50
|
+
/** Whether a fold silently drops somebody's contribution. */
|
|
51
|
+
export declare function foldDiscards(fold: Fold): boolean;
|
|
52
|
+
/** One mod's claim on one slot. */
|
|
53
|
+
export interface Claim {
|
|
54
|
+
/** The pack that contributed. */
|
|
55
|
+
packId: string;
|
|
56
|
+
/** The section it came from, when the pack attributed it to one. */
|
|
57
|
+
sectionId?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Set when a section's band moved this claim away from its pack's own
|
|
60
|
+
* position, so the report can explain an order the load list does not show.
|
|
61
|
+
*/
|
|
62
|
+
band?: string;
|
|
63
|
+
}
|
|
64
|
+
/** One thing more than one mod contributed to. */
|
|
65
|
+
export interface ContestedSlot {
|
|
66
|
+
layer: ContestedLayer;
|
|
67
|
+
/** Stable identity, for deduping and tests: "core:kobold.speed", "graphics:2". */
|
|
68
|
+
key: string;
|
|
69
|
+
/** What a player would call it: "kobold's speed", "the Graphics row for mode 2". */
|
|
70
|
+
what: string;
|
|
71
|
+
/** How this layer resolves the claims, and therefore whether one is discarded. */
|
|
72
|
+
fold: Fold;
|
|
73
|
+
/** Every claim, in load order. */
|
|
74
|
+
claims: Claim[];
|
|
75
|
+
/** The pack whose claim takes effect, when the fold picks one. */
|
|
76
|
+
winner?: string;
|
|
77
|
+
}
|
|
78
|
+
/** A pack's display name, for lines a player reads; falls back to the id. */
|
|
79
|
+
export type NameOf = (packId: string) => string;
|
|
80
|
+
/**
|
|
81
|
+
* One plain-language line per contested slot.
|
|
82
|
+
*
|
|
83
|
+
* Written so the sentence tells the player whether to DO anything: a discarding
|
|
84
|
+
* fold names the loser and the lever that changes it, a combining fold says the
|
|
85
|
+
* contributions stack. A line that just says "conflict" is what makes players
|
|
86
|
+
* stop reading conflict lists.
|
|
87
|
+
*/
|
|
88
|
+
export declare function describeContested(slot: ContestedSlot, nameOf?: NameOf): string;
|
|
89
|
+
/**
|
|
90
|
+
* Build the contested slots for one layer from a flat list of claims.
|
|
91
|
+
*
|
|
92
|
+
* `claims` is every claim on every key of that layer, IN LOAD ORDER. Keys with a
|
|
93
|
+
* single claimant are dropped - one mod changing something is not a conflict,
|
|
94
|
+
* and listing it would bury the ones that are.
|
|
95
|
+
*
|
|
96
|
+
* Two claims from the SAME pack on one key do not contest each other: a pack
|
|
97
|
+
* whose base contributions and one of its own sections both touch a field has
|
|
98
|
+
* made one decision in two places, and the pack's own order settles it.
|
|
99
|
+
*/
|
|
100
|
+
export declare function contestedSlots(layer: ContestedLayer, fold: Fold, claims: readonly {
|
|
101
|
+
key: string;
|
|
102
|
+
what: string;
|
|
103
|
+
claim: Claim;
|
|
104
|
+
}[]): ContestedSlot[];
|
|
105
|
+
/**
|
|
106
|
+
* An author's `conflicts` claim that applies to the current mod set.
|
|
107
|
+
*
|
|
108
|
+
* Carried separately from the contested slots because it is a DECLARATION rather
|
|
109
|
+
* than an observation: nobody measured a collision, an author stated one. It is
|
|
110
|
+
* shown with their reason attached and it never blocks - ratified decision 18,
|
|
111
|
+
* the engine labels and does not forbid, and a third-party author does not get a
|
|
112
|
+
* veto over the player's setup.
|
|
113
|
+
*/
|
|
114
|
+
export interface DeclaredConflict {
|
|
115
|
+
/** The pack making the claim. */
|
|
116
|
+
packId: string;
|
|
117
|
+
/** The pack it names. */
|
|
118
|
+
with: string;
|
|
119
|
+
/** The claimant's own sections the claim is about, if it scoped itself. */
|
|
120
|
+
scope?: string[];
|
|
121
|
+
/** The author's stated reason. */
|
|
122
|
+
because: string;
|
|
123
|
+
}
|
|
124
|
+
/** The warning line for a declared conflict. */
|
|
125
|
+
export declare function describeDeclaredConflict(conflict: DeclaredConflict, nameOf?: NameOf): string;
|
|
126
|
+
//# sourceMappingURL=contested.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contested.d.ts","sourceRoot":"","sources":["../src/contested.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GACtB,QAAQ,GACR,UAAU,GACV,WAAW,GACX,MAAM,GACN,YAAY,CAAC;AAEjB;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,IAAI,GACZ,WAAW,GACX,aAAa,GACb,aAAa,GACb,gBAAgB,GAChB,SAAS,GACT,SAAS,CAAC;AAEd,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAEhD;AAED,mCAAmC;AACnC,MAAM,WAAW,KAAK;IACpB,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,cAAc,CAAC;IACtB,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,kFAAkF;IAClF,IAAI,EAAE,IAAI,CAAC;IACX,kCAAkC;IAClC,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,6EAA6E;AAC7E,MAAM,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;AAehD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,GAAE,MAAmB,GAAG,MAAM,CA0B1F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE,IAAI,EACV,MAAM,EAAE,SAAS;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,EAAE,GAC7D,aAAa,EAAE,CA6BjB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,kCAAkC;IAClC,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gDAAgD;AAChD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,GAAE,MAAmB,GAC1B,MAAM,CAGR"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One conflict report over every composition layer.
|
|
3
|
+
*
|
|
4
|
+
* WHAT WAS WRONG. `computeConflictReport` (conflicts.ts) walks `pack.files`, so
|
|
5
|
+
* it sees CONTENT RECORDS and nothing else. Four other layers collided in
|
|
6
|
+
* complete silence:
|
|
7
|
+
*
|
|
8
|
+
* - GRAPHICS: two mods claiming one grafID. Nothing said so, and until
|
|
9
|
+
* 2026-08-01 the loser was the LATER mod, backwards from every other layer.
|
|
10
|
+
* - BEHAVIOUR: two mods contributing the same ModHooks member. For a
|
|
11
|
+
* last-answer hook the earlier mod's rule simply never runs, and until
|
|
12
|
+
* 2026-08-02 it was the LATER mod's that did not, backwards from the row the
|
|
13
|
+
* player reads.
|
|
14
|
+
* - RULES: two mods declaring the same flag string. `resolveModRules` is a flat
|
|
15
|
+
* last-wins namespace, so one mod silently reads the other's toggle.
|
|
16
|
+
* - CONTROLLER: two mods shipping an autoplayer. There is one slot; the second
|
|
17
|
+
* install wins and the first is not told.
|
|
18
|
+
*
|
|
19
|
+
* A report that sees one layer of five cannot be the substrate for author-
|
|
20
|
+
* declared conflict resolution, which is what this whole model needs it to be.
|
|
21
|
+
*
|
|
22
|
+
* WHY THE FOLD IS PART OF THE ANSWER. The layers do not resolve the same way and
|
|
23
|
+
* pretending they do would be the RimWorld trap - XML, then xpath, then C#, each
|
|
24
|
+
* with its own effective precedence, so "load order" quietly means three
|
|
25
|
+
* different things. Here the report NAMES the fold, because "these two combine"
|
|
26
|
+
* and "one of these is being ignored" are different news for the player and only
|
|
27
|
+
* one of them needs acting on.
|
|
28
|
+
*
|
|
29
|
+
* DERIVED, NOT DECLARED. Every claim is observed from what a mod actually
|
|
30
|
+
* contributes - the refs in its files, the hooks its factory returned, the
|
|
31
|
+
* grafIDs its manifest claims, the controller it handed over. A `touches` field
|
|
32
|
+
* in the manifest would have been easier and would drift the first time an
|
|
33
|
+
* author forgot to update it, which is the failure this report exists to catch.
|
|
34
|
+
*/
|
|
35
|
+
/** Whether a fold silently drops somebody's contribution. */
|
|
36
|
+
export function foldDiscards(fold) {
|
|
37
|
+
return fold === "last-wins" || fold === "last-answer" || fold === "single-slot";
|
|
38
|
+
}
|
|
39
|
+
/** "frost", "frost and runes", "frost, runes and mist". */
|
|
40
|
+
function joinNames(names) {
|
|
41
|
+
if (names.length <= 1)
|
|
42
|
+
return names.join("");
|
|
43
|
+
if (names.length === 2)
|
|
44
|
+
return `${names[0]} and ${names[1]}`;
|
|
45
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
46
|
+
}
|
|
47
|
+
/** "frost" or "frost (kobold rebalance)" when the claim named a section. */
|
|
48
|
+
function claimName(claim, nameOf) {
|
|
49
|
+
const base = nameOf(claim.packId);
|
|
50
|
+
return claim.sectionId ? `${base} (${claim.sectionId})` : base;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One plain-language line per contested slot.
|
|
54
|
+
*
|
|
55
|
+
* Written so the sentence tells the player whether to DO anything: a discarding
|
|
56
|
+
* fold names the loser and the lever that changes it, a combining fold says the
|
|
57
|
+
* contributions stack. A line that just says "conflict" is what makes players
|
|
58
|
+
* stop reading conflict lists.
|
|
59
|
+
*/
|
|
60
|
+
export function describeContested(slot, nameOf = (id) => id) {
|
|
61
|
+
const names = slot.claims.map((c) => claimName(c, nameOf));
|
|
62
|
+
const who = joinNames(names);
|
|
63
|
+
const winner = slot.winner ? nameOf(slot.winner) : undefined;
|
|
64
|
+
/* A band that repositioned the winning claim is the one thing the load-order
|
|
65
|
+
* list cannot show, so it is worth a clause of its own. */
|
|
66
|
+
const winning = slot.claims.find((c) => c.packId === slot.winner);
|
|
67
|
+
const banded = winning?.band && winning.band !== "normal"
|
|
68
|
+
? ` (its "${winning.sectionId}" part is set to load ${winning.band})`
|
|
69
|
+
: "";
|
|
70
|
+
switch (slot.fold) {
|
|
71
|
+
case "last-wins":
|
|
72
|
+
return `${who} all change ${slot.what}; ${winner} wins${banded} because it loads last.`;
|
|
73
|
+
case "last-answer":
|
|
74
|
+
return `${who} all handle ${slot.what}; only ${winner} runs${banded} because it loads last - the rest never get asked.`;
|
|
75
|
+
case "single-slot":
|
|
76
|
+
return `${who} each provide ${slot.what}, and there is room for one; ${winner} takes it${banded} and the others do nothing.`;
|
|
77
|
+
case "all-must-agree":
|
|
78
|
+
return `${who} all have a say in ${slot.what}; every one of them has to agree, so any single refusal decides.`;
|
|
79
|
+
case "chained":
|
|
80
|
+
return `${who} all change ${slot.what}, in load order, each one seeing the last one's result.`;
|
|
81
|
+
case "any-yes":
|
|
82
|
+
return `${who} all ask for ${slot.what}; one asking is enough, so they do not conflict.`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the contested slots for one layer from a flat list of claims.
|
|
87
|
+
*
|
|
88
|
+
* `claims` is every claim on every key of that layer, IN LOAD ORDER. Keys with a
|
|
89
|
+
* single claimant are dropped - one mod changing something is not a conflict,
|
|
90
|
+
* and listing it would bury the ones that are.
|
|
91
|
+
*
|
|
92
|
+
* Two claims from the SAME pack on one key do not contest each other: a pack
|
|
93
|
+
* whose base contributions and one of its own sections both touch a field has
|
|
94
|
+
* made one decision in two places, and the pack's own order settles it.
|
|
95
|
+
*/
|
|
96
|
+
export function contestedSlots(layer, fold, claims) {
|
|
97
|
+
const byKey = new Map();
|
|
98
|
+
for (const entry of claims) {
|
|
99
|
+
const slot = byKey.get(entry.key) ?? { what: entry.what, claims: [] };
|
|
100
|
+
slot.claims.push(entry.claim);
|
|
101
|
+
byKey.set(entry.key, slot);
|
|
102
|
+
}
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const [key, slot] of byKey) {
|
|
105
|
+
const packs = new Set(slot.claims.map((c) => c.packId));
|
|
106
|
+
if (packs.size < 2)
|
|
107
|
+
continue;
|
|
108
|
+
/* Every discarding fold now picks the SAME claim - the last one - which is
|
|
109
|
+
* the property this whole model is for. Kept as a `foldDiscards` test rather
|
|
110
|
+
* than collapsed into "last unless undefined", so adding a fold that picks
|
|
111
|
+
* differently has to come here and say so. */
|
|
112
|
+
const winner = foldDiscards(fold)
|
|
113
|
+
? slot.claims[slot.claims.length - 1]?.packId
|
|
114
|
+
: undefined;
|
|
115
|
+
out.push({
|
|
116
|
+
layer,
|
|
117
|
+
key,
|
|
118
|
+
what: slot.what,
|
|
119
|
+
fold,
|
|
120
|
+
claims: slot.claims,
|
|
121
|
+
...(winner === undefined ? {} : { winner }),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
/** The warning line for a declared conflict. */
|
|
127
|
+
export function describeDeclaredConflict(conflict, nameOf = (id) => id) {
|
|
128
|
+
const where = conflict.scope?.length ? ` over ${conflict.scope.join(", ")}` : "";
|
|
129
|
+
return `${nameOf(conflict.packId)} says it conflicts with ${nameOf(conflict.with)}${where}: ${conflict.because}`;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=contested.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contested.js","sourceRoot":"","sources":["../src/contested.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
CHANGED
|
@@ -27,6 +27,21 @@
|
|
|
27
27
|
* A pack can fail either, both, or neither, and the reasons a player must read are
|
|
28
28
|
* different in each case - so this returns a discriminated verdict rather than a
|
|
29
29
|
* boolean, and the caller keeps them apart.
|
|
30
|
+
*
|
|
31
|
+
* AND A FAILED RANGE IS NOT ALWAYS A REFUSAL (2026-08-02). Ratified decision 18
|
|
32
|
+
* is that the engine LABELS and does not FORBID, and this gate was applying it
|
|
33
|
+
* backwards: an out-of-range `engine` refused to load a pack of JSON that would
|
|
34
|
+
* have composed perfectly well. Nothing in a data pack's manifest can make its
|
|
35
|
+
* data unloadable - the worst case is a patch aimed at a record that has since
|
|
36
|
+
* moved, and composePacks now reports that one op and keeps the rest.
|
|
37
|
+
*
|
|
38
|
+
* So the range is a GATE on a pack that ships code and a LABEL on one that does
|
|
39
|
+
* not, and `modApi` is the signal, because the manifest already requires it of
|
|
40
|
+
* exactly the packs that ship a plugin.js. Code is the thing that genuinely
|
|
41
|
+
* breaks across an engine release: it calls functions, and a function that has
|
|
42
|
+
* been renamed is a crash rather than a warning. This is the difference between
|
|
43
|
+
* an engine patch that costs every content author a release and one that costs
|
|
44
|
+
* them nothing, which is the whole reason the distinction is here.
|
|
30
45
|
*/
|
|
31
46
|
import type { PackManifest } from "./manifest.js";
|
|
32
47
|
/**
|
|
@@ -43,6 +58,18 @@ export type EngineVerdict = {
|
|
|
43
58
|
} | {
|
|
44
59
|
readonly ok: false;
|
|
45
60
|
readonly kind: "out-of-range" | "bad-manifest";
|
|
61
|
+
/**
|
|
62
|
+
* Whether this is a REFUSAL or a warning to show beside a pack that loads
|
|
63
|
+
* anyway. True exactly when the pack ships code (`modApi` is declared).
|
|
64
|
+
*
|
|
65
|
+
* Separate from `ok` rather than folded into it, because a caller that
|
|
66
|
+
* only wants to know "did the versions line up" and a caller deciding "do I
|
|
67
|
+
* load this" are asking different questions, and collapsing them is how the
|
|
68
|
+
* warning would end up either suppressed or treated as fatal. `ok: false,
|
|
69
|
+
* blocks: false` is a real and common state: the pack loads, and the player
|
|
70
|
+
* is told why it might misbehave.
|
|
71
|
+
*/
|
|
72
|
+
readonly blocks: boolean;
|
|
46
73
|
readonly why: string;
|
|
47
74
|
};
|
|
48
75
|
/**
|
|
@@ -58,5 +85,14 @@ export type EngineVerdict = {
|
|
|
58
85
|
* Pure, and takes the version as an argument rather than importing it, so a test
|
|
59
86
|
* can drive any build and so mod-sdk stays independent of core.
|
|
60
87
|
*/
|
|
61
|
-
export declare function engineVerdict(manifest: Pick<PackManifest, "engine">, engineVersion: string
|
|
88
|
+
export declare function engineVerdict(manifest: Pick<PackManifest, "engine" | "modApi">, engineVersion: string,
|
|
89
|
+
/**
|
|
90
|
+
* Whether this pack ships CODE, and therefore whether a mismatch is a gate or
|
|
91
|
+
* a label. Defaults to what the manifest says, and is overridable for the one
|
|
92
|
+
* caller that knows better: the plugin loader is holding a plugin.js it can
|
|
93
|
+
* see, and it runs this gate BEFORE it checks whether `modApi` was declared at
|
|
94
|
+
* all - so a code pack that forgot the field must not buy the lenient path
|
|
95
|
+
* with the omission.
|
|
96
|
+
*/
|
|
97
|
+
hasCode?: boolean): EngineVerdict;
|
|
62
98
|
//# sourceMappingURL=engine.d.ts.map
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GACrB;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB,CAAC;AAIN;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,IAAI,CAAC,YAAY,EAAE,QAAQ,GAAG,QAAQ,CAAC,EACjD,aAAa,EAAE,MAAM;AACrB;;;;;;;GAOG;AACH,OAAO,GAAE,OAAuC,GAC/C,aAAa,CA0Df"}
|