hoi4-agent-tools 4.0.3 → 4.0.4
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 +10 -0
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +5 -3
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +5 -1
- package/dist/hoi4_agent_tools/core/index.js +11 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +3 -1
- package/dist/hoi4_agent_tools/core/job-worker-host.js +1 -1
- package/dist/hoi4_agent_tools/core/render-progress.js +7 -0
- package/dist/hoi4_agent_tools/gui/layout.js +15 -8
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +3 -3
- package/dist/hoi4_agent_tools/gui/source-graph.js +58 -26
- package/dist/hoi4_agent_tools/gui/studio.js +97 -31
- package/dist/hoi4_agent_tools/gui/tool-operations.js +8 -2
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +2 -1
- package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +3 -1
- package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/progress.js +12 -0
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +2 -2
- package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +5 -2
- package/dist/hoi4_agent_tools/technology/job-operations.js +1 -0
- package/dist/hoi4_agent_tools/technology/service.js +18 -37
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +8 -0
- package/docs/technology.md +2 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.0.4 - 2026-10-14
|
|
6
|
+
|
|
7
|
+
- Preview individual GUI windows without redundant full symbol indexes, workspace-wide asset fallback or unbound country-flag inventories; reuse condition definitions across render variants.
|
|
8
|
+
- Index technology sources once across layers and scope folder rendering to definitions, layouts and unlock targets. Full inspection retains inbound grants and references.
|
|
9
|
+
- Report render phases and include the last phase in worker deadline errors. Existing parsing, raster-memory, canonical-path and stale-source limits remain enforced.
|
|
10
|
+
- Resolve transitive localisation `$KEY$` references across files and bounded scenario substitutions; retain diagnostics for cycles and missing runtime values.
|
|
11
|
+
- Honour numeric flag presence in scalar visibility checks, matching valued-flag super-event localisation conditions.
|
|
12
|
+
- Preserve supplied GUI scenarios by default; exploratory `generatedScenarios` is now opt-in. Explicit state/resolution matrices and public schema shapes are unchanged.
|
|
13
|
+
- Retain regression coverage for hidden sub-windows, timed animated-sprite frames, 16-bit DDS portraits, declared font colours, worker recovery and render-memory ceilings.
|
|
14
|
+
|
|
5
15
|
## 4.0.3 - 2026-10-12
|
|
6
16
|
|
|
7
17
|
- Publish the README with the supplied HOI4 MCP Server thumbnail and direct links to the demo video on YouTube.
|
|
@@ -31,11 +31,13 @@ export class ClausewitzEvaluationDefinitions {
|
|
|
31
31
|
scriptedTriggers = new Map();
|
|
32
32
|
localConstants = new Map();
|
|
33
33
|
static build(snapshot) {
|
|
34
|
-
const cached = definitionCache.get(snapshot);
|
|
34
|
+
const cached = definitionCache.get(snapshot.files);
|
|
35
35
|
if (cached !== undefined)
|
|
36
36
|
return cached;
|
|
37
37
|
const result = new ClausewitzEvaluationDefinitions();
|
|
38
|
-
for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined &&
|
|
38
|
+
for (const file of snapshot.files.filter(({ shadowedBy, relativePath, bytes }) => shadowedBy === undefined &&
|
|
39
|
+
relativePath.toLowerCase().endsWith('.txt') &&
|
|
40
|
+
bytes.includes(0x40))) {
|
|
39
41
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
40
42
|
const locals = new Map();
|
|
41
43
|
for (const assignment of scalarAssignments(document.root)) {
|
|
@@ -84,7 +86,7 @@ export class ClausewitzEvaluationDefinitions {
|
|
|
84
86
|
});
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
|
-
definitionCache.set(snapshot, result);
|
|
89
|
+
definitionCache.set(snapshot.files, result);
|
|
88
90
|
return result;
|
|
89
91
|
}
|
|
90
92
|
}
|
|
@@ -286,7 +286,11 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
286
286
|
: { state: value ? 'true' : 'false', unresolved: [] };
|
|
287
287
|
}
|
|
288
288
|
if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
|
|
289
|
-
const
|
|
289
|
+
const stored = key === 'has_global_flag'
|
|
290
|
+
? (scopeStateValue(scenario, `global_flag.${right}`, scopeContext) ??
|
|
291
|
+
scopeStateValue(scenario, `flag.${right}`, scopeContext))
|
|
292
|
+
: scopeStateValue(scenario, `flag.${right}`, scopeContext);
|
|
293
|
+
const explicit = typeof stored === 'number' && Number.isFinite(stored) ? true : bool(stored);
|
|
290
294
|
if (explicit !== undefined)
|
|
291
295
|
return { state: explicit ? 'true' : 'false', unresolved: [] };
|
|
292
296
|
const flags = key === 'has_global_flag' && scenario.globalFlags !== undefined
|
|
@@ -383,6 +383,17 @@ export class SymbolIndex {
|
|
|
383
383
|
},
|
|
384
384
|
});
|
|
385
385
|
}
|
|
386
|
+
/** Record parser coverage without allocating symbols for a domain that builds its own graph. */
|
|
387
|
+
recordSourceCoverage(file, diagnostics) {
|
|
388
|
+
const limits = sourcePartialLimitDiagnostics(diagnostics);
|
|
389
|
+
if (limits.length > 0) {
|
|
390
|
+
this.recordSkippedSource(file, limits);
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (isWorkspaceOwnedSource(file) && file.shadowedBy === undefined)
|
|
394
|
+
this.addDiagnostics(diagnostics);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
386
397
|
addSymbol(record) {
|
|
387
398
|
if (this.symbols.length >= INDEX_RECORD_LIMIT) {
|
|
388
399
|
this.#complete = false;
|
|
@@ -106,10 +106,11 @@ export class JobExecutor {
|
|
|
106
106
|
const controller = new AbortController();
|
|
107
107
|
const watchStop = new AbortController();
|
|
108
108
|
let watchFailure;
|
|
109
|
+
let lastProgressMessage = claimed.record.progress?.message;
|
|
109
110
|
const deadlineTimer = options.deadlineMs === undefined || operation.mutation
|
|
110
111
|
? undefined
|
|
111
112
|
: setTimeout(() => {
|
|
112
|
-
watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED',
|
|
113
|
+
watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED', `The job ran past its deadline and was stopped${lastProgressMessage === undefined ? '' : ` during: ${lastProgressMessage.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`);
|
|
113
114
|
controller.abort(watchFailure);
|
|
114
115
|
}, Math.max(0, options.deadlineMs - Date.now()));
|
|
115
116
|
const watch = (async () => {
|
|
@@ -136,6 +137,7 @@ export class JobExecutor {
|
|
|
136
137
|
signal: controller.signal,
|
|
137
138
|
checkpoint: claimed.record.checkpoint,
|
|
138
139
|
progress: async (progress) => {
|
|
140
|
+
lastProgressMessage = progress.message;
|
|
139
141
|
await this.jobs.store.updateOwned(scope, id, owner.token, { progress }, controller.signal);
|
|
140
142
|
},
|
|
141
143
|
saveCheckpoint: async (checkpoint) => {
|
|
@@ -309,7 +309,7 @@ export class JobWorkerHost {
|
|
|
309
309
|
await stopChild(child, this.stoppedByHost);
|
|
310
310
|
await this.jobs.failInterrupted(workspaceId, id, {
|
|
311
311
|
code: 'JOB_DEADLINE_EXCEEDED',
|
|
312
|
-
message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped; narrow its selector or limits, or raise jobDeadlineSeconds`,
|
|
312
|
+
message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped${record.progress === undefined ? '' : ` during: ${record.progress.message.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`,
|
|
313
313
|
}, principal);
|
|
314
314
|
return;
|
|
315
315
|
}
|
|
@@ -219,15 +219,20 @@ function resolveTokenText(value, scenario, localisation, rowValues) {
|
|
|
219
219
|
/^[A-Za-z0-9_.-]+$/u.test(value) &&
|
|
220
220
|
(value.includes('_') || value === value.toUpperCase()))
|
|
221
221
|
missingLocalisation = true;
|
|
222
|
-
|
|
222
|
+
const expandTokens = (input, stack, depth) => replaceTextBounded(input, /\$([A-Za-z0-9_.-]+)\$/gu, (match) => {
|
|
223
223
|
const key = match[1] ?? '';
|
|
224
|
-
const replacement = scenario.localisation[key] ??
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
const replacement = scenario.localisation[key] ??
|
|
225
|
+
localisation.get(key) ??
|
|
226
|
+
rowValues?.[key] ??
|
|
227
|
+
scenario.values[key] ??
|
|
228
|
+
scenario.variables[key];
|
|
229
|
+
if (replacement === undefined || stack.has(key) || depth >= 16) {
|
|
230
|
+
unresolved.push(match[0]);
|
|
231
|
+
return match[0];
|
|
228
232
|
}
|
|
229
|
-
return replacement;
|
|
233
|
+
return expandTokens(String(replacement), new Set([...stack, key]), depth + 1);
|
|
230
234
|
}, 'GUI localisation substitution');
|
|
235
|
+
text = expandTokens(text, new Set([value]), 0);
|
|
231
236
|
text = replaceTextBounded(text, /\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match) => {
|
|
232
237
|
const key = match[1] ?? '';
|
|
233
238
|
const shortKey = key.slice(key.lastIndexOf('.') + 1);
|
|
@@ -2026,7 +2031,9 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2026
2031
|
const localisation = new Map(graph.localisation
|
|
2027
2032
|
.filter((entry) => entry.language.toLowerCase() === scenario.language.toLowerCase() ||
|
|
2028
2033
|
entry.language.toLowerCase() === `l_${scenario.language.toLowerCase()}`)
|
|
2029
|
-
|
|
2034
|
+
// Graph localisation is ordered with the active (highest-priority) definition first.
|
|
2035
|
+
.map((entry) => [entry.key, entry.value])
|
|
2036
|
+
.reverse());
|
|
2030
2037
|
if (localisation.size === 0) {
|
|
2031
2038
|
for (const entry of graph.localisation)
|
|
2032
2039
|
if (!localisation.has(entry.key))
|
|
@@ -2058,7 +2065,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2058
2065
|
const countryFlagProperties = new Map();
|
|
2059
2066
|
const conditionDefinitions = relevantScriptedGuiNames.size === 0
|
|
2060
2067
|
? undefined
|
|
2061
|
-
: ClausewitzEvaluationDefinitions.build({ files:
|
|
2068
|
+
: ClausewitzEvaluationDefinitions.build({ files: scannedFiles });
|
|
2062
2069
|
const conditionScenario = guiConditionScenario(scenario);
|
|
2063
2070
|
for (const scripted of graph.scriptedGuis.filter(({ name }) => relevantScriptedGuiNames.has(name))) {
|
|
2064
2071
|
const source = scannedFiles.find(({ displayPath }) => displayPath === scripted.sourcePath);
|
|
@@ -3,7 +3,7 @@ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.j
|
|
|
3
3
|
import { evaluateGuiCondition, guiConditionScenario, guiConditionVariables, guiExplicitConditionValues, } from './scenario-conditions.js';
|
|
4
4
|
import { parsePreviewScenario } from './scenario.js';
|
|
5
5
|
const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
|
|
6
|
-
const
|
|
6
|
+
export const generatedCountryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
|
|
7
7
|
const countryIdeologies = [
|
|
8
8
|
'fascism',
|
|
9
9
|
'democratic',
|
|
@@ -435,7 +435,7 @@ function generatedRows(count, random, options) {
|
|
|
435
435
|
return Array.from({ length: count }, (_unused, index) => {
|
|
436
436
|
const countryIndex = (countryOffset + index) % countryNames.length;
|
|
437
437
|
const name = countryNames[countryIndex];
|
|
438
|
-
const tag =
|
|
438
|
+
const tag = generatedCountryTags[countryIndex];
|
|
439
439
|
const ideology = countryIdeologies[countryIndex];
|
|
440
440
|
const value = generatedNumber(options, random);
|
|
441
441
|
return {
|
|
@@ -587,7 +587,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options, de
|
|
|
587
587
|
const elementStates = { ...base.elementStates };
|
|
588
588
|
const scopedCountries = new Map();
|
|
589
589
|
const generatedCountryIndex = random.integer(0, countryNames.length - 1);
|
|
590
|
-
const generatedCountryTag =
|
|
590
|
+
const generatedCountryTag = generatedCountryTags[generatedCountryIndex];
|
|
591
591
|
for (const key of tokens.numeric)
|
|
592
592
|
setIfMissing(values, key, generatedNumberForKey(key, options, random, numericConstraints.ranges.get(key)));
|
|
593
593
|
for (const [elementName, { minimum, maximum }] of progressRanges) {
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { compareCodeUnits, deterministicId } from '../core/canonical.js';
|
|
3
3
|
import { DiagnosticCollector } from '../core/diagnostic-collector.js';
|
|
4
4
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
5
|
+
import { SymbolIndex } from '../core/index.js';
|
|
5
6
|
import { ServiceError } from '../core/result.js';
|
|
6
7
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_ELEMENTS, GUI_GRAPH_MAX_NODES } from './limits.js';
|
|
7
8
|
import { assignments, childBlocks, firstScalar, locationFor, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
|
|
@@ -1306,7 +1307,8 @@ function referenceLabel(edge) {
|
|
|
1306
1307
|
return String(value);
|
|
1307
1308
|
return value === undefined ? edge.to : JSON.stringify(value);
|
|
1308
1309
|
}
|
|
1309
|
-
export function buildGuiSourceGraph(files,
|
|
1310
|
+
export function buildGuiSourceGraph(files, providedIndex, additionalLocalisationKeys = [], limits = {}) {
|
|
1311
|
+
const sharedIndex = providedIndex ?? new SymbolIndex();
|
|
1310
1312
|
const activeFiles = files.filter(({ shadowedBy }) => shadowedBy === undefined);
|
|
1311
1313
|
const nodes = [];
|
|
1312
1314
|
const edges = [];
|
|
@@ -1323,25 +1325,6 @@ export function buildGuiSourceGraph(files, sharedIndex, additionalLocalisationKe
|
|
|
1323
1325
|
category: 'layout',
|
|
1324
1326
|
message: 'GUI source graph diagnostics exceeded the fixed global result ceiling',
|
|
1325
1327
|
});
|
|
1326
|
-
diagnostics.pushMany(sharedIndex.diagnostics);
|
|
1327
|
-
if (!sharedIndex.complete) {
|
|
1328
|
-
diagnostics.push({
|
|
1329
|
-
code: 'GUI_INVENTORY_PARTIAL',
|
|
1330
|
-
severity: 'warning',
|
|
1331
|
-
category: 'reference',
|
|
1332
|
-
message: sharedIndex.skippedSourceCount === 0
|
|
1333
|
-
? 'The GUI inventory is incomplete because the shared index reached a fixed record limit'
|
|
1334
|
-
: `The GUI inventory skipped ${sharedIndex.skippedSourceCount} over-limit source${sharedIndex.skippedSourceCount === 1 ? '' : 's'}`,
|
|
1335
|
-
details: {
|
|
1336
|
-
skippedSourceCount: sharedIndex.skippedSourceCount,
|
|
1337
|
-
retainedSkippedSources: sharedIndex.skippedSources.length,
|
|
1338
|
-
skippedSources: sharedIndex.skippedSources.slice(0, 10).map(({ path, reasonCodes }) => ({
|
|
1339
|
-
path,
|
|
1340
|
-
reasonCodes,
|
|
1341
|
-
})),
|
|
1342
|
-
},
|
|
1343
|
-
});
|
|
1344
|
-
}
|
|
1345
1328
|
const sourceHashes = {};
|
|
1346
1329
|
const textureNodes = new Map();
|
|
1347
1330
|
const fontAssetNodes = new Map();
|
|
@@ -1423,13 +1406,20 @@ export function buildGuiSourceGraph(files, sharedIndex, additionalLocalisationKe
|
|
|
1423
1406
|
if (kind === undefined || fileNodeId === undefined) {
|
|
1424
1407
|
if (/\.(?:txt|gui|gfx)$/u.test(normalized)) {
|
|
1425
1408
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
1409
|
+
if (providedIndex === undefined &&
|
|
1410
|
+
!sharedIndex.recordSourceCoverage(file, document.diagnostics))
|
|
1411
|
+
continue;
|
|
1426
1412
|
if (file.rootKind === 'mod' || file.rootKind === 'fixture')
|
|
1427
1413
|
diagnostics.pushMany(document.diagnostics);
|
|
1428
|
-
|
|
1414
|
+
if (/^common\/decision(?:s|_categories)\//u.test(normalized))
|
|
1415
|
+
indexDecisionEntries(document, file, nodes, pendingDecisionEdges);
|
|
1429
1416
|
}
|
|
1430
1417
|
continue;
|
|
1431
1418
|
}
|
|
1432
1419
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
1420
|
+
if (providedIndex === undefined &&
|
|
1421
|
+
!sharedIndex.recordSourceCoverage(file, document.diagnostics))
|
|
1422
|
+
continue;
|
|
1433
1423
|
if (file.rootKind === 'mod' || file.rootKind === 'fixture')
|
|
1434
1424
|
diagnostics.pushMany(document.diagnostics);
|
|
1435
1425
|
if (kind === 'gui_file')
|
|
@@ -1451,14 +1441,22 @@ export function buildGuiSourceGraph(files, sharedIndex, additionalLocalisationKe
|
|
|
1451
1441
|
const referencedLocalisationKeys = referencedGuiLocalisationKeys(elements, scriptedLocalisation);
|
|
1452
1442
|
for (const key of additionalLocalisationKeys)
|
|
1453
1443
|
referencedLocalisationKeys.add(key);
|
|
1444
|
+
const maximumCandidates = Math.min(GUI_GRAPH_MAX_NODES, limits.maximumLocalisationCandidates ?? GUI_GRAPH_MAX_NODES);
|
|
1445
|
+
if (!Number.isSafeInteger(maximumCandidates) || maximumCandidates < 1)
|
|
1446
|
+
throw new ServiceError('GUI_LOCALISATION_BUDGET_INVALID', 'Localisation candidate limit must be a positive safe integer');
|
|
1447
|
+
let candidateCount = 0;
|
|
1448
|
+
const localisationCandidates = new Map();
|
|
1454
1449
|
for (const { file, fileNodeId } of localisationFiles.sort((left, right) => right.file.loadOrder - left.file.loadOrder ||
|
|
1455
1450
|
compareCodeUnits(left.file.displayPath, right.file.displayPath))) {
|
|
1456
1451
|
const document = parseLocalisation(file.bytes, file.displayPath);
|
|
1452
|
+
if (providedIndex === undefined &&
|
|
1453
|
+
!sharedIndex.recordSourceCoverage(file, document.diagnostics))
|
|
1454
|
+
continue;
|
|
1457
1455
|
if (file.rootKind === 'mod' || file.rootKind === 'fixture')
|
|
1458
1456
|
diagnostics.pushMany(document.diagnostics);
|
|
1459
1457
|
for (const entry of document.entries) {
|
|
1460
|
-
if (
|
|
1461
|
-
|
|
1458
|
+
if (++candidateCount > maximumCandidates)
|
|
1459
|
+
throw new ServiceError('GUI_LOCALISATION_BUDGET_BLOCKED', 'Localisation dependency inventory exceeds the fixed candidate ceiling', { maximumCandidates });
|
|
1462
1460
|
const value = {
|
|
1463
1461
|
key: entry.key,
|
|
1464
1462
|
language: entry.language,
|
|
@@ -1466,21 +1464,55 @@ export function buildGuiSourceGraph(files, sharedIndex, additionalLocalisationKe
|
|
|
1466
1464
|
sourcePath: file.displayPath,
|
|
1467
1465
|
location: locationFor(file.displayPath, document.lineIndex, entry.start, entry.end),
|
|
1468
1466
|
};
|
|
1467
|
+
const candidates = localisationCandidates.get(entry.key) ?? [];
|
|
1468
|
+
candidates.push({ value, fileNodeId });
|
|
1469
|
+
localisationCandidates.set(entry.key, candidates);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const pendingKeys = [...referencedLocalisationKeys];
|
|
1473
|
+
for (const key of pendingKeys) {
|
|
1474
|
+
for (const { value, fileNodeId } of localisationCandidates.get(key) ?? []) {
|
|
1469
1475
|
addDomainEntry(localisation, value, 'localisation');
|
|
1470
1476
|
const id = deterministicId('gui_loc', value);
|
|
1471
1477
|
addNode(nodes, {
|
|
1472
1478
|
id,
|
|
1473
1479
|
kind: 'localisation',
|
|
1474
|
-
name:
|
|
1475
|
-
path:
|
|
1480
|
+
name: key,
|
|
1481
|
+
path: value.sourcePath,
|
|
1476
1482
|
...(value.location === undefined ? {} : { location: value.location }),
|
|
1477
|
-
metadata: { language:
|
|
1483
|
+
metadata: { language: value.language, value: value.value },
|
|
1478
1484
|
});
|
|
1479
1485
|
addEdge(edges, 'contains', fileNodeId, id, true, {}, value.location);
|
|
1486
|
+
for (const match of value.value.matchAll(/\$([A-Za-z0-9_.-]+)(?:\|[^$]*)?\$/gu)) {
|
|
1487
|
+
const dependency = match[1];
|
|
1488
|
+
if (!referencedLocalisationKeys.has(dependency)) {
|
|
1489
|
+
referencedLocalisationKeys.add(dependency);
|
|
1490
|
+
pendingKeys.push(dependency);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1480
1493
|
}
|
|
1481
1494
|
}
|
|
1482
1495
|
linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scriptedLocalisation, localisation, textureNodes, fontAssetNodes, pendingDecisionEdges);
|
|
1483
1496
|
linkAnimationSources(nodes, edges, animationSources, sprites, textureNodes, animationFrameNodes);
|
|
1497
|
+
diagnostics.pushMany(sharedIndex.diagnostics);
|
|
1498
|
+
if (!sharedIndex.complete) {
|
|
1499
|
+
diagnostics.push({
|
|
1500
|
+
code: 'GUI_INVENTORY_PARTIAL',
|
|
1501
|
+
severity: 'warning',
|
|
1502
|
+
category: 'reference',
|
|
1503
|
+
message: sharedIndex.skippedSourceCount === 0
|
|
1504
|
+
? 'The GUI inventory is incomplete because the shared index reached a fixed record limit'
|
|
1505
|
+
: `The GUI inventory skipped ${sharedIndex.skippedSourceCount} over-limit source${sharedIndex.skippedSourceCount === 1 ? '' : 's'}`,
|
|
1506
|
+
details: {
|
|
1507
|
+
skippedSourceCount: sharedIndex.skippedSourceCount,
|
|
1508
|
+
retainedSkippedSources: sharedIndex.skippedSources.length,
|
|
1509
|
+
skippedSources: sharedIndex.skippedSources.slice(0, 10).map(({ path, reasonCodes }) => ({
|
|
1510
|
+
path,
|
|
1511
|
+
reasonCodes,
|
|
1512
|
+
})),
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1484
1516
|
markPartialReferenceEdges(edges, sharedIndex);
|
|
1485
1517
|
diagnostics.pushMany(referenceDiagnostics(edges));
|
|
1486
1518
|
if (nodes.length > GUI_GRAPH_MAX_NODES ||
|
|
@@ -9,6 +9,7 @@ import { CoreEngine } from '../core/engine.js';
|
|
|
9
9
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
10
10
|
import { assertRenderDimensions, RenderBudget, RENDER_MAX_PIXELS } from '../core/render-budget.js';
|
|
11
11
|
import { escapeFileGlob } from '../core/file-glob.js';
|
|
12
|
+
import { renderStage } from '../core/render-progress.js';
|
|
12
13
|
import { ServiceError } from '../core/result.js';
|
|
13
14
|
import { WorkspaceScanner } from '../core/scanner.js';
|
|
14
15
|
import { applyReplacements, decodeSource, encodeSource, parseClausewitz, parseLocalisation, SOURCE_MAX_BYTES, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
|
|
@@ -22,7 +23,7 @@ import { projectGuiGraphForArtifact } from './inspection-artifact.js';
|
|
|
22
23
|
import { buildGuiScene } from './layout.js';
|
|
23
24
|
import { GUI_VALIDATION_MAX_DIAGNOSTICS } from './limits.js';
|
|
24
25
|
import { compareGuiImages, galleryDimensions, renderGallerySvg, renderGuiScene, } from './renderer.js';
|
|
25
|
-
import { generateGuiPreviewScenarios, resolveScenarioScriptedLocalisation, } from './scenario-generator.js';
|
|
26
|
+
import { generatedCountryTags, generateGuiPreviewScenarios, resolveScenarioScriptedLocalisation, } from './scenario-generator.js';
|
|
26
27
|
import { parseGeneratedScenarioOptions, parsePreviewScenario } from './scenario.js';
|
|
27
28
|
import { buildGuiSourceGraph } from './source-graph.js';
|
|
28
29
|
import { assertGuiSourcePatchesSafe } from './source-patch.js';
|
|
@@ -242,13 +243,17 @@ function referenceVariants(value) {
|
|
|
242
243
|
return candidates;
|
|
243
244
|
}
|
|
244
245
|
const FALLBACK_NAMES_PER_PATTERN = 100;
|
|
245
|
-
function basenameFallbackPatterns(exactPaths, exactFiles) {
|
|
246
|
+
function basenameFallbackPatterns(exactPaths, exactFiles, assetRoots = ['gfx', 'interface', 'fonts']) {
|
|
246
247
|
const resolved = new Set(exactFiles
|
|
247
248
|
.filter(({ shadowedBy }) => shadowedBy === undefined)
|
|
248
249
|
.map(({ relativePath }) => relativePath.replaceAll('\\', '/').toLowerCase()));
|
|
250
|
+
const stem = (value) => value.toLowerCase().replace(/\.(?:png|bmp|tga|dds|svg|fnt|ttf|otf|woff2?)$/u, '');
|
|
251
|
+
const resolvedStems = new Set([...resolved].map(stem));
|
|
249
252
|
const names = [
|
|
250
253
|
...new Set(exactPaths
|
|
251
|
-
.filter((candidate) => !/[*?[\]{}!]/u.test(candidate) &&
|
|
254
|
+
.filter((candidate) => !/[*?[\]{}!]/u.test(candidate) &&
|
|
255
|
+
!resolved.has(candidate.toLowerCase()) &&
|
|
256
|
+
!resolvedStems.has(stem(candidate)))
|
|
252
257
|
.map((candidate) => path.posix.basename(candidate))),
|
|
253
258
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
254
259
|
// One brace group searches many names in a single bounded wildcard pattern, so a window
|
|
@@ -261,7 +266,9 @@ function basenameFallbackPatterns(exactPaths, exactFiles) {
|
|
|
261
266
|
const chunk = grouped.slice(start, start + FALLBACK_NAMES_PER_PATTERN).map(escapeFileGlob);
|
|
262
267
|
patterns.push(chunk.length === 1 ? `**/${chunk[0]}` : `**/{${chunk.join(',')}}`);
|
|
263
268
|
}
|
|
264
|
-
return
|
|
269
|
+
return [...new Set(assetRoots)]
|
|
270
|
+
.flatMap((root) => patterns.map((pattern) => `${escapeFileGlob(normalizeConfiguredRoot(root))}/${pattern}`))
|
|
271
|
+
.sort(compareCodeUnits);
|
|
265
272
|
}
|
|
266
273
|
function selectedElements(graph, windowName, additionalTemplateNames = []) {
|
|
267
274
|
const byId = new Map(graph.elements.map((element) => [element.id, element]));
|
|
@@ -549,10 +556,14 @@ export function scenarioCountryFlagPatterns(scenarios) {
|
|
|
549
556
|
for (const values of [
|
|
550
557
|
scenario.values,
|
|
551
558
|
scenario.scriptedGui,
|
|
559
|
+
scenario.country ?? {},
|
|
552
560
|
...Object.values(scenario.lists).flat(),
|
|
553
561
|
]) {
|
|
554
562
|
for (const [key, value] of Object.entries(values)) {
|
|
555
|
-
if (key.endsWith('.countryTag')
|
|
563
|
+
if ((key.endsWith('.countryTag') ||
|
|
564
|
+
key.endsWith('.tag') ||
|
|
565
|
+
/(?:^|\.)GetFlag$/u.test(key) ||
|
|
566
|
+
['countryTag', 'GetTag', 'tag'].includes(key)) &&
|
|
556
567
|
typeof value === 'string' &&
|
|
557
568
|
/^[A-Za-z0-9_]{2,64}$/u.test(value))
|
|
558
569
|
tags.add(value.toUpperCase());
|
|
@@ -573,8 +584,14 @@ export function scenarioCountryFlagPatterns(scenarios) {
|
|
|
573
584
|
function scenarioCountryHistoryPatterns(scenarios) {
|
|
574
585
|
return scenarios.some(scenarioNeedsCountryHistory) ? ['history/countries/*.txt'] : [];
|
|
575
586
|
}
|
|
576
|
-
function scenarioCountryAssetPatterns(scenarios) {
|
|
577
|
-
|
|
587
|
+
function scenarioCountryAssetPatterns(scenarios, generated = false) {
|
|
588
|
+
const flags = generated
|
|
589
|
+
? [
|
|
590
|
+
...scenarios,
|
|
591
|
+
...generatedCountryTags.map((tag) => parsePreviewScenario({ id: `generated-${tag}`, country: { tag } })),
|
|
592
|
+
]
|
|
593
|
+
: scenarios;
|
|
594
|
+
return [...scenarioCountryFlagPatterns(flags), ...scenarioCountryHistoryPatterns(scenarios)];
|
|
578
595
|
}
|
|
579
596
|
function bmFontPagePatterns(files) {
|
|
580
597
|
const references = files.flatMap((file) => {
|
|
@@ -972,13 +989,9 @@ export class ScriptedGuiStudio {
|
|
|
972
989
|
return cached;
|
|
973
990
|
}
|
|
974
991
|
const retainedFiles = [...files];
|
|
975
|
-
// The previous graph of this scope still indexes every unchanged file.
|
|
976
|
-
const previous = [...this.#graphCache].find(([existing]) => existing.startsWith(`${scope}:`))?.[1].index;
|
|
977
|
-
const index = this.engine.indexFiles(retainedFiles, previous);
|
|
978
992
|
const scanned = {
|
|
979
993
|
files: retainedFiles,
|
|
980
|
-
graph: buildGuiSourceGraph(retainedFiles,
|
|
981
|
-
index,
|
|
994
|
+
graph: buildGuiSourceGraph(retainedFiles, undefined, additionalLocalisationKeys),
|
|
982
995
|
};
|
|
983
996
|
// One graph per scope: a preview builds layout, game-layout and definition graphs, and a
|
|
984
997
|
// single shared slot made each repeat call rebuild all of them.
|
|
@@ -1003,24 +1016,33 @@ export class ScriptedGuiStudio {
|
|
|
1003
1016
|
signal?.throwIfAborted();
|
|
1004
1017
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
1005
1018
|
const [snapshot, localisationFiles] = await Promise.all([
|
|
1006
|
-
this.
|
|
1019
|
+
this.scanner.scan(workspace, {
|
|
1020
|
+
patterns: guiDefinitionPatterns(workspace),
|
|
1021
|
+
...(signal === undefined ? {} : { signal }),
|
|
1022
|
+
}),
|
|
1007
1023
|
this.scanner.scan(workspace, {
|
|
1008
1024
|
patterns: guiLocalisationPatterns(workspace, languages),
|
|
1009
1025
|
...(signal === undefined ? {} : { signal }),
|
|
1010
1026
|
}),
|
|
1011
1027
|
]);
|
|
1012
1028
|
signal?.throwIfAborted();
|
|
1013
|
-
const files = mergeScannedFiles(snapshot
|
|
1029
|
+
const files = mergeScannedFiles(snapshot, localisationFiles);
|
|
1014
1030
|
return this.graphForFiles(files, `workspace:${workspaceId}`);
|
|
1015
1031
|
}
|
|
1016
|
-
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = [], includeImmediateTooltip = false) {
|
|
1032
|
+
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = [], includeImmediateTooltip = false, progress) {
|
|
1033
|
+
const stage = (phase) => renderStage({
|
|
1034
|
+
...(signal === undefined ? {} : { signal }),
|
|
1035
|
+
...(progress === undefined ? {} : { progress }),
|
|
1036
|
+
}, phase);
|
|
1037
|
+
await stage('Scanning GUI layouts');
|
|
1017
1038
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
1018
1039
|
const layoutPatterns = guiLayoutPatterns(workspace);
|
|
1019
|
-
const overlayLayouts = await this.
|
|
1040
|
+
const overlayLayouts = await this.scanner.scan(workspace, {
|
|
1020
1041
|
patterns: layoutPatterns,
|
|
1021
1042
|
rootKinds: ['mod', 'dependency', 'fixture'],
|
|
1022
|
-
|
|
1023
|
-
|
|
1043
|
+
...(signal === undefined ? {} : { signal }),
|
|
1044
|
+
});
|
|
1045
|
+
let layoutFiles = overlayLayouts;
|
|
1024
1046
|
let layoutGraph = this.graphForFiles(layoutFiles, `layout:${workspaceId}`).graph;
|
|
1025
1047
|
const selectedLayoutElements = selectedElements(layoutGraph, windowName, additionalTemplateNames);
|
|
1026
1048
|
const requiredNames = new Set([windowName, ...additionalTemplateNames]);
|
|
@@ -1028,25 +1050,38 @@ export class ScriptedGuiStudio {
|
|
|
1028
1050
|
collectNamedAttributes(element.attributes, new Set(['verticalScrollbar', 'horizontalScrollbar', 'scrollbarType', 'scrollbartype']), requiredNames);
|
|
1029
1051
|
const availableNames = new Set(layoutGraph.elements.map(({ name }) => name));
|
|
1030
1052
|
if ([...requiredNames].some((name) => !availableNames.has(name))) {
|
|
1031
|
-
const gameLayouts = await this.
|
|
1032
|
-
|
|
1053
|
+
const gameLayouts = await this.scanner.scan(workspace, {
|
|
1054
|
+
patterns: layoutPatterns,
|
|
1055
|
+
rootKinds: ['game', 'dlc'],
|
|
1056
|
+
...(signal === undefined ? {} : { signal }),
|
|
1057
|
+
});
|
|
1058
|
+
const gameGraph = this.graphForFiles(gameLayouts, `game-layout:${workspaceId}`).graph;
|
|
1033
1059
|
const selectedPaths = new Set(selectedElements(gameGraph, windowName, [...requiredNames]).map(({ sourcePath }) => sourcePath));
|
|
1034
|
-
layoutFiles = mergeScannedFiles(layoutFiles, gameLayouts.
|
|
1060
|
+
layoutFiles = mergeScannedFiles(layoutFiles, gameLayouts.filter(({ displayPath }) => selectedPaths.has(displayPath)));
|
|
1035
1061
|
layoutGraph = this.graphForFiles(layoutFiles, `merged-layout:${workspaceId}`).graph;
|
|
1036
1062
|
}
|
|
1037
1063
|
if (!layoutGraph.elements.some(({ name }) => name === windowName))
|
|
1038
1064
|
assertTargetWindowAvailable(layoutGraph, windowName);
|
|
1065
|
+
await stage('Scanning GUI definitions and localisation');
|
|
1039
1066
|
const [linkedDefinitions, localisation] = await Promise.all([
|
|
1040
|
-
this.
|
|
1041
|
-
|
|
1067
|
+
this.scanner.scan(workspace, {
|
|
1068
|
+
patterns: guiLinkedDefinitionPatterns(workspace),
|
|
1069
|
+
...(signal === undefined ? {} : { signal }),
|
|
1070
|
+
}),
|
|
1071
|
+
this.scanner.scan(workspace, {
|
|
1072
|
+
patterns: guiLocalisationPatterns(workspace, languages),
|
|
1073
|
+
...(signal === undefined ? {} : { signal }),
|
|
1074
|
+
}),
|
|
1042
1075
|
]);
|
|
1043
|
-
const definitionFiles = mergeScannedFiles(layoutFiles, linkedDefinitions
|
|
1076
|
+
const definitionFiles = mergeScannedFiles(layoutFiles, linkedDefinitions, localisation);
|
|
1077
|
+
await stage('Building GUI source graph');
|
|
1044
1078
|
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}`, additionalLocalisationKeys);
|
|
1045
1079
|
assertTargetWindowAvailable(definitions.graph, windowName);
|
|
1046
1080
|
const referencedPatterns = [
|
|
1047
|
-
...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames, includeImmediateTooltip),
|
|
1081
|
+
...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames, includeImmediateTooltip).filter((pattern) => !pattern.startsWith('gfx/flags/')),
|
|
1048
1082
|
...additionalAssetPatterns,
|
|
1049
1083
|
];
|
|
1084
|
+
await stage('Loading selected GUI assets');
|
|
1050
1085
|
const exactReferenced = referencedPatterns.length === 0
|
|
1051
1086
|
? []
|
|
1052
1087
|
: await this.scanner.scan(workspace, {
|
|
@@ -1054,7 +1089,11 @@ export class ScriptedGuiStudio {
|
|
|
1054
1089
|
...(signal === undefined ? {} : { signal }),
|
|
1055
1090
|
});
|
|
1056
1091
|
signal?.throwIfAborted();
|
|
1057
|
-
const fallbackPatterns = basenameFallbackPatterns(referencedPatterns, exactReferenced
|
|
1092
|
+
const fallbackPatterns = basenameFallbackPatterns(referencedPatterns, exactReferenced, [
|
|
1093
|
+
...workspace.registration.roots.gfx,
|
|
1094
|
+
...workspace.registration.roots.interface,
|
|
1095
|
+
'fonts',
|
|
1096
|
+
]);
|
|
1058
1097
|
const fallbackReferenced = fallbackPatterns.length === 0
|
|
1059
1098
|
? []
|
|
1060
1099
|
: await this.scanner.scan(workspace, {
|
|
@@ -1070,7 +1109,11 @@ export class ScriptedGuiStudio {
|
|
|
1070
1109
|
...(signal === undefined ? {} : { signal }),
|
|
1071
1110
|
});
|
|
1072
1111
|
signal?.throwIfAborted();
|
|
1073
|
-
const fontPageFallbackPatterns = basenameFallbackPatterns(fontPagePatterns, exactFontPages
|
|
1112
|
+
const fontPageFallbackPatterns = basenameFallbackPatterns(fontPagePatterns, exactFontPages, [
|
|
1113
|
+
...workspace.registration.roots.gfx,
|
|
1114
|
+
...workspace.registration.roots.interface,
|
|
1115
|
+
'fonts',
|
|
1116
|
+
]);
|
|
1074
1117
|
const fallbackFontPages = fontPageFallbackPatterns.length === 0
|
|
1075
1118
|
? []
|
|
1076
1119
|
: await this.scanner.scan(workspace, {
|
|
@@ -1079,7 +1122,26 @@ export class ScriptedGuiStudio {
|
|
|
1079
1122
|
});
|
|
1080
1123
|
const fontPages = mergeScannedFiles(exactFontPages, fallbackFontPages);
|
|
1081
1124
|
signal?.throwIfAborted();
|
|
1082
|
-
const
|
|
1125
|
+
const windowElements = selectedElements(definitions.graph, windowName, additionalTemplateNames);
|
|
1126
|
+
const selectedPaths = new Set(windowElements.map(({ sourcePath }) => sourcePath));
|
|
1127
|
+
const namedSprites = new Set(additionalSpriteNames);
|
|
1128
|
+
const namedFonts = new Set();
|
|
1129
|
+
for (const element of windowElements) {
|
|
1130
|
+
collectNamedAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), namedSprites);
|
|
1131
|
+
collectNamedAttributes(element.attributes, new Set(['font', 'buttonFont']), namedFonts);
|
|
1132
|
+
}
|
|
1133
|
+
const assetPaths = new Set(referencedPatterns.map((value) => value.toLowerCase()));
|
|
1134
|
+
for (const sprite of definitions.graph.sprites)
|
|
1135
|
+
if (namedSprites.has(sprite.name) ||
|
|
1136
|
+
[sprite.texturePath, sprite.texturePath2, sprite.effectFile].some((value) => value !== undefined &&
|
|
1137
|
+
referenceVariants(value).some((variant) => assetPaths.has(variant.toLowerCase()))))
|
|
1138
|
+
selectedPaths.add(sprite.sourcePath);
|
|
1139
|
+
for (const font of definitions.graph.fonts)
|
|
1140
|
+
if (namedFonts.has(font.name) ||
|
|
1141
|
+
font.assetPaths.some((value) => referenceVariants(value).some((variant) => assetPaths.has(variant.toLowerCase()))))
|
|
1142
|
+
selectedPaths.add(font.sourcePath);
|
|
1143
|
+
const files = mergeScannedFiles(definitions.files.filter((file) => !/\.(?:gui|gfx)$/iu.test(file.relativePath) || selectedPaths.has(file.displayPath)), referenced, fontPages);
|
|
1144
|
+
await stage('Resolving selected GUI assets');
|
|
1083
1145
|
return this.graphForFiles(files, `window:${workspaceId}:${windowName}`, additionalLocalisationKeys);
|
|
1084
1146
|
}
|
|
1085
1147
|
async lint(input) {
|
|
@@ -1089,7 +1151,7 @@ export class ScriptedGuiStudio {
|
|
|
1089
1151
|
const generatedOptions = input.generatedScenarios === undefined
|
|
1090
1152
|
? undefined
|
|
1091
1153
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
1092
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryAssetPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]), [placeholderScenario, ...explicitRelatedScenarios].some(({ tooltip }) => tooltip !== undefined));
|
|
1154
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryAssetPatterns([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.enabled === true), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]), [placeholderScenario, ...explicitRelatedScenarios].some(({ tooltip }) => tooltip !== undefined), input.progress);
|
|
1093
1155
|
let builtDefinitions;
|
|
1094
1156
|
const evaluationDefinitions = () => (builtDefinitions ??= ClausewitzEvaluationDefinitions.build(scanned));
|
|
1095
1157
|
const resolveLocalisation = (target) => resolveScenarioScriptedLocalisation(scanned.graph, input.windowName, target, evaluationDefinitions);
|
|
@@ -1108,6 +1170,7 @@ export class ScriptedGuiStudio {
|
|
|
1108
1170
|
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
1109
1171
|
const budget = new RenderBudget();
|
|
1110
1172
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
1173
|
+
await renderStage(input, 'Laying out GUI and decoding visible assets');
|
|
1111
1174
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
1112
1175
|
const relatedScenes = [];
|
|
1113
1176
|
for (const related of relatedScenarios) {
|
|
@@ -1179,7 +1242,7 @@ export class ScriptedGuiStudio {
|
|
|
1179
1242
|
placeholderScenario,
|
|
1180
1243
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1181
1244
|
...explicitRelatedScenarios,
|
|
1182
|
-
]), scenarioLocalisationKeys([
|
|
1245
|
+
], generatedOptions?.enabled === true), scenarioLocalisationKeys([
|
|
1183
1246
|
placeholderScenario,
|
|
1184
1247
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1185
1248
|
...explicitRelatedScenarios,
|
|
@@ -1187,7 +1250,7 @@ export class ScriptedGuiStudio {
|
|
|
1187
1250
|
placeholderScenario,
|
|
1188
1251
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1189
1252
|
...explicitRelatedScenarios,
|
|
1190
|
-
].some(({ tooltip }) => tooltip !== undefined));
|
|
1253
|
+
].some(({ tooltip }) => tooltip !== undefined), input.progress);
|
|
1191
1254
|
let builtDefinitions;
|
|
1192
1255
|
const evaluationDefinitions = () => (builtDefinitions ??= ClausewitzEvaluationDefinitions.build(scanned));
|
|
1193
1256
|
const resolveLocalisation = (target) => resolveScenarioScriptedLocalisation(scanned.graph, input.windowName, target, evaluationDefinitions);
|
|
@@ -1207,7 +1270,9 @@ export class ScriptedGuiStudio {
|
|
|
1207
1270
|
const baselineScenario = requestedBaselineScenario ??
|
|
1208
1271
|
parsePreviewScenario({ ...scenario, id: `${scenario.id}-comparison`, state: 'normal' });
|
|
1209
1272
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
1273
|
+
await renderStage(input, 'Laying out GUI and decoding visible assets');
|
|
1210
1274
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
1275
|
+
await renderStage(input, 'Rasterizing GUI views');
|
|
1211
1276
|
const render = await renderGuiScene(scene, undefined, input.signal, budget);
|
|
1212
1277
|
const fullRenderCache = new Map([
|
|
1213
1278
|
[visualSceneKey(scene), fullImage(render)],
|
|
@@ -1319,6 +1384,7 @@ export class ScriptedGuiStudio {
|
|
|
1319
1384
|
validation.diagnostics.push(...stateValidation.diagnostics, ...resolutionValidation.diagnostics);
|
|
1320
1385
|
validation.checks.push(...stateValidation.checks, ...resolutionValidation.checks);
|
|
1321
1386
|
const completeValidation = withGraphDiagnostics(validationGraph, validation);
|
|
1387
|
+
await renderStage(input, 'Writing GUI galleries and evidence');
|
|
1322
1388
|
const scenarioGallerySvg = renderGallerySvg(`${input.windowName} scripted scenario matrix`, scenarioItems);
|
|
1323
1389
|
const stateGallerySvg = renderGallerySvg(`${input.windowName} state matrix`, stateItems);
|
|
1324
1390
|
const resolutionGallerySvg = renderGallerySvg(`${input.windowName} resolution and UI-scale matrix`, resolutionItems);
|