@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.
Files changed (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. 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
+ }