@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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/locText: a localization value as the PLAYER reads it.
|
|
3
|
+
*
|
|
4
|
+
* paradox/lookupLoc answers the value verbatim, which is what an editor needs.
|
|
5
|
+
* A panel that SHOWS the value needs the sentence. Measured over the 609
|
|
6
|
+
* `culture_parameter_*` values one game ships: 280 carry a real datafunction
|
|
7
|
+
* call, 145 of them the single shape
|
|
8
|
+
* `[GetTrait('rough_terrain_expert').GetName( GetNullCharacter )]`, and 130
|
|
9
|
+
* values nest another key as `$key$`. A form that prints those verbatim shows
|
|
10
|
+
* the modder brackets instead of "The Rough Terrain Expert Commander Trait is
|
|
11
|
+
* more common".
|
|
12
|
+
*
|
|
13
|
+
* Nothing here is written for one game. The words come from the loc index, the
|
|
14
|
+
* KIND a `Get<Something>('name')` chain names comes from the definition index,
|
|
15
|
+
* and the loc key that kind's names take comes from the active profile's
|
|
16
|
+
* schema. A profile whose schema knows the kind resolves the chain; one whose
|
|
17
|
+
* schema does not falls back to the definition name and says so, which is the
|
|
18
|
+
* same behavior for every game without a line of per-game code.
|
|
19
|
+
*
|
|
20
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
21
|
+
*/
|
|
22
|
+
import type { LocTextResult, LocTextValue } from "@px-lsp/protocol/protocol";
|
|
23
|
+
import { chipFor, stripFormatting, tokenize } from "../gui/textResolve";
|
|
24
|
+
|
|
25
|
+
export interface LocTextDeps {
|
|
26
|
+
/** The configured language's value for a loc key, or undefined. */
|
|
27
|
+
loc(key: string): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The definition kinds a name is indexed under, shadow-resolved, WITHOUT the
|
|
30
|
+
* loc_key kind (every name a value mentions is also a loc key).
|
|
31
|
+
*/
|
|
32
|
+
kindsOf(name: string): string[];
|
|
33
|
+
/**
|
|
34
|
+
* The loc key patterns a kind's names take (`$` = the name), most specific
|
|
35
|
+
* first, straight off the schema entry. An unknown kind has none.
|
|
36
|
+
*/
|
|
37
|
+
patternsOf(kind: string): string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The value, plus one level of whatever it inlines. A `$key$` hop or a
|
|
42
|
+
* resolved chain lands on another value that may itself hold markup (the
|
|
43
|
+
* `court_physician` key IS a `[GetCourtPositionType(...).GetName()]` call),
|
|
44
|
+
* and one more pass turns that into words. Deeper is the game's own business.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_DEPTH = 2;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `$key$`, with an optional format spec (`$VALUE|0$`). A key the loc index has
|
|
50
|
+
* is substituted; anything else stays verbatim and marks the value unresolved,
|
|
51
|
+
* because a slot the panel cannot fill is not a word.
|
|
52
|
+
*/
|
|
53
|
+
const NESTED_KEY = /\$([A-Za-z0-9_.\-']+)(?:\|[^$]*)?\$/g;
|
|
54
|
+
|
|
55
|
+
/** `[prestige_i]`: an icon the game draws and plain text has no glyph for. */
|
|
56
|
+
const ICON_TAG = /^[a-z0-9_]+_i$/;
|
|
57
|
+
|
|
58
|
+
/** `[culture|E]`: a link to a game concept, shown as that concept's word. */
|
|
59
|
+
const CONCEPT_LINK = /^([A-Za-z0-9_]+)\|[A-Za-z]+$/;
|
|
60
|
+
|
|
61
|
+
/** `Localize('k')` / `Concept('k')` / `Concept('k','shown')`. */
|
|
62
|
+
const LOCALIZE_CALL = /^(?:Localize|Concept)\s*\(\s*'([^']*)'(?:\s*,\s*'([^']*)')?\s*\)$/;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `SelectLocalization( HasDlcFeature('x'), 'on', 'off' )`: the game picks by a
|
|
66
|
+
* condition the server cannot evaluate. The first branch is the one the
|
|
67
|
+
* feature-on player reads, which is what a creator preview should show.
|
|
68
|
+
*/
|
|
69
|
+
const SELECT_CALL = /^SelectLocalization\s*\(.*?,\s*'([^']*)'\s*,\s*'([^']*)'\s*\)$/s;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* `Get<Something>('name')` followed by member hops:
|
|
73
|
+
* `GetTrait('brave').GetName( GetNullCharacter )`, `GetMaA('bowmen').GetName`,
|
|
74
|
+
* `GetCourtPositionType('x').GetName()`.
|
|
75
|
+
*/
|
|
76
|
+
const DEF_CHAIN = /^Get(\w+)\s*\(\s*'([^']+)'\s*\)\s*((?:\.\s*\w+\s*(?:\([^()]*\))?\s*)*)$/;
|
|
77
|
+
|
|
78
|
+
/** The members that mean "the name the player reads", measured in the corpus. */
|
|
79
|
+
const NAME_MEMBERS = new Set(["GetName", "GetTypeName", "GetNameNoTooltip"]);
|
|
80
|
+
|
|
81
|
+
/** The concept kind's own schema entry states the key a `[x|E]` link reaches. */
|
|
82
|
+
const CONCEPT_KIND = "game_concept";
|
|
83
|
+
|
|
84
|
+
/** `men_at_arms` -> `MenAtArms`, the spelling a datafunction name would use. */
|
|
85
|
+
function pascal(kind: string): string {
|
|
86
|
+
return kind
|
|
87
|
+
.split("_")
|
|
88
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
89
|
+
.join("");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The key patterns to try for a kind, most specific first, `$` last. */
|
|
93
|
+
function patternsWithFallback(kind: string | null, deps: LocTextDeps): string[] {
|
|
94
|
+
const own = kind === null ? [] : deps.patternsOf(kind);
|
|
95
|
+
return [...new Set([...own, "$"])];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The first pattern that resolves for `name`, or undefined. */
|
|
99
|
+
function locOfName(name: string, kind: string | null, deps: LocTextDeps): string | undefined {
|
|
100
|
+
for (const pattern of patternsWithFallback(kind, deps)) {
|
|
101
|
+
const value = deps.loc(pattern.replace("$", name));
|
|
102
|
+
if (value !== undefined && value !== "") return value;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The kinds to try for `Get<Head>('name')`, best first: a kind whose PascalCase
|
|
109
|
+
* spelling the function name carries (`GetCourtPositionType` -> `court_position`)
|
|
110
|
+
* outranks the rest, and a name the index does not know still gets the bare
|
|
111
|
+
* `$` pattern from `patternsWithFallback`.
|
|
112
|
+
*/
|
|
113
|
+
function candidateKinds(head: string, name: string, deps: LocTextDeps): (string | null)[] {
|
|
114
|
+
const kinds = deps.kindsOf(name);
|
|
115
|
+
const named = kinds.filter((kind) => head.includes(pascal(kind)));
|
|
116
|
+
const rest = kinds.filter((kind) => !named.includes(kind));
|
|
117
|
+
return [...named, ...rest, null];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface Rendered {
|
|
121
|
+
text: string;
|
|
122
|
+
resolved: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A value a call resolved to, as words. One more pass turns its own markup
|
|
127
|
+
* into text; past {@link MAX_DEPTH} the formatting is stripped and the rest
|
|
128
|
+
* kept, so the recursion cannot run away on a self-referencing key.
|
|
129
|
+
*/
|
|
130
|
+
function inline(value: string, deps: LocTextDeps, depth: number): Rendered {
|
|
131
|
+
if (depth + 1 >= MAX_DEPTH) return { text: stripFormatting(value).trim(), resolved: true };
|
|
132
|
+
return render(value, deps, depth + 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** One `[ ... ]` expression as words, and whether it became any. */
|
|
136
|
+
function renderFn(fn: string, deps: LocTextDeps, depth: number): Rendered {
|
|
137
|
+
// `|0`, `|V`, `|E`: format specifiers after the chain. The concept-link form
|
|
138
|
+
// needs its suffix, so it is matched before they are dropped.
|
|
139
|
+
const concept = CONCEPT_LINK.exec(fn.trim());
|
|
140
|
+
if (concept) {
|
|
141
|
+
const value = locOfName(concept[1], CONCEPT_KIND, deps);
|
|
142
|
+
return value === undefined ? { text: concept[1], resolved: false } : inline(value, deps, depth);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const source = fn.split("|")[0].trim();
|
|
146
|
+
if (ICON_TAG.test(source)) return { text: "", resolved: true };
|
|
147
|
+
|
|
148
|
+
const localize = LOCALIZE_CALL.exec(source);
|
|
149
|
+
if (localize) {
|
|
150
|
+
const key = localize[2] ?? localize[1];
|
|
151
|
+
const value = deps.loc(key);
|
|
152
|
+
return value === undefined ? { text: key, resolved: false } : inline(value, deps, depth);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const select = SELECT_CALL.exec(source);
|
|
156
|
+
if (select) {
|
|
157
|
+
const value = deps.loc(select[1]);
|
|
158
|
+
return value === undefined ? { text: select[1], resolved: false } : inline(value, deps, depth);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const chain = DEF_CHAIN.exec(source);
|
|
162
|
+
const members = chain ? [...chain[3].matchAll(/\.\s*(\w+)/g)].map((m) => m[1]) : [];
|
|
163
|
+
if (chain && members.length > 0 && NAME_MEMBERS.has(members[members.length - 1])) {
|
|
164
|
+
const name = chain[2];
|
|
165
|
+
for (const kind of candidateKinds(chain[1], name, deps)) {
|
|
166
|
+
const value = locOfName(name, kind, deps);
|
|
167
|
+
if (value !== undefined) return inline(value, deps, depth);
|
|
168
|
+
}
|
|
169
|
+
return { text: name, resolved: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Anything else is a value only the running game has. The chain's last word
|
|
173
|
+
// is what the preview shows, never an invented one.
|
|
174
|
+
return { text: chipFor(source), resolved: false };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `$key$` hops, one level per pass; an unfilled slot stays and marks the value. */
|
|
178
|
+
function substituteKeys(s: string, deps: LocTextDeps, resolved: { ok: boolean }): string {
|
|
179
|
+
return s.replace(NESTED_KEY, (whole, key: string) => {
|
|
180
|
+
const value = deps.loc(key);
|
|
181
|
+
if (value === undefined) {
|
|
182
|
+
resolved.ok = false;
|
|
183
|
+
return whole;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function render(raw: string, deps: LocTextDeps, depth: number): Rendered {
|
|
190
|
+
const state = { ok: true };
|
|
191
|
+
const parts = tokenize(substituteKeys(raw, deps, state)).map((part) => {
|
|
192
|
+
if (part.fn === undefined) return { text: stripFormatting(part.literal ?? ""), resolved: true };
|
|
193
|
+
const out = renderFn(part.fn, deps, depth);
|
|
194
|
+
if (!out.resolved) state.ok = false;
|
|
195
|
+
return out;
|
|
196
|
+
});
|
|
197
|
+
// A dropped icon leaves the space on both sides of it behind.
|
|
198
|
+
const text = parts
|
|
199
|
+
.map((part) => part.text)
|
|
200
|
+
.join("")
|
|
201
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
202
|
+
.trim();
|
|
203
|
+
return { text, resolved: state.ok };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** One value rendered; exported for the unit test and for other server features. */
|
|
207
|
+
export function renderLocValue(raw: string, deps: LocTextDeps): LocTextValue {
|
|
208
|
+
const out = render(raw, deps, 0);
|
|
209
|
+
return { raw, text: out.text, resolved: out.resolved };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function computeLocText(keys: readonly string[], deps: LocTextDeps): LocTextResult {
|
|
213
|
+
const values: Record<string, LocTextValue> = {};
|
|
214
|
+
for (const key of keys) {
|
|
215
|
+
if (key in values) continue;
|
|
216
|
+
const raw = deps.loc(key);
|
|
217
|
+
// A key the loc index cannot find is absent: the client already shows the
|
|
218
|
+
// key itself there, and an empty string would read as a defined blank.
|
|
219
|
+
if (raw === undefined) continue;
|
|
220
|
+
values[key] = renderLocValue(raw, deps);
|
|
221
|
+
}
|
|
222
|
+
return { values };
|
|
223
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `paradox/snippets` answer: everything a host can offer to insert at a
|
|
3
|
+
* cursor in one script document.
|
|
4
|
+
*
|
|
5
|
+
* Two sources, both derived. The document's folder decides its definition kind,
|
|
6
|
+
* and that kind's measured skeleton (schema/skeletons.ts) supplies the
|
|
7
|
+
* definition and its child blocks. The cursor's block context decides which
|
|
8
|
+
* engine triggers/effects are legal there, and each of those contributes the
|
|
9
|
+
* block form of the `usage:` example its own script_docs entry ships
|
|
10
|
+
* (features/blockSnippets.ts). Nothing is added that the game did not state.
|
|
11
|
+
*/
|
|
12
|
+
import type { SnippetItem } from "@px-lsp/protocol/protocol";
|
|
13
|
+
import type { TokenData } from "@px-lsp/protocol/types";
|
|
14
|
+
import { detectContextFromParse } from "../context";
|
|
15
|
+
import type { ParseResult } from "../parser";
|
|
16
|
+
import type { KindSkeleton } from "../schema/skeletons";
|
|
17
|
+
import { blockTemplateFor } from "./blockSnippets";
|
|
18
|
+
import { skeletonsFor } from "./definitionSkeletons";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Engine tokens the answer carries. A picker filters as the user types, so the
|
|
22
|
+
* cap is only about payload: the list is frequency-ordered, and past ~60 the
|
|
23
|
+
* tail is tokens nobody reaches for by name.
|
|
24
|
+
*/
|
|
25
|
+
const MAX_TOKENS = 60;
|
|
26
|
+
|
|
27
|
+
export function buildSnippetList(
|
|
28
|
+
parse: ParseResult,
|
|
29
|
+
offset: number,
|
|
30
|
+
kind: string | null,
|
|
31
|
+
skeletons: Record<string, KindSkeleton> | undefined,
|
|
32
|
+
tokens: TokenData[],
|
|
33
|
+
counts: Record<string, number>
|
|
34
|
+
): SnippetItem[] {
|
|
35
|
+
const out: SnippetItem[] = kind
|
|
36
|
+
? skeletonsFor(parse, kind, skeletons).map((offer) => ({
|
|
37
|
+
id: offer.id,
|
|
38
|
+
label: offer.label,
|
|
39
|
+
detail: offer.detail,
|
|
40
|
+
form: offer.form,
|
|
41
|
+
snippet: offer.text.snippet,
|
|
42
|
+
plain: offer.text.plain,
|
|
43
|
+
}))
|
|
44
|
+
: [];
|
|
45
|
+
|
|
46
|
+
const { context } = detectContextFromParse(parse, offset);
|
|
47
|
+
const seen = new Set<string>();
|
|
48
|
+
const candidates: Array<{ token: TokenData; count: number }> = [];
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
// The same context gate key-position completion applies: a trigger block
|
|
51
|
+
// takes no effects and an effect block takes no triggers.
|
|
52
|
+
if (context === "trigger" && (token.kind === "effect" || token.kind === "modifier")) continue;
|
|
53
|
+
if (context === "effect" && (token.kind === "trigger" || token.kind === "modifier")) continue;
|
|
54
|
+
if (seen.has(token.name)) continue;
|
|
55
|
+
if (!blockTemplateFor(token)) continue;
|
|
56
|
+
seen.add(token.name);
|
|
57
|
+
candidates.push({ token, count: counts[token.name] ?? 0 });
|
|
58
|
+
}
|
|
59
|
+
candidates.sort((a, b) => b.count - a.count || (a.token.name < b.token.name ? -1 : 1));
|
|
60
|
+
for (const { token } of candidates.slice(0, MAX_TOKENS)) {
|
|
61
|
+
const template = blockTemplateFor(token)!;
|
|
62
|
+
out.push({
|
|
63
|
+
id: token.name,
|
|
64
|
+
label: token.name,
|
|
65
|
+
detail: `${token.kind} block, from the game's own usage example`,
|
|
66
|
+
form: "token",
|
|
67
|
+
snippet: template.snippet,
|
|
68
|
+
plain: template.plain,
|
|
69
|
+
});
|
|
70
|
+
// An example that marks fields `# optional` offers both: the required
|
|
71
|
+
// fields, and the whole thing. The pair costs one cap slot, not two.
|
|
72
|
+
if (template.full)
|
|
73
|
+
out.push({
|
|
74
|
+
id: `${token.name}.full`,
|
|
75
|
+
label: `${token.name} (all fields)`,
|
|
76
|
+
detail: `${token.kind} block with its optional fields, from the game's own usage example`,
|
|
77
|
+
form: "token",
|
|
78
|
+
snippet: template.full.snippet,
|
|
79
|
+
plain: template.full.plain,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
package/src/games/ck3/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* and bundled-data import, assembled behind the GameProfile interface.
|
|
4
4
|
*/
|
|
5
5
|
import type { GameProfile } from "../profile";
|
|
6
|
+
import type { SkeletonData } from "../../schema/skeletons";
|
|
6
7
|
import { ck3Meta } from "./meta";
|
|
7
8
|
import { BLOCK_REF_FIELDS, CK3_SCHEMA, PREFIX_REFS, REF_FIELDS } from "./schema";
|
|
8
9
|
import { STRUCTURE_SOURCES } from "./structures";
|
|
@@ -13,6 +14,8 @@ import { CK3_SAVE_SCHEMA } from "./saveSchema";
|
|
|
13
14
|
// and scripts/build-gui-schema.ts for regeneration.
|
|
14
15
|
import BUNDLED_DATA_TYPES from "../../../data/ck3/dataTypes.json";
|
|
15
16
|
import GUI_SCHEMA from "../../../data/ck3/guiSchema.json";
|
|
17
|
+
// Definition skeletons measured over the vanilla tree (scripts/build-skeletons.ts).
|
|
18
|
+
import SKELETONS from "../../../data/ck3/skeletons.json";
|
|
16
19
|
|
|
17
20
|
export const ck3Profile: GameProfile = {
|
|
18
21
|
...ck3Meta,
|
|
@@ -30,8 +33,21 @@ export const ck3Profile: GameProfile = {
|
|
|
30
33
|
scripted_gui: { key: "scope", default: "character" },
|
|
31
34
|
},
|
|
32
35
|
modifierPlaceholders: CK3_MODIFIER_PLACEHOLDERS,
|
|
36
|
+
// The three triggers the measured condition shapes need. Over the game's own
|
|
37
|
+
// 21 dynasty legacy tracks every `is_shown` opens with `has_dlc_feature`, and
|
|
38
|
+
// over its 105 dynasty perks every `can_be_picked` is either
|
|
39
|
+
// `has_dlc_feature = <feature>` (10) or `<scripted trigger> = yes` (44).
|
|
40
|
+
conditionValues: {
|
|
41
|
+
// triggers.log enumerates the whole set on the trigger itself.
|
|
42
|
+
has_dlc_feature: { from: "docList" },
|
|
43
|
+
// `has_game_rule = unrestricted_dynasty_legacies_all` names a SETTING, and
|
|
44
|
+
// a setting is an inner block of a game_rule (00_game_rules.txt).
|
|
45
|
+
has_game_rule: { from: "innerKeys", kind: "game_rule", except: ["categories", "default"] },
|
|
46
|
+
scripted_trigger: { from: "kind", kind: "scripted_trigger" },
|
|
47
|
+
},
|
|
33
48
|
bundledDataTypes: BUNDLED_DATA_TYPES,
|
|
34
49
|
guiSchema: GUI_SCHEMA,
|
|
50
|
+
skeletons: (SKELETONS as unknown as SkeletonData).kinds,
|
|
35
51
|
saveSchema: CK3_SAVE_SCHEMA,
|
|
36
52
|
wikiNote: "Source: CK3 wiki (may lag behind the current game version)",
|
|
37
53
|
diagnosticSource: "ck3-script",
|
package/src/games/ck3/meta.ts
CHANGED
|
@@ -12,9 +12,12 @@ export const ck3Meta: GameMeta = {
|
|
|
12
12
|
shortName: "CK3",
|
|
13
13
|
engine: "jomini",
|
|
14
14
|
descriptor: "mod",
|
|
15
|
-
configDirName: ".
|
|
15
|
+
configDirName: ".px-toolkit",
|
|
16
|
+
legacyConfigDirName: ".ck3modding",
|
|
16
17
|
docsFolderName: "Crusader Kings III",
|
|
17
18
|
steamAppId: 1158310,
|
|
19
|
+
// Verified on the 1.19 install: dlc_001.dds .. dlc_029.dds.
|
|
20
|
+
dlcIconDir: "gfx/interface/icons/dlc",
|
|
18
21
|
eventNamespaces: true,
|
|
19
22
|
// The engine's own default metrics: they were measured on this game's font,
|
|
20
23
|
// and the layout engine reuses them for games whose probe has not run.
|
|
@@ -22,6 +25,60 @@ export const ck3Meta: GameMeta = {
|
|
|
22
25
|
guiTextMetrics: GITAN_MEASURED_METRICS,
|
|
23
26
|
scaffolds: CK3_SCAFFOLDS,
|
|
24
27
|
tiger: { binaryName: "ck3-tiger", repoSlug: "amtep/tiger", confName: "ck3-tiger.conf" },
|
|
28
|
+
// Visual creators. Every row is backed by a folder the schema table already
|
|
29
|
+
// indexes and a shape read out of the game's own files:
|
|
30
|
+
// trait common/traits, documented by common/traits/_traits.info
|
|
31
|
+
// dynasty_legacy common/dynasty_legacies, per _dynasty_legacies.info
|
|
32
|
+
// ("Dynasty Legacies are containers for perks"); the perks
|
|
33
|
+
// themselves live in common/dynasty_perks (`legacy = <track>`)
|
|
34
|
+
// culture common/culture/cultures (00_arabic.txt: color, ethos,
|
|
35
|
+
// heritage, language, traditions, name_list, parents…)
|
|
36
|
+
// culture_tradition common/culture/traditions, whose _traditions.info adds
|
|
37
|
+
// `category` and `layers` to the shape _cultural_traits.info
|
|
38
|
+
// documents for every cultural trait
|
|
39
|
+
// dynasty_tree NOT a definition kind: history/characters linked by
|
|
40
|
+
// father/mother/dynasty/house (history/characters/*.txt)
|
|
41
|
+
creators: [
|
|
42
|
+
{
|
|
43
|
+
kind: "trait",
|
|
44
|
+
label: "Trait Creator",
|
|
45
|
+
icon: "sparkles",
|
|
46
|
+
tip: "Design a character trait and write it into the mod.",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
kind: "dynasty_legacy",
|
|
50
|
+
label: "Dynasty Legacy Creator",
|
|
51
|
+
icon: "layers",
|
|
52
|
+
tip: "Build a legacy track and its perks.",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
kind: "culture",
|
|
56
|
+
label: "Culture Creator",
|
|
57
|
+
icon: "globe",
|
|
58
|
+
tip: "Compose a culture from the game's own pillars and traditions.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
kind: "culture_tradition",
|
|
62
|
+
label: "Tradition Creator",
|
|
63
|
+
icon: "flame",
|
|
64
|
+
tip: "Design a culture tradition, its layered icon and its modifiers.",
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
kind: "dynasty_tree",
|
|
68
|
+
label: "Dynasty Tree",
|
|
69
|
+
icon: "users",
|
|
70
|
+
tip: "See and edit a dynasty's characters across history.",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
// Measured on the 1.19 install: common/modifier_definition_formats/ holds 13
|
|
74
|
+
// files documented by _definitions.info in the same folder (decimals, color,
|
|
75
|
+
// prefix/suffix/negative_suffix as loc keys, percent, already_percent,
|
|
76
|
+
// hidden, no_difference_sign), and gui/texticons.gui holds the `texticon`
|
|
77
|
+
// blocks a `[gold_i]` in one of those loc values resolves to.
|
|
78
|
+
modifierFormats: {
|
|
79
|
+
folder: "common/modifier_definition_formats",
|
|
80
|
+
textIcons: "gui/texticons.gui",
|
|
81
|
+
},
|
|
25
82
|
// Date-format keys verified in ck3.exe 1.19 (the gamedate.cpp string block);
|
|
26
83
|
// formats mirror game/localization/english/core_l_english.yml. Vanilla
|
|
27
84
|
// appends $ERA$/$ERA_BCE$ itself - dropped here, the {era} slot replaces it.
|
|
@@ -64,4 +121,27 @@ export const ck3Meta: GameMeta = {
|
|
|
64
121
|
// "" keeps the pre-profile cache filenames (docsCache.json, vanillaIndex-*.json)
|
|
65
122
|
// so existing users' caches survive the M2 restructure.
|
|
66
123
|
cacheSuffix: "",
|
|
124
|
+
// Coat-of-arms coverage measured against this install (1.19.0.6, 2026-09-03):
|
|
125
|
+
// parseCoaFile reads all 2992 vanilla definitions in the 10 files of
|
|
126
|
+
// common/coat_of_arms/coat_of_arms with 0 parse errors, and 100% of both the
|
|
127
|
+
// 7800 texture references (1629 files, every one decoding) and the 15327
|
|
128
|
+
// colors resolve, the named ones out of the 112 in common/named_colors. 239
|
|
129
|
+
// flags (8.0%) carried something the model dropped, 387 of those 399 keys
|
|
130
|
+
// being `depth` on an instance, which the model now carries; Vic3, which has
|
|
131
|
+
// shipped the builder since 0.3.2, drops something on 8.3% of its flags, so
|
|
132
|
+
// CK3 is no worse off.
|
|
133
|
+
flagBuilder: true,
|
|
134
|
+
// Measured on this install (1.19.0.6, 2026-09-03): the in-game designer's
|
|
135
|
+
// own files are all present and parse - 38 visible patterns of 42 rows in
|
|
136
|
+
// gfx/coat_of_arms/patterns/50_coa_designer_patterns.txt, 1576 visible
|
|
137
|
+
// emblems of 1578 rows in 13 categories in
|
|
138
|
+
// colored_emblems/50_coa_designer_emblems.txt, 13 palette colors in
|
|
139
|
+
// color_palettes/50_coa_designer_palettes.txt, 35 whole layouts in
|
|
140
|
+
// emblem_layouts/50_coa_designer_emblem_layouts.txt, the
|
|
141
|
+
// `coa_designer_blank_default` template in
|
|
142
|
+
// common/coat_of_arms/coat_of_arms/99_coa_designer_templates.txt and 35
|
|
143
|
+
// preview frames under gfx/interface/coat_of_arms. Victoria 3 ships no
|
|
144
|
+
// gfx/coat_of_arms/color_palettes or emblem_layouts at all, and EU5 no coa
|
|
145
|
+
// designer either, so neither sets this.
|
|
146
|
+
coaDesigner: true,
|
|
67
147
|
};
|
package/src/games/ck3/schema.ts
CHANGED
|
@@ -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
|
|
@@ -29,7 +29,20 @@ const CK3_SCHEMA_BASE: SchemaEntry[] = [
|
|
|
29
29
|
{ path: "localization", kind: "loc_key", ext: ".yml", extraction: "loc-key" },
|
|
30
30
|
|
|
31
31
|
// --- Characters, traits, interactions ---
|
|
32
|
-
|
|
32
|
+
// locPatterns and iconFolder verbatim from common/traits/_traits.info
|
|
33
|
+
// ("== Loc/icon ==": "the name key is trait_<key>, and the desc key is
|
|
34
|
+
// trait_<key>_desc", "The default icon path is
|
|
35
|
+
// gfx/interface/icons/traits/<trait>.dds"). trait_$_desc stays OUT of
|
|
36
|
+
// requiredLoc: schemaVanilla.test.ts measures it at 90.4% of vanilla traits,
|
|
37
|
+
// under the 95% bar a missing-loc diagnostic needs.
|
|
38
|
+
{
|
|
39
|
+
path: "common/traits",
|
|
40
|
+
kind: "trait",
|
|
41
|
+
requiredLoc: ["trait_$"],
|
|
42
|
+
locPatterns: ["trait_$", "trait_$_desc"],
|
|
43
|
+
iconFolder: "gfx/interface/icons/traits",
|
|
44
|
+
rootScopes: ["character"],
|
|
45
|
+
},
|
|
33
46
|
{
|
|
34
47
|
path: "common/character_interactions",
|
|
35
48
|
kind: "character_interaction",
|
|
@@ -97,13 +110,43 @@ const CK3_SCHEMA_BASE: SchemaEntry[] = [
|
|
|
97
110
|
{ path: "common/court_types", kind: "court_type" },
|
|
98
111
|
|
|
99
112
|
// --- Culture ---
|
|
100
|
-
{
|
|
101
|
-
|
|
113
|
+
{
|
|
114
|
+
path: "common/culture/cultures",
|
|
115
|
+
kind: "culture",
|
|
116
|
+
requiredLoc: ["$"],
|
|
117
|
+
// Every one of the 244 vanilla cultures defines all three keys in
|
|
118
|
+
// game/localization/english/culture/cultures_l_english.yml (`bedouin`,
|
|
119
|
+
// `bedouin_prefix`, `bedouin_collective_noun`), measured 244/244 in
|
|
120
|
+
// 2026-09. requiredLoc keeps only `$` because the diagnostic that hangs
|
|
121
|
+
// off it is the conservative one; a creator writes the whole set.
|
|
122
|
+
locPatterns: ["$", "$_prefix", "$_collective_noun"],
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
path: "common/culture/pillars",
|
|
126
|
+
kind: "culture_pillar",
|
|
127
|
+
// One folder, five families: game/common/culture/pillars/_pillars.info
|
|
128
|
+
// documents `type = ethos/heritage/language/martial` on top of the common
|
|
129
|
+
// cultural-trait shape, and the files spell it that way
|
|
130
|
+
// (00_ethos.txt `type = ethos`, 00_martial_custom.txt `type =
|
|
131
|
+
// martial_custom`, 00_head_determination.txt `type = head_determination`).
|
|
132
|
+
groupKey: "type",
|
|
133
|
+
// Measured 2026-09-04 in game/localization/english against the 162 vanilla
|
|
134
|
+
// pillars: `$_name` 160, `$_desc` 9, bare `$` 4. The _pillars.info doc
|
|
135
|
+
// names only the generic-desc fallback, so the name pattern is a measurement.
|
|
136
|
+
requiredLoc: ["$_name"],
|
|
137
|
+
locPatterns: ["$_name", "$_desc"],
|
|
138
|
+
},
|
|
102
139
|
{
|
|
103
140
|
path: "common/culture/traditions",
|
|
104
141
|
kind: "culture_tradition",
|
|
105
142
|
// def names already carry the `tradition_` prefix; loc key is `<name>_name`.
|
|
106
143
|
requiredLoc: ["$_name"],
|
|
144
|
+
// All 196 vanilla traditions also define `$_desc`, measured 196/196 in
|
|
145
|
+
// game/localization/english against common/culture/traditions in 2026-09
|
|
146
|
+
// (`tradition_winter_warriors_name` / `_desc`). requiredLoc keeps only
|
|
147
|
+
// `$_name` because the diagnostic that hangs off it is the conservative
|
|
148
|
+
// one; a creator writes the whole set.
|
|
149
|
+
locPatterns: ["$_name", "$_desc"],
|
|
107
150
|
rootScopes: ["culture"],
|
|
108
151
|
},
|
|
109
152
|
{ path: "common/culture/innovations", kind: "innovation", requiredLoc: ["$"], rootScopes: ["culture"] },
|
|
@@ -125,8 +168,33 @@ const CK3_SCHEMA_BASE: SchemaEntry[] = [
|
|
|
125
168
|
// dynasties top-level keys are numeric ids; huge and never typed → not completable.
|
|
126
169
|
{ path: "common/dynasties", kind: "dynasty", completable: false },
|
|
127
170
|
{ path: "common/dynasty_houses", kind: "dynasty_house", completable: false },
|
|
128
|
-
|
|
129
|
-
|
|
171
|
+
// _dynasty_legacies.info, "Generated loc keys: key + "_name"". All 21 vanilla
|
|
172
|
+
// legacies define it (measured 2026-09-03), so it clears the requiredLoc bar.
|
|
173
|
+
// $_desc is not in the .info doc, but all 21 vanilla tracks define it
|
|
174
|
+
// (localization/english/dynasty_legacies/legacies_l_english.yml, e.g.
|
|
175
|
+
// blood_legacy_track_desc), so a creator asks for it too; it stays out of
|
|
176
|
+
// requiredLoc, where only the keys the doc itself states belong.
|
|
177
|
+
// iconFolder: window_dynasty_legacy.gui draws the picture from
|
|
178
|
+
// "[DynastyLegacy.GetIcon]", a code-side path with no script key behind it,
|
|
179
|
+
// and the files it finds are gfx/interface/icons/dynasty/<track key>.dds
|
|
180
|
+
// (22 files for the 21 tracks). Name-derived, so a creator writes the
|
|
181
|
+
// picture under the track's own name and never a path into the block.
|
|
182
|
+
// A track reads a SECOND name-derived picture, the window's illustration at
|
|
183
|
+
// gfx/interface/illustrations/legacy_tracks/<key>.dds ("[DynastyLegacy.
|
|
184
|
+
// GetTrackIcon]", 21 files); iconFolder holds one path, so that one is left
|
|
185
|
+
// to ck3-tiger, which names it as a missing-file warning on the track.
|
|
186
|
+
{
|
|
187
|
+
path: "common/dynasty_legacies",
|
|
188
|
+
kind: "dynasty_legacy",
|
|
189
|
+
requiredLoc: ["$_name"],
|
|
190
|
+
locPatterns: ["$_name", "$_desc"],
|
|
191
|
+
iconFolder: "gfx/interface/icons/dynasty",
|
|
192
|
+
},
|
|
193
|
+
// _dynasty_perks.info, "Generated loc keys: key + "_name"" — and only that:
|
|
194
|
+
// all 105 vanilla perks define $_name, none defines $_desc (measured
|
|
195
|
+
// 2026-09-03 over localization/english). No iconFolder: gfx/interface/icons
|
|
196
|
+
// has a dynasty folder for the tracks and nothing for the perks.
|
|
197
|
+
{ path: "common/dynasty_perks", kind: "dynasty_perk", locPatterns: ["$_name"] },
|
|
130
198
|
|
|
131
199
|
// --- Activities & schemes ---
|
|
132
200
|
{ path: "common/activities/activity_types", kind: "activity_type", rootScopes: ["character"] },
|
|
@@ -190,7 +258,15 @@ const CK3_SCHEMA_BASE: SchemaEntry[] = [
|
|
|
190
258
|
// remaining common/ folder with a standard top-level `name = { ... }`
|
|
191
259
|
// layout, verified by parsing vanilla files. Ordered by definition count.
|
|
192
260
|
{ path: "common/game_concepts", kind: "game_concept", requiredLoc: ["game_concept_$"] },
|
|
193
|
-
{
|
|
261
|
+
{
|
|
262
|
+
path: "common/domiciles/buildings",
|
|
263
|
+
kind: "domicile_building",
|
|
264
|
+
rootScopes: ["character", "domicile"],
|
|
265
|
+
// Measured 2026-09-04: all 1620 vanilla domicile buildings define
|
|
266
|
+
// `$_domicile_building` (109 also `$_domicile_building_desc`), none a bare `$`.
|
|
267
|
+
requiredLoc: ["$_domicile_building"],
|
|
268
|
+
locPatterns: ["$_domicile_building", "$_domicile_building_desc"],
|
|
269
|
+
},
|
|
194
270
|
{ path: "common/domiciles/types", kind: "domicile_type", rootScopes: ["character"] },
|
|
195
271
|
{ path: "common/artifacts/features", kind: "artifact_feature" },
|
|
196
272
|
{ path: "common/artifacts/feature_groups", kind: "artifact_feature_group" },
|
|
@@ -824,6 +824,43 @@ const CURATED: Record<string, StructureSpec> = {
|
|
|
824
824
|
const KEY_PATCHES: Record<string, Record<string, Partial<KeySpec> & { doc?: string }>> = {
|
|
825
825
|
trait: {
|
|
826
826
|
valid_sex: { values: "enum:all|male|female", doc: "Which sex can have the trait. Default: all." },
|
|
827
|
+
// `opposites = { chaste }` in game/common/traits/00_traits.txt: the entries
|
|
828
|
+
// are trait names. Deliberately NOT a global REF_FIELD - scripted_relations
|
|
829
|
+
// spells the same key over relation names (00_scripted_relations.txt,
|
|
830
|
+
// `friend = { opposites = { rival ... } }`), and a global row would
|
|
831
|
+
// mis-resolve there.
|
|
832
|
+
opposites: { refKinds: ["trait"] },
|
|
833
|
+
},
|
|
834
|
+
culture: {
|
|
835
|
+
// game/common/culture/cultures/00_arabic.txt: `traditions = {
|
|
836
|
+
// tradition_tribe_unity ... }` names definitions of
|
|
837
|
+
// common/culture/traditions, and `parents = { bedouin assyrian }` names
|
|
838
|
+
// other cultures (bedouin is the top-level key of the same file).
|
|
839
|
+
traditions: { refKinds: ["culture_tradition"] },
|
|
840
|
+
parents: { refKinds: ["culture"] },
|
|
841
|
+
// The five pillar keys all name definitions of common/culture/pillars
|
|
842
|
+
// (00_arabic.txt: `ethos = ethos_stoic heritage = heritage_arabic
|
|
843
|
+
// language = language_arabic martial_custom = martial_custom_male_only
|
|
844
|
+
// head_determination = head_determination_domain`, each of them a
|
|
845
|
+
// top-level key in common/culture/pillars/00_*.txt). One folder, so one
|
|
846
|
+
// ref kind; the pillar's own `type` sorts the five pickers apart (the
|
|
847
|
+
// schema entry's groupKey).
|
|
848
|
+
ethos: { refKinds: ["culture_pillar"] },
|
|
849
|
+
heritage: { refKinds: ["culture_pillar"] },
|
|
850
|
+
language: { refKinds: ["culture_pillar"] },
|
|
851
|
+
martial_custom: { refKinds: ["culture_pillar"] },
|
|
852
|
+
head_determination: { refKinds: ["culture_pillar"] },
|
|
853
|
+
// `name_list = name_list_bedouin` (00_arabic.txt) names a top-level key of
|
|
854
|
+
// common/culture/name_lists (00_arabic.txt line 1 there).
|
|
855
|
+
name_list: { refKinds: ["name_list"] },
|
|
856
|
+
},
|
|
857
|
+
dynasty_perk: {
|
|
858
|
+
// _dynasty_perks.info spells the block `traits = { trait_name = int }`, and
|
|
859
|
+
// game/common/dynasty_perks/00_dynasty_perks.txt writes it that way
|
|
860
|
+
// (blood_legacy_4: `beauty_good_1 = 100`, `fecund = 50`), so the entry keys
|
|
861
|
+
// are trait names and the numbers are AI chances. Per-KIND and not a global
|
|
862
|
+
// RefField: `traits` elsewhere is a history list, a filter, a count.
|
|
863
|
+
traits: { refKinds: ["trait"] },
|
|
827
864
|
},
|
|
828
865
|
scheme_type: {
|
|
829
866
|
category: { values: "enum:personal|contract|hostile" },
|
package/src/games/eu5/index.ts
CHANGED
|
@@ -8,9 +8,11 @@
|
|
|
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
|
+
import type { SkeletonData } from "../../schema/skeletons";
|
|
15
|
+
import SKELETONS from "../../../data/eu5/skeletons.json";
|
|
14
16
|
import type { RefField, SchemaEntry } from "../../schema/types";
|
|
15
17
|
import { JOMINI_VARIABLE_BLOCK_REFS } from "../jomini/variables";
|
|
16
18
|
import { eu5Meta } from "./meta";
|
|
@@ -68,6 +70,10 @@ export const eu5Profile: GameProfile = {
|
|
|
68
70
|
blockRefFields: { ...JOMINI_VARIABLE_BLOCK_REFS },
|
|
69
71
|
// No `_*.info` docs ship with EU5, so the structures layer has no source.
|
|
70
72
|
structureSources: {},
|
|
73
|
+
// Nobody here owns EU5, so its vanilla tree has never been measured and
|
|
74
|
+
// data/eu5/skeletons.json is empty. Wired anyway: a harvest run on a machine
|
|
75
|
+
// that has the game turns skeletons on with no code change.
|
|
76
|
+
skeletons: (SKELETONS as unknown as SkeletonData).kinds,
|
|
71
77
|
modifierPlaceholders: {},
|
|
72
78
|
// No bundled wiki tokens in the preview cut, so nothing ever renders this.
|
|
73
79
|
wikiNote: "",
|
package/src/games/eu5/meta.ts
CHANGED
|
@@ -16,12 +16,16 @@ export const eu5Meta: GameMeta = {
|
|
|
16
16
|
// EU5 mods carry .metadata/metadata.json (plus a required thumbnail.png);
|
|
17
17
|
// descriptor.mod is at most a vestigial launcher artifact.
|
|
18
18
|
descriptor: "metadata",
|
|
19
|
-
configDirName: ".
|
|
19
|
+
configDirName: ".px-toolkit",
|
|
20
|
+
legacyConfigDirName: ".eu5modding",
|
|
20
21
|
docsFolderName: "Europa Universalis V",
|
|
21
22
|
// EU5's `script_docs` console command writes to Documents/.../docs, not logs/.
|
|
22
23
|
scriptDocsSubdir: "docs",
|
|
23
24
|
dataTypesCommand: "dump_data_types",
|
|
24
25
|
steamAppId: 3450310,
|
|
26
|
+
// dlcIconDir deliberately absent: no live install has been checked, so the
|
|
27
|
+
// Workshop panel shows this game's DLC (read from `<gameDir>/dlc/`) with the
|
|
28
|
+
// folder's own thumbnail.png, and falls back to Steam when there is none.
|
|
25
29
|
eventNamespaces: true,
|
|
26
30
|
scaffolds: EU5_SCAFFOLDS,
|
|
27
31
|
// uiFont and guiTextMetrics deliberately absent: neither the font file nor
|