@px-lsp/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +334 -0
- package/THIRD-PARTY-NOTICES.md +83 -0
- package/data/ck3/dataTypes.json +2195 -0
- package/data/ck3/data_types/data_types_common.txt +2040 -0
- package/data/ck3/data_types/data_types_gui.txt +5264 -0
- package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
- package/data/ck3/data_types/data_types_script.txt +4251 -0
- package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
- package/data/ck3/freqs.json +1 -0
- package/data/ck3/guiSchema.json +6344 -0
- package/data/ck3/script_docs/effects.log +16059 -0
- package/data/ck3/script_docs/event_targets.log +2098 -0
- package/data/ck3/script_docs/modifiers.log +2228 -0
- package/data/ck3/script_docs/on_actions.log +5275 -0
- package/data/ck3/script_docs/triggers.log +11991 -0
- package/data/ck3/structures.json +9743 -0
- package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
- package/data/ck3/wikidocs/Data_types.md +2568 -0
- package/data/ck3/wikidocs/Effects_list.md +1176 -0
- package/data/ck3/wikidocs/Scopes_list.md +341 -0
- package/data/ck3/wikidocs/Triggers_list.md +1097 -0
- package/data/eu5/data_types/data_types_common.txt +2087 -0
- package/data/eu5/data_types/data_types_gui.txt +6732 -0
- package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
- package/data/eu5/data_types/data_types_script.txt +5688 -0
- package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
- package/data/vic3/data_types/data_types_common.txt +2021 -0
- package/data/vic3/data_types/data_types_gui.txt +5592 -0
- package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
- package/data/vic3/data_types/data_types_script.txt +2817 -0
- package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
- package/data/vic3/freqs.json +1 -0
- package/data/vic3/guiSchema.json +5578 -0
- package/data/vic3/script_docs/effects.log +38135 -0
- package/data/vic3/script_docs/event_targets.log +2028 -0
- package/data/vic3/script_docs/modifiers.log +18954 -0
- package/data/vic3/script_docs/on_actions.log +1561 -0
- package/data/vic3/script_docs/triggers.log +15738 -0
- package/data/vic3/structures.json +10189 -0
- package/dist/server.js +63668 -0
- package/media/px-lsp.svg +12 -0
- package/package.json +50 -0
- package/src/clientMode.ts +60 -0
- package/src/coa/coa.ts +184 -0
- package/src/coa/coaParse.ts +267 -0
- package/src/context.ts +78 -0
- package/src/contextKeywords.ts +224 -0
- package/src/data/dataBindingMacros.ts +82 -0
- package/src/data/dataFnDocs.ts +152 -0
- package/src/data/dataFnUsage.ts +431 -0
- package/src/data/dataTypes.ts +279 -0
- package/src/data/defines.ts +123 -0
- package/src/data/docsParser.ts +453 -0
- package/src/data/keywordDocs.ts +98 -0
- package/src/data/modifierTemplates.ts +143 -0
- package/src/data/textFormatting.ts +165 -0
- package/src/data/wikiDocs.ts +187 -0
- package/src/dds/decoder.ts +1007 -0
- package/src/dds/encode.ts +235 -0
- package/src/dds/index.ts +58 -0
- package/src/dds/png.ts +96 -0
- package/src/dds/tga.ts +62 -0
- package/src/documents.ts +35 -0
- package/src/features/assetPaths.ts +169 -0
- package/src/features/codeActions.ts +148 -0
- package/src/features/colors.ts +244 -0
- package/src/features/completion.ts +961 -0
- package/src/features/datafunction.ts +729 -0
- package/src/features/definition.ts +84 -0
- package/src/features/diagnostics.ts +244 -0
- package/src/features/folding.ts +106 -0
- package/src/features/formatting.ts +60 -0
- package/src/features/guiLanguage.ts +366 -0
- package/src/features/guiNavigation.ts +140 -0
- package/src/features/guiTree.ts +97 -0
- package/src/features/hover.ts +817 -0
- package/src/features/hoverRender.ts +222 -0
- package/src/features/inlayHints.ts +147 -0
- package/src/features/locFormatting.ts +127 -0
- package/src/features/references.ts +70 -0
- package/src/features/rename.ts +135 -0
- package/src/features/scopeAt.ts +65 -0
- package/src/features/semanticTokens.ts +188 -0
- package/src/features/signatureHelp.ts +72 -0
- package/src/features/symbols.ts +241 -0
- package/src/features/textureHover.ts +143 -0
- package/src/features/workspaceSymbols.ts +69 -0
- package/src/games/active.ts +19 -0
- package/src/games/ck3/ambientScopes.ts +273 -0
- package/src/games/ck3/index.ts +38 -0
- package/src/games/ck3/meta.ts +28 -0
- package/src/games/ck3/modifierPlaceholders.ts +61 -0
- package/src/games/ck3/saveSchema.ts +134 -0
- package/src/games/ck3/scaffolds.ts +197 -0
- package/src/games/ck3/schema.ts +422 -0
- package/src/games/ck3/structures.ts +887 -0
- package/src/games/eu5/index.ts +75 -0
- package/src/games/eu5/meta.ts +44 -0
- package/src/games/eu5/scaffolds.ts +49 -0
- package/src/games/eu5/schema.generated.ts +1043 -0
- package/src/games/jomini/variables.ts +134 -0
- package/src/games/profile.ts +205 -0
- package/src/games/registry.ts +27 -0
- package/src/games/vic3/index.ts +52 -0
- package/src/games/vic3/meta.ts +55 -0
- package/src/games/vic3/saveSchema.ts +77 -0
- package/src/games/vic3/scaffolds.ts +135 -0
- package/src/games/vic3/schema.ts +650 -0
- package/src/games/vic3/structures.ts +33 -0
- package/src/gui/anchorSpec.ts +66 -0
- package/src/gui/declMarkers.ts +30 -0
- package/src/gui/fillGeometry.ts +101 -0
- package/src/gui/guiDefs.ts +386 -0
- package/src/gui/guiDependencies.ts +352 -0
- package/src/gui/guiLinks.ts +64 -0
- package/src/gui/layoutEngine.ts +1998 -0
- package/src/gui/layoutService.ts +221 -0
- package/src/gui/measuredMetrics.ts +21 -0
- package/src/gui/previewService.ts +89 -0
- package/src/gui/saveSchema.ts +220 -0
- package/src/gui/saveValues.ts +399 -0
- package/src/gui/saveZip.ts +60 -0
- package/src/gui/sourceEdit.ts +535 -0
- package/src/gui/sourceEditService.ts +439 -0
- package/src/gui/sourceModel.ts +603 -0
- package/src/gui/textResolve.ts +145 -0
- package/src/gui/textureInfo.ts +106 -0
- package/src/gui/vocabulary.ts +149 -0
- package/src/gui/widgetEdit.ts +52 -0
- package/src/gui/widgetInfo.ts +245 -0
- package/src/index/docComments.ts +103 -0
- package/src/index/extract.ts +252 -0
- package/src/index/indexer.ts +369 -0
- package/src/index/intern.ts +101 -0
- package/src/index/lazyRefs.ts +145 -0
- package/src/index/modOrigin.ts +69 -0
- package/src/index/references.ts +534 -0
- package/src/overview/dependencies.ts +240 -0
- package/src/overview/eventBanner.ts +95 -0
- package/src/overview/eventDetail.ts +482 -0
- package/src/overview/eventGraph.ts +617 -0
- package/src/overview/eventVocabulary.ts +214 -0
- package/src/overview/locCoverage.ts +138 -0
- package/src/overview/modOverview.ts +29 -0
- package/src/overview/overrides.ts +89 -0
- package/src/parseCache.ts +81 -0
- package/src/parser/cst.ts +257 -0
- package/src/parser/encoding.ts +106 -0
- package/src/parser/index.ts +7 -0
- package/src/parser/lexer.ts +245 -0
- package/src/parser/locParser.ts +276 -0
- package/src/parser/parser.ts +360 -0
- package/src/schema/freqs.ts +70 -0
- package/src/schema/loader.ts +113 -0
- package/src/schema/types.ts +142 -0
- package/src/scopes/inference.ts +478 -0
- package/src/scopes/model.ts +148 -0
- package/src/scopes/varTypes.ts +290 -0
- package/src/server.ts +1894 -0
- package/src/serverData.ts +98 -0
- package/src/structure.ts +56 -0
- package/src/wordAt.ts +49 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/eventDetail: everything the graph inspector and the event simulator
|
|
3
|
+
* show about one event: localized title/desc/options (with their editable loc
|
|
4
|
+
* sites), each block rendered back as readable pseudo-script, the events and
|
|
5
|
+
* on_actions each block hands control to, and every referenced saved scope /
|
|
6
|
+
* variable / scripted effect/trigger / script value / chained event WITH its
|
|
7
|
+
* definition site so the webview can jump straight to it.
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import type {
|
|
11
|
+
EventDetail,
|
|
12
|
+
EventFieldInfo,
|
|
13
|
+
EventLocField,
|
|
14
|
+
EventOptionInfo,
|
|
15
|
+
EventRefInfo,
|
|
16
|
+
EventScriptLine,
|
|
17
|
+
EventSectionInfo,
|
|
18
|
+
EventStepTarget,
|
|
19
|
+
} from "@px-lsp/protocol/protocol";
|
|
20
|
+
import type { ServerData } from "../serverData";
|
|
21
|
+
import type { SchemaData } from "../schema/loader";
|
|
22
|
+
import { decode, LineIndex, parseScript, type BlockNode, type Statement } from "../parser";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Blocks rendered as a step of their own: the union across game profiles,
|
|
26
|
+
* which is safe because the names do not collide — `on_trigger_fail` and
|
|
27
|
+
* `cancellation_trigger` each exist in exactly one supported game (measured
|
|
28
|
+
* against both vanilla event trees: 1074 sites for the latter, 0 in the
|
|
29
|
+
* other), and a key the game does not have simply never matches.
|
|
30
|
+
*/
|
|
31
|
+
const SECTION_KEYS = new Set(["trigger", "immediate", "after", "on_trigger_fail", "cancellation_trigger"]);
|
|
32
|
+
/**
|
|
33
|
+
* Keys that gate or label an option instead of describing what it does, so
|
|
34
|
+
* `effectKeys` stays a summary of effects. The union across game profiles
|
|
35
|
+
* again; `default_option` (2138 vanilla sites) and `highlighted_option` (225)
|
|
36
|
+
* belong to the game the last four do not. Like `custom_tooltip` these still
|
|
37
|
+
* RENDER — only the summary drops them.
|
|
38
|
+
*/
|
|
39
|
+
const OPTION_META_KEYS = new Set([
|
|
40
|
+
"name",
|
|
41
|
+
"trigger",
|
|
42
|
+
"ai_chance",
|
|
43
|
+
"show_as_unavailable",
|
|
44
|
+
"flag",
|
|
45
|
+
"custom_tooltip",
|
|
46
|
+
"default_option",
|
|
47
|
+
"highlighted_option",
|
|
48
|
+
]);
|
|
49
|
+
/** Not rendered as an option effect: these gate or label the option. */
|
|
50
|
+
const OPTION_NON_EFFECT_KEYS = new Set(["name", "trigger", "ai_chance", "ai_value"]);
|
|
51
|
+
const EVENT_ID = /^[A-Za-z][A-Za-z0-9_-]*\.\d+$/;
|
|
52
|
+
/** A step-into target has to look like a name, not a weight or a `$PARAM$`. */
|
|
53
|
+
const TARGET_NAME = /^[A-Za-z][A-Za-z0-9_.-]*$/;
|
|
54
|
+
const SCOPE_PREFIX = /^(scope|var|local_var|global_var):([A-Za-z0-9_.-]+)$/;
|
|
55
|
+
const MAX_SECTION_KEYS = 12;
|
|
56
|
+
const MAX_REFS = 200;
|
|
57
|
+
/** Per-block line cap: the same budget the Studio's simulator uses. */
|
|
58
|
+
const MAX_BLOCK_LINES = 60;
|
|
59
|
+
const MAX_TARGETS = 40;
|
|
60
|
+
const MAX_FIRES = 24;
|
|
61
|
+
|
|
62
|
+
export function computeEventDetail(data: ServerData, schema: SchemaData, id: string): EventDetail | null {
|
|
63
|
+
const def = data.index.lookup(id).find((d) => d.kind === "event");
|
|
64
|
+
if (!def) return null;
|
|
65
|
+
let text: string;
|
|
66
|
+
try {
|
|
67
|
+
text = decode(fs.readFileSync(def.file)).text;
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const parse = parseScript(text);
|
|
72
|
+
const li = new LineIndex(text);
|
|
73
|
+
const stmt = parse.root.statements.find(
|
|
74
|
+
(s): s is Statement & { kind: "assignment" } =>
|
|
75
|
+
s.kind === "assignment" && s.key.text === id && childBlock(s) !== null
|
|
76
|
+
);
|
|
77
|
+
if (!stmt) return null;
|
|
78
|
+
const block = childBlock(stmt)!;
|
|
79
|
+
const lineOf = (offset: number) => li.positionAt(offset).line;
|
|
80
|
+
|
|
81
|
+
const detail: EventDetail = {
|
|
82
|
+
id,
|
|
83
|
+
file: def.file,
|
|
84
|
+
line: lineOf(stmt.key.range.start),
|
|
85
|
+
endLine: block.closeBrace !== null ? lineOf(block.closeBrace) : lineOf(block.range.end),
|
|
86
|
+
bodyLine: lineOf(block.range.start) + 1,
|
|
87
|
+
fields: scalarFields(block, lineOf),
|
|
88
|
+
sections: [],
|
|
89
|
+
options: [],
|
|
90
|
+
refs: [],
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const child of block.statements) {
|
|
94
|
+
if (child.kind !== "assignment") continue;
|
|
95
|
+
const key = child.key.text.toLowerCase();
|
|
96
|
+
const scalar = child.value?.kind === "scalar" ? child.value.text : null;
|
|
97
|
+
const sub = childBlock(child);
|
|
98
|
+
if (key === "type" && scalar) detail.type = scalar;
|
|
99
|
+
else if (key === "hidden" && scalar) detail.hidden = scalar === "yes";
|
|
100
|
+
else if (key === "theme" && scalar) detail.theme = scalar;
|
|
101
|
+
else if (key === "title") detail.title = scalar ? locField(data, scalar) : { key: "", dynamic: true };
|
|
102
|
+
else if (key === "desc") detail.desc = scalar ? locField(data, scalar) : { key: "", dynamic: true };
|
|
103
|
+
// The third event-level string, where the game has one. Read at the
|
|
104
|
+
// event's TOP level only: as an option key the same word labels the
|
|
105
|
+
// option instead, and that is not the event's own text.
|
|
106
|
+
else if (key === "flavor") detail.flavor = scalar ? locField(data, scalar) : { key: "", dynamic: true };
|
|
107
|
+
else if (SECTION_KEYS.has(key) && sub)
|
|
108
|
+
detail.sections.push(section(data, schema, child.key.text, sub, lineOf));
|
|
109
|
+
else if (key === "option" && sub) detail.options.push(option(data, schema, sub, lineOf));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
detail.refs = collectRefs(data, id, block, lineOf);
|
|
113
|
+
return detail;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The scalar `key = value` statements written directly in a block, with their
|
|
118
|
+
* lines, so an editor can rewrite one in place. Blocks are skipped: they are
|
|
119
|
+
* reported as sections / options. Duplicates keep the LAST site, which is the
|
|
120
|
+
* one the game reads and the one an edit must land on.
|
|
121
|
+
*/
|
|
122
|
+
function scalarFields(block: BlockNode, lineOf: (o: number) => number): EventFieldInfo[] {
|
|
123
|
+
const byKey = new Map<string, EventFieldInfo>();
|
|
124
|
+
for (const s of block.statements) {
|
|
125
|
+
if (s.kind !== "assignment" || s.key.quoted) continue;
|
|
126
|
+
if (s.value?.kind !== "scalar") continue;
|
|
127
|
+
byKey.set(s.key.text, {
|
|
128
|
+
key: s.key.text,
|
|
129
|
+
value: s.value.text,
|
|
130
|
+
line: lineOf(s.key.range.start),
|
|
131
|
+
...(s.value.quoted ? { quoted: true } : {}),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return [...byKey.values()].sort((a, b) => a.line - b.line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function childBlock(stmt: Statement): BlockNode | null {
|
|
138
|
+
if (stmt.kind !== "assignment") return null;
|
|
139
|
+
const v = stmt.value;
|
|
140
|
+
if (!v) return null;
|
|
141
|
+
if (v.kind === "block") return v;
|
|
142
|
+
if (v.kind === "tagged-block") return v.block;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Resolved loc: mod entry (editable in place) or the shadow-resolved value. */
|
|
147
|
+
function locField(data: ServerData, key: string): EventLocField {
|
|
148
|
+
const field: EventLocField = { key };
|
|
149
|
+
const locs = data.index.lookup(key).filter((d) => d.kind === "loc_key");
|
|
150
|
+
const best = locs[0];
|
|
151
|
+
if (best) {
|
|
152
|
+
field.text = best.value;
|
|
153
|
+
if (best.source === "mod") {
|
|
154
|
+
field.file = best.file;
|
|
155
|
+
field.line = best.line;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return field;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function section(
|
|
162
|
+
data: ServerData,
|
|
163
|
+
schema: SchemaData,
|
|
164
|
+
name: string,
|
|
165
|
+
block: BlockNode,
|
|
166
|
+
lineOf: (o: number) => number
|
|
167
|
+
): EventSectionInfo {
|
|
168
|
+
const keys: string[] = [];
|
|
169
|
+
const seen = new Set<string>();
|
|
170
|
+
for (const s of block.statements) {
|
|
171
|
+
if (s.kind !== "assignment") continue;
|
|
172
|
+
if (seen.has(s.key.text)) continue;
|
|
173
|
+
seen.add(s.key.text);
|
|
174
|
+
if (keys.length < MAX_SECTION_KEYS) keys.push(s.key.text);
|
|
175
|
+
}
|
|
176
|
+
const rendered = renderBlock(block, lineOf);
|
|
177
|
+
const targets = collectTargets(data, schema, block, lineOf);
|
|
178
|
+
return {
|
|
179
|
+
name,
|
|
180
|
+
line: lineOf(block.range.start),
|
|
181
|
+
keys,
|
|
182
|
+
lines: rendered.lines,
|
|
183
|
+
totalLines: rendered.totalLines,
|
|
184
|
+
targets: targets.targets,
|
|
185
|
+
targetsTotal: targets.total,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function option(
|
|
190
|
+
data: ServerData,
|
|
191
|
+
schema: SchemaData,
|
|
192
|
+
block: BlockNode,
|
|
193
|
+
lineOf: (o: number) => number
|
|
194
|
+
): EventOptionInfo {
|
|
195
|
+
const rendered = renderBlock(block, lineOf, OPTION_NON_EFFECT_KEYS);
|
|
196
|
+
const targets = collectTargets(data, schema, block, lineOf);
|
|
197
|
+
const info: EventOptionInfo = {
|
|
198
|
+
line: lineOf(block.range.start),
|
|
199
|
+
bodyLine: lineOf(block.range.start) + 1,
|
|
200
|
+
fields: scalarFields(block, lineOf),
|
|
201
|
+
effectKeys: [],
|
|
202
|
+
hasTrigger: false,
|
|
203
|
+
hasAiChance: false,
|
|
204
|
+
lines: rendered.lines,
|
|
205
|
+
totalLines: rendered.totalLines,
|
|
206
|
+
targets: targets.targets,
|
|
207
|
+
targetsTotal: targets.total,
|
|
208
|
+
};
|
|
209
|
+
const seen = new Set<string>();
|
|
210
|
+
for (const s of block.statements) {
|
|
211
|
+
if (s.kind !== "assignment") continue;
|
|
212
|
+
const key = s.key.text.toLowerCase();
|
|
213
|
+
const sub = childBlock(s);
|
|
214
|
+
if (key === "name") {
|
|
215
|
+
if (s.value?.kind === "scalar") info.name = locField(data, s.value.text);
|
|
216
|
+
else if (!info.name) info.name = { key: "", dynamic: true };
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (key === "trigger") {
|
|
220
|
+
info.hasTrigger = true;
|
|
221
|
+
// Rendered so the inspector can EDIT the gate, not merely mention it.
|
|
222
|
+
if (sub && !info.trigger)
|
|
223
|
+
info.trigger = { line: lineOf(s.key.range.start), ...renderBlock(sub, lineOf) };
|
|
224
|
+
}
|
|
225
|
+
if (key === "ai_chance") {
|
|
226
|
+
info.hasAiChance = true;
|
|
227
|
+
if (sub && !info.aiChance)
|
|
228
|
+
info.aiChance = { line: lineOf(s.key.range.start), ...renderBlock(sub, lineOf) };
|
|
229
|
+
}
|
|
230
|
+
if (OPTION_META_KEYS.has(key) || seen.has(s.key.text)) continue;
|
|
231
|
+
seen.add(s.key.text);
|
|
232
|
+
if (info.effectKeys.length < MAX_SECTION_KEYS) info.effectKeys.push(s.key.text);
|
|
233
|
+
}
|
|
234
|
+
return info;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Flatten a block back into indented pseudo-script. `totalLines` counts every
|
|
239
|
+
* line the block would produce, so a capped render can say how much it hid
|
|
240
|
+
* instead of silently truncating.
|
|
241
|
+
*/
|
|
242
|
+
function renderBlock(
|
|
243
|
+
block: BlockNode,
|
|
244
|
+
lineOf: (o: number) => number,
|
|
245
|
+
skipTopLevel?: Set<string>
|
|
246
|
+
): { lines: EventScriptLine[]; totalLines: number } {
|
|
247
|
+
const lines: EventScriptLine[] = [];
|
|
248
|
+
let totalLines = 0;
|
|
249
|
+
const push = (depth: number, text: string, offset: number) => {
|
|
250
|
+
totalLines++;
|
|
251
|
+
if (lines.length < MAX_BLOCK_LINES) lines.push({ depth, text, line: lineOf(offset) });
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const flatten = (b: BlockNode, depth: number): void => {
|
|
255
|
+
for (const s of b.statements) {
|
|
256
|
+
if (s.kind === "value") {
|
|
257
|
+
if (s.value.kind === "scalar") {
|
|
258
|
+
push(depth, s.value.text, s.value.range.start);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const inner = s.value.kind === "block" ? s.value : s.value.block;
|
|
262
|
+
push(depth, "{", inner.range.start);
|
|
263
|
+
flatten(inner, depth + 1);
|
|
264
|
+
push(depth, "}", inner.closeBrace ?? inner.range.end);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (depth === 0 && skipTopLevel?.has(s.key.text.toLowerCase())) continue;
|
|
268
|
+
const head = s.op ? `${s.key.text} ${s.op}` : s.key.text;
|
|
269
|
+
const v = s.value;
|
|
270
|
+
if (!v) {
|
|
271
|
+
push(depth, head, s.key.range.start);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (v.kind === "scalar") {
|
|
275
|
+
push(depth, `${head} ${v.quoted ? `"${v.text}"` : v.text}`, s.key.range.start);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const inner = v.kind === "block" ? v : v.block;
|
|
279
|
+
const tag = v.kind === "tagged-block" ? `${v.tag.text} ` : "";
|
|
280
|
+
push(depth, `${head} ${tag}{`, s.key.range.start);
|
|
281
|
+
flatten(inner, depth + 1);
|
|
282
|
+
push(depth, "}", inner.closeBrace ?? inner.range.end);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
flatten(block, 0);
|
|
286
|
+
return { lines, totalLines };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Ref-field kinds that mean "control moves here". */
|
|
290
|
+
function stepKind(kinds: string[]): "event" | "on_action" | null {
|
|
291
|
+
if (kinds.includes("event")) return "event";
|
|
292
|
+
if (kinds.includes("on_action")) return "on_action";
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Every event/on_action a block hands control to, in source order. Driven by
|
|
298
|
+
* the profile's reference fields rather than a hard-coded key list, so
|
|
299
|
+
* `trigger_event`, `on_action(s)`, `events`, `random_events`, `first_valid`
|
|
300
|
+
* and any per-game equivalent are all covered by the same walk.
|
|
301
|
+
*
|
|
302
|
+
* The block form `trigger_event = { id = X … }` is the one special case: `id`
|
|
303
|
+
* is too generic to be a schema ref field of its own (see REF_FIELDS), so it
|
|
304
|
+
* counts only inside a block whose owning key already references events or
|
|
305
|
+
* on_actions, and it keeps that key as its `via`.
|
|
306
|
+
*/
|
|
307
|
+
function collectTargets(
|
|
308
|
+
data: ServerData,
|
|
309
|
+
schema: SchemaData,
|
|
310
|
+
block: BlockNode,
|
|
311
|
+
lineOf: (o: number) => number,
|
|
312
|
+
resolveOnActions = true
|
|
313
|
+
): { targets: EventStepTarget[]; total: number } {
|
|
314
|
+
const out: EventStepTarget[] = [];
|
|
315
|
+
const seen = new Set<string>();
|
|
316
|
+
// Counted past the cap: a capped list that reported its own length as the
|
|
317
|
+
// total would understate what the block really does.
|
|
318
|
+
let total = 0;
|
|
319
|
+
|
|
320
|
+
const add = (via: string, name: string, offset: number, wanted: "event" | "on_action") => {
|
|
321
|
+
// `random_events = { 800 = 0 }` weights "no event" as a literal 0.
|
|
322
|
+
if (!TARGET_NAME.test(name)) return;
|
|
323
|
+
const key = `${via}:${name}`;
|
|
324
|
+
if (seen.has(key)) return;
|
|
325
|
+
seen.add(key);
|
|
326
|
+
total++;
|
|
327
|
+
if (out.length >= MAX_TARGETS) return;
|
|
328
|
+
const target: EventStepTarget = { via, name, kind: "unknown", line: lineOf(offset) };
|
|
329
|
+
const defs = data.index.lookup(name).filter((d) => d.kind === wanted);
|
|
330
|
+
const def = defs[0];
|
|
331
|
+
if (def) {
|
|
332
|
+
target.kind = wanted;
|
|
333
|
+
target.file = def.file;
|
|
334
|
+
target.defLine = def.line;
|
|
335
|
+
if (defs.length > 1) target.defCount = defs.length;
|
|
336
|
+
if (wanted === "on_action" && resolveOnActions) {
|
|
337
|
+
const fired = resolveOnAction(data, schema, def.file, name);
|
|
338
|
+
if (fired) {
|
|
339
|
+
target.firesTotal = fired.total;
|
|
340
|
+
target.fires = fired.targets.slice(0, MAX_FIRES);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
out.push(target);
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** The enclosing reference field, so entries inside its block keep its name. */
|
|
348
|
+
type Field = { via: string; kind: "event" | "on_action" };
|
|
349
|
+
|
|
350
|
+
const walk = (b: BlockNode, inherited: Field | null): void => {
|
|
351
|
+
for (const s of b.statements) {
|
|
352
|
+
if (s.kind === "value") {
|
|
353
|
+
// A bare list entry inherits the enclosing field (`events = { a b }`).
|
|
354
|
+
if (inherited && s.value.kind === "scalar" && !s.value.quoted)
|
|
355
|
+
add(inherited.via, s.value.text, s.value.range.start, inherited.kind);
|
|
356
|
+
else if (s.value.kind === "block") walk(s.value, null);
|
|
357
|
+
else if (s.value.kind === "tagged-block") walk(s.value.block, null);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const key = s.key.quoted ? "" : s.key.text.toLowerCase();
|
|
361
|
+
const kind = stepKind(schema.refFields.get(key)?.kinds ?? []);
|
|
362
|
+
const own: Field | null = kind ? { via: key, kind } : null;
|
|
363
|
+
const v = s.value;
|
|
364
|
+
if (own && v?.kind === "scalar" && !v.quoted) {
|
|
365
|
+
add(own.via, v.text, v.range.start, own.kind);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
// `random_events = { 100 = evt.1 }`: the weight is the key, the event the
|
|
369
|
+
// value; `trigger_event = { id = evt.1 … }` names the event under `id`.
|
|
370
|
+
if (inherited && v?.kind === "scalar" && !v.quoted && (/^\d+(\.\d+)?$/.test(key) || key === "id"))
|
|
371
|
+
add(inherited.via, v.text, v.range.start, inherited.kind);
|
|
372
|
+
const sub = childBlock(s);
|
|
373
|
+
if (sub) walk(sub, own);
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
walk(block, null);
|
|
377
|
+
return { targets: out, total };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* What an on_action fires, read from its own definition one level deep. Null
|
|
382
|
+
* when the definition cannot be read; an empty list is the honest "its
|
|
383
|
+
* definition names no events" answer.
|
|
384
|
+
*/
|
|
385
|
+
function resolveOnAction(
|
|
386
|
+
data: ServerData,
|
|
387
|
+
schema: SchemaData,
|
|
388
|
+
file: string,
|
|
389
|
+
name: string
|
|
390
|
+
): { targets: EventStepTarget[]; total: number } | null {
|
|
391
|
+
let text: string;
|
|
392
|
+
try {
|
|
393
|
+
text = decode(fs.readFileSync(file)).text;
|
|
394
|
+
} catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
const parse = parseScript(text);
|
|
398
|
+
const li = new LineIndex(text);
|
|
399
|
+
const stmt = parse.root.statements.find(
|
|
400
|
+
(s): s is Statement & { kind: "assignment" } =>
|
|
401
|
+
s.kind === "assignment" && s.key.text === name && childBlock(s) !== null
|
|
402
|
+
);
|
|
403
|
+
if (!stmt) return null;
|
|
404
|
+
// resolveOnActions=false: one level only, so a self-chaining on_action pair
|
|
405
|
+
// cannot recurse.
|
|
406
|
+
return collectTargets(data, schema, childBlock(stmt)!, (o) => li.positionAt(o).line, false);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Every reference inside the event body, deduped by kind+name, each with its
|
|
411
|
+
* definition/save site from the index: scope:/var: prefixed names, keys that
|
|
412
|
+
* resolve to scripted effects/triggers, values that resolve to script values,
|
|
413
|
+
* and chained event ids.
|
|
414
|
+
*/
|
|
415
|
+
function collectRefs(
|
|
416
|
+
data: ServerData,
|
|
417
|
+
selfId: string,
|
|
418
|
+
block: BlockNode,
|
|
419
|
+
lineOf: (o: number) => number
|
|
420
|
+
): EventRefInfo[] {
|
|
421
|
+
const refs = new Map<string, EventRefInfo>();
|
|
422
|
+
|
|
423
|
+
const add = (kind: EventRefInfo["kind"], name: string, offset: number, defKinds: string[]) => {
|
|
424
|
+
const mapKey = `${kind}:${name}`;
|
|
425
|
+
if (refs.has(mapKey) || refs.size >= MAX_REFS) return;
|
|
426
|
+
const ref: EventRefInfo = { name, kind, line: lineOf(offset) };
|
|
427
|
+
const defs = data.index.lookupAll(name).filter((d) => defKinds.includes(d.kind));
|
|
428
|
+
if (defs.length > 0) {
|
|
429
|
+
// Prefer mod sites (the ones the user can edit).
|
|
430
|
+
const best = defs.find((d) => d.source === "mod") ?? defs[0];
|
|
431
|
+
ref.defFile = best.file;
|
|
432
|
+
ref.defLine = best.line;
|
|
433
|
+
ref.defCount = defs.length;
|
|
434
|
+
}
|
|
435
|
+
refs.set(mapKey, ref);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const scanScalar = (textValue: string, offset: number, isKey: boolean) => {
|
|
439
|
+
const prefixed = SCOPE_PREFIX.exec(textValue);
|
|
440
|
+
if (prefixed) {
|
|
441
|
+
const bare = prefixed[2].split(".")[0]; // scope:x.culture → x
|
|
442
|
+
if (prefixed[1] === "scope") add("saved_scope", bare, offset, ["saved_scope"]);
|
|
443
|
+
else add("variable", bare, offset, ["variable"]);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (!isKey && EVENT_ID.test(textValue) && textValue !== selfId) {
|
|
447
|
+
if (data.index.lookup(textValue).some((d) => d.kind === "event"))
|
|
448
|
+
add("event", textValue, offset, ["event"]);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const defs = data.index.lookup(textValue);
|
|
452
|
+
if (defs.length === 0) return;
|
|
453
|
+
if (isKey) {
|
|
454
|
+
const scripted = defs.find((d) => d.kind === "scripted_effect" || d.kind === "scripted_trigger");
|
|
455
|
+
if (scripted) {
|
|
456
|
+
add(scripted.kind as "scripted_effect" | "scripted_trigger", textValue, offset, [scripted.kind]);
|
|
457
|
+
}
|
|
458
|
+
} else if (defs.some((d) => d.kind === "script_value")) {
|
|
459
|
+
add("script_value", textValue, offset, ["script_value"]);
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const walk = (b: BlockNode) => {
|
|
464
|
+
for (const s of b.statements) {
|
|
465
|
+
if (s.kind === "assignment") {
|
|
466
|
+
if (!s.key.quoted) scanScalar(s.key.text, s.key.range.start, true);
|
|
467
|
+
if (s.value?.kind === "scalar" && !s.value.quoted)
|
|
468
|
+
scanScalar(s.value.text, s.value.range.start, false);
|
|
469
|
+
const sub = childBlock(s);
|
|
470
|
+
if (sub) walk(sub);
|
|
471
|
+
} else if (s.value.kind === "scalar" && !s.value.quoted) {
|
|
472
|
+
scanScalar(s.value.text, s.value.range.start, false);
|
|
473
|
+
} else if (s.value.kind === "block") {
|
|
474
|
+
walk(s.value);
|
|
475
|
+
} else if (s.value.kind === "tagged-block") {
|
|
476
|
+
walk(s.value.block);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
walk(block);
|
|
481
|
+
return [...refs.values()];
|
|
482
|
+
}
|