@heroiclands/package-build 17.1.0 → 18.0.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/CHANGELOG.md +1773 -0
- package/CONTENT.md +288 -72
- package/README.md +1 -1
- package/bin/content-build.mjs +218 -137
- package/bin/package-build.mjs +43 -0
- package/content-config.mjs +62 -67
- package/docs/content-format.md +768 -58
- package/engine/actor-compiler.mjs +586 -0
- package/engine/address-charset.mjs +11 -8
- package/engine/address-diff.mjs +266 -22
- package/engine/anchored-sections.mjs +83 -0
- package/engine/anchors.mjs +83 -0
- package/engine/base-compiler.mjs +148 -18
- package/engine/bundle-notes.mjs +276 -0
- package/engine/bundles.mjs +307 -0
- package/engine/code-fences.mjs +103 -0
- package/engine/compile-corpus.mjs +89 -0
- package/engine/content-address.mjs +267 -5
- package/engine/content-format-check.mjs +54 -11
- package/engine/content-format.mjs +37 -5
- package/engine/content-index.mjs +394 -111
- package/engine/content-links.mjs +185 -77
- package/engine/content-lint.mjs +232 -14
- package/engine/content-package.mjs +2 -1
- package/engine/content-tables.mjs +99 -3
- package/engine/document-subtypes.mjs +49 -6
- package/engine/field-reference.mjs +1 -1
- package/engine/field-spec.mjs +56 -6
- package/engine/folder-notes.mjs +470 -0
- package/engine/foreign-catalog.mjs +195 -4
- package/engine/{manifest-emit.mjs → foundry-entries.mjs} +78 -81
- package/engine/frontmatter-lint.mjs +245 -19
- package/engine/frontmatter.mjs +88 -8
- package/engine/generate.mjs +304 -13
- package/engine/helpers.mjs +293 -64
- package/engine/ids.mjs +137 -2
- package/engine/index-records.mjs +126 -0
- package/engine/index.mjs +23 -5
- package/engine/item-compiler.mjs +349 -0
- package/engine/item-docs.mjs +7 -1
- package/engine/item-registry.mjs +6 -0
- package/engine/journals.mjs +93 -27
- package/engine/macros.mjs +4 -2
- package/engine/metadata-index.mjs +495 -0
- package/engine/note-claims.mjs +146 -25
- package/engine/note-ids.mjs +112 -0
- package/engine/note-renames.mjs +134 -0
- package/engine/note-schemas.mjs +25 -0
- package/engine/note-vocabulary.mjs +110 -27
- package/engine/pack-config.mjs +4 -0
- package/engine/pack-router.mjs +23 -0
- package/engine/retired-fields.mjs +137 -3
- package/engine/scenes.mjs +66 -34
- package/engine/schema-check.mjs +58 -17
- package/engine/site-build.mjs +71 -12
- package/engine/site-index.mjs +20 -7
- package/engine/sql-tables.mjs +485 -0
- package/engine/subtype-registry.mjs +102 -0
- package/engine/system-block.mjs +96 -12
- package/engine/systems.mjs +201 -0
- package/engine/web-wikilinks.mjs +50 -20
- package/engine/wikilink-syntax.mjs +17 -8
- package/engine/wikilinks.mjs +124 -51
- package/engine/yaml-lint.mjs +282 -0
- package/hm3/actors.mjs +352 -0
- package/hm3/default-item-art.mjs +75 -0
- package/hm3/document-subtypes.mjs +134 -0
- package/hm3/index.mjs +56 -0
- package/hm3/item-builders.mjs +84 -0
- package/hm3/item-fields.mjs +180 -0
- package/hm3/items.mjs +96 -0
- package/hm3/template-priority.mjs +85 -0
- package/labels.mjs +191 -0
- package/manifest.mjs +43 -2
- package/package.json +19 -4
- package/release.mjs +62 -7
- package/sohl/actors.mjs +33 -487
- package/sohl/being-info.mjs +16 -7
- package/sohl/default-item-art.mjs +14 -3
- package/sohl/document-subtypes.mjs +16 -10
- package/sohl/item-builders.mjs +14 -5
- package/sohl/item-fields.mjs +68 -7
- package/sohl/items.mjs +44 -258
- package/sohl/note-schemas.mjs +2 -2
- package/types/content-config.d.mts +21 -42
- package/types/engine/actor-compiler.d.mts +204 -0
- package/types/engine/address-charset.d.mts +11 -8
- package/types/engine/address-diff.d.mts +53 -5
- package/types/engine/anchored-sections.d.mts +21 -0
- package/types/engine/anchors.d.mts +20 -0
- package/types/engine/base-compiler.d.mts +17 -17
- package/types/engine/bundle-notes.d.mts +173 -0
- package/types/engine/bundles.d.mts +60 -0
- package/types/engine/code-fences.d.mts +43 -0
- package/types/engine/compile-corpus.d.mts +32 -0
- package/types/engine/content-address.d.mts +205 -5
- package/types/engine/content-format-check.d.mts +6 -2
- package/types/engine/content-format.d.mts +57 -1
- package/types/engine/content-index.d.mts +64 -80
- package/types/engine/content-links.d.mts +52 -7
- package/types/engine/content-lint.d.mts +10 -1
- package/types/engine/content-package.d.mts +2 -1
- package/types/engine/content-tables.d.mts +21 -39
- package/types/engine/document-subtypes.d.mts +37 -3
- package/types/engine/field-spec.d.mts +76 -5
- package/types/engine/folder-notes.d.mts +159 -0
- package/types/engine/foreign-catalog.d.mts +53 -0
- package/types/engine/{manifest-emit.d.mts → foundry-entries.d.mts} +16 -38
- package/types/engine/frontmatter-lint.d.mts +10 -2
- package/types/engine/frontmatter.d.mts +64 -0
- package/types/engine/generate.d.mts +38 -0
- package/types/engine/helpers.d.mts +94 -30
- package/types/engine/ids.d.mts +96 -0
- package/types/engine/index-records.d.mts +68 -0
- package/types/engine/index.d.mts +9 -3
- package/types/engine/item-compiler.d.mts +131 -0
- package/types/engine/journals.d.mts +47 -9
- package/types/engine/metadata-index.d.mts +226 -0
- package/types/engine/note-claims.d.mts +57 -11
- package/types/engine/note-ids.d.mts +38 -0
- package/types/engine/note-renames.d.mts +102 -0
- package/types/engine/note-vocabulary.d.mts +44 -8
- package/types/engine/retired-fields.d.mts +75 -0
- package/types/engine/scenes.d.mts +3 -2
- package/types/engine/schema-check.d.mts +25 -4
- package/types/engine/site-build.d.mts +4 -4
- package/types/engine/site-index.d.mts +1 -1
- package/types/engine/sql-tables.d.mts +185 -0
- package/types/engine/subtype-registry.d.mts +49 -0
- package/types/engine/system-block.d.mts +40 -1
- package/types/engine/systems.d.mts +106 -0
- package/types/engine/web-wikilinks.d.mts +4 -2
- package/types/engine/wikilink-syntax.d.mts +10 -3
- package/types/engine/wikilinks.d.mts +41 -13
- package/types/engine/yaml-lint.d.mts +107 -0
- package/types/hm3/actors.d.mts +48 -0
- package/types/hm3/default-item-art.d.mts +42 -0
- package/types/hm3/document-subtypes.d.mts +24 -0
- package/types/hm3/index.d.mts +7 -0
- package/types/hm3/item-builders.d.mts +11 -0
- package/types/hm3/item-fields.d.mts +12 -0
- package/types/hm3/items.d.mts +23 -0
- package/types/hm3/template-priority.d.mts +21 -0
- package/types/manifest.d.mts +22 -1
- package/types/release.d.mts +9 -4
- package/types/sohl/actors.d.mts +7 -74
- package/types/sohl/being-info.d.mts +8 -27
- package/types/sohl/default-item-art.d.mts +5 -3
- package/types/sohl/items.d.mts +17 -32
- package/engine/foreign-manifests.mjs +0 -126
- package/engine/kb-manifest.mjs +0 -490
- package/types/engine/foreign-manifests.d.mts +0 -43
- package/types/engine/kb-manifest.d.mts +0 -241
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the Song of Heroic Lands (SoHL) system for Foundry VTT.
|
|
3
|
+
* Copyright (c) 2024-2026 Tom Rodriguez ("Toasty") — <toasty@heroiclands.org>
|
|
4
|
+
*
|
|
5
|
+
* This work is licensed under the GNU General Public License v3.0 (GPLv3).
|
|
6
|
+
* You may copy, modify, and distribute it under the terms of that license.
|
|
7
|
+
*
|
|
8
|
+
* For full terms, see the LICENSE.md file in the project root or visit:
|
|
9
|
+
* https://www.gnu.org/licenses/gpl-3.0.html
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* **The Actor pass, for any system** — the parts of compiling a note into a
|
|
16
|
+
* Foundry Actor that belong to the note format rather than to a game system
|
|
17
|
+
* (#139).
|
|
18
|
+
*
|
|
19
|
+
* The whole of it lived in `sohl/actors.mjs`, where the system-specific facts
|
|
20
|
+
* were already funnelled through one map (`static documentSubtypes`, added by
|
|
21
|
+
* #79) and one block constant. A second system needs everything except the
|
|
22
|
+
* shape of the `system` block itself, so that everything moved here and each
|
|
23
|
+
* half declares what differs:
|
|
24
|
+
*
|
|
25
|
+
* | stated by the subclass | what it decides |
|
|
26
|
+
* | --- | --- |
|
|
27
|
+
* | `static documentSubtypes` | the block its notes write, the types it claims, the subtype each becomes |
|
|
28
|
+
* | `buildEntry()` | the document its system's data model actually wants |
|
|
29
|
+
*
|
|
30
|
+
* What is shared is the machinery an actor of *any* system needs:
|
|
31
|
+
*
|
|
32
|
+
* - **The predefined-item catalogue.** Every Item pack's compiled output, read
|
|
33
|
+
* as one address space keyed by `subType:shortcode`, with a dependency
|
|
34
|
+
* catalogue behind it as a fallback rather than a peer.
|
|
35
|
+
* - **Reference translation.** A being addresses its embedded items in the
|
|
36
|
+
* *note* vocabulary and the catalogue is keyed in the *document's*, so
|
|
37
|
+
* {@link SystemActorCompiler#embeddedSubtype} translates each reference
|
|
38
|
+
* forward through this system's map before the lookup (#140).
|
|
39
|
+
* - **Embedding.** Merging a note's overlay onto a catalogue entry, deriving a
|
|
40
|
+
* stable embedded id from the owning actor and the address, and re-keying the
|
|
41
|
+
* embedded document and its effects for the LevelDB flattening.
|
|
42
|
+
* - **Anchored prose.** `{#appearance}` / `{#dossier}` are pulled out by
|
|
43
|
+
* {@link module:engine/anchored-sections}, re-exported here for the passes
|
|
44
|
+
* that reach it through this module; *which document field* each lands in is
|
|
45
|
+
* the system's, and is decided in `buildEntry`.
|
|
46
|
+
*
|
|
47
|
+
* @module
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import fs from "fs";
|
|
51
|
+
import path from "path";
|
|
52
|
+
import log from "loglevel";
|
|
53
|
+
|
|
54
|
+
import { makeId } from "./helpers.mjs";
|
|
55
|
+
import { emitDiagnostic } from "./diagnostics.mjs";
|
|
56
|
+
// The `{#appearance}` / `{#dossier}` convention is the note format's, so the
|
|
57
|
+
// extraction is shared; which field a section lands in stays the system's.
|
|
58
|
+
export { extractAnchorSection, renderSection } from "./anchored-sections.mjs";
|
|
59
|
+
import { BasePackCompiler } from "./base-compiler.mjs";
|
|
60
|
+
import { contentPackage } from "./content-package.mjs";
|
|
61
|
+
// Which Foundry Actor subtype a note's `type` compiles into, and which note
|
|
62
|
+
// types are actors at all. Looked up in the system's declared map, never
|
|
63
|
+
// inferred from the type itself (#79).
|
|
64
|
+
import { mapsNoteType, noteTypesFor, referencedSubtype } from "./document-subtypes.mjs";
|
|
65
|
+
import { locateFrontmatterKey } from "./retired-fields.mjs";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Strip compendium-only fields from a predefined item before embedding it
|
|
69
|
+
* inside an actor's `items[]`. These fields belong on a top-level
|
|
70
|
+
* compendium document, not on an embedded one.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} item - The catalogue entry.
|
|
73
|
+
* @returns {object} The entry, without its compendium-only fields.
|
|
74
|
+
*/
|
|
75
|
+
export function stripCompendiumFields(item) {
|
|
76
|
+
// eslint-disable-next-line no-unused-vars
|
|
77
|
+
const { _key, _stats, ownership, folder, ...rest } = item;
|
|
78
|
+
return rest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether a value is a plain object, for {@link deepMerge}.
|
|
83
|
+
*
|
|
84
|
+
* @param {unknown} v - The value.
|
|
85
|
+
* @returns {boolean} True for a plain object.
|
|
86
|
+
*/
|
|
87
|
+
export function isPlainObject(v) {
|
|
88
|
+
return (
|
|
89
|
+
v !== null &&
|
|
90
|
+
typeof v === "object" &&
|
|
91
|
+
!Array.isArray(v) &&
|
|
92
|
+
Object.getPrototypeOf(v) === Object.prototype
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Recursively merge `overlay` onto `base`. Plain objects merge key-by-key;
|
|
98
|
+
* everything else (arrays, primitives, null) replaces. Inputs are not
|
|
99
|
+
* mutated.
|
|
100
|
+
*
|
|
101
|
+
* @param {any} base - The catalogue entry.
|
|
102
|
+
* @param {any} overlay - The note's own values.
|
|
103
|
+
* @returns {any} The merged value.
|
|
104
|
+
*/
|
|
105
|
+
export function deepMerge(base, overlay) {
|
|
106
|
+
if (overlay === undefined) return base;
|
|
107
|
+
if (!isPlainObject(base) || !isPlainObject(overlay)) {
|
|
108
|
+
return overlay;
|
|
109
|
+
}
|
|
110
|
+
const out = { ...base };
|
|
111
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
112
|
+
out[k] = k in base ? deepMerge(base[k], v) : v;
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The key one predefined item is held under, and every place that spells it.
|
|
119
|
+
*
|
|
120
|
+
* **The vocabulary is the document's, not the note's** — `subType` is the
|
|
121
|
+
* Foundry Item subtype the compiled document carries, which is the only thing a
|
|
122
|
+
* compiled pack (or an extracted dependency catalogue) records about what an
|
|
123
|
+
* item *is*. An actor's frontmatter addresses the same item in the *note*
|
|
124
|
+
* vocabulary, so a reference is translated forward through the system's map
|
|
125
|
+
* before it reaches this function; see
|
|
126
|
+
* {@link SystemActorCompiler#embeddedSubtype} for why the translation goes that
|
|
127
|
+
* way and not the other (#140).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} subType - The Foundry Item subtype.
|
|
130
|
+
* @param {string} shortcode - The item's `system.shortcode`.
|
|
131
|
+
* @returns {string} The address, `subType:shortcode`.
|
|
132
|
+
*/
|
|
133
|
+
export function itemAddress(subType, shortcode) {
|
|
134
|
+
return `${subType}:${shortcode}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* What identifies one embedded item on its actor.
|
|
139
|
+
*
|
|
140
|
+
* **Its own `system.shortcode`** — not the entry's top-level `shortcode`, which
|
|
141
|
+
* merely *selects* the catalogue template the entry is written from and is
|
|
142
|
+
* never written to the document. Two daggers may share a selector; they are two
|
|
143
|
+
* embodiments and each must declare its own.
|
|
144
|
+
*
|
|
145
|
+
* The name is a last resort, for a **stand-alone** entry that names no template
|
|
146
|
+
* and states no shortcode. It is a poor identity — presentation, and free to be
|
|
147
|
+
* localized or to diverge — so it is not a fallback the compiler is content
|
|
148
|
+
* with: two entries reducing to one identity is refused either way, and the
|
|
149
|
+
* message says to state a `system.shortcode`.
|
|
150
|
+
*
|
|
151
|
+
* @param {object} item - The merged embedded item.
|
|
152
|
+
* @returns {string} The identity, for {@link embeddedItemId}.
|
|
153
|
+
*/
|
|
154
|
+
export function embeddedIdentity(item) {
|
|
155
|
+
const own = item?.system?.shortcode;
|
|
156
|
+
if (typeof own === "string" && own.trim()) return own.trim();
|
|
157
|
+
return typeof item?.name === "string" ? item.name : "";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The `_id` of one item embedded on an actor.
|
|
162
|
+
*
|
|
163
|
+
* Seeded by the owning actor, because an embedded id must be unique within its
|
|
164
|
+
* **parent document** and nothing wider — so the namespace is already exactly
|
|
165
|
+
* the scope the uniqueness is required in, and the largest namespace in the
|
|
166
|
+
* four corpora holds 180 items, at which 64 bits collide with probability
|
|
167
|
+
* around 10⁻¹⁵.
|
|
168
|
+
*
|
|
169
|
+
* **It takes no index** (#268). Keying on a position meant reordering a being's
|
|
170
|
+
* item list renumbered every id after the change, so a re-import created new
|
|
171
|
+
* documents beside the old ones — while nothing about those documents had
|
|
172
|
+
* changed, only their neighbours. The identity always exists or must be stated;
|
|
173
|
+
* see {@link embeddedIdentity}.
|
|
174
|
+
*
|
|
175
|
+
* Keyed by the **document subtype**, so renaming a note type (#78) leaves every
|
|
176
|
+
* embedded id where it was.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} actorId - The owning actor's id.
|
|
179
|
+
* @param {string} subType - The Foundry Item subtype.
|
|
180
|
+
* @param {string} identity - From {@link embeddedIdentity}.
|
|
181
|
+
* @returns {string} A 16-character Foundry id.
|
|
182
|
+
*/
|
|
183
|
+
export function embeddedItemId(actorId, subType, identity) {
|
|
184
|
+
return makeId(actorId, itemAddress(subType, identity));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Load every JSON file under each of `itemsSourceDirs`, returning one Map keyed
|
|
189
|
+
* by {@link itemAddress} — the compiled document's **subtype** and its
|
|
190
|
+
* `system.shortcode`. Folder docs and entries without a shortcode are skipped.
|
|
191
|
+
* The `_key` field is stripped from each entry — it is not part of the item
|
|
192
|
+
* data model.
|
|
193
|
+
*
|
|
194
|
+
* The directories are read as one address space, because an actor names an item
|
|
195
|
+
* by `(type, shortcode)` and never by the pack it happens to ship in. Two local
|
|
196
|
+
* Item packs claiming the same address is therefore ambiguous rather than a
|
|
197
|
+
* last-one-wins ordering detail, and fails here.
|
|
198
|
+
*
|
|
199
|
+
* A **foreign** directory — the extracted item catalogue of a package this
|
|
200
|
+
* repository depends on but does not contain — is a fallback rather than a
|
|
201
|
+
* peer. A repository must be able to ship its own `skill:awar` that stands in
|
|
202
|
+
* front of the system's, so a local address shadows a foreign one instead of
|
|
203
|
+
* colliding with it. Local directories are therefore read first, and anything
|
|
204
|
+
* already claimed is left alone.
|
|
205
|
+
*
|
|
206
|
+
* @param {readonly string[]} itemsSourceDirs - Every local Item pack's JSON tree.
|
|
207
|
+
* @param {readonly string[]} [foreignSourceDirs] - Extracted dependency
|
|
208
|
+
* catalogues, consulted only for addresses no local pack defines.
|
|
209
|
+
* @returns {Map<string, object>} The predefined items, by address.
|
|
210
|
+
*/
|
|
211
|
+
export function loadItemsMap(itemsSourceDirs, foreignSourceDirs = []) {
|
|
212
|
+
const map = new Map();
|
|
213
|
+
const source = new Map();
|
|
214
|
+
const shadowed = [];
|
|
215
|
+
for (const itemsSourceDir of itemsSourceDirs) {
|
|
216
|
+
if (!fs.existsSync(itemsSourceDir)) {
|
|
217
|
+
// The generator orders the actors pass after every Item pass (#73),
|
|
218
|
+
// so a whole-package build cannot reach this. What can is a run
|
|
219
|
+
// restricted to this one pack, or a caller constructing the
|
|
220
|
+
// compiler itself — neither of which reordering a pack list fixes,
|
|
221
|
+
// so the message no longer suggests it.
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Items source directory ${itemsSourceDir} does not exist — ` +
|
|
224
|
+
`an actor resolves its embedded items against the Item ` +
|
|
225
|
+
`packs' compiled output, so those packs must be compiled ` +
|
|
226
|
+
`before this one`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
for (const name of fs.readdirSync(itemsSourceDir)) {
|
|
230
|
+
if (!name.endsWith(".json")) continue;
|
|
231
|
+
if (name.startsWith("folder_")) continue;
|
|
232
|
+
const full = path.join(itemsSourceDir, name);
|
|
233
|
+
let doc;
|
|
234
|
+
try {
|
|
235
|
+
doc = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
emitDiagnostic({
|
|
238
|
+
file: full,
|
|
239
|
+
severity: "warning",
|
|
240
|
+
message: `unparseable item JSON, skipping: ${err.message}`,
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const shortcode = doc?.system?.shortcode;
|
|
245
|
+
if (!doc?.type || !shortcode) continue;
|
|
246
|
+
const address = itemAddress(doc.type, shortcode);
|
|
247
|
+
const owner = source.get(address);
|
|
248
|
+
if (owner && owner !== itemsSourceDir) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Two Item packs both define "${address}" (${owner} and ` +
|
|
251
|
+
`${itemsSourceDir}); an actor addresses an item by ` +
|
|
252
|
+
`(type, shortcode), so the address must be unique across ` +
|
|
253
|
+
`every Item pack`,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
source.set(address, itemsSourceDir);
|
|
257
|
+
// eslint-disable-next-line no-unused-vars
|
|
258
|
+
const { _key, ...rest } = doc;
|
|
259
|
+
map.set(address, rest);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
for (const foreignDir of foreignSourceDirs) {
|
|
263
|
+
for (const name of fs.readdirSync(foreignDir)) {
|
|
264
|
+
if (!name.endsWith(".json")) continue;
|
|
265
|
+
if (name.startsWith("folder_")) continue;
|
|
266
|
+
const full = path.join(foreignDir, name);
|
|
267
|
+
let doc;
|
|
268
|
+
try {
|
|
269
|
+
doc = JSON.parse(fs.readFileSync(full, "utf8"));
|
|
270
|
+
} catch (err) {
|
|
271
|
+
emitDiagnostic({
|
|
272
|
+
file: full,
|
|
273
|
+
severity: "warning",
|
|
274
|
+
message: `unparseable item JSON, skipping: ${err.message}`,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const shortcode = doc?.system?.shortcode;
|
|
279
|
+
if (!doc?.type || !shortcode) continue;
|
|
280
|
+
const address = itemAddress(doc.type, shortcode);
|
|
281
|
+
if (map.has(address)) {
|
|
282
|
+
// Deliberate: this repository defines it, so its version wins.
|
|
283
|
+
if (source.has(address)) shadowed.push(address);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
// eslint-disable-next-line no-unused-vars
|
|
287
|
+
const { _key, ...rest } = doc;
|
|
288
|
+
map.set(address, rest);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (shadowed.length) {
|
|
292
|
+
log.info(
|
|
293
|
+
`${shadowed.length} dependency item(s) shadowed by this ` +
|
|
294
|
+
`repository's own: ${shadowed.slice(0, 5).join(", ")}` +
|
|
295
|
+
(shadowed.length > 5 ? ", …" : ""),
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
return map;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* The Actor compile pass of one game system.
|
|
303
|
+
*
|
|
304
|
+
* A subclass declares its {@link SystemActorCompiler.documentSubtypes} and
|
|
305
|
+
* implements `buildEntry`, which is where the system's own data model is: what
|
|
306
|
+
* an actor's `system` block holds, which art it defaults to, which frontmatter
|
|
307
|
+
* becomes embedded items. Nothing above that line is a system's business.
|
|
308
|
+
*/
|
|
309
|
+
export class SystemActorCompiler extends BasePackCompiler {
|
|
310
|
+
static id = "actors";
|
|
311
|
+
static label = "actor";
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Which `(actor, subType:identity)` each resolved entry claimed, and the
|
|
315
|
+
* entry that claimed it first.
|
|
316
|
+
*
|
|
317
|
+
* The scope the check needs is **one note**, and the actor's id supplies it
|
|
318
|
+
* for free: it is unique per note, so keying the map by it scopes the claim
|
|
319
|
+
* without this having to be told where a note begins and ends. That matters
|
|
320
|
+
* because {@link SystemActorCompiler#resolveEmbedded} is called per entry
|
|
321
|
+
* and has no note lifecycle of its own.
|
|
322
|
+
*
|
|
323
|
+
* `frontmatter-lint.mjs` makes the same finding from frontmatter alone
|
|
324
|
+
* (#228), and this does not replace it — the lint is a separate command, so
|
|
325
|
+
* without a check here a colliding pair would compile to two documents with
|
|
326
|
+
* one `_id` and reach the LevelDB packer as an opaque duplicate key.
|
|
327
|
+
*
|
|
328
|
+
* @type {Map<string, string>}
|
|
329
|
+
*/
|
|
330
|
+
#embeddedClaims = new Map();
|
|
331
|
+
|
|
332
|
+
// An actor's embedded items are resolved against the *output* of the item
|
|
333
|
+
// passes, so every Item pack compiles before this one. Declared rather than
|
|
334
|
+
// left to the order `packs:` happens to list (#73).
|
|
335
|
+
static readsPackOutputOf = Object.freeze(["Item"]);
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* An Actor **is** a system's data, so this pack takes only notes carrying
|
|
339
|
+
* this system's block (#58).
|
|
340
|
+
*/
|
|
341
|
+
static requiresSystemBlock = true;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The note-type → document-subtype map this pass compiles against.
|
|
345
|
+
*
|
|
346
|
+
* Stated by the class rather than reached for through a module import, so
|
|
347
|
+
* every subtype decision the pass makes — the actor's own, and each
|
|
348
|
+
* embedded item reference's — reads one declaration that a subclass
|
|
349
|
+
* compiling for another system replaces.
|
|
350
|
+
*
|
|
351
|
+
* @type {import("./document-subtypes.mjs").DocumentSubtypeMap|undefined}
|
|
352
|
+
*/
|
|
353
|
+
static documentSubtypes = undefined;
|
|
354
|
+
|
|
355
|
+
/** @type {readonly string[]} */
|
|
356
|
+
itemsSourceDirs;
|
|
357
|
+
foreignSourceDirs;
|
|
358
|
+
|
|
359
|
+
constructor({ itemsSourceDirs = [], foreignSourceDirs = [], ...options }) {
|
|
360
|
+
super(options);
|
|
361
|
+
// Where the items passes wrote their JSON. Stated by the caller rather
|
|
362
|
+
// than assumed to be this pack's sibling: the packs' locations are
|
|
363
|
+
// configuration, and a consumer may put them anywhere (#1508). Every
|
|
364
|
+
// Item pack, because a repository may ship more than one (#1566).
|
|
365
|
+
//
|
|
366
|
+
// **Optional, and empty is a legitimate package (#49).** This used to
|
|
367
|
+
// throw unless at least one Item pack was declared, which asked a
|
|
368
|
+
// package to declare the very thing it may exist not to have. An Item
|
|
369
|
+
// pack is system-bound by construction — Foundry requires `system` on
|
|
370
|
+
// Item packs — so a deliberately system-agnostic module could satisfy
|
|
371
|
+
// the guard only by naming a system. `harn-ensemble` is the case:
|
|
372
|
+
// 2,512 beings whose embedded items address the `sohl` and `hm3`
|
|
373
|
+
// catalogues, and five affiliation notes of its own.
|
|
374
|
+
//
|
|
375
|
+
// The guard also did not test what it claimed. It counted *declared
|
|
376
|
+
// directories*, not resolvable items, so an empty Item pack satisfied
|
|
377
|
+
// it while a being naming a missing item still failed later. The
|
|
378
|
+
// condition actually cared about is checked where it can be reported
|
|
379
|
+
// precisely: {@link SystemActorCompiler#resolveEmbedded} already errors
|
|
380
|
+
// per unresolved `(type, shortcode)`, naming the actor. A package whose
|
|
381
|
+
// actors embed nothing, or whose every address resolves against a
|
|
382
|
+
// dependency catalogue through `foreignSourceDirs`, now compiles with
|
|
383
|
+
// no Item pack at all — and one that is genuinely missing an item
|
|
384
|
+
// still fails, saying which item and which actor rather than which
|
|
385
|
+
// pack is absent.
|
|
386
|
+
Object.defineProperty(this, "itemsSourceDirs", {
|
|
387
|
+
value: Object.freeze([...itemsSourceDirs]),
|
|
388
|
+
writable: false,
|
|
389
|
+
});
|
|
390
|
+
// The dependency catalogues, if any. Not required: a repository that
|
|
391
|
+
// holds every item its actors name needs none, and one that declares
|
|
392
|
+
// no `itemCatalog: true` relationship gets an empty list.
|
|
393
|
+
Object.defineProperty(this, "foreignSourceDirs", {
|
|
394
|
+
value: Object.freeze([...foreignSourceDirs]),
|
|
395
|
+
writable: false,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* This pass's system map, or a message naming the class that forgot it.
|
|
401
|
+
*
|
|
402
|
+
* @returns {import("./document-subtypes.mjs").DocumentSubtypeMap} The map.
|
|
403
|
+
*/
|
|
404
|
+
get documentSubtypes() {
|
|
405
|
+
const map = /** @type {typeof SystemActorCompiler} */ (this.constructor).documentSubtypes;
|
|
406
|
+
if (!map) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
`${this.constructor.name} declares no \`documentSubtypes\` — an ` +
|
|
409
|
+
`Actor pass reads its system's map to know which notes it ` +
|
|
410
|
+
`claims and what each becomes.`,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return map;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* The frontmatter block this pass reads.
|
|
418
|
+
*
|
|
419
|
+
* @returns {string} The system's block name.
|
|
420
|
+
*/
|
|
421
|
+
get system() {
|
|
422
|
+
return this.documentSubtypes.block;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* @param {object} fm - The note's frontmatter.
|
|
427
|
+
* @returns {boolean} True for a note type the system maps onto an `Actor`.
|
|
428
|
+
*/
|
|
429
|
+
selects(fm) {
|
|
430
|
+
return mapsNoteType(this.documentSubtypes, fm.type, "Actor");
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* The predefined items each actor's embedded items resolve against, loaded
|
|
435
|
+
* before the walk from the items passes' output.
|
|
436
|
+
*
|
|
437
|
+
* @returns {Promise<void>}
|
|
438
|
+
*/
|
|
439
|
+
async prepare() {
|
|
440
|
+
await super.prepare();
|
|
441
|
+
this.itemsMap = loadItemsMap(this.itemsSourceDirs, this.foreignSourceDirs);
|
|
442
|
+
log.info(`Loaded ${this.itemsMap.size} predefined items for actor resolution`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** @inheritdoc */
|
|
446
|
+
reportDetail(stats) {
|
|
447
|
+
log.debug(
|
|
448
|
+
`Skipped ${stats.skippedOther} non-actor file(s) ` +
|
|
449
|
+
`(not ${noteTypesFor(this.documentSubtypes, "Actor").join("/")}, ` +
|
|
450
|
+
`package:${contentPackage()})`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* The Foundry Item subtype an embedded reference's `type` addresses.
|
|
456
|
+
*
|
|
457
|
+
* **The reference is in the note vocabulary; the address is in the
|
|
458
|
+
* document's** (#140). An actor writes `(type, shortcode)` with the type an
|
|
459
|
+
* author authors, while {@link itemAddress} keys the predefined items by
|
|
460
|
+
* the subtype each compiled document carries — so exactly one of the two
|
|
461
|
+
* sides has to translate, and it is this one. The system's map is a
|
|
462
|
+
* function from note type to subtype by construction; the reverse is not,
|
|
463
|
+
* and a compiled document records nothing about the note that produced it,
|
|
464
|
+
* so there is no honest way to key the addresses the other way round.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} type - The type the reference names.
|
|
467
|
+
* @returns {import("./document-subtypes.mjs").ReferencedSubtype} The
|
|
468
|
+
* subtype, or why the reference names none.
|
|
469
|
+
*/
|
|
470
|
+
embeddedSubtype(type) {
|
|
471
|
+
return referencedSubtype(this.documentSubtypes, type, "Item");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Resolve one embedded item from a `(type, shortcode?, overlay)`
|
|
476
|
+
* descriptor. If `shortcode` is given, the predefined item is fetched
|
|
477
|
+
* from `itemsMap` and the overlay deep-merged on top. If absent, the
|
|
478
|
+
* descriptor must carry enough fields to stand alone. The embedded
|
|
479
|
+
* item's `_id` is regenerated deterministically from
|
|
480
|
+
* `(actorId, subType, shortcode, indexKey)` so re-exports are stable —
|
|
481
|
+
* from the **document subtype**, so that renaming a note type (#78) leaves
|
|
482
|
+
* every embedded id exactly where it was.
|
|
483
|
+
* Returns null if the descriptor cannot be resolved.
|
|
484
|
+
*
|
|
485
|
+
* @param {Map<string, object>} itemsMap - The predefined items, by address.
|
|
486
|
+
* @param {string} actorId - The owning actor's id, seeding embedded ids.
|
|
487
|
+
* @param {string} type - The **note** type the reference names.
|
|
488
|
+
* @param {string|null} shortcode - The referenced item's shortcode, or
|
|
489
|
+
* `null` for a stand-alone entry.
|
|
490
|
+
* @param {object} [overlay] - The entry's remaining properties.
|
|
491
|
+
* @param {string} indexKey - Where the reference sits, for a diagnostic.
|
|
492
|
+
* It no longer reaches the id (#268) — it names the entry in a message.
|
|
493
|
+
* @param {string} ctx - Diagnostic context (the actor's label).
|
|
494
|
+
* @param {object} [at] - Where to locate a finding.
|
|
495
|
+
* @param {string} [at.fmKey] - The frontmatter key the reference sits
|
|
496
|
+
* under, so an unresolved one is reported at the reference rather than
|
|
497
|
+
* at the note.
|
|
498
|
+
* @returns {object|null} The embedded item, or null when it resolved to
|
|
499
|
+
* nothing — always with a finding emitted.
|
|
500
|
+
*/
|
|
501
|
+
resolveEmbedded(itemsMap, actorId, type, shortcode, overlay, indexKey, ctx, { fmKey } = {}) {
|
|
502
|
+
// Where a finding about this reference points. The value locates the
|
|
503
|
+
// exact entry in a list; the key is the fallback when it cannot be
|
|
504
|
+
// found, which still beats naming the note alone.
|
|
505
|
+
const where = () =>
|
|
506
|
+
locateFrontmatterKey(this.currentNote?.absPath, fmKey ?? "items", shortcode || type);
|
|
507
|
+
|
|
508
|
+
const { subType, problem } = this.embeddedSubtype(type);
|
|
509
|
+
if (problem) {
|
|
510
|
+
this.noteError(`${ctx}: ${indexKey}: ${problem}`, where());
|
|
511
|
+
this.errorCount++;
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
const address = itemAddress(/** @type {string} */ (subType), shortcode ?? "");
|
|
515
|
+
|
|
516
|
+
let base = null;
|
|
517
|
+
if (shortcode) {
|
|
518
|
+
base = itemsMap.get(address);
|
|
519
|
+
if (!base) {
|
|
520
|
+
// Both vocabularies where they differ, so an author sees why an
|
|
521
|
+
// address they wrote did not land where they expected.
|
|
522
|
+
const translated =
|
|
523
|
+
subType === type ? "" : (
|
|
524
|
+
` (looked up as "${address}", the ` +
|
|
525
|
+
`${this.documentSubtypes.system} Item subtype a ` +
|
|
526
|
+
`"${type}" note compiles into)`
|
|
527
|
+
);
|
|
528
|
+
this.noteError(
|
|
529
|
+
`${ctx}: no predefined item for "${type}:${shortcode}"${translated}`,
|
|
530
|
+
where(),
|
|
531
|
+
);
|
|
532
|
+
this.errorCount++;
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
base = stripCompendiumFields(base);
|
|
536
|
+
} else if (overlay && overlay.name && overlay.system) {
|
|
537
|
+
base = { type: subType, name: overlay.name, system: {} };
|
|
538
|
+
} else {
|
|
539
|
+
this.noteError(
|
|
540
|
+
`${ctx}: embedded item missing shortcode and not enough fields to stand alone`,
|
|
541
|
+
where(),
|
|
542
|
+
);
|
|
543
|
+
this.errorCount++;
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const merged = overlay ? deepMerge(base, overlay) : base;
|
|
547
|
+
merged.type = subType;
|
|
548
|
+
const identity = embeddedIdentity(merged);
|
|
549
|
+
const claim = `${actorId}\u0000${itemAddress(/** @type {string} */ (subType), identity)}`;
|
|
550
|
+
const first = this.#embeddedClaims.get(claim);
|
|
551
|
+
if (first !== undefined) {
|
|
552
|
+
this.noteError(
|
|
553
|
+
`${ctx}: ${indexKey}: ` +
|
|
554
|
+
`"${itemAddress(/** @type {string} */ (subType), identity)}" is ` +
|
|
555
|
+
`already the identity of "${first}" on this actor. ` +
|
|
556
|
+
`(type, shortcode) says *which entity* an embedded item is, ` +
|
|
557
|
+
`so two entries sharing one denote a single thing and every ` +
|
|
558
|
+
`lookup by it is ambiguous. Give this entry its own ` +
|
|
559
|
+
`\`system.shortcode\`` +
|
|
560
|
+
(shortcode ?
|
|
561
|
+
` — a top-level \`shortcode\` only selects the template ` +
|
|
562
|
+
`this entry is written from and never reaches the document`
|
|
563
|
+
: "") +
|
|
564
|
+
`, or delete it if it is a duplicate.`,
|
|
565
|
+
where(),
|
|
566
|
+
);
|
|
567
|
+
this.errorCount++;
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
this.#embeddedClaims.set(claim, indexKey);
|
|
571
|
+
merged._id = embeddedItemId(actorId, /** @type {string} */ (subType), identity);
|
|
572
|
+
// Foundry's pack compiler flattens the document hierarchy into LevelDB,
|
|
573
|
+
// storing each embedded document under its own `_key`. Embedded items
|
|
574
|
+
// therefore need a hierarchical key, as do any effects they carry
|
|
575
|
+
// (re-keyed under this actor's item rather than the items-pack key they
|
|
576
|
+
// inherited). Mirrors the items pack convention in the Item compiler.
|
|
577
|
+
merged._key = `!actors.items!${actorId}.${merged._id}`;
|
|
578
|
+
if (Array.isArray(merged.effects)) {
|
|
579
|
+
for (const effect of merged.effects) {
|
|
580
|
+
if (!effect?._id) continue;
|
|
581
|
+
effect._key = `!actors.items.effects!${actorId}.${merged._id}.${effect._id}`;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return merged;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
@@ -14,19 +14,22 @@
|
|
|
14
14
|
/**
|
|
15
15
|
* The one charset every segment of a canonical address is held to (#59).
|
|
16
16
|
*
|
|
17
|
-
* An address is a hyphen-joined tuple — `
|
|
18
|
-
* by **counting segments**, with a
|
|
19
|
-
* for exactly one reason: the hyphen
|
|
20
|
-
* segment may contain one. Take that away
|
|
21
|
-
* vocabulary to match against, a longest-match
|
|
22
|
-
* name that is a prefix of another — none of
|
|
17
|
+
* An address is a hyphen-joined tuple — `package-system-type-shortcode`, so
|
|
18
|
+
* `sohl-none-doc-gear` — and it is read back by **counting segments**, with a
|
|
19
|
+
* fixed meaning per position. That is sound for exactly one reason: the hyphen
|
|
20
|
+
* is *purely* a separator, because no segment may contain one. Take that away
|
|
21
|
+
* and reading an address needs a vocabulary to match against, a longest-match
|
|
22
|
+
* rule, and an answer for every name that is a prefix of another — none of
|
|
23
|
+
* which exist.
|
|
23
24
|
*
|
|
24
25
|
* So the charset is not a tidiness rule. It is the premise the address grammar
|
|
25
26
|
* rests on, and the issue's word for how it should be held is **enforced rather
|
|
26
27
|
* than assumed**: a value that breaks it is refused where it is written, not
|
|
27
28
|
* discovered later as addresses that fail to parse and report nothing about
|
|
28
|
-
* why. `harn-adventures` was that case —
|
|
29
|
-
*
|
|
29
|
+
* why. `harn-adventures` was that case — a package whose own name carries the
|
|
30
|
+
* separator emits keys one segment too long, so `harn-adventures-sohl-skill-melee`
|
|
31
|
+
* counts five where the grammar requires four, and every one of them failed as
|
|
32
|
+
* a `null` return.
|
|
30
33
|
*
|
|
31
34
|
* This module is a **leaf with no local imports**, so the validator a
|
|
32
35
|
* consumer's `package-build.config.mjs` reaches (`content-config.mjs`) can name
|