hoi4-agent-tools 2.1.0 → 2.2.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/CHANGELOG.md +13 -0
- package/README.md +7 -2
- package/dist/bin/stdio.js +6 -1
- package/dist/hoi4_agent_tools/core/engine.js +9 -0
- package/dist/hoi4_agent_tools/core/index.js +292 -1
- package/dist/hoi4_agent_tools/focus/render.js +37 -13
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/server/result.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +530 -0
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +39 -2
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +368 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +77 -0
- package/dist/hoi4_agent_tools/technology/compare.js +240 -0
- package/dist/hoi4_agent_tools/technology/graph.js +1196 -0
- package/dist/hoi4_agent_tools/technology/index.js +7 -0
- package/dist/hoi4_agent_tools/technology/model.js +2 -0
- package/dist/hoi4_agent_tools/technology/queries.js +300 -0
- package/dist/hoi4_agent_tools/technology/render.js +641 -0
- package/dist/hoi4_agent_tools/technology/service.js +686 -0
- package/dist/hoi4_agent_tools/technology/source-analysis.js +967 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +5 -1
- package/docs/setup.md +1 -1
- package/docs/technology.md +90 -0
- package/package.json +12 -3
- package/server.json +3 -3
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { compareCodeUnits, deterministicId } from '../core/canonical.js';
|
|
3
|
+
import { assignments, childBlocks, firstScalar, nodeLocation, parseClausewitz, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
|
|
4
|
+
const technologyStructuralFields = new Set([
|
|
5
|
+
'ai_research_weights',
|
|
6
|
+
'ai_will_do',
|
|
7
|
+
'allow',
|
|
8
|
+
'allow_branch',
|
|
9
|
+
'categories',
|
|
10
|
+
'desc',
|
|
11
|
+
'doctrine',
|
|
12
|
+
'doctrine_name',
|
|
13
|
+
'enable_ability',
|
|
14
|
+
'enable_abilities',
|
|
15
|
+
'enable_building',
|
|
16
|
+
'enable_equipment_modules',
|
|
17
|
+
'enable_equipments',
|
|
18
|
+
'enable_subunits',
|
|
19
|
+
'enable_tactic',
|
|
20
|
+
'folder',
|
|
21
|
+
'force_use_small_tech_layout',
|
|
22
|
+
'on_research_complete',
|
|
23
|
+
'on_research_complete_limit',
|
|
24
|
+
'path',
|
|
25
|
+
'research_cost',
|
|
26
|
+
'show_equipment_icon',
|
|
27
|
+
'start_year',
|
|
28
|
+
'sub_technologies',
|
|
29
|
+
'tags',
|
|
30
|
+
'xp_boost_cost',
|
|
31
|
+
'xp_research_bonus',
|
|
32
|
+
'xp_unlock_cost',
|
|
33
|
+
'xor',
|
|
34
|
+
]);
|
|
35
|
+
const eventDefinitionKeys = new Set([
|
|
36
|
+
'country_event',
|
|
37
|
+
'news_event',
|
|
38
|
+
'state_event',
|
|
39
|
+
'unit_leader_event',
|
|
40
|
+
'operative_leader_event',
|
|
41
|
+
]);
|
|
42
|
+
const decisionStructuralKeys = new Set([
|
|
43
|
+
'allowed',
|
|
44
|
+
'available',
|
|
45
|
+
'icon',
|
|
46
|
+
'picture',
|
|
47
|
+
'priority',
|
|
48
|
+
'scripted_gui',
|
|
49
|
+
'target_root_trigger',
|
|
50
|
+
'visible',
|
|
51
|
+
'visible_when_empty',
|
|
52
|
+
]);
|
|
53
|
+
const staticIdentifier = /^[A-Za-z0-9_.:-]+$/u;
|
|
54
|
+
function normalizedPath(file) {
|
|
55
|
+
return file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
function provenance(file, document, node, symbol) {
|
|
58
|
+
return {
|
|
59
|
+
path: file.displayPath,
|
|
60
|
+
rootKind: file.rootKind,
|
|
61
|
+
loadOrder: file.loadOrder,
|
|
62
|
+
location: nodeLocation(document, node, symbol),
|
|
63
|
+
sourceHash: file.sha256,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function sourceText(document, node) {
|
|
67
|
+
return document.text.slice(node.start, node.end);
|
|
68
|
+
}
|
|
69
|
+
function scalarEntries(block) {
|
|
70
|
+
return block.entries.filter((entry) => entry.type === 'scalar');
|
|
71
|
+
}
|
|
72
|
+
function scalarValues(block) {
|
|
73
|
+
return scalarEntries(block).map(({ value }) => value);
|
|
74
|
+
}
|
|
75
|
+
function assignmentScalars(block, key) {
|
|
76
|
+
return assignments(block, key).flatMap(({ value }) => (value.type === 'scalar' ? [value] : []));
|
|
77
|
+
}
|
|
78
|
+
function firstValue(block, key) {
|
|
79
|
+
return firstScalar(block, key)?.value;
|
|
80
|
+
}
|
|
81
|
+
function booleanValue(value) {
|
|
82
|
+
if (value?.toLowerCase() === 'yes')
|
|
83
|
+
return true;
|
|
84
|
+
if (value?.toLowerCase() === 'no')
|
|
85
|
+
return false;
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
function numericLiteral(value) {
|
|
89
|
+
if (value === undefined || !/^-?(?:\d+\.?\d*|\.\d+)$/u.test(value))
|
|
90
|
+
return undefined;
|
|
91
|
+
const parsed = Number(value);
|
|
92
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
93
|
+
}
|
|
94
|
+
function constantsIn(block) {
|
|
95
|
+
const result = new Map();
|
|
96
|
+
for (const assignment of assignments(block)) {
|
|
97
|
+
if (!assignment.key.value.startsWith('@') || assignment.value.type !== 'scalar')
|
|
98
|
+
continue;
|
|
99
|
+
result.set(assignment.key.value, assignment.value.value);
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function resolvedNumber(value, constants) {
|
|
104
|
+
let current = value;
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
for (let depth = 0; depth < 32; depth += 1) {
|
|
107
|
+
const literal = numericLiteral(current);
|
|
108
|
+
if (literal !== undefined)
|
|
109
|
+
return literal;
|
|
110
|
+
if (current === undefined || !current.startsWith('@') || seen.has(current))
|
|
111
|
+
return undefined;
|
|
112
|
+
seen.add(current);
|
|
113
|
+
current = constants.get(current);
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
function aiMetadata(block) {
|
|
118
|
+
const ai = childBlocks(block, 'ai_will_do')[0];
|
|
119
|
+
const weights = childBlocks(block, 'ai_research_weights')[0];
|
|
120
|
+
const researchWeights = Object.fromEntries((weights === undefined ? [] : assignments(weights))
|
|
121
|
+
.flatMap(({ key, value }) => value.type === 'scalar' ? [[key.value, value.value]] : [])
|
|
122
|
+
.sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
123
|
+
if (ai === undefined)
|
|
124
|
+
return { present: false, zero: 'unknown', researchWeights };
|
|
125
|
+
const base = firstValue(ai, 'base');
|
|
126
|
+
const factor = firstValue(ai, 'factor');
|
|
127
|
+
const zero = numericLiteral(base ?? factor) === 0
|
|
128
|
+
? true
|
|
129
|
+
: base === undefined && factor === undefined
|
|
130
|
+
? 'unknown'
|
|
131
|
+
: false;
|
|
132
|
+
return {
|
|
133
|
+
present: true,
|
|
134
|
+
...(base === undefined ? {} : { base }),
|
|
135
|
+
...(factor === undefined ? {} : { factor }),
|
|
136
|
+
zero,
|
|
137
|
+
researchWeights,
|
|
138
|
+
expression: sourceTextForBlock(ai),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function sourceTextForBlock(block) {
|
|
142
|
+
// The exact source text is attached later when an unsupported expression needs it. AI metadata
|
|
143
|
+
// keeps a stable structural summary to avoid retaining a second copy of every source file.
|
|
144
|
+
return assignments(block)
|
|
145
|
+
.map(({ key, value }) => `${key.value}=${value.type === 'scalar' ? value.value : '{...}'}`)
|
|
146
|
+
.join(' ');
|
|
147
|
+
}
|
|
148
|
+
function hiddenStatus(block) {
|
|
149
|
+
const allow = childBlocks(block, 'allow')[0];
|
|
150
|
+
if (allow === undefined)
|
|
151
|
+
return false;
|
|
152
|
+
const always = booleanValue(firstValue(allow, 'always'));
|
|
153
|
+
if (always === false)
|
|
154
|
+
return true;
|
|
155
|
+
if (always === true && assignments(allow).length === 1)
|
|
156
|
+
return false;
|
|
157
|
+
return 'unknown';
|
|
158
|
+
}
|
|
159
|
+
function isDynamic(value) {
|
|
160
|
+
return !staticIdentifier.test(value) || /\[|\]|\$|\?|^var:|^event_target:/u.test(value);
|
|
161
|
+
}
|
|
162
|
+
function unresolved(kind, expression, ownerId, location, code, message) {
|
|
163
|
+
return {
|
|
164
|
+
id: deterministicId('tech-unresolved', { kind, expression, ownerId, location }),
|
|
165
|
+
kind,
|
|
166
|
+
expression,
|
|
167
|
+
...(ownerId === undefined ? {} : { ownerId }),
|
|
168
|
+
...(location === undefined ? {} : { location }),
|
|
169
|
+
confidence: 'unresolved',
|
|
170
|
+
blockers: [{ code, message, ...(location === undefined ? {} : { location }) }],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function addDynamicOrStatic(values, ownerId, document, staticAction, output, kind) {
|
|
174
|
+
for (const value of values) {
|
|
175
|
+
if (!isDynamic(value.value)) {
|
|
176
|
+
staticAction(value);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const location = nodeLocation(document, value, ownerId);
|
|
180
|
+
output.push(unresolved(kind, value.value, ownerId, location, 'TECH_DYNAMIC_REFERENCE_UNRESOLVED', `Dynamic technology reference ${value.value} cannot be resolved statically`));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function parseTechnologyDefinitions(file, document, fragment) {
|
|
184
|
+
for (const wrapper of childBlocks(document.root, 'technologies')) {
|
|
185
|
+
const constants = constantsIn(wrapper);
|
|
186
|
+
for (const definition of assignments(wrapper)) {
|
|
187
|
+
if (definition.key.value.startsWith('@') || definition.value.type !== 'block')
|
|
188
|
+
continue;
|
|
189
|
+
const block = definition.value;
|
|
190
|
+
const id = definition.key.value;
|
|
191
|
+
const source = provenance(file, document, definition, id);
|
|
192
|
+
const folders = [];
|
|
193
|
+
for (const folder of childBlocks(block, 'folder')) {
|
|
194
|
+
const folderScalar = firstScalar(folder, 'name');
|
|
195
|
+
if (folderScalar === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
const folderId = folderScalar.value;
|
|
198
|
+
if (isDynamic(folderId)) {
|
|
199
|
+
fragment.unresolved.push(unresolved('dynamic_reference', folderId, id, nodeLocation(document, folderScalar, id), 'TECH_DYNAMIC_FOLDER_UNRESOLVED', `Dynamic folder reference ${folderId} cannot be resolved statically`));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
folders.push(folderId);
|
|
203
|
+
const position = childBlocks(folder, 'position')[0];
|
|
204
|
+
const xScalar = position === undefined ? undefined : firstScalar(position, 'x');
|
|
205
|
+
const yScalar = position === undefined ? undefined : firstScalar(position, 'y');
|
|
206
|
+
const x = resolvedNumber(xScalar?.value, constants);
|
|
207
|
+
const y = resolvedNumber(yScalar?.value, constants);
|
|
208
|
+
fragment.placements.push({
|
|
209
|
+
id: deterministicId('tech-placement', {
|
|
210
|
+
technologyId: id,
|
|
211
|
+
folderId,
|
|
212
|
+
offset: folder.start,
|
|
213
|
+
path: file.displayPath,
|
|
214
|
+
}),
|
|
215
|
+
technologyId: id,
|
|
216
|
+
folderId,
|
|
217
|
+
...(x === undefined ? {} : { x }),
|
|
218
|
+
...(y === undefined ? {} : { y }),
|
|
219
|
+
...(xScalar === undefined || x !== undefined ? {} : { xExpression: xScalar.value }),
|
|
220
|
+
...(yScalar === undefined || y !== undefined ? {} : { yExpression: yScalar.value }),
|
|
221
|
+
sourceAccurate: true,
|
|
222
|
+
location: nodeLocation(document, folder, id),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
for (const pathBlock of childBlocks(block, 'path')) {
|
|
226
|
+
const target = firstScalar(pathBlock, 'leads_to_tech');
|
|
227
|
+
if (target === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
if (isDynamic(target.value)) {
|
|
230
|
+
fragment.unresolved.push(unresolved('dynamic_reference', target.value, id, nodeLocation(document, target, id), 'TECH_DYNAMIC_PREREQUISITE_UNRESOLVED', `Dynamic prerequisite target ${target.value} cannot be resolved statically`));
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
fragment.edges.push({
|
|
234
|
+
id: deterministicId('tech-edge', {
|
|
235
|
+
kind: 'prerequisite',
|
|
236
|
+
from: id,
|
|
237
|
+
to: target.value,
|
|
238
|
+
path: file.displayPath,
|
|
239
|
+
offset: target.start,
|
|
240
|
+
}),
|
|
241
|
+
kind: 'prerequisite',
|
|
242
|
+
from: id,
|
|
243
|
+
to: target.value,
|
|
244
|
+
...(firstValue(pathBlock, 'research_cost_coeff') === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: { coefficient: firstValue(pathBlock, 'research_cost_coeff') }),
|
|
247
|
+
...(booleanValue(firstValue(pathBlock, 'ignore_for_layout')) === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { ignoreForLayout: booleanValue(firstValue(pathBlock, 'ignore_for_layout')) }),
|
|
250
|
+
location: nodeLocation(document, target, id),
|
|
251
|
+
confidence: 'confirmed',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
for (const xor of childBlocks(block, 'xor')) {
|
|
255
|
+
addDynamicOrStatic(scalarEntries(xor), id, document, (target) => fragment.edges.push({
|
|
256
|
+
id: deterministicId('tech-edge', {
|
|
257
|
+
kind: 'exclusive',
|
|
258
|
+
from: id,
|
|
259
|
+
to: target.value,
|
|
260
|
+
path: file.displayPath,
|
|
261
|
+
offset: target.start,
|
|
262
|
+
}),
|
|
263
|
+
kind: 'exclusive',
|
|
264
|
+
from: id,
|
|
265
|
+
to: target.value,
|
|
266
|
+
location: nodeLocation(document, target, id),
|
|
267
|
+
confidence: 'confirmed',
|
|
268
|
+
}), fragment.unresolved, 'dynamic_reference');
|
|
269
|
+
}
|
|
270
|
+
const subTechnologies = [];
|
|
271
|
+
for (const sub of childBlocks(block, 'sub_technologies')) {
|
|
272
|
+
addDynamicOrStatic(scalarEntries(sub), id, document, (target) => {
|
|
273
|
+
subTechnologies.push(target.value);
|
|
274
|
+
fragment.edges.push({
|
|
275
|
+
id: deterministicId('tech-edge', {
|
|
276
|
+
kind: 'sub_technology',
|
|
277
|
+
from: id,
|
|
278
|
+
to: target.value,
|
|
279
|
+
path: file.displayPath,
|
|
280
|
+
offset: target.start,
|
|
281
|
+
}),
|
|
282
|
+
kind: 'sub_technology',
|
|
283
|
+
from: id,
|
|
284
|
+
to: target.value,
|
|
285
|
+
location: nodeLocation(document, target, id),
|
|
286
|
+
confidence: 'confirmed',
|
|
287
|
+
});
|
|
288
|
+
}, fragment.unresolved, 'dynamic_reference');
|
|
289
|
+
}
|
|
290
|
+
const unlockSignatureFields = [];
|
|
291
|
+
const addUnlock = (kind, target, level) => {
|
|
292
|
+
if (isDynamic(target.value)) {
|
|
293
|
+
fragment.unresolved.push(unresolved('dynamic_reference', target.value, id, nodeLocation(document, target, id), 'TECH_DYNAMIC_UNLOCK_UNRESOLVED', `Dynamic unlock target ${target.value} cannot be resolved statically`));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
unlockSignatureFields.push(`${kind}:${target.value}:${level ?? ''}`);
|
|
297
|
+
fragment.unlocks.push({
|
|
298
|
+
id: deterministicId('tech-unlock', {
|
|
299
|
+
technologyId: id,
|
|
300
|
+
kind,
|
|
301
|
+
targetId: target.value,
|
|
302
|
+
level,
|
|
303
|
+
path: file.displayPath,
|
|
304
|
+
offset: target.start,
|
|
305
|
+
}),
|
|
306
|
+
technologyId: id,
|
|
307
|
+
kind,
|
|
308
|
+
targetId: target.value,
|
|
309
|
+
...(level === undefined ? {} : { level }),
|
|
310
|
+
location: nodeLocation(document, target, id),
|
|
311
|
+
confidence: 'confirmed',
|
|
312
|
+
resolved: 'unknown',
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
for (const [field, kind] of [
|
|
316
|
+
['enable_equipments', 'equipment'],
|
|
317
|
+
['enable_equipment_modules', 'equipment_module'],
|
|
318
|
+
['enable_subunits', 'sub_unit'],
|
|
319
|
+
['enable_abilities', 'ability'],
|
|
320
|
+
]) {
|
|
321
|
+
for (const unlockBlock of childBlocks(block, field))
|
|
322
|
+
for (const target of scalarEntries(unlockBlock))
|
|
323
|
+
addUnlock(kind, target);
|
|
324
|
+
}
|
|
325
|
+
for (const building of childBlocks(block, 'enable_building')) {
|
|
326
|
+
const target = firstScalar(building, 'building');
|
|
327
|
+
if (target !== undefined)
|
|
328
|
+
addUnlock('building', target, firstValue(building, 'level'));
|
|
329
|
+
}
|
|
330
|
+
for (const tactic of assignmentScalars(block, 'enable_tactic'))
|
|
331
|
+
addUnlock('tactic', tactic);
|
|
332
|
+
for (const ability of assignmentScalars(block, 'enable_ability'))
|
|
333
|
+
addUnlock('ability', ability);
|
|
334
|
+
for (const assignment of assignments(block)) {
|
|
335
|
+
if (!assignment.key.value.startsWith('enable_') ||
|
|
336
|
+
technologyStructuralFields.has(assignment.key.value))
|
|
337
|
+
continue;
|
|
338
|
+
if (assignment.value.type === 'scalar')
|
|
339
|
+
addUnlock('other', assignment.value);
|
|
340
|
+
else
|
|
341
|
+
for (const target of scalarEntries(assignment.value))
|
|
342
|
+
addUnlock('other', target);
|
|
343
|
+
}
|
|
344
|
+
const categories = childBlocks(block, 'categories').flatMap(scalarValues);
|
|
345
|
+
const tags = childBlocks(block, 'tags').flatMap(scalarValues);
|
|
346
|
+
const effectKeys = [
|
|
347
|
+
...new Set(assignments(block)
|
|
348
|
+
.map(({ key }) => key.value)
|
|
349
|
+
.filter((key) => !technologyStructuralFields.has(key) && !key.startsWith('@'))),
|
|
350
|
+
].sort(compareCodeUnits);
|
|
351
|
+
const unsupportedFields = fragment.placements
|
|
352
|
+
.filter(({ technologyId, xExpression, yExpression }) => technologyId === id && (xExpression !== undefined || yExpression !== undefined))
|
|
353
|
+
.map((placement) => ({
|
|
354
|
+
field: 'folder.position',
|
|
355
|
+
expression: `${placement.xExpression ?? placement.x ?? '?'}:${placement.yExpression ?? placement.y ?? '?'}`,
|
|
356
|
+
location: placement.location,
|
|
357
|
+
reason: 'The source coordinate expression is not a resolvable file-local numeric constant',
|
|
358
|
+
}));
|
|
359
|
+
const doctrine = booleanValue(firstValue(block, 'doctrine')) === true;
|
|
360
|
+
fragment.technologies.push({
|
|
361
|
+
id,
|
|
362
|
+
kind: doctrine ? 'legacy_doctrine' : 'technology',
|
|
363
|
+
source,
|
|
364
|
+
rawSource: document.text.slice(definition.start, definition.end),
|
|
365
|
+
...(firstValue(block, 'start_year') === undefined
|
|
366
|
+
? {}
|
|
367
|
+
: { startYear: firstValue(block, 'start_year') }),
|
|
368
|
+
...(firstValue(block, 'research_cost') === undefined
|
|
369
|
+
? {}
|
|
370
|
+
: { researchCost: firstValue(block, 'research_cost') }),
|
|
371
|
+
...(firstValue(block, 'doctrine_name') === undefined
|
|
372
|
+
? {}
|
|
373
|
+
: { doctrineName: firstValue(block, 'doctrine_name') }),
|
|
374
|
+
hidden: hiddenStatus(block),
|
|
375
|
+
folders: [...new Set(folders)].sort(compareCodeUnits),
|
|
376
|
+
categories: [...new Set(categories)].sort(compareCodeUnits),
|
|
377
|
+
tags: [...new Set(tags)].sort(compareCodeUnits),
|
|
378
|
+
subTechnologies: [...new Set(subTechnologies)].sort(compareCodeUnits),
|
|
379
|
+
ai: aiMetadata(block),
|
|
380
|
+
iconSprite: `GFX_${id}_medium`,
|
|
381
|
+
effectKeys,
|
|
382
|
+
effectSignatureFields: [...effectKeys, ...unlockSignatureFields].sort(compareCodeUnits),
|
|
383
|
+
unsupportedFields,
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function parseTechnologyTags(file, document, fragment) {
|
|
389
|
+
for (const block of childBlocks(document.root, 'technology_categories')) {
|
|
390
|
+
for (const scalar of scalarEntries(block)) {
|
|
391
|
+
fragment.categories.push({
|
|
392
|
+
id: scalar.value,
|
|
393
|
+
kind: 'category',
|
|
394
|
+
source: provenance(file, document, scalar, scalar.value),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
for (const block of childBlocks(document.root, 'technology_tags')) {
|
|
399
|
+
for (const scalar of scalarEntries(block)) {
|
|
400
|
+
fragment.categories.push({
|
|
401
|
+
id: scalar.value,
|
|
402
|
+
kind: 'tag',
|
|
403
|
+
source: provenance(file, document, scalar, scalar.value),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const wrapper of childBlocks(document.root, 'technology_folders')) {
|
|
408
|
+
for (const definition of assignments(wrapper)) {
|
|
409
|
+
if (definition.value.type !== 'block')
|
|
410
|
+
continue;
|
|
411
|
+
const id = definition.key.value;
|
|
412
|
+
const block = definition.value;
|
|
413
|
+
fragment.folders.push({
|
|
414
|
+
id,
|
|
415
|
+
doctrine: booleanValue(firstValue(block, 'doctrine')) === true,
|
|
416
|
+
...(firstValue(block, 'ledger') === undefined
|
|
417
|
+
? {}
|
|
418
|
+
: { ledger: firstValue(block, 'ledger') }),
|
|
419
|
+
...(childBlocks(block, 'available')[0] === undefined
|
|
420
|
+
? {}
|
|
421
|
+
: { availableExpression: sourceText(document, childBlocks(block, 'available')[0]) }),
|
|
422
|
+
source: provenance(file, document, definition, id),
|
|
423
|
+
localisation: { language: 'l_english', status: 'missing' },
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function parseUnlockTargets(file, document, fragment) {
|
|
429
|
+
const sourcePath = normalizedPath(file);
|
|
430
|
+
const wrappers = [];
|
|
431
|
+
if (sourcePath.startsWith('common/units/equipment/modules/'))
|
|
432
|
+
wrappers.push({ key: 'equipment_modules', kind: 'equipment_module' });
|
|
433
|
+
else if (sourcePath.startsWith('common/units/equipment/'))
|
|
434
|
+
wrappers.push({ key: 'equipments', kind: 'equipment' });
|
|
435
|
+
if (sourcePath.startsWith('common/units/') && !sourcePath.startsWith('common/units/equipment/'))
|
|
436
|
+
wrappers.push({ key: 'sub_units', kind: 'sub_unit' });
|
|
437
|
+
if (sourcePath.startsWith('common/buildings/'))
|
|
438
|
+
wrappers.push({ key: 'buildings', kind: 'building' });
|
|
439
|
+
if (sourcePath.startsWith('common/abilities/'))
|
|
440
|
+
wrappers.push({ key: 'ability', kind: 'ability' });
|
|
441
|
+
if (sourcePath === 'common/combat_tactics.txt' || sourcePath.startsWith('common/combat_tactics/'))
|
|
442
|
+
wrappers.push({ key: 'combat_tactics', kind: 'tactic' });
|
|
443
|
+
for (const { key, kind } of wrappers) {
|
|
444
|
+
for (const wrapper of childBlocks(document.root, key)) {
|
|
445
|
+
for (const definition of assignments(wrapper)) {
|
|
446
|
+
if (definition.value.type !== 'block' || definition.key.value === 'limit')
|
|
447
|
+
continue;
|
|
448
|
+
const id = definition.key.value;
|
|
449
|
+
fragment.unlockTargets.push({
|
|
450
|
+
id: `${kind}:${id}`,
|
|
451
|
+
kind,
|
|
452
|
+
targetId: id,
|
|
453
|
+
source: provenance(file, document, definition, id),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
function doctrineKind(sourcePath) {
|
|
460
|
+
if (sourcePath.startsWith('common/doctrines/folders/'))
|
|
461
|
+
return 'folder';
|
|
462
|
+
if (sourcePath.startsWith('common/doctrines/grand_doctrines/'))
|
|
463
|
+
return 'grand_doctrine';
|
|
464
|
+
if (sourcePath.startsWith('common/doctrines/tracks/'))
|
|
465
|
+
return 'track';
|
|
466
|
+
if (sourcePath.startsWith('common/doctrines/subdoctrines/'))
|
|
467
|
+
return 'subdoctrine';
|
|
468
|
+
return undefined;
|
|
469
|
+
}
|
|
470
|
+
function parseModernDoctrines(file, document, fragment) {
|
|
471
|
+
const kind = doctrineKind(normalizedPath(file));
|
|
472
|
+
if (kind === undefined)
|
|
473
|
+
return;
|
|
474
|
+
for (const definition of assignments(document.root)) {
|
|
475
|
+
if (definition.value.type !== 'block' || definition.key.value.startsWith('@'))
|
|
476
|
+
continue;
|
|
477
|
+
const block = definition.value;
|
|
478
|
+
const id = definition.key.value;
|
|
479
|
+
const trackIds = [
|
|
480
|
+
...assignmentScalars(block, 'track').map(({ value }) => value),
|
|
481
|
+
...childBlocks(block, 'track').flatMap(scalarValues),
|
|
482
|
+
...childBlocks(block, 'tracks').flatMap(scalarValues),
|
|
483
|
+
];
|
|
484
|
+
const exclusiveIds = childBlocks(block, 'xor').flatMap(scalarValues);
|
|
485
|
+
const base = {
|
|
486
|
+
id,
|
|
487
|
+
kind,
|
|
488
|
+
...(firstValue(block, 'folder') === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: { folderId: firstValue(block, 'folder') }),
|
|
491
|
+
trackIds: [...new Set(trackIds)].sort(compareCodeUnits),
|
|
492
|
+
exclusiveIds: [...new Set(exclusiveIds)].sort(compareCodeUnits),
|
|
493
|
+
...(firstValue(block, 'name') === undefined ? {} : { nameKey: firstValue(block, 'name') }),
|
|
494
|
+
...(firstValue(block, 'description') === undefined
|
|
495
|
+
? {}
|
|
496
|
+
: { descriptionKey: firstValue(block, 'description') }),
|
|
497
|
+
...(firstValue(block, 'xp_cost') === undefined
|
|
498
|
+
? {}
|
|
499
|
+
: { xpCost: firstValue(block, 'xp_cost') }),
|
|
500
|
+
...(firstValue(block, 'xp_type') === undefined
|
|
501
|
+
? {}
|
|
502
|
+
: { xpType: firstValue(block, 'xp_type') }),
|
|
503
|
+
ai: aiMetadata(block),
|
|
504
|
+
effectKeys: assignments(block)
|
|
505
|
+
.map(({ key }) => key.value)
|
|
506
|
+
.filter((field) => !new Set([
|
|
507
|
+
'active',
|
|
508
|
+
'ai_will_do',
|
|
509
|
+
'allow_in_multiple_tracks',
|
|
510
|
+
'available',
|
|
511
|
+
'description',
|
|
512
|
+
'folder',
|
|
513
|
+
'icon',
|
|
514
|
+
'mastery',
|
|
515
|
+
'max_track_columns',
|
|
516
|
+
'max_track_rows',
|
|
517
|
+
'milestones',
|
|
518
|
+
'name',
|
|
519
|
+
'rewards',
|
|
520
|
+
'track',
|
|
521
|
+
'tracks',
|
|
522
|
+
'visible',
|
|
523
|
+
'xp_cost',
|
|
524
|
+
'xp_type',
|
|
525
|
+
'xor',
|
|
526
|
+
]).has(field))
|
|
527
|
+
.sort(compareCodeUnits),
|
|
528
|
+
source: provenance(file, document, definition, id),
|
|
529
|
+
...(firstValue(block, 'icon') === undefined
|
|
530
|
+
? {}
|
|
531
|
+
: { iconSprite: firstValue(block, 'icon') }),
|
|
532
|
+
};
|
|
533
|
+
fragment.doctrineDefinitions.push(base);
|
|
534
|
+
if (kind !== 'subdoctrine')
|
|
535
|
+
continue;
|
|
536
|
+
for (const rewards of childBlocks(block, 'rewards')) {
|
|
537
|
+
let index = 0;
|
|
538
|
+
for (const entry of rewards.entries) {
|
|
539
|
+
if (entry.type === 'scalar')
|
|
540
|
+
continue;
|
|
541
|
+
const rewardId = entry.type === 'assignment' ? entry.key.value : `${id}_reward_${String(index + 1)}`;
|
|
542
|
+
const rewardBlock = entry.type === 'assignment'
|
|
543
|
+
? entry.value.type === 'block'
|
|
544
|
+
? entry.value
|
|
545
|
+
: undefined
|
|
546
|
+
: entry;
|
|
547
|
+
index += 1;
|
|
548
|
+
fragment.doctrineDefinitions.push({
|
|
549
|
+
id: rewardId,
|
|
550
|
+
kind: 'reward',
|
|
551
|
+
trackIds: [...base.trackIds],
|
|
552
|
+
parentId: id,
|
|
553
|
+
exclusiveIds: [],
|
|
554
|
+
ai: { present: false, zero: 'unknown', researchWeights: {} },
|
|
555
|
+
effectKeys: rewardBlock === undefined
|
|
556
|
+
? []
|
|
557
|
+
: assignments(rewardBlock)
|
|
558
|
+
.map(({ key }) => key.value)
|
|
559
|
+
.sort(compareCodeUnits),
|
|
560
|
+
source: provenance(file, document, entry, rewardId),
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function parseTechnologyGridboxes(file, document, fragment, signal) {
|
|
567
|
+
if (!normalizedPath(file).endsWith('.gui'))
|
|
568
|
+
return;
|
|
569
|
+
const stack = [
|
|
570
|
+
{ block: document.root, namedContainers: [] },
|
|
571
|
+
];
|
|
572
|
+
let visited = 0;
|
|
573
|
+
while (stack.length > 0) {
|
|
574
|
+
if (visited % 128 === 0)
|
|
575
|
+
signal?.throwIfAborted();
|
|
576
|
+
visited += 1;
|
|
577
|
+
const current = stack.pop();
|
|
578
|
+
for (const assignment of assignments(current.block)) {
|
|
579
|
+
if (assignment.value.type !== 'block')
|
|
580
|
+
continue;
|
|
581
|
+
const block = assignment.value;
|
|
582
|
+
const elementType = assignment.key.value.toLowerCase();
|
|
583
|
+
const name = firstScalar(block, 'name');
|
|
584
|
+
if (elementType === 'gridboxtype' && name?.value.endsWith('_tree') === true) {
|
|
585
|
+
const position = childBlocks(block, 'position')[0];
|
|
586
|
+
const slotSize = childBlocks(block, 'slotsize')[0];
|
|
587
|
+
const x = numericLiteral(position === undefined ? undefined : firstValue(position, 'x'));
|
|
588
|
+
const y = numericLiteral(position === undefined ? undefined : firstValue(position, 'y'));
|
|
589
|
+
const width = numericLiteral(slotSize === undefined ? undefined : firstValue(slotSize, 'width'));
|
|
590
|
+
const height = numericLiteral(slotSize === undefined ? undefined : firstValue(slotSize, 'height'));
|
|
591
|
+
const gridbox = {
|
|
592
|
+
id: deterministicId('tech-gridbox', {
|
|
593
|
+
name: name.value,
|
|
594
|
+
path: file.displayPath,
|
|
595
|
+
offset: assignment.start,
|
|
596
|
+
}),
|
|
597
|
+
name: name.value,
|
|
598
|
+
...(current.namedContainers.at(-1) === undefined
|
|
599
|
+
? {}
|
|
600
|
+
: { folderId: current.namedContainers.at(-1) }),
|
|
601
|
+
position: {
|
|
602
|
+
...(x === undefined ? {} : { x }),
|
|
603
|
+
...(y === undefined ? {} : { y }),
|
|
604
|
+
...(position === undefined || x !== undefined
|
|
605
|
+
? {}
|
|
606
|
+
: { xExpression: firstValue(position, 'x') ?? '<missing>' }),
|
|
607
|
+
...(position === undefined || y !== undefined
|
|
608
|
+
? {}
|
|
609
|
+
: { yExpression: firstValue(position, 'y') ?? '<missing>' }),
|
|
610
|
+
},
|
|
611
|
+
slotSize: {
|
|
612
|
+
...(width === undefined ? {} : { width }),
|
|
613
|
+
...(height === undefined ? {} : { height }),
|
|
614
|
+
...(slotSize === undefined || width !== undefined
|
|
615
|
+
? {}
|
|
616
|
+
: { widthExpression: firstValue(slotSize, 'width') ?? '<missing>' }),
|
|
617
|
+
...(slotSize === undefined || height !== undefined
|
|
618
|
+
? {}
|
|
619
|
+
: { heightExpression: firstValue(slotSize, 'height') ?? '<missing>' }),
|
|
620
|
+
},
|
|
621
|
+
...(firstValue(block, 'format') === undefined
|
|
622
|
+
? {}
|
|
623
|
+
: { format: firstValue(block, 'format') }),
|
|
624
|
+
location: nodeLocation(document, assignment, name.value),
|
|
625
|
+
sourcePath: file.displayPath,
|
|
626
|
+
loadOrder: file.loadOrder,
|
|
627
|
+
};
|
|
628
|
+
fragment.gridboxes.push(gridbox);
|
|
629
|
+
}
|
|
630
|
+
const namedContainers = name !== undefined &&
|
|
631
|
+
(elementType === 'containerwindowtype' || elementType === 'windowtype')
|
|
632
|
+
? [...current.namedContainers, name.value]
|
|
633
|
+
: current.namedContainers;
|
|
634
|
+
stack.push({ block, namedContainers });
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function initialOwner(file) {
|
|
639
|
+
const sourcePath = normalizedPath(file);
|
|
640
|
+
if (sourcePath.startsWith('history/countries/')) {
|
|
641
|
+
const filename = path.posix.basename(file.relativePath.replaceAll('\\', '/'));
|
|
642
|
+
return { kind: 'country_history', id: filename.slice(0, 3).toUpperCase() };
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
function nextOwner(file, assignment, depth, ancestors, current) {
|
|
647
|
+
if (assignment.value.type !== 'block')
|
|
648
|
+
return current;
|
|
649
|
+
const sourcePath = normalizedPath(file);
|
|
650
|
+
const block = assignment.value;
|
|
651
|
+
const key = assignment.key.value;
|
|
652
|
+
if (key === 'focus') {
|
|
653
|
+
const id = firstValue(block, 'id');
|
|
654
|
+
if (id !== undefined)
|
|
655
|
+
return { kind: 'focus', id };
|
|
656
|
+
}
|
|
657
|
+
if (eventDefinitionKeys.has(key)) {
|
|
658
|
+
const id = firstValue(block, 'id');
|
|
659
|
+
if (id !== undefined)
|
|
660
|
+
return { kind: 'event', id };
|
|
661
|
+
}
|
|
662
|
+
if (sourcePath.startsWith('common/scripted_effects/') && depth === 0)
|
|
663
|
+
return { kind: 'scripted_effect', id: key };
|
|
664
|
+
if (sourcePath.startsWith('common/on_actions/') && ancestors.at(-1) === 'on_actions')
|
|
665
|
+
return { kind: 'on_action', id: key };
|
|
666
|
+
if (sourcePath.startsWith('common/decisions/') &&
|
|
667
|
+
depth === 1 &&
|
|
668
|
+
!decisionStructuralKeys.has(key)) {
|
|
669
|
+
const mission = firstScalar(block, 'days_mission_timeout') !== undefined ||
|
|
670
|
+
firstScalar(block, 'days_remove') !== undefined ||
|
|
671
|
+
/mission/iu.test(key);
|
|
672
|
+
return { kind: mission ? 'mission' : 'decision', id: key };
|
|
673
|
+
}
|
|
674
|
+
if (sourcePath.startsWith('common/ideas/') && ancestors[0] === 'ideas' && ancestors.length >= 1)
|
|
675
|
+
return { kind: 'idea', id: key };
|
|
676
|
+
if (sourcePath.startsWith('common/characters/') && ancestors.length >= 1)
|
|
677
|
+
return { kind: 'character', id: key };
|
|
678
|
+
if (sourcePath.startsWith('history/countries/') && key === '1936.1.1')
|
|
679
|
+
return current === undefined ? undefined : { kind: 'startup_effect', id: current.id };
|
|
680
|
+
return current;
|
|
681
|
+
}
|
|
682
|
+
function referenceOwner(file, owner) {
|
|
683
|
+
return (owner ?? {
|
|
684
|
+
kind: 'other',
|
|
685
|
+
id: file.relativePath.replaceAll('\\', '/'),
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
function addExternalReference(output, input) {
|
|
689
|
+
output.push({
|
|
690
|
+
id: deterministicId('tech-external', {
|
|
691
|
+
kind: input.kind,
|
|
692
|
+
sourceKind: input.sourceKind,
|
|
693
|
+
sourceId: input.sourceId,
|
|
694
|
+
technologyId: input.technologyId,
|
|
695
|
+
categoryId: input.categoryId,
|
|
696
|
+
location: input.location,
|
|
697
|
+
expression: input.expression,
|
|
698
|
+
}),
|
|
699
|
+
...input,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
function parseExternalReferences(file, document, fragment, context) {
|
|
703
|
+
const stack = [
|
|
704
|
+
{
|
|
705
|
+
block: document.root,
|
|
706
|
+
depth: 0,
|
|
707
|
+
ancestors: [],
|
|
708
|
+
...(initialOwner(file) === undefined ? {} : { owner: initialOwner(file) }),
|
|
709
|
+
},
|
|
710
|
+
];
|
|
711
|
+
let visited = 0;
|
|
712
|
+
while (stack.length > 0) {
|
|
713
|
+
if (visited % 256 === 0)
|
|
714
|
+
context.signal?.throwIfAborted();
|
|
715
|
+
visited += 1;
|
|
716
|
+
const current = stack.pop();
|
|
717
|
+
for (const assignment of [...assignments(current.block)].reverse()) {
|
|
718
|
+
const key = assignment.key.value;
|
|
719
|
+
const currentOwner = referenceOwner(file, current.owner);
|
|
720
|
+
if (assignment.value.type === 'scalar') {
|
|
721
|
+
if (['has_tech', 'has_technology', 'is_researching_technology'].includes(key)) {
|
|
722
|
+
const target = assignment.value;
|
|
723
|
+
addExternalReference(fragment.externalReferences, {
|
|
724
|
+
kind: 'reference',
|
|
725
|
+
sourceKind: currentOwner.kind,
|
|
726
|
+
sourceId: currentOwner.id,
|
|
727
|
+
technologyId: target.value,
|
|
728
|
+
expression: sourceText(document, assignment),
|
|
729
|
+
location: nodeLocation(document, target, currentOwner.id),
|
|
730
|
+
helperStack: currentOwner.kind === 'scripted_effect' ? [currentOwner.id] : [],
|
|
731
|
+
confidence: isDynamic(target.value) ? 'unresolved' : 'confirmed',
|
|
732
|
+
dynamic: isDynamic(target.value),
|
|
733
|
+
metadata: { trigger: key },
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
if (current.depth > 0 &&
|
|
737
|
+
context.helperIds.has(key) &&
|
|
738
|
+
!(currentOwner.kind === 'scripted_effect' && currentOwner.id === key)) {
|
|
739
|
+
fragment.helperCalls.push({
|
|
740
|
+
id: deterministicId('tech-helper-call', {
|
|
741
|
+
sourceKind: currentOwner.kind,
|
|
742
|
+
sourceId: currentOwner.id,
|
|
743
|
+
helperId: key,
|
|
744
|
+
path: file.displayPath,
|
|
745
|
+
offset: assignment.start,
|
|
746
|
+
}),
|
|
747
|
+
sourceKind: currentOwner.kind,
|
|
748
|
+
sourceId: currentOwner.id,
|
|
749
|
+
helperId: key,
|
|
750
|
+
location: nodeLocation(document, assignment, currentOwner.id),
|
|
751
|
+
confidence: 'high',
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
const owner = nextOwner(file, assignment, current.depth, current.ancestors, current.owner);
|
|
757
|
+
const selectedOwner = referenceOwner(file, owner);
|
|
758
|
+
const block = assignment.value;
|
|
759
|
+
if (key === 'set_technology') {
|
|
760
|
+
for (const target of assignments(block)) {
|
|
761
|
+
if (target.key.value === 'popup')
|
|
762
|
+
continue;
|
|
763
|
+
const targetId = target.key.value;
|
|
764
|
+
const expression = sourceText(document, target);
|
|
765
|
+
const location = nodeLocation(document, target, selectedOwner.id);
|
|
766
|
+
if (isDynamic(targetId)) {
|
|
767
|
+
fragment.unresolved.push(unresolved('dynamic_reference', expression, selectedOwner.id, location, 'TECH_DYNAMIC_GRANT_UNRESOLVED', 'Dynamic set_technology target cannot be resolved statically'));
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const value = target.value.type === 'scalar' ? target.value.value : expression;
|
|
771
|
+
const removal = value === '0' || value.toLowerCase() === 'no';
|
|
772
|
+
addExternalReference(fragment.externalReferences, {
|
|
773
|
+
kind: selectedOwner.kind === 'country_history' || selectedOwner.kind === 'startup_effect'
|
|
774
|
+
? 'starting_technology'
|
|
775
|
+
: removal
|
|
776
|
+
? 'remove'
|
|
777
|
+
: 'grant',
|
|
778
|
+
sourceKind: selectedOwner.kind,
|
|
779
|
+
sourceId: selectedOwner.id,
|
|
780
|
+
technologyId: targetId,
|
|
781
|
+
expression,
|
|
782
|
+
location,
|
|
783
|
+
helperStack: selectedOwner.kind === 'scripted_effect' ? [selectedOwner.id] : [],
|
|
784
|
+
confidence: 'confirmed',
|
|
785
|
+
dynamic: false,
|
|
786
|
+
metadata: { value },
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (key === 'add_tech_bonus' || key === 'add_doctrine_cost_reduction') {
|
|
791
|
+
for (const target of [
|
|
792
|
+
...assignmentScalars(block, 'technology'),
|
|
793
|
+
...assignmentScalars(block, 'category'),
|
|
794
|
+
]) {
|
|
795
|
+
const targetKind = assignments(block, 'technology').some(({ value }) => value === target)
|
|
796
|
+
? 'technology'
|
|
797
|
+
: 'category';
|
|
798
|
+
const expression = sourceText(document, target);
|
|
799
|
+
const location = nodeLocation(document, target, selectedOwner.id);
|
|
800
|
+
if (isDynamic(target.value)) {
|
|
801
|
+
fragment.unresolved.push(unresolved('dynamic_reference', expression, selectedOwner.id, location, 'TECH_DYNAMIC_BONUS_UNRESOLVED', 'Dynamic research-bonus target cannot be resolved statically'));
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
addExternalReference(fragment.externalReferences, {
|
|
805
|
+
kind: 'research_bonus',
|
|
806
|
+
sourceKind: selectedOwner.kind,
|
|
807
|
+
sourceId: selectedOwner.id,
|
|
808
|
+
...(targetKind === 'technology'
|
|
809
|
+
? { technologyId: target.value }
|
|
810
|
+
: { categoryId: target.value }),
|
|
811
|
+
expression,
|
|
812
|
+
location,
|
|
813
|
+
helperStack: selectedOwner.kind === 'scripted_effect' ? [selectedOwner.id] : [],
|
|
814
|
+
confidence: 'confirmed',
|
|
815
|
+
dynamic: false,
|
|
816
|
+
metadata: {
|
|
817
|
+
effect: key,
|
|
818
|
+
bonus: firstValue(block, 'bonus') ?? firstValue(block, 'cost_reduction') ?? null,
|
|
819
|
+
uses: firstValue(block, 'uses') ?? null,
|
|
820
|
+
aheadReduction: firstValue(block, 'ahead_reduction') ?? null,
|
|
821
|
+
},
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (key === 'research_bonus') {
|
|
826
|
+
for (const target of assignments(block)) {
|
|
827
|
+
if (target.value.type !== 'scalar')
|
|
828
|
+
continue;
|
|
829
|
+
const location = nodeLocation(document, target, selectedOwner.id);
|
|
830
|
+
addExternalReference(fragment.externalReferences, {
|
|
831
|
+
kind: 'research_bonus',
|
|
832
|
+
sourceKind: selectedOwner.kind,
|
|
833
|
+
sourceId: selectedOwner.id,
|
|
834
|
+
categoryId: target.key.value,
|
|
835
|
+
expression: sourceText(document, target),
|
|
836
|
+
location,
|
|
837
|
+
helperStack: selectedOwner.kind === 'scripted_effect' ? [selectedOwner.id] : [],
|
|
838
|
+
confidence: isDynamic(target.key.value) ? 'unresolved' : 'confirmed',
|
|
839
|
+
dynamic: isDynamic(target.key.value),
|
|
840
|
+
metadata: { value: target.value.value, effect: 'research_bonus' },
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (key === 'technology_sharing_group') {
|
|
845
|
+
const sharingId = firstValue(block, 'id') ?? `${file.displayPath}:${assignment.start}`;
|
|
846
|
+
for (const category of childBlocks(block, 'categories').flatMap(scalarEntries)) {
|
|
847
|
+
addExternalReference(fragment.externalReferences, {
|
|
848
|
+
kind: 'technology_sharing',
|
|
849
|
+
sourceKind: 'technology_sharing',
|
|
850
|
+
sourceId: sharingId,
|
|
851
|
+
categoryId: category.value,
|
|
852
|
+
expression: sourceText(document, category),
|
|
853
|
+
location: nodeLocation(document, category, sharingId),
|
|
854
|
+
helperStack: [],
|
|
855
|
+
confidence: isDynamic(category.value) ? 'unresolved' : 'confirmed',
|
|
856
|
+
dynamic: isDynamic(category.value),
|
|
857
|
+
metadata: {},
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (current.depth > 0 &&
|
|
862
|
+
context.helperIds.has(key) &&
|
|
863
|
+
!(selectedOwner.kind === 'scripted_effect' && selectedOwner.id === key)) {
|
|
864
|
+
const call = {
|
|
865
|
+
id: deterministicId('tech-helper-call', {
|
|
866
|
+
sourceKind: selectedOwner.kind,
|
|
867
|
+
sourceId: selectedOwner.id,
|
|
868
|
+
helperId: key,
|
|
869
|
+
path: file.displayPath,
|
|
870
|
+
offset: assignment.start,
|
|
871
|
+
}),
|
|
872
|
+
sourceKind: selectedOwner.kind,
|
|
873
|
+
sourceId: selectedOwner.id,
|
|
874
|
+
helperId: key,
|
|
875
|
+
location: nodeLocation(document, assignment, selectedOwner.id),
|
|
876
|
+
confidence: 'high',
|
|
877
|
+
};
|
|
878
|
+
fragment.helperCalls.push(call);
|
|
879
|
+
}
|
|
880
|
+
stack.push({
|
|
881
|
+
block,
|
|
882
|
+
depth: current.depth + 1,
|
|
883
|
+
ancestors: [...current.ancestors, key],
|
|
884
|
+
...(owner === undefined ? {} : { owner }),
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
export function analyzeTechnologySource(file, context) {
|
|
890
|
+
context.signal?.throwIfAborted();
|
|
891
|
+
const fragment = {
|
|
892
|
+
sourcePath: file.displayPath,
|
|
893
|
+
sourceHash: file.sha256,
|
|
894
|
+
technologies: [],
|
|
895
|
+
folders: [],
|
|
896
|
+
placements: [],
|
|
897
|
+
gridboxes: [],
|
|
898
|
+
edges: [],
|
|
899
|
+
categories: [],
|
|
900
|
+
doctrineDefinitions: [],
|
|
901
|
+
unlockTargets: [],
|
|
902
|
+
unlocks: [],
|
|
903
|
+
externalReferences: [],
|
|
904
|
+
helperCalls: [],
|
|
905
|
+
unresolved: [],
|
|
906
|
+
diagnostics: [],
|
|
907
|
+
};
|
|
908
|
+
if (!/\.(?:txt|gui|gfx)$/iu.test(file.relativePath))
|
|
909
|
+
return fragment;
|
|
910
|
+
if (!file.bytes.includes(0x3d))
|
|
911
|
+
return fragment;
|
|
912
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
913
|
+
const limitDiagnostics = sourcePartialLimitDiagnostics(document.diagnostics);
|
|
914
|
+
if (limitDiagnostics.length > 0) {
|
|
915
|
+
fragment.diagnostics.push(...limitDiagnostics);
|
|
916
|
+
fragment.unresolved.push(unresolved('partial_source', file.displayPath, undefined, limitDiagnostics.find(({ location }) => location !== undefined)?.location, 'TECH_SOURCE_PARTIAL', 'Technology analysis skipped a source that exceeded a shared parser limit'));
|
|
917
|
+
return fragment;
|
|
918
|
+
}
|
|
919
|
+
fragment.diagnostics.push(...document.diagnostics);
|
|
920
|
+
const sourcePath = normalizedPath(file);
|
|
921
|
+
if (sourcePath.startsWith('common/technologies/'))
|
|
922
|
+
parseTechnologyDefinitions(file, document, fragment);
|
|
923
|
+
if (sourcePath.startsWith('common/technology_tags/'))
|
|
924
|
+
parseTechnologyTags(file, document, fragment);
|
|
925
|
+
parseUnlockTargets(file, document, fragment);
|
|
926
|
+
parseModernDoctrines(file, document, fragment);
|
|
927
|
+
parseTechnologyGridboxes(file, document, fragment, context.signal);
|
|
928
|
+
parseExternalReferences(file, document, fragment, context);
|
|
929
|
+
context.signal?.throwIfAborted();
|
|
930
|
+
return fragment;
|
|
931
|
+
}
|
|
932
|
+
export function technologySourceFragmentCacheKey(file, catalogFingerprint) {
|
|
933
|
+
return `${file.sha256}:${catalogFingerprint}`;
|
|
934
|
+
}
|
|
935
|
+
export function technologySourceFiles(files) {
|
|
936
|
+
return files
|
|
937
|
+
.filter(({ shadowedBy, relativePath }) => {
|
|
938
|
+
if (shadowedBy !== undefined)
|
|
939
|
+
return false;
|
|
940
|
+
const sourcePath = relativePath.replaceAll('\\', '/').toLowerCase();
|
|
941
|
+
return (sourcePath.startsWith('common/technologies/') ||
|
|
942
|
+
sourcePath.startsWith('common/technology_tags/') ||
|
|
943
|
+
sourcePath.startsWith('common/technology_sharing/') ||
|
|
944
|
+
sourcePath.startsWith('common/doctrines/') ||
|
|
945
|
+
sourcePath.startsWith('common/units/') ||
|
|
946
|
+
sourcePath.startsWith('common/buildings/') ||
|
|
947
|
+
sourcePath.startsWith('common/abilities/') ||
|
|
948
|
+
sourcePath.startsWith('common/national_focus/') ||
|
|
949
|
+
sourcePath.startsWith('common/decisions/') ||
|
|
950
|
+
sourcePath.startsWith('common/scripted_effects/') ||
|
|
951
|
+
sourcePath.startsWith('common/on_actions/') ||
|
|
952
|
+
sourcePath.startsWith('common/ideas/') ||
|
|
953
|
+
sourcePath.startsWith('common/characters/') ||
|
|
954
|
+
sourcePath.startsWith('events/') ||
|
|
955
|
+
sourcePath.startsWith('history/countries/') ||
|
|
956
|
+
(sourcePath.startsWith('interface/') && sourcePath.endsWith('.gui')) ||
|
|
957
|
+
sourcePath === 'common/combat_tactics.txt');
|
|
958
|
+
})
|
|
959
|
+
.sort((left, right) => compareCodeUnits(left.displayPath, right.displayPath));
|
|
960
|
+
}
|
|
961
|
+
export function technologyAnalysisDiagnostics(fragments, includeGameDiagnostics = false) {
|
|
962
|
+
return fragments
|
|
963
|
+
.flatMap(({ diagnostics }) => diagnostics)
|
|
964
|
+
.filter((diagnostic) => includeGameDiagnostics ||
|
|
965
|
+
diagnostic.location?.path.startsWith('mod:') === true ||
|
|
966
|
+
diagnostic.location?.path.startsWith('fixture:') === true);
|
|
967
|
+
}
|