@px-lsp/server 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/data/ck3/skeletons.json +1 -0
  3. package/data/eu5/skeletons.json +1 -0
  4. package/data/vic3/skeletons.json +1 -0
  5. package/dist/browser-data/ck3/docs.json +1 -1
  6. package/dist/browser-data/ck3/tokens.json +1 -1
  7. package/dist/browser-data/vic3/tokens.json +1 -1
  8. package/dist/browser.js +50 -37
  9. package/dist/server.js +2000 -352
  10. package/dist/types/packages/server/src/features/blockSnippets.d.ts +20 -4
  11. package/dist/types/packages/server/src/features/completion.d.ts +11 -0
  12. package/dist/types/packages/server/src/features/definitionSkeletons.d.ts +24 -0
  13. package/dist/types/packages/server/src/games/ck3/schema.d.ts +1 -1
  14. package/dist/types/packages/server/src/games/eu5/index.d.ts +1 -1
  15. package/dist/types/packages/server/src/games/profile.d.ts +85 -1
  16. package/dist/types/packages/server/src/schema/skeletons.d.ts +102 -0
  17. package/dist/types/packages/server/src/schema/types.d.ts +30 -0
  18. package/package.json +3 -3
  19. package/src/coa/coa.ts +31 -0
  20. package/src/coa/coaDesigner.ts +131 -0
  21. package/src/coa/coaParse.ts +3 -0
  22. package/src/creators/definitionEdit.ts +198 -0
  23. package/src/creators/definitionForm.ts +479 -0
  24. package/src/creators/modifierFormats.ts +0 -0
  25. package/src/data/docsParser.ts +21 -4
  26. package/src/features/blockSnippets.ts +151 -20
  27. package/src/features/calendarDates.ts +8 -5
  28. package/src/features/completion.ts +38 -4
  29. package/src/features/definitionSkeletons.ts +109 -0
  30. package/src/features/inlayHints.ts +5 -2
  31. package/src/features/locText.ts +223 -0
  32. package/src/features/snippetList.ts +83 -0
  33. package/src/games/ck3/index.ts +16 -0
  34. package/src/games/ck3/meta.ts +81 -1
  35. package/src/games/ck3/schema.ts +83 -7
  36. package/src/games/ck3/structures.ts +37 -0
  37. package/src/games/eu5/index.ts +7 -1
  38. package/src/games/eu5/meta.ts +5 -1
  39. package/src/games/profile.ts +77 -1
  40. package/src/games/vic3/index.ts +4 -0
  41. package/src/games/vic3/meta.ts +5 -1
  42. package/src/gui/sourceModel.ts +51 -8
  43. package/src/gui/textResolve.ts +3 -3
  44. package/src/overview/dynastyTree.ts +493 -0
  45. package/src/overview/eventGraph.ts +21 -1
  46. package/src/overview/eventVocabulary.ts +33 -33
  47. package/src/overview/exampleWiki.ts +4 -1
  48. package/src/schema/loader.ts +2 -1
  49. package/src/schema/skeletons.ts +182 -0
  50. package/src/schema/types.ts +30 -0
  51. package/src/server.ts +147 -6
@@ -10,18 +10,25 @@
10
10
  *
11
11
  * - the example's leading identifier must be the token's own name (dumps
12
12
  * routinely paste a sibling's example: `add_title_law_effects` shows
13
- * `add_title_law = …`);
13
+ * `add_title_law = …`). A `<scope> = { name = { … } }` wrapper around a
14
+ * single such block is peeled off first; any other placeholder key rejects;
14
15
  * - the example must open a block and its braces must balance (truncated
15
16
  * dumps that never close are dropped);
16
17
  * - every key must be a plain lowercase identifier, or a `a/b/c` alternation
17
18
  * of them (this drops pseudo-key weight lists, `X1 = { … } X2 = { … } …`);
18
- * - a `#` comment means the example enumerates alternatives ("# or:") that
19
- * cannot be collapsed into one template;
19
+ * - a `#` comment is read ONLY when it says the field is optional
20
+ * (`#Optional`, `# optional way to …`); every other comment means the
21
+ * example enumerates alternatives ("# or:") or explains something we cannot
22
+ * turn into script, and rejects the example;
20
23
  * - `(optional)` and `...` truncate the body at that point;
21
24
  * - `<name>` placeholders become tabstops, concrete example values become
22
25
  * pre-filled tabstops.
23
26
  *
24
- * Both forms are produced at once: `snippet` for clients that declared
27
+ * An example that marks fields optional yields TWO templates rather than none:
28
+ * `snippet`/`plain` carry the required fields only, `full` carries every field
29
+ * the example shows.
30
+ *
31
+ * Both text forms are produced at once: `snippet` for clients that declared
25
32
  * snippetSupport, `plain` (no `${`, insertable as literal text) for the rest.
26
33
  */
27
34
  import type { TokenData } from "@px-lsp/protocol/types";
@@ -30,6 +37,15 @@ export interface BlockTemplate {
30
37
  snippet: string;
31
38
  /** Plain-text skeleton, free of `${`. */
32
39
  plain: string;
40
+ /**
41
+ * The same block with the fields the example marked `# optional` put back.
42
+ * Present only when the example marked at least one, so `full` never repeats
43
+ * what `snippet`/`plain` already say. Tabstops are numbered per form.
44
+ */
45
+ full?: {
46
+ snippet: string;
47
+ plain: string;
48
+ };
33
49
  }
34
50
  /** The block template for a token, or null when its example does not qualify. */
35
51
  export declare function blockTemplateFor(token: TokenData): BlockTemplate | null;
@@ -60,6 +60,17 @@ export declare class CompletionFeature {
60
60
  */
61
61
  private mergedCount;
62
62
  provide(document: TextDocument, offset: number, rootScopes: Set<Scope> | null, entry?: SchemaEntry | null, limit?: number): CompletionResult;
63
+ /**
64
+ * Definition and child-block skeletons (schema/skeletons.ts) as items of their
65
+ * own kind, ranked right after the block's structure keys.
66
+ *
67
+ * Gated on a BLANK line tail: a multi-line insert must not land inside an
68
+ * existing statement, and half a definition pasted over `= { … }` is worse
69
+ * than no offer. That gate also keeps these items out of every rank-eval
70
+ * sample, which always leaves the `= …` in place when it strips a key, so the
71
+ * ranking of existing items is unchanged by construction.
72
+ */
73
+ private skeletonItems;
63
74
  /**
64
75
  * Completing a scripted effect/trigger/modifier inserts a ready-to-fill
65
76
  * block: one `PARAM = <tabstop>` line per $PARAM$ the definition's body
@@ -0,0 +1,24 @@
1
+ import type { ParseResult } from "../parser";
2
+ import { type KindSkeleton, type RenderedSkeleton } from "../schema/skeletons";
3
+ export interface SkeletonOffer {
4
+ /** Stable id: `<kind>` for the definition, `<kind>.<block>` for a child block. */
5
+ id: string;
6
+ /** What the item reads as: "new event", "option block". */
7
+ label: string;
8
+ /** How many of the game's own definitions the shape was measured over. */
9
+ detail: string;
10
+ /** A whole definition, or one of its child blocks. */
11
+ form: "definition" | "block";
12
+ text: RenderedSkeleton;
13
+ }
14
+ /**
15
+ * Skeletons offered at `offset` in a document the schema classifies as `kind`.
16
+ * Empty when the game has no measured skeleton for the kind, or when the cursor
17
+ * is deeper than a definition body (an effect block wants effects, not a form).
18
+ */
19
+ export declare function skeletonsAt(parse: ParseResult, offset: number, kind: string, skeletons: Record<string, KindSkeleton> | undefined): SkeletonOffer[];
20
+ /**
21
+ * Every skeleton the document's kind has, position-independent: what a picker
22
+ * offers, where the modder chooses the insert point rather than the cursor.
23
+ */
24
+ export declare function skeletonsFor(parse: ParseResult, kind: string, skeletons: Record<string, KindSkeleton> | undefined): SkeletonOffer[];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The bundled CK3 schema table (rework plan AD-3). One entry per game folder
3
3
  * we understand; community-editable in-repo, extendable per-workspace via
4
- * .ck3modding/schema.json.
4
+ * .px-toolkit/schema.json.
5
5
  *
6
6
  * Every entry was verified against a real CK3 install (game version with
7
7
  * common/ ~150 subfolders): the folder was listed, a file opened, and the
@@ -8,7 +8,7 @@
8
8
  * upstream, commit and license) and has NOT been checked against a live EU5
9
9
  * install. Folder→kind mappings are only as right as those rules are; report
10
10
  * gaps with the "Schema gap" issue form, and work around them locally with the
11
- * `<mod>/.eu5modding/schema.json` overlay.
11
+ * `<mod>/.px-toolkit/schema.json` overlay.
12
12
  */
13
13
  import type { GameProfile } from "../profile";
14
14
  export declare const eu5Profile: GameProfile;
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { CalendarLocSpec } from "@px-lsp/protocol/calendarLoc";
12
12
  import type { DefRootKey, RefField, SchemaEntry, StructureSpec } from "../schema/types";
13
+ import type { KindSkeleton } from "../schema/skeletons";
13
14
  import type { PlaceholderSpec } from "../data/modifierTemplates";
14
15
  import type { GuiLayoutQuirks, GuiTextMetrics } from "../gui/layoutEngine";
15
16
  import type { SaveSchema } from "../gui/saveSchema";
@@ -68,8 +69,10 @@ export interface GameMeta {
68
69
  engine: "jomini" | "clausewitz-classic";
69
70
  /** Mod descriptor convention: Paradox-launcher `.mod` file vs `.metadata/metadata.json`. */
70
71
  descriptor: "mod" | "metadata";
71
- /** Per-workspace config dir holding schema.json / playset.json overlays. */
72
+ /** Per-mod config dir holding schema.json / playset.json overlays, workshop.json, the listing folder. */
72
73
  configDirName: string;
74
+ /** The pre-0.4.0 per-game name of that dir, still read as a fallback and renamed on first write. */
75
+ legacyConfigDirName?: string;
73
76
  /** Game folder under `Documents/Paradox Interactive/` (script_docs logs live in its logs/). */
74
77
  docsFolderName: string;
75
78
  /**
@@ -85,6 +88,12 @@ export interface GameMeta {
85
88
  */
86
89
  dataTypesCommand?: string;
87
90
  steamAppId: number;
91
+ /**
92
+ * Folder under the game data dir holding one icon per DLC, named after the
93
+ * DLC folder's number (`dlc_003.dds` or `dlc003.dds`). Absent = the install
94
+ * ships no such folder and the DLC's own `thumbnail.png` is used instead.
95
+ */
96
+ dlcIconDir?: string;
88
97
  /** Whether event files declare `namespace = x` and use `ns.N` event ids. */
89
98
  eventNamespaces: boolean;
90
99
  /**
@@ -126,6 +135,42 @@ export interface GameMeta {
126
135
  * not open for this game.
127
136
  */
128
137
  flagBuilder?: boolean;
138
+ /**
139
+ * The game ships its own in-game Coat of Arms designer, and the files that
140
+ * drive it (`gfx/coat_of_arms/{patterns,colored_emblems,color_palettes,
141
+ * emblem_layouts}/50_coa_designer_*.txt` and the `99_coa_designer_templates`
142
+ * coat-of-arms templates). Present = the toolkit offers the designer
143
+ * instead of the raw Flag Builder, when those files are actually on disk.
144
+ * Absent = the Flag Builder, as before.
145
+ */
146
+ coaDesigner?: boolean;
147
+ /**
148
+ * Visual content creators offered for this game, in Create-group order. A
149
+ * row is a definition kind the schema knows (the panel's command opens that
150
+ * creator), except `dynasty_tree`, which is a view over history files rather
151
+ * than one definition. Absent = no creator has been built against this
152
+ * game's own files, and the Create group shows only the scaffolds.
153
+ */
154
+ creators?: {
155
+ /** Schema kind, or the id of a bespoke creator that owns no single kind. */
156
+ kind: string;
157
+ label: string;
158
+ /** A Lucide icon name present in the client's webviews/shared/icons.ts. */
159
+ icon: string;
160
+ /** One-line hint for the panel row. */
161
+ tip?: string;
162
+ }[];
163
+ /**
164
+ * Where the game states how a modifier is PRINTED: the folder of format
165
+ * definitions (one block per modifier), and the `.gui` file whose `texticon`
166
+ * blocks map an icon name to its sprite. Both paths are relative to the game
167
+ * data dir. Absent = this game's print rules have not been read out of its
168
+ * own files, and `paradox/modifierFormats` answers null rather than guessing.
169
+ */
170
+ modifierFormats?: {
171
+ folder: string;
172
+ textIcons: string;
173
+ };
129
174
  /**
130
175
  * Database entry-mode prefixes legal on top-level definition keys
131
176
  * (EU5's `REPLACE:key`). The indexer strips a leading `<MODE>:` before
@@ -174,6 +219,30 @@ export interface GameMeta {
174
219
  */
175
220
  cacheSuffix: string;
176
221
  }
222
+ /**
223
+ * Where a creator's condition builder gets the values of one trigger
224
+ * (`DefinitionForm.conditions`). Each source is something the server already
225
+ * holds, so a game patch changes the list without a release:
226
+ *
227
+ * docList the trigger's own script_docs entry enumerates them on its
228
+ * metadata line ("has_dlc_feature … Traits: Valid Features: a,
229
+ * b, and c", CK3 triggers.log 1.19)
230
+ * kind every indexed definition of a definition kind
231
+ * innerKeys the inner block keys of every definition of a kind, minus
232
+ * `except` (a CK3 game rule's settings ARE its inner blocks;
233
+ * the other two keys are `categories` and `default`, per
234
+ * common/game_rules/_game_rules.info)
235
+ */
236
+ export type ConditionValueSource = {
237
+ from: "docList";
238
+ } | {
239
+ from: "kind";
240
+ kind: string;
241
+ } | {
242
+ from: "innerKeys";
243
+ kind: string;
244
+ except?: string[];
245
+ };
177
246
  /** A game's full knowledge bundle: meta plus the tables the engine consumes. */
178
247
  export interface GameProfile extends GameMeta {
179
248
  /** Folder→definition-kind table (see schema/types.ts). */
@@ -193,6 +262,14 @@ export interface GameProfile extends GameMeta {
193
262
  * those entries always win.
194
263
  */
195
264
  structures?: Record<string, StructureSpec>;
265
+ /**
266
+ * Definition skeletons per kind (data/<id>/skeletons.json, harvested by
267
+ * scripts/build-skeletons.ts): the shape a new definition of the kind takes,
268
+ * measured over the game's own files. Absent or empty = nobody has measured
269
+ * this game's vanilla tree, and completion offers no skeleton for it rather
270
+ * than a shape written from memory.
271
+ */
272
+ skeletons?: Record<string, KindSkeleton>;
196
273
  /**
197
274
  * Definition kinds that declare their own root scope in their body, keyed by
198
275
  * kind (scopes/inference.ts). Absent = every kind's root scope comes from the
@@ -201,6 +278,13 @@ export interface GameProfile extends GameMeta {
201
278
  defRootKeys?: Record<string, DefRootKey>;
202
279
  /** Templated-modifier placeholder table (data/modifierTemplates.ts). */
203
280
  modifierPlaceholders: Record<string, PlaceholderSpec>;
281
+ /**
282
+ * Triggers a creator's condition builder may offer a value list for, keyed
283
+ * by trigger name (`paradox/definitionForm`'s `conditions`). Absent = the
284
+ * creators offer no picker for this game and fall back to free input, which
285
+ * is the honest answer for a game whose sources have not been measured.
286
+ */
287
+ conditionValues?: Record<string, ConditionValueSource>;
204
288
  /**
205
289
  * Bundled `[ ... ]` data-function tables (data/<id>/dataTypes.json), when the
206
290
  * game ships one. Shape is data/dataTypes.ts's bundled-JSON shape; typed
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Definition skeletons: the canonical shape of one definition of a kind,
3
+ * derived from measured vanilla usage by scripts/build-skeletons.ts and shipped
4
+ * as packages/server/data/<game>/skeletons.json.
5
+ *
6
+ * Nothing here is written by hand. A key is in a skeleton because at least
7
+ * SKELETON_MAJORITY of the game's own definitions of that kind carry it; its
8
+ * order is the median position it holds in them; its pre-filled value is the
9
+ * value the corpus uses most, and only where the key's whole measured
10
+ * vocabulary is small enough to be a real choice. A key whose vocabulary is
11
+ * wider but entirely NUMERIC still gets the number the corpus writes most,
12
+ * since a key name is valid script nowhere in a numeric slot. Anything wider
13
+ * than that (loc keys, names, paths) gets its own name as placeholder text,
14
+ * which says "fill this in" without asserting a value.
15
+ *
16
+ * Both insert forms are rendered at once, exactly like features/blockSnippets.ts:
17
+ * `snippet` for clients that declared snippetSupport, `plain` (free of `${`) for
18
+ * the rest.
19
+ *
20
+ * No `vscode` imports: plain data plus pure renderers.
21
+ */
22
+ /**
23
+ * Share of a kind's definitions a key must appear in to enter the skeleton.
24
+ * Half: a skeleton is what a definition of this kind USUALLY looks like, not
25
+ * the union of everything the folder has ever used. Applied identically to the
26
+ * keys of a nested block (share of that block's own occurrences).
27
+ */
28
+ export declare const SKELETON_MAJORITY = 0.5;
29
+ /** One key of a skeleton body. */
30
+ export interface SkeletonKey {
31
+ key: string;
32
+ /**
33
+ * The key's whole measured value vocabulary, most-used first. Set only where
34
+ * the corpus uses few enough distinct values for the list to be a choice
35
+ * rather than a guess; absent = the value is a placeholder.
36
+ */
37
+ choices?: string[];
38
+ /**
39
+ * The single most-used measured value, set only where the vocabulary is too
40
+ * wide to offer as a choice AND every value the corpus writes for the key is
41
+ * a number. A key name is valid script nowhere in a numeric slot, so a
42
+ * measured number is the honest placeholder; it stays a tabstop, so it still
43
+ * reads as "fill this in".
44
+ */
45
+ placeholder?: string;
46
+ /**
47
+ * Nested one level: the block this key opens, carrying the keys measured in
48
+ * at least SKELETON_MAJORITY of its occurrences. An empty array = the block
49
+ * has no stable shape, so it is inserted empty.
50
+ */
51
+ block?: SkeletonKey[];
52
+ }
53
+ /** A named child block of a definition, offered on its own. */
54
+ export interface SkeletonBlock {
55
+ /** Occurrences of the block measured across the kind's definitions. */
56
+ sampled: number;
57
+ keys: SkeletonKey[];
58
+ }
59
+ export interface KindSkeleton {
60
+ /** Vanilla definitions of this kind the skeleton was measured over. */
61
+ sampled: number;
62
+ keys: SkeletonKey[];
63
+ /**
64
+ * The file-level key the kind's definition names were measured to derive
65
+ * from: at least SKELETON_MAJORITY of the folder's files declare it as a
66
+ * top-level scalar, and at least that share of the kind's names are
67
+ * `<its value>.<number>` (`namespace = intrigue` → `intrigue.0001`). Absent =
68
+ * names are plain identifiers and files need no header line.
69
+ */
70
+ nameFromHeader?: string;
71
+ /** Child-block skeletons, keyed by the block's key. */
72
+ blocks?: Record<string, SkeletonBlock>;
73
+ }
74
+ export interface SkeletonData {
75
+ meta: {
76
+ generated: string;
77
+ sources: string[];
78
+ majority: number;
79
+ };
80
+ /** Definition kind, as the schema table spells it. */
81
+ kinds: Record<string, KindSkeleton>;
82
+ }
83
+ /** Both insert forms of one skeleton. */
84
+ export interface RenderedSkeleton {
85
+ /** `${n:…}` tabstop form; only for clients declaring snippetSupport. */
86
+ snippet: string;
87
+ /** Plain-text skeleton, free of `${`. */
88
+ plain: string;
89
+ }
90
+ export interface SkeletonRenderOptions {
91
+ /**
92
+ * Value of the header key the target document already declares
93
+ * (`namespace = my_events`), so a generated name matches its own file.
94
+ */
95
+ headerValue?: string;
96
+ /** Write the header line above the definition (the document declares none). */
97
+ withHeader?: boolean;
98
+ }
99
+ /** The whole definition, header line included when the document lacks it. */
100
+ export declare function renderDefinitionSkeleton(kind: string, skel: KindSkeleton, opts?: SkeletonRenderOptions): RenderedSkeleton;
101
+ /** One named child block (`option = { name = … }`), inserted on its own. */
102
+ export declare function renderBlockSkeleton(name: string, block: SkeletonBlock): RenderedSkeleton;
@@ -46,6 +46,13 @@ export interface KeySpec {
46
46
  * vocabulary, the completion regression rank-eval caught in 2026-07.
47
47
  */
48
48
  curated?: boolean;
49
+ /**
50
+ * Definition kinds this key's value names, where a global `RefField` would
51
+ * be wrong because the key means different things in different folders
52
+ * (`opposites` is a trait list here and a scripted_relation list there).
53
+ * Per-kind and therefore unambiguous; read by the creators' form request.
54
+ */
55
+ refKinds?: string[];
49
56
  }
50
57
  /** The document shape of a definition kind: top-level keys plus named sub-blocks. */
51
58
  export interface StructureSpec {
@@ -96,6 +103,29 @@ export interface SchemaEntry {
96
103
  * of this kind actually define.
97
104
  */
98
105
  requiredLoc?: string[];
106
+ /**
107
+ * Every loc key the game generates for a definition of this kind, `$` = the
108
+ * name, as the folder's `_*.info` doc states them. A SUPERSET of
109
+ * `requiredLoc`, which may only carry the patterns ≥95% of vanilla actually
110
+ * defines because a diagnostic hangs off it; a creator writes the whole set
111
+ * and asks the modder for each. Absent = `requiredLoc` is the whole set.
112
+ */
113
+ locPatterns?: string[];
114
+ /**
115
+ * Where the game looks for a definition's default icon, relative to the
116
+ * game/mod root, forward slashes, no trailing slash. Only set where the
117
+ * folder's `_*.info` doc states the path; absent = this kind has no
118
+ * name-derived icon and a creator offers no icon picker for it.
119
+ */
120
+ iconFolder?: string;
121
+ /**
122
+ * Key inside a definition's body whose value sorts this kind's definitions
123
+ * into families, when one folder holds several (`type = ethos` in
124
+ * common/culture/pillars). A creator that draws one picker per family reads
125
+ * it through paradox/definitionForm; nothing else does. Absent = the kind is
126
+ * one flat list.
127
+ */
128
+ groupKey?: string;
99
129
  /** Scope(s) a definition of this type pushes as root in its script blocks (Phase 3). */
100
130
  rootScopes?: string[];
101
131
  /** Include definitions of this kind in completion lists (default true).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@px-lsp/server",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "license": "GPL-3.0-or-later",
5
5
  "description": "Language server for Paradox script (Crusader Kings III first), usable over node-ipc or stdio from any LSP client.",
6
6
  "keywords": [
@@ -43,10 +43,10 @@
43
43
  "./*": "./src/*.ts"
44
44
  },
45
45
  "dependencies": {
46
+ "@px-lsp/protocol": "0.2.1",
46
47
  "vscode-languageserver": "^10.1.0",
47
48
  "vscode-languageserver-textdocument": "^1.0.12",
48
- "vscode-uri": "^3.1.0",
49
- "@px-lsp/protocol": "0.2.0"
49
+ "vscode-uri": "^3.1.0"
50
50
  },
51
51
  "scripts": {
52
52
  "compile": "esbuild src/server.ts --bundle --outfile=dist/server.js --format=cjs --platform=node --target=node18 --banner:js=\"#!/usr/bin/env node\"",
package/src/coa/coa.ts CHANGED
@@ -31,6 +31,20 @@ export interface CoaInstance {
31
31
  rotation: number;
32
32
  scale: [number, number];
33
33
  position: [number, number];
34
+ /**
35
+ * The in-game designer's z key, absent on a hand-written instance.
36
+ *
37
+ * Measured on the vanilla corpus (game version 1.19.0.6): the 387 uses in 01_landed_titles.txt and
38
+ * 90_dynasties.txt hold only the values 1.01 .. 13.01, and in 229 of the 234
39
+ * definitions that carry any exactly one instance has none, so the designer
40
+ * writes `<draw index>.01` and omits the first. It spans layers (134
41
+ * definitions interleave them), but WITHIN one layer the file order is
42
+ * already the depth order in 336 of 337 layers (the exception,
43
+ * 90_dynasties.txt `dali_duan`, repeats 0 and 1.01), so sorting by it would
44
+ * change nothing there. The renderer therefore keeps file order and the
45
+ * value is carried only so a round trip does not drop it.
46
+ */
47
+ depth?: number;
34
48
  }
35
49
 
36
50
  export interface CoaSubInstance {
@@ -50,6 +64,22 @@ export interface CoaFlag {
50
64
  layers: CoaLayer[];
51
65
  }
52
66
 
67
+ /**
68
+ * One row of a game's coat-of-arms designer catalog (a pattern or an emblem),
69
+ * in the order its file lists it. The reader is coaDesigner.ts; the type lives
70
+ * here because the webview app carries it and must not bundle the parser.
71
+ */
72
+ export interface DesignerEntry {
73
+ /** The texture file name, e.g. `ce_fleur.dds`. */
74
+ file: string;
75
+ /** Color buttons to show. Absent in the file means "as many as the kind has". */
76
+ colors: number;
77
+ /** False for `visible = no`: the entry declares colors but stays out of the grid. */
78
+ visible: boolean;
79
+ /** Emblems only; "" for a pattern. */
80
+ category: string;
81
+ }
82
+
53
83
  /** The slots a flag or a layer may fill, in the order the game names them. */
54
84
  export const COLOR_SLOTS = [
55
85
  "color1",
@@ -174,6 +204,7 @@ export function writeFlag(flag: CoaFlag): string {
174
204
  if (i.rotation !== 0) parts.push(`rotation = ${fmt(i.rotation)}`);
175
205
  parts.push(`scale = { ${fmt(i.scale[0])} ${fmt(i.scale[1])} }`);
176
206
  parts.push(`position = { ${fmt(i.position[0])} ${fmt(i.position[1])} }`);
207
+ if (i.depth !== undefined) parts.push(`depth = ${fmt(i.depth)}`);
177
208
  lines.push(`\t\tinstance = { ${parts.join(" ")} }`);
178
209
  }
179
210
  }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The files behind the game's own Coat of Arms designer, parsed.
3
+ *
4
+ * Everything the designer offers is script the game ships, and every list in
5
+ * it is ordered by the file rather than alphabetically (both catalog files say
6
+ * so in their own header comment), so these readers keep file order:
7
+ *
8
+ * - `gfx/coat_of_arms/patterns/50_coa_designer_patterns.txt` and
9
+ * `gfx/coat_of_arms/colored_emblems/50_coa_designer_emblems.txt`:
10
+ * `file.dds = { colors = N visible = no category = animals }`. `colors` is
11
+ * how many color buttons the entry shows, `visible = no` keeps an entry out
12
+ * of the grid while still declaring its color count, `category` groups the
13
+ * emblem grid (emblems only).
14
+ * - `gfx/coat_of_arms/color_palettes/50_coa_designer_palettes.txt`:
15
+ * `coa_designer_background_colors = { red = {} ... }`, names into
16
+ * `common/named_colors`.
17
+ * - `gfx/coat_of_arms/emblem_layouts/50_coa_designer_emblem_layouts.txt`:
18
+ * whole coats of arms written against the `@pattern`, `@color_1..3`,
19
+ * `@texture_1..2` placeholders defined at the top of that file. They are
20
+ * read UNRESOLVED (parseCoaFile leaves a non-numeric `@name` as it stands),
21
+ * which is exactly what a layout is: a shape with holes for the design's own
22
+ * pattern, colors and emblems.
23
+ * - `common/coat_of_arms/coat_of_arms/99_coa_designer_templates.txt`: the
24
+ * `template = { }` block holding `coa_designer_blank_default`, whose colors
25
+ * are `list "normal_colors"` references into
26
+ * `common/coat_of_arms/template_lists/color_lists.txt`.
27
+ *
28
+ * Measured against the vanilla files (game version 1.19.0.6): 42 pattern rows
29
+ * of which 38 are visible, 1578 emblem rows of which 1576 are visible across 13
30
+ * categories, 13 palette colors, 35 layouts and 1 template.
31
+ *
32
+ * Host and test side only (it parses script), like coaParse.ts.
33
+ */
34
+ import { parseScript } from "../parser/parser";
35
+ import type { BlockNode } from "../parser/cst";
36
+ import { parseFlag } from "./coaParse";
37
+ import type { CoaFlag, DesignerEntry } from "./coa";
38
+
39
+ export type { DesignerEntry };
40
+
41
+ /** Every `@name = value` at the top of a designer file, as raw text. */
42
+ export function parseAtDefaults(text: string): Record<string, string> {
43
+ const { root } = parseScript(text);
44
+ const out: Record<string, string> = {};
45
+ for (const s of root.statements) {
46
+ if (s.kind !== "assignment" || !s.value || s.value.kind !== "scalar") continue;
47
+ if (s.key.text.startsWith("@")) out[s.key.text] = s.value.text;
48
+ }
49
+ return out;
50
+ }
51
+
52
+ /**
53
+ * A designer catalog file (patterns or emblems). `maxColors` fills in the
54
+ * count for a row that names none, which the files' own header calls "assume
55
+ * maximum number of colors".
56
+ */
57
+ export function parseDesignerCatalog(text: string, maxColors: number): DesignerEntry[] {
58
+ const { root } = parseScript(text);
59
+ const out: DesignerEntry[] = [];
60
+ for (const s of root.statements) {
61
+ if (s.kind !== "assignment" || s.value?.kind !== "block") continue;
62
+ const entry: DesignerEntry = { file: s.key.text, colors: maxColors, visible: true, category: "" };
63
+ for (const a of s.value.statements) {
64
+ if (a.kind !== "assignment" || a.value?.kind !== "scalar") continue;
65
+ if (a.key.text === "colors") {
66
+ const n = Number(a.value.text);
67
+ if (Number.isFinite(n)) entry.colors = n;
68
+ } else if (a.key.text === "visible") entry.visible = a.value.text !== "no";
69
+ else if (a.key.text === "category") entry.category = a.value.text;
70
+ }
71
+ out.push(entry);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** The palette's color names, in list order: `coa_designer_background_colors = { red = {} … }`. */
77
+ export function parseDesignerPalette(text: string): string[] {
78
+ const { root } = parseScript(text);
79
+ for (const s of root.statements) {
80
+ if (s.kind !== "assignment" || s.key.text !== "coa_designer_background_colors") continue;
81
+ if (s.value?.kind !== "block") continue;
82
+ return s.value.statements
83
+ .filter((c) => c.kind === "assignment")
84
+ .map((c) => (c as { key: { text: string } }).key.text);
85
+ }
86
+ return [];
87
+ }
88
+
89
+ /**
90
+ * The first color of each `color_lists` entry: what a template's
91
+ * `list "normal_colors"` resolves to here. The game rolls a weight
92
+ * (`30 = "red"`) against the character; a designer that has no character picks
93
+ * the list's first entry, which is also the heaviest one in every vanilla list.
94
+ */
95
+ export function parseColorLists(text: string): Record<string, string> {
96
+ const { root } = parseScript(text);
97
+ const out: Record<string, string> = {};
98
+ for (const s of root.statements) {
99
+ if (s.kind !== "assignment" || s.key.text !== "color_lists" || s.value?.kind !== "block") continue;
100
+ for (const list of s.value.statements) {
101
+ if (list.kind !== "assignment" || list.value?.kind !== "block") continue;
102
+ for (const row of list.value.statements) {
103
+ if (row.kind !== "assignment" || row.value?.kind !== "scalar") continue;
104
+ if (!/^\d+$/.test(row.key.text)) continue;
105
+ out[list.key.text] = row.value.text;
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ return out;
111
+ }
112
+
113
+ /**
114
+ * The designer templates of a `template = { }` file. `list "x"` colors are
115
+ * replaced by `colorLists[x]` before parsing, so the caller gets a plain flag.
116
+ */
117
+ export function parseDesignerTemplates(text: string, colorLists: Record<string, string>): CoaFlag[] {
118
+ const resolved = text.replace(/\blist\s+"([^"]+)"/g, (whole, name: string) =>
119
+ colorLists[name] !== undefined ? `"${colorLists[name]}"` : whole
120
+ );
121
+ const { root } = parseScript(resolved);
122
+ const out: CoaFlag[] = [];
123
+ for (const s of root.statements) {
124
+ if (s.kind !== "assignment" || s.key.text !== "template" || s.value?.kind !== "block") continue;
125
+ for (const t of s.value.statements) {
126
+ if (t.kind !== "assignment" || t.value?.kind !== "block") continue;
127
+ out.push(parseFlag(t.key.text, t.value as BlockNode));
128
+ }
129
+ }
130
+ return out;
131
+ }
@@ -138,6 +138,9 @@ function parseInstances(statements: Statement[], vars: Vars): CoaInstance[] {
138
138
  if (a.key.text === "rotation" && a.value.kind === "scalar") {
139
139
  const r = resolveNumber(a.value.text, vars);
140
140
  if (Number.isFinite(r)) inst.rotation = r;
141
+ } else if (a.key.text === "depth" && a.value.kind === "scalar") {
142
+ const d = resolveNumber(a.value.text, vars);
143
+ if (Number.isFinite(d)) inst.depth = d;
141
144
  } else if (a.key.text === "scale" && a.value.kind === "block") inst.scale = pair(a.value, vars, [1, 1]);
142
145
  else if (a.key.text === "position" && a.value.kind === "block")
143
146
  inst.position = pair(a.value, vars, [0.5, 0.5]);