@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.
- package/README.md +1 -1
- package/data/ck3/skeletons.json +1 -0
- package/data/eu5/skeletons.json +1 -0
- package/data/vic3/skeletons.json +1 -0
- package/dist/browser-data/ck3/docs.json +1 -1
- package/dist/browser-data/ck3/tokens.json +1 -1
- package/dist/browser-data/vic3/tokens.json +1 -1
- package/dist/browser.js +50 -37
- package/dist/server.js +2000 -352
- package/dist/types/packages/server/src/features/blockSnippets.d.ts +20 -4
- package/dist/types/packages/server/src/features/completion.d.ts +11 -0
- package/dist/types/packages/server/src/features/definitionSkeletons.d.ts +24 -0
- package/dist/types/packages/server/src/games/ck3/schema.d.ts +1 -1
- package/dist/types/packages/server/src/games/eu5/index.d.ts +1 -1
- package/dist/types/packages/server/src/games/profile.d.ts +85 -1
- package/dist/types/packages/server/src/schema/skeletons.d.ts +102 -0
- package/dist/types/packages/server/src/schema/types.d.ts +30 -0
- package/package.json +3 -3
- package/src/coa/coa.ts +31 -0
- package/src/coa/coaDesigner.ts +131 -0
- package/src/coa/coaParse.ts +3 -0
- package/src/creators/definitionEdit.ts +198 -0
- package/src/creators/definitionForm.ts +479 -0
- package/src/creators/modifierFormats.ts +0 -0
- package/src/data/docsParser.ts +21 -4
- package/src/features/blockSnippets.ts +151 -20
- package/src/features/calendarDates.ts +8 -5
- package/src/features/completion.ts +38 -4
- package/src/features/definitionSkeletons.ts +109 -0
- package/src/features/inlayHints.ts +5 -2
- package/src/features/locText.ts +223 -0
- package/src/features/snippetList.ts +83 -0
- package/src/games/ck3/index.ts +16 -0
- package/src/games/ck3/meta.ts +81 -1
- package/src/games/ck3/schema.ts +83 -7
- package/src/games/ck3/structures.ts +37 -0
- package/src/games/eu5/index.ts +7 -1
- package/src/games/eu5/meta.ts +5 -1
- package/src/games/profile.ts +77 -1
- package/src/games/vic3/index.ts +4 -0
- package/src/games/vic3/meta.ts +5 -1
- package/src/gui/sourceModel.ts +51 -8
- package/src/gui/textResolve.ts +3 -3
- package/src/overview/dynastyTree.ts +493 -0
- package/src/overview/eventGraph.ts +21 -1
- package/src/overview/eventVocabulary.ts +33 -33
- package/src/overview/exampleWiki.ts +4 -1
- package/src/schema/loader.ts +2 -1
- package/src/schema/skeletons.ts +182 -0
- package/src/schema/types.ts +30 -0
- 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
|
|
19
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* .
|
|
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>/.
|
|
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-
|
|
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.
|
|
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
|
+
}
|
package/src/coa/coaParse.ts
CHANGED
|
@@ -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]);
|