hoi4-agent-tools 3.2.0 → 3.4.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 +19 -0
- package/README.md +4 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +16 -0
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +38 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
- package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
- package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
- package/dist/hoi4_agent_tools/package-check/service.js +231 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/render.js +286 -31
- package/dist/hoi4_agent_tools/technology/service.js +55 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/events.md +3 -1
- package/docs/focus.md +10 -0
- package/docs/gui.md +49 -3
- package/docs/map.md +8 -0
- package/docs/mechanics.md +70 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +7 -2
- package/package.json +2 -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/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 +650 -0
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +164 -0
- package/schemas/package-manifest.schema.json +142 -0
- 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/scenario-suite.schema.json +1004 -0
- package/schemas/scenario-test-input.schema.json +1057 -0
- package/schemas/shared-scenario.schema.json +237 -0
- package/server.json +2 -2
|
@@ -26,6 +26,7 @@ import { buildGuiSourceGraph } from './source-graph.js';
|
|
|
26
26
|
import { assertGuiSourcePatchesSafe } from './source-patch.js';
|
|
27
27
|
import { validateGuiScene, validateResolutionDrift, validateStateMatrix } from './validators.js';
|
|
28
28
|
const staticGuiDefinitionPatterns = [
|
|
29
|
+
'common/focus_inlay_windows/**/*.txt',
|
|
29
30
|
'common/scripted_triggers/**/*.txt',
|
|
30
31
|
'common/script_constants/**/*.txt',
|
|
31
32
|
'common/scripted_localisation/**/*.txt',
|
|
@@ -248,8 +249,14 @@ function basenameFallbackPatterns(exactPaths, exactFiles) {
|
|
|
248
249
|
.map((candidate) => `**/${path.posix.basename(candidate)}`)),
|
|
249
250
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
250
251
|
}
|
|
251
|
-
function selectedElements(graph, windowName) {
|
|
252
|
+
function selectedElements(graph, windowName, additionalTemplateNames = []) {
|
|
252
253
|
const byId = new Map(graph.elements.map((element) => [element.id, element]));
|
|
254
|
+
const byName = new Map();
|
|
255
|
+
for (const element of graph.elements) {
|
|
256
|
+
const definitions = byName.get(element.name) ?? [];
|
|
257
|
+
definitions.push(element);
|
|
258
|
+
byName.set(element.name, definitions);
|
|
259
|
+
}
|
|
253
260
|
const relatedScriptedGuiNames = new Set(graph.scriptedGuis
|
|
254
261
|
.filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName)
|
|
255
262
|
.map(({ name }) => name));
|
|
@@ -265,7 +272,7 @@ function selectedElements(graph, windowName) {
|
|
|
265
272
|
addedRelatedScriptedGui = true;
|
|
266
273
|
}
|
|
267
274
|
}
|
|
268
|
-
const selectedRootNames = new Set([windowName]);
|
|
275
|
+
const selectedRootNames = new Set([windowName, ...additionalTemplateNames]);
|
|
269
276
|
for (const scripted of graph.scriptedGuis.filter(({ name }) => relatedScriptedGuiNames.has(name))) {
|
|
270
277
|
if (scripted.windowName !== undefined)
|
|
271
278
|
selectedRootNames.add(scripted.windowName);
|
|
@@ -289,6 +296,11 @@ function selectedElements(graph, windowName) {
|
|
|
289
296
|
continue;
|
|
290
297
|
selected.set(id, element);
|
|
291
298
|
pending.push(...element.childIds);
|
|
299
|
+
const referenced = new Set();
|
|
300
|
+
collectNamedAttributes(element.attributes, new Set(['verticalScrollbar', 'horizontalScrollbar', 'scrollbarType', 'scrollbartype']), referenced);
|
|
301
|
+
for (const name of referenced)
|
|
302
|
+
for (const candidate of byName.get(name) ?? [])
|
|
303
|
+
pending.push(candidate.id);
|
|
292
304
|
}
|
|
293
305
|
return [...selected.values()];
|
|
294
306
|
}
|
|
@@ -331,14 +343,14 @@ function localisationIconSpriteCandidates(token) {
|
|
|
331
343
|
const normalized = token.startsWith('GFX_') ? token : `GFX_${token}`;
|
|
332
344
|
return [...new Set([normalized, `${normalized}_texticon`, `${normalized}_text_icon`])];
|
|
333
345
|
}
|
|
334
|
-
export function referencedAssetPatternsForWindow(graph, windowName, additionalSpriteNames = [], languages = ['l_english']) {
|
|
346
|
+
export function referencedAssetPatternsForWindow(graph, windowName, additionalSpriteNames = [], languages = ['l_english'], additionalTemplateNames = []) {
|
|
335
347
|
const spriteNames = new Set(additionalSpriteNames);
|
|
336
348
|
const fontNames = new Set();
|
|
337
349
|
const textValues = new Set();
|
|
338
|
-
for (const element of selectedElements(graph, windowName)) {
|
|
350
|
+
for (const element of selectedElements(graph, windowName, additionalTemplateNames)) {
|
|
339
351
|
collectNamedAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), spriteNames);
|
|
340
352
|
collectNamedAttributes(element.attributes, new Set(['font', 'buttonFont']), fontNames);
|
|
341
|
-
collectNamedAttributes(element.attributes, new Set(['text', 'buttonText']), textValues);
|
|
353
|
+
collectNamedAttributes(element.attributes, new Set(['text', 'buttonText', 'context_aware_text']), textValues);
|
|
342
354
|
}
|
|
343
355
|
for (const localisation of graph.localisation) {
|
|
344
356
|
if (textValues.has(localisation.key))
|
|
@@ -391,6 +403,10 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
391
403
|
const match = /^\[([^\]]+)\]$/u.exec(image);
|
|
392
404
|
return match?.[1] === undefined ? [] : [match[1]];
|
|
393
405
|
}));
|
|
406
|
+
for (const gui of graph.scriptedGuis.filter(({ name }) => relatedScriptedGuiNames.has(name)))
|
|
407
|
+
for (const image of gui.imageDefinitions ?? [])
|
|
408
|
+
for (const choice of image.choices)
|
|
409
|
+
spriteNames.add(choice.spriteName);
|
|
394
410
|
const usesCountryFlags = graph.scriptedGuis
|
|
395
411
|
.filter(({ name }) => relatedScriptedGuiNames.has(name))
|
|
396
412
|
.flatMap(({ propertyDefinitions }) => propertyDefinitions)
|
|
@@ -434,7 +450,7 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
434
450
|
normalizedLanguages.has(normalized.replace(/^l_/u, '')));
|
|
435
451
|
})));
|
|
436
452
|
const references = [
|
|
437
|
-
...[...selectedSprites.values()].flatMap(({ texturePath, texturePath2 }) => [texturePath, texturePath2].filter((value) => value !== undefined)),
|
|
453
|
+
...[...selectedSprites.values()].flatMap(({ texturePath, texturePath2, effectFile }) => [texturePath, texturePath2, effectFile, effectFile?.replace(/\.lua$/iu, '.shader')].filter((value) => value !== undefined)),
|
|
438
454
|
...selectedFonts.flatMap(({ assetPaths }) => assetPaths),
|
|
439
455
|
...selectedManifests.flatMap((manifest) => [
|
|
440
456
|
manifest.sheet.path,
|
|
@@ -477,6 +493,35 @@ function scenarioSpriteNames(scenarios, additionalText = []) {
|
|
|
477
493
|
}
|
|
478
494
|
return [...sprites].sort((left, right) => compareCodeUnits(left, right));
|
|
479
495
|
}
|
|
496
|
+
function scenarioTemplateNames(scenarios) {
|
|
497
|
+
return [
|
|
498
|
+
...new Set(scenarios.flatMap(({ lists }) => Object.values(lists).flatMap((rows) => rows.flatMap(({ entryContainer }) => typeof entryContainer === 'string' && entryContainer.length > 0 ? [entryContainer] : [])))),
|
|
499
|
+
].sort(compareCodeUnits);
|
|
500
|
+
}
|
|
501
|
+
/** Native row bindings admit only bounded country tokens, never caller-supplied paths or globs. */
|
|
502
|
+
export function scenarioCountryFlagPatterns(scenarios) {
|
|
503
|
+
const tags = new Set();
|
|
504
|
+
for (const scenario of scenarios) {
|
|
505
|
+
for (const values of [
|
|
506
|
+
scenario.values,
|
|
507
|
+
scenario.scriptedGui,
|
|
508
|
+
...Object.values(scenario.lists).flat(),
|
|
509
|
+
]) {
|
|
510
|
+
for (const [key, value] of Object.entries(values)) {
|
|
511
|
+
if (key.endsWith('.countryTag') &&
|
|
512
|
+
typeof value === 'string' &&
|
|
513
|
+
/^[A-Za-z0-9_]{2,64}$/u.test(value))
|
|
514
|
+
tags.add(value.toUpperCase());
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return [...tags]
|
|
519
|
+
.sort(compareCodeUnits)
|
|
520
|
+
.flatMap((tag) => ['', 'medium/', 'small/'].flatMap((directory) => [
|
|
521
|
+
`gfx/flags/${directory}${tag}.{bmp,dds,png,tga}`,
|
|
522
|
+
`gfx/flags/${directory}${tag}_*.{bmp,dds,png,tga}`,
|
|
523
|
+
]));
|
|
524
|
+
}
|
|
480
525
|
function bmFontPagePatterns(files) {
|
|
481
526
|
const references = files.flatMap((file) => {
|
|
482
527
|
if (!file.relativePath.toLowerCase().endsWith('.fnt') || file.shadowedBy !== undefined)
|
|
@@ -556,21 +601,130 @@ function assertUniqueScenarioIds(scenarios) {
|
|
|
556
601
|
seen.add(scenario.id);
|
|
557
602
|
}
|
|
558
603
|
}
|
|
559
|
-
function scenarioMatrixEvidence(scenes) {
|
|
604
|
+
function scenarioMatrixEvidence(graph, scenes) {
|
|
560
605
|
const primary = scenes[0];
|
|
561
606
|
if (primary === undefined)
|
|
562
607
|
return { offline: true, scenarios: [], changes: [] };
|
|
608
|
+
const visibilityNames = new Set();
|
|
609
|
+
const enabledNames = new Set();
|
|
610
|
+
for (const scripted of graph.scriptedGuis) {
|
|
611
|
+
for (const trigger of scripted.triggerDefinitions) {
|
|
612
|
+
if (trigger.name.endsWith('_visible'))
|
|
613
|
+
visibilityNames.add(trigger.elementName);
|
|
614
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
615
|
+
enabledNames.add(trigger.elementName);
|
|
616
|
+
}
|
|
617
|
+
for (const property of scripted.propertyDefinitions) {
|
|
618
|
+
if (Object.keys(property.attributes).some((name) => name.toLowerCase() === 'visible'))
|
|
619
|
+
visibilityNames.add(property.elementName);
|
|
620
|
+
if (Object.keys(property.attributes).some((name) => name.toLowerCase() === 'enabled'))
|
|
621
|
+
enabledNames.add(property.elementName);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
for (const scene of scenes) {
|
|
625
|
+
for (const name of Object.keys(scene.scenario.visibility))
|
|
626
|
+
visibilityNames.add(name);
|
|
627
|
+
for (const name of [
|
|
628
|
+
...Object.keys(scene.scenario.values),
|
|
629
|
+
...Object.keys(scene.scenario.scriptedGui),
|
|
630
|
+
]) {
|
|
631
|
+
if (name.endsWith('.visible'))
|
|
632
|
+
visibilityNames.add(name.slice(0, -'.visible'.length));
|
|
633
|
+
if (name.endsWith('.enabled'))
|
|
634
|
+
enabledNames.add(name.slice(0, -'.enabled'.length));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const branchCoverage = [...new Set([...visibilityNames, ...enabledNames])]
|
|
638
|
+
.sort(compareCodeUnits)
|
|
639
|
+
.flatMap((name) => {
|
|
640
|
+
const matches = scenes
|
|
641
|
+
.map((scene) => ({
|
|
642
|
+
scene,
|
|
643
|
+
elements: scene.elements.filter((element) => element.name === name),
|
|
644
|
+
}))
|
|
645
|
+
.filter(({ elements }) => elements.length > 0);
|
|
646
|
+
if (matches.length === 0)
|
|
647
|
+
return [];
|
|
648
|
+
const shown = matches
|
|
649
|
+
.filter(({ elements }) => elements.some(({ visible }) => visible))
|
|
650
|
+
.map(({ scene }) => scene.scenario.id);
|
|
651
|
+
const hidden = matches
|
|
652
|
+
.filter(({ elements }) => elements.every(({ visible }) => !visible))
|
|
653
|
+
.map(({ scene }) => scene.scenario.id);
|
|
654
|
+
const enabled = matches
|
|
655
|
+
.filter(({ elements }) => elements.some(({ clickable }) => clickable))
|
|
656
|
+
.map(({ scene }) => scene.scenario.id);
|
|
657
|
+
const disabled = matches
|
|
658
|
+
.filter(({ elements }) => elements.every(({ clickable }) => !clickable))
|
|
659
|
+
.map(({ scene }) => scene.scenario.id);
|
|
660
|
+
return [
|
|
661
|
+
{
|
|
662
|
+
element: name,
|
|
663
|
+
sourcePaths: [
|
|
664
|
+
...new Set(matches.flatMap(({ elements }) => elements.map(({ sourcePath }) => sourcePath))),
|
|
665
|
+
].sort(compareCodeUnits),
|
|
666
|
+
...(visibilityNames.has(name)
|
|
667
|
+
? { visibility: { shown, hidden, covered: shown.length > 0 && hidden.length > 0 } }
|
|
668
|
+
: {}),
|
|
669
|
+
...(enabledNames.has(name)
|
|
670
|
+
? { enabled: { enabled, disabled, covered: enabled.length > 0 && disabled.length > 0 } }
|
|
671
|
+
: {}),
|
|
672
|
+
},
|
|
673
|
+
];
|
|
674
|
+
});
|
|
563
675
|
const elementKey = (element) => `${element.sourceId}#${element.rowIndex ?? 0}`;
|
|
564
676
|
const primaryByKey = new Map(primary.elements.map((element) => [elementKey(element), element]));
|
|
565
677
|
return {
|
|
566
678
|
offline: true,
|
|
679
|
+
branchCoverage,
|
|
680
|
+
branchCoverageSummary: {
|
|
681
|
+
elements: branchCoverage.length,
|
|
682
|
+
uncoveredVisibility: branchCoverage.filter((entry) => 'visibility' in entry && !entry.visibility.covered).length,
|
|
683
|
+
uncoveredEnabled: branchCoverage.filter((entry) => 'enabled' in entry && !entry.enabled.covered).length,
|
|
684
|
+
},
|
|
567
685
|
scenarios: scenes.map((scene) => ({
|
|
568
686
|
scenario: scene.scenario,
|
|
569
687
|
sourceRevision: scene.sourceRevision,
|
|
570
688
|
fidelity: scene.fidelity,
|
|
689
|
+
labelMeasurements: scene.elements
|
|
690
|
+
.filter(({ visible, text }) => visible && text !== undefined)
|
|
691
|
+
.map(({ id, name, sourcePath, rect, unclippedRect, text }) => ({
|
|
692
|
+
id,
|
|
693
|
+
name,
|
|
694
|
+
sourcePath,
|
|
695
|
+
box: rect,
|
|
696
|
+
unclippedBox: unclippedRect,
|
|
697
|
+
measuredText: {
|
|
698
|
+
width: text.measuredWidth,
|
|
699
|
+
height: text.measuredHeight,
|
|
700
|
+
metricSource: text.metricSource,
|
|
701
|
+
overflowX: text.overflowX,
|
|
702
|
+
overflowY: text.overflowY,
|
|
703
|
+
},
|
|
704
|
+
})),
|
|
705
|
+
backgroundMeasurements: scene.elements
|
|
706
|
+
.filter(({ visible, sprite, secondarySprite, elementType }) => visible &&
|
|
707
|
+
(sprite !== undefined || secondarySprite !== undefined) &&
|
|
708
|
+
/(?:containerwindow|windowtype|buttontype|icontype|progressbar)/iu.test(elementType))
|
|
709
|
+
.map(({ id, name, parentId, sourcePath, rect, unclippedRect, sprite, secondarySprite }) => ({
|
|
710
|
+
id,
|
|
711
|
+
name,
|
|
712
|
+
parentId,
|
|
713
|
+
sourcePath,
|
|
714
|
+
box: rect,
|
|
715
|
+
unclippedBox: unclippedRect,
|
|
716
|
+
sprite: sprite?.spriteName ?? secondarySprite?.spriteName,
|
|
717
|
+
texture: sprite?.texturePath ?? secondarySprite?.texturePath,
|
|
718
|
+
})),
|
|
571
719
|
visibleElements: scene.elements
|
|
572
720
|
.filter(({ visible }) => visible)
|
|
573
721
|
.map(({ id, name }) => ({ id, name })),
|
|
722
|
+
hiddenElements: scene.elements
|
|
723
|
+
.filter(({ visible }) => !visible)
|
|
724
|
+
.map(({ id, name, visibilityReason }) => ({ id, name, reason: visibilityReason })),
|
|
725
|
+
disabledElements: scene.elements
|
|
726
|
+
.filter(({ disabledReason }) => disabledReason !== undefined)
|
|
727
|
+
.map(({ id, name, disabledReason }) => ({ id, name, reason: disabledReason })),
|
|
574
728
|
})),
|
|
575
729
|
changes: scenes.slice(1).map((scene) => {
|
|
576
730
|
const currentByKey = new Map(scene.elements.map((element) => [elementKey(element), element]));
|
|
@@ -581,6 +735,8 @@ function scenarioMatrixEvidence(scenes) {
|
|
|
581
735
|
const resized = [];
|
|
582
736
|
const textChanged = [];
|
|
583
737
|
const frameChanged = [];
|
|
738
|
+
const disabled = [];
|
|
739
|
+
const enabled = [];
|
|
584
740
|
for (const key of keys) {
|
|
585
741
|
const before = primaryByKey.get(key);
|
|
586
742
|
const after = currentByKey.get(key);
|
|
@@ -588,6 +744,10 @@ function scenarioMatrixEvidence(scenes) {
|
|
|
588
744
|
shown.push(after.name);
|
|
589
745
|
if (before?.visible === true && after?.visible !== true)
|
|
590
746
|
hidden.push(before.name);
|
|
747
|
+
if (before?.clickable === true && after?.clickable === false)
|
|
748
|
+
disabled.push(after.name);
|
|
749
|
+
if (before?.clickable === false && after?.clickable === true)
|
|
750
|
+
enabled.push(after.name);
|
|
591
751
|
if (before === undefined || after === undefined)
|
|
592
752
|
continue;
|
|
593
753
|
if (before.unclippedRect.x !== after.unclippedRect.x ||
|
|
@@ -609,6 +769,8 @@ function scenarioMatrixEvidence(scenes) {
|
|
|
609
769
|
resized,
|
|
610
770
|
textChanged,
|
|
611
771
|
frameChanged,
|
|
772
|
+
disabled,
|
|
773
|
+
enabled,
|
|
612
774
|
};
|
|
613
775
|
}),
|
|
614
776
|
};
|
|
@@ -744,7 +906,7 @@ export class ScriptedGuiStudio {
|
|
|
744
906
|
const files = mergeScannedFiles(snapshot.files, localisationFiles);
|
|
745
907
|
return this.graphForFiles(files, `workspace:${workspaceId}`);
|
|
746
908
|
}
|
|
747
|
-
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = []) {
|
|
909
|
+
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = []) {
|
|
748
910
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
749
911
|
const layoutPatterns = guiLayoutPatterns(workspace);
|
|
750
912
|
const overlayLayouts = await this.engine.scan(workspaceId, {
|
|
@@ -753,12 +915,15 @@ export class ScriptedGuiStudio {
|
|
|
753
915
|
}, principal, signal);
|
|
754
916
|
let layoutFiles = overlayLayouts.files;
|
|
755
917
|
let layoutGraph = this.graphForFiles(layoutFiles, `layout:${workspaceId}`).graph;
|
|
756
|
-
|
|
918
|
+
const selectedLayoutElements = selectedElements(layoutGraph, windowName, additionalTemplateNames);
|
|
919
|
+
const requiredNames = new Set([windowName, ...additionalTemplateNames]);
|
|
920
|
+
for (const element of selectedLayoutElements)
|
|
921
|
+
collectNamedAttributes(element.attributes, new Set(['verticalScrollbar', 'horizontalScrollbar', 'scrollbarType', 'scrollbartype']), requiredNames);
|
|
922
|
+
const availableNames = new Set(layoutGraph.elements.map(({ name }) => name));
|
|
923
|
+
if ([...requiredNames].some((name) => !availableNames.has(name))) {
|
|
757
924
|
const gameLayouts = await this.engine.scan(workspaceId, { patterns: layoutPatterns, rootKinds: ['game'] }, principal, signal);
|
|
758
925
|
const gameGraph = this.graphForFiles(gameLayouts.files, `game-layout:${workspaceId}`).graph;
|
|
759
|
-
const selectedPaths = new Set(gameGraph.
|
|
760
|
-
.filter(({ name }) => name === windowName)
|
|
761
|
-
.map(({ sourcePath }) => sourcePath));
|
|
926
|
+
const selectedPaths = new Set(selectedElements(gameGraph, windowName, [...requiredNames]).map(({ sourcePath }) => sourcePath));
|
|
762
927
|
layoutFiles = mergeScannedFiles(layoutFiles, gameLayouts.files.filter(({ displayPath }) => selectedPaths.has(displayPath)));
|
|
763
928
|
layoutGraph = this.graphForFiles(layoutFiles, `layout:${workspaceId}`).graph;
|
|
764
929
|
}
|
|
@@ -771,7 +936,10 @@ export class ScriptedGuiStudio {
|
|
|
771
936
|
const definitionFiles = mergeScannedFiles(layoutFiles, linkedDefinitions.files, localisation.files);
|
|
772
937
|
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}`);
|
|
773
938
|
assertTargetWindowAvailable(definitions.graph, windowName);
|
|
774
|
-
const referencedPatterns =
|
|
939
|
+
const referencedPatterns = [
|
|
940
|
+
...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames),
|
|
941
|
+
...additionalAssetPatterns,
|
|
942
|
+
];
|
|
775
943
|
const exactReferenced = referencedPatterns.length === 0
|
|
776
944
|
? []
|
|
777
945
|
: await this.scanner.scan(workspace, {
|
|
@@ -814,7 +982,7 @@ export class ScriptedGuiStudio {
|
|
|
814
982
|
const generatedOptions = input.generatedScenarios === undefined
|
|
815
983
|
? undefined
|
|
816
984
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
817
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples));
|
|
985
|
+
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]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]));
|
|
818
986
|
const generatedScenarios = generatedOptions === undefined
|
|
819
987
|
? []
|
|
820
988
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
|
|
@@ -844,7 +1012,7 @@ export class ScriptedGuiStudio {
|
|
|
844
1012
|
const budget = new RenderBudget();
|
|
845
1013
|
const beforeScenario = parsePreviewScenario(input.before);
|
|
846
1014
|
const afterScenario = parsePreviewScenario(input.after);
|
|
847
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]));
|
|
1015
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]));
|
|
848
1016
|
const beforeCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, beforeScenario.language);
|
|
849
1017
|
const afterCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, afterScenario.language);
|
|
850
1018
|
const before = await buildGuiScene(scanned.graph, scanned.files, input.windowName, beforeScenario, beforeCatalog);
|
|
@@ -893,7 +1061,15 @@ export class ScriptedGuiStudio {
|
|
|
893
1061
|
placeholderScenario,
|
|
894
1062
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
895
1063
|
...explicitRelatedScenarios,
|
|
896
|
-
], generatedOptions?.textSamples)
|
|
1064
|
+
], generatedOptions?.textSamples), scenarioTemplateNames([
|
|
1065
|
+
placeholderScenario,
|
|
1066
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1067
|
+
...explicitRelatedScenarios,
|
|
1068
|
+
]), scenarioCountryFlagPatterns([
|
|
1069
|
+
placeholderScenario,
|
|
1070
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1071
|
+
...explicitRelatedScenarios,
|
|
1072
|
+
]));
|
|
897
1073
|
const generatedScenarios = generatedOptions === undefined
|
|
898
1074
|
? []
|
|
899
1075
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
|
|
@@ -979,6 +1155,35 @@ export class ScriptedGuiStudio {
|
|
|
979
1155
|
const baselineScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, baselineScenario, catalog);
|
|
980
1156
|
const baselineImage = await renderFull(baselineScene);
|
|
981
1157
|
const comparison = await compareGuiImages(baselineImage.png, fullImage(render).png, budget, input.signal);
|
|
1158
|
+
let sourceBaselineScene;
|
|
1159
|
+
let sourceBaselineImage;
|
|
1160
|
+
let sourceComparison;
|
|
1161
|
+
if (input.sourceBaseline !== undefined) {
|
|
1162
|
+
const { relativePath, source } = input.sourceBaseline;
|
|
1163
|
+
guiTextPackageFileKind(workspace, relativePath, true);
|
|
1164
|
+
const original = scanned.files.find((file) => file.rootKind === 'mod' &&
|
|
1165
|
+
portableSourcePathKey(file.relativePath) === portableSourcePathKey(relativePath));
|
|
1166
|
+
if (original === undefined)
|
|
1167
|
+
throw new ServiceError('GUI_SOURCE_BASELINE_FILE_MISSING', 'Source baseline must identify a scanned mod-owned GUI file', { relativePath });
|
|
1168
|
+
const bytes = Buffer.from(source, 'utf8');
|
|
1169
|
+
if (bytes.length > SOURCE_MAX_BYTES)
|
|
1170
|
+
throw new ServiceError('GUI_SOURCE_BASELINE_TOO_LARGE', 'Source baseline exceeds the file byte limit');
|
|
1171
|
+
const baselineFiles = overlayGuiTextPackageFiles(scanned.files, [
|
|
1172
|
+
{
|
|
1173
|
+
relativePath,
|
|
1174
|
+
kind: 'gui',
|
|
1175
|
+
bytes,
|
|
1176
|
+
absolutePath: original.absolutePath,
|
|
1177
|
+
loadOrder: original.loadOrder,
|
|
1178
|
+
},
|
|
1179
|
+
]);
|
|
1180
|
+
const baselineGraph = this.graphForFiles(baselineFiles, `source-baseline:${input.workspaceId}`).graph;
|
|
1181
|
+
if (!baselineGraph.elements.some(({ name }) => name === input.windowName))
|
|
1182
|
+
throw new ServiceError('GUI_SOURCE_BASELINE_WINDOW_MISSING', 'Source baseline does not define the requested GUI window', { relativePath, windowName: input.windowName });
|
|
1183
|
+
sourceBaselineScene = await buildGuiScene(baselineGraph, baselineFiles, input.windowName, scenario, new GuiAssetCatalog(baselineGraph, baselineFiles, budget, scenario.language));
|
|
1184
|
+
sourceBaselineImage = await renderFull(sourceBaselineScene);
|
|
1185
|
+
sourceComparison = await compareGuiImages(sourceBaselineImage.png, fullImage(render).png, budget, input.signal);
|
|
1186
|
+
}
|
|
982
1187
|
const renderedSourceIds = [
|
|
983
1188
|
...scenarioScenes,
|
|
984
1189
|
...stateScenes,
|
|
@@ -1048,7 +1253,7 @@ export class ScriptedGuiStudio {
|
|
|
1048
1253
|
add(`${slug}-validation.json`, 'application/json', `${canonicalJson(completeValidation)}\n`, 'gui-validation-report', [...scenarioScenes, ...stateScenes, ...resolutionScenes]);
|
|
1049
1254
|
add(`${slug}-scenario-matrix.svg`, 'image/svg+xml', scenarioGallerySvg, 'gui-scripted-scenario-matrix', scenarioScenes);
|
|
1050
1255
|
add(`${slug}-scenario-matrix.png`, 'image/png', scenarioGalleryPng, 'gui-scripted-scenario-matrix', scenarioScenes);
|
|
1051
|
-
add(`${slug}-scenario-matrix.json`, 'application/json', `${canonicalJson(scenarioMatrixEvidence(scenarioScenes))}\n`, 'gui-scripted-scenario-matrix-json', scenarioScenes);
|
|
1256
|
+
add(`${slug}-scenario-matrix.json`, 'application/json', `${canonicalJson(scenarioMatrixEvidence(scanned.graph, scenarioScenes))}\n`, 'gui-scripted-scenario-matrix-json', scenarioScenes);
|
|
1052
1257
|
add(`${slug}-state-matrix.svg`, 'image/svg+xml', stateGallerySvg, 'gui-state-matrix', stateScenes);
|
|
1053
1258
|
add(`${slug}-state-matrix.png`, 'image/png', stateGalleryPng, 'gui-state-matrix', stateScenes);
|
|
1054
1259
|
add(`${slug}-state-matrix.json`, 'application/json', `${canonicalJson({ offline: true, scenarios: stateScenes.map(({ scenario: stateScenario, fidelity }) => ({ scenario: stateScenario, fidelity })) })}\n`, 'gui-state-matrix-json', stateScenes);
|
|
@@ -1060,6 +1265,23 @@ export class ScriptedGuiStudio {
|
|
|
1060
1265
|
baselineScene,
|
|
1061
1266
|
]);
|
|
1062
1267
|
add(`${slug}-comparison.json`, 'application/json', comparison.json, 'gui-before-after-comparison', [scene, baselineScene]);
|
|
1268
|
+
if (sourceBaselineScene !== undefined &&
|
|
1269
|
+
sourceBaselineImage !== undefined &&
|
|
1270
|
+
sourceComparison !== undefined) {
|
|
1271
|
+
add(`${slug}-source-baseline.png`, 'image/png', sourceBaselineImage.png, 'gui-source-baseline', [sourceBaselineScene]);
|
|
1272
|
+
add(`${slug}-source-comparison.png`, 'image/png', sourceComparison.png, 'gui-source-comparison', [sourceBaselineScene, scene]);
|
|
1273
|
+
add(`${slug}-source-comparison.json`, 'application/json', `${canonicalJson({
|
|
1274
|
+
kind: 'gui-source-comparison',
|
|
1275
|
+
scenario,
|
|
1276
|
+
before: {
|
|
1277
|
+
sourceRevision: sourceBaselineScene.sourceRevision,
|
|
1278
|
+
sourcePath: input.sourceBaseline.relativePath,
|
|
1279
|
+
sourceHash: sha256Bytes(Buffer.from(input.sourceBaseline.source, 'utf8')),
|
|
1280
|
+
},
|
|
1281
|
+
after: { sourceRevision: scene.sourceRevision },
|
|
1282
|
+
comparison: JSON.parse(sourceComparison.json),
|
|
1283
|
+
})}\n`, 'gui-source-comparison', [sourceBaselineScene, scene]);
|
|
1284
|
+
}
|
|
1063
1285
|
const stored = await this.artifacts.withAtomicChunkedWrites(workspace, writes, (artifacts) => Promise.resolve([...artifacts]), input.signal);
|
|
1064
1286
|
return {
|
|
1065
1287
|
artifacts: stored,
|
|
@@ -1069,6 +1291,7 @@ export class ScriptedGuiStudio {
|
|
|
1069
1291
|
stateScenes,
|
|
1070
1292
|
resolutionScenes,
|
|
1071
1293
|
comparison,
|
|
1294
|
+
...(sourceComparison === undefined ? {} : { sourceComparison }),
|
|
1072
1295
|
validation: completeValidation,
|
|
1073
1296
|
};
|
|
1074
1297
|
}
|
|
@@ -1193,7 +1416,7 @@ export class ScriptedGuiStudio {
|
|
|
1193
1416
|
const previewScenario = input.scenario === undefined ? undefined : parsePreviewScenario(input.scenario);
|
|
1194
1417
|
const initiallyScanned = input.windowName === undefined
|
|
1195
1418
|
? await this.scan(input.workspaceId, input.principal, input.signal)
|
|
1196
|
-
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]));
|
|
1419
|
+
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]));
|
|
1197
1420
|
const exactTargets = await this.scanner.scan(workspace, {
|
|
1198
1421
|
patterns: prepared.map(({ relativePath }) => relativePath),
|
|
1199
1422
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
@@ -1213,7 +1436,10 @@ export class ScriptedGuiStudio {
|
|
|
1213
1436
|
const scenario = parsePreviewScenario(input.scenario);
|
|
1214
1437
|
const renderBudget = new RenderBudget();
|
|
1215
1438
|
let proposedGraph = buildGuiSourceGraph(proposedFiles, this.engine.indexFiles(proposedFiles));
|
|
1216
|
-
const proposedAssetPatterns =
|
|
1439
|
+
const proposedAssetPatterns = [
|
|
1440
|
+
...referencedAssetPatternsForWindow(proposedGraph, input.windowName, [], [scenario.language], scenarioTemplateNames([scenario])),
|
|
1441
|
+
...scenarioCountryFlagPatterns([scenario]),
|
|
1442
|
+
];
|
|
1217
1443
|
const exactProposedAssets = proposedAssetPatterns.length === 0
|
|
1218
1444
|
? []
|
|
1219
1445
|
: await this.scanner.scan(workspace, {
|
|
@@ -235,6 +235,7 @@ export async function renderGui(studio, input, context) {
|
|
|
235
235
|
...(input.comparisonScenario === undefined
|
|
236
236
|
? {}
|
|
237
237
|
: { comparisonScenario: input.comparisonScenario }),
|
|
238
|
+
...(input.sourceBaseline === undefined ? {} : { sourceBaseline: input.sourceBaseline }),
|
|
238
239
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
239
240
|
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
240
241
|
});
|
|
@@ -257,6 +258,14 @@ export async function renderGui(studio, input, context) {
|
|
|
257
258
|
changedPixels: rendered.comparison.changedPixels,
|
|
258
259
|
changedRatio: rendered.comparison.changedRatio,
|
|
259
260
|
},
|
|
261
|
+
...(rendered.sourceComparison === undefined
|
|
262
|
+
? {}
|
|
263
|
+
: {
|
|
264
|
+
sourceComparison: {
|
|
265
|
+
changedPixels: rendered.sourceComparison.changedPixels,
|
|
266
|
+
changedRatio: rendered.sourceComparison.changedRatio,
|
|
267
|
+
},
|
|
268
|
+
}),
|
|
260
269
|
fidelityCounts: Object.fromEntries(Object.entries(rendered.render.fidelity).map(([key, values]) => [key, values.length])),
|
|
261
270
|
offlineRepresentation: true,
|
|
262
271
|
});
|
|
@@ -207,6 +207,36 @@ function validateScenarioExpectations(scene, diagnostics) {
|
|
|
207
207
|
function validateButtonLabelsAndBackgrounds(scene, diagnostics) {
|
|
208
208
|
const visible = scene.elements.filter((element) => element.visible);
|
|
209
209
|
const byId = new Map(scene.elements.map((element) => [element.id, element]));
|
|
210
|
+
const backgrounds = new Map();
|
|
211
|
+
for (const element of visible) {
|
|
212
|
+
if (element.elementType !== 'background' ||
|
|
213
|
+
element.parentId === undefined ||
|
|
214
|
+
area(element.rect) === 0)
|
|
215
|
+
continue;
|
|
216
|
+
const previous = backgrounds.get(element.parentId);
|
|
217
|
+
const rect = element.unclippedRect;
|
|
218
|
+
const x = Math.min(previous?.x ?? rect.x, rect.x);
|
|
219
|
+
const y = Math.min(previous?.y ?? rect.y, rect.y);
|
|
220
|
+
backgrounds.set(element.parentId, {
|
|
221
|
+
x,
|
|
222
|
+
y,
|
|
223
|
+
width: Math.max(previous === undefined ? x : previous.x + previous.width, rect.x + rect.width) - x,
|
|
224
|
+
height: Math.max(previous === undefined ? y : previous.y + previous.height, rect.y + rect.height) -
|
|
225
|
+
y,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const backgroundOwner = (element) => {
|
|
229
|
+
const visited = new Set();
|
|
230
|
+
let parent = element.parentId === undefined ? undefined : byId.get(element.parentId);
|
|
231
|
+
while (parent !== undefined && !visited.has(parent.id)) {
|
|
232
|
+
visited.add(parent.id);
|
|
233
|
+
const rect = parent.sprite === undefined ? backgrounds.get(parent.id) : parent.unclippedRect;
|
|
234
|
+
if (rect !== undefined)
|
|
235
|
+
return { owner: parent, rect };
|
|
236
|
+
parent = parent.parentId === undefined ? undefined : byId.get(parent.parentId);
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
};
|
|
210
240
|
const buttons = visible.filter((element) => element.clickable && /button/iu.test(element.elementType));
|
|
211
241
|
const labels = visible.filter((element) => element.text !== undefined);
|
|
212
242
|
const inferredButtonByLabel = new Map();
|
|
@@ -249,22 +279,18 @@ function validateButtonLabelsAndBackgrounds(scene, diagnostics) {
|
|
|
249
279
|
deltaY: delta.y,
|
|
250
280
|
}));
|
|
251
281
|
}
|
|
252
|
-
|
|
253
|
-
while (parent !== undefined && parent.sprite === undefined)
|
|
254
|
-
parent = parent.parentId === undefined ? undefined : byId.get(parent.parentId);
|
|
282
|
+
const parent = backgroundOwner(label);
|
|
255
283
|
if (parent !== undefined &&
|
|
256
|
-
!contains(parent.
|
|
257
|
-
intersection(parent.
|
|
258
|
-
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${label.name} crosses the visible background boundary of ${parent.name} in scenario ${scene.scenario.id}.`, label, { scenarioId: scene.scenario.id, element: label.name, background: parent.name }));
|
|
284
|
+
!contains(parent.rect, label.unclippedRect, 0.5) &&
|
|
285
|
+
intersection(parent.rect, label.unclippedRect) !== undefined)
|
|
286
|
+
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${label.name} crosses the visible background boundary of ${parent.owner.name} in scenario ${scene.scenario.id}.`, label, { scenarioId: scene.scenario.id, element: label.name, background: parent.owner.name }));
|
|
259
287
|
}
|
|
260
288
|
for (const control of visible.filter((element) => element.clickable && element.text === undefined)) {
|
|
261
|
-
|
|
262
|
-
while (parent !== undefined && parent.sprite === undefined)
|
|
263
|
-
parent = parent.parentId === undefined ? undefined : byId.get(parent.parentId);
|
|
289
|
+
const parent = backgroundOwner(control);
|
|
264
290
|
if (parent !== undefined &&
|
|
265
|
-
!contains(parent.
|
|
266
|
-
intersection(parent.
|
|
267
|
-
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${control.name} crosses the visible background boundary of ${parent.name} in scenario ${scene.scenario.id}.`, control, { scenarioId: scene.scenario.id, element: control.name, background: parent.name }));
|
|
291
|
+
!contains(parent.rect, control.unclippedRect, 0.5) &&
|
|
292
|
+
intersection(parent.rect, control.unclippedRect) !== undefined)
|
|
293
|
+
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${control.name} crosses the visible background boundary of ${parent.owner.name} in scenario ${scene.scenario.id}.`, control, { scenarioId: scene.scenario.id, element: control.name, background: parent.owner.name }));
|
|
268
294
|
}
|
|
269
295
|
}
|
|
270
296
|
function sameRect(left, right) {
|
|
@@ -2,7 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
3
3
|
import { compareCodeUnits, deterministicId, sha256Bytes } from '../core/canonical.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
|
-
import { applyReplacements, assignments, encodeSource, parseLocalisation, } from '../core/source/index.js';
|
|
5
|
+
import { applyReplacements, assignments, encodeSource, parseLocalisation, parseClausewitz, } from '../core/source/index.js';
|
|
6
6
|
import { rgbKey } from './bmp.js';
|
|
7
7
|
import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SELECTED_PIXEL_LIMIT, } from './limits.js';
|
|
8
8
|
import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseTextDocument, } from './model.js';
|
|
@@ -2718,6 +2718,72 @@ function renumberMapping(existingIds, operation) {
|
|
|
2718
2718
|
...(targetOccupied ? [[operation.toId, operation.fromId]] : []),
|
|
2719
2719
|
]);
|
|
2720
2720
|
}
|
|
2721
|
+
const EXTERNAL_SCRIPT_REFERENCE_KEYS = {
|
|
2722
|
+
province: new Set(['province', 'province_id', 'start_province', 'target_province']),
|
|
2723
|
+
state: new Set([
|
|
2724
|
+
'state',
|
|
2725
|
+
'state_id',
|
|
2726
|
+
'start_state',
|
|
2727
|
+
'target_state',
|
|
2728
|
+
'owns_state',
|
|
2729
|
+
'controls_state',
|
|
2730
|
+
'transfer_state',
|
|
2731
|
+
'transfer_state_to',
|
|
2732
|
+
]),
|
|
2733
|
+
'strategic-region': new Set(['strategic_region', 'strategic_region_id']),
|
|
2734
|
+
};
|
|
2735
|
+
function externalScriptFile(relativePath) {
|
|
2736
|
+
return /^(?:events|common\/(?:national_focus|decisions|scripted_effects|scripted_triggers|on_actions|ideas|characters)|history\/countries)\/.+\.txt$/u.test(relativePath);
|
|
2737
|
+
}
|
|
2738
|
+
function remapExternalScriptReferences(index, changes, mapping, entity, operationId) {
|
|
2739
|
+
const keys = EXTERNAL_SCRIPT_REFERENCE_KEYS[entity];
|
|
2740
|
+
for (const file of index.sourceFiles) {
|
|
2741
|
+
if (file.shadowedBy !== undefined || !externalScriptFile(file.relativePath))
|
|
2742
|
+
continue;
|
|
2743
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
2744
|
+
if (document.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker'))
|
|
2745
|
+
throw new ServiceError('MAP_EXTERNAL_SCRIPT_PARSE_BLOCKED', 'A selected external script cannot be safely inspected for typed map references', {
|
|
2746
|
+
relativePath: file.relativePath,
|
|
2747
|
+
diagnosticCodes: document.diagnostics.map(({ code }) => code),
|
|
2748
|
+
});
|
|
2749
|
+
const replacements = [];
|
|
2750
|
+
const visit = (entry) => {
|
|
2751
|
+
if (entry.type === 'block') {
|
|
2752
|
+
for (const child of entry.entries)
|
|
2753
|
+
visit(child);
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
if (entry.type !== 'assignment')
|
|
2757
|
+
return;
|
|
2758
|
+
if (entry.value.type === 'block') {
|
|
2759
|
+
for (const child of entry.value.entries)
|
|
2760
|
+
visit(child);
|
|
2761
|
+
return;
|
|
2762
|
+
}
|
|
2763
|
+
if (!keys.has(entry.key.value) || !/^\d+$/u.test(entry.value.value))
|
|
2764
|
+
return;
|
|
2765
|
+
const replacement = mapping.get(Number(entry.value.value));
|
|
2766
|
+
if (replacement === undefined)
|
|
2767
|
+
return;
|
|
2768
|
+
if (file.rootKind !== 'mod' && file.rootKind !== 'fixture')
|
|
2769
|
+
throw new ServiceError('MAP_EXTERNAL_REFERENCE_READ_ONLY', 'A typed external map reference is owned by a read-only source', {
|
|
2770
|
+
relativePath: file.relativePath,
|
|
2771
|
+
rootKind: file.rootKind,
|
|
2772
|
+
key: entry.key.value,
|
|
2773
|
+
id: entry.value.value,
|
|
2774
|
+
});
|
|
2775
|
+
replacements.push({
|
|
2776
|
+
start: entry.value.start,
|
|
2777
|
+
end: entry.value.end,
|
|
2778
|
+
text: String(replacement),
|
|
2779
|
+
description: `Renumber typed ${entity} reference in ${entry.key.value}`,
|
|
2780
|
+
});
|
|
2781
|
+
};
|
|
2782
|
+
visit(document.root);
|
|
2783
|
+
if (replacements.length > 0)
|
|
2784
|
+
addChange(changes, file.relativePath, applyReplacements(document, replacements), operationId, 'text/plain');
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2721
2787
|
function renameLocalisationKeys(index, changes, keys, operationId) {
|
|
2722
2788
|
if (keys.size === 0)
|
|
2723
2789
|
return;
|
|
@@ -2838,6 +2904,7 @@ function applyRenumberMapEntity(base, changes, operation) {
|
|
|
2838
2904
|
commitRegion(changes, region, uniqueSorted(region.provinces.map((id) => remapProvinceId(mapping, id))), operation.id);
|
|
2839
2905
|
}
|
|
2840
2906
|
remapReferenceFiles(index, changes, mapping, operation.id);
|
|
2907
|
+
remapExternalScriptReferences(index, changes, mapping, operation.entity, operation.id);
|
|
2841
2908
|
if (operation.renameLocalisation ?? true) {
|
|
2842
2909
|
const keys = new Map();
|
|
2843
2910
|
for (const [fromId, toId] of mapping) {
|
|
@@ -2863,6 +2930,7 @@ function applyRenumberMapEntity(base, changes, operation) {
|
|
|
2863
2930
|
commitState(changes, state, data, { id: true, ...(standardName ? { name: true } : {}) }, operation.id);
|
|
2864
2931
|
}
|
|
2865
2932
|
remapBuildingPositionStateIds(index, changes, mapping, operation.id);
|
|
2933
|
+
remapExternalScriptReferences(index, changes, mapping, operation.entity, operation.id);
|
|
2866
2934
|
if (operation.renameLocalisation ?? true)
|
|
2867
2935
|
renameLocalisationKeys(currentIndex(base, changes), changes, localisationKeys, operation.id);
|
|
2868
2936
|
return;
|
|
@@ -2881,6 +2949,7 @@ function applyRenumberMapEntity(base, changes, operation) {
|
|
|
2881
2949
|
addChange(changes, region.file.relativePath, patchRegion(region, region.provinces, { id: toId, ...(standardName ? { name } : {}) }, false), operation.id, 'text/plain');
|
|
2882
2950
|
}
|
|
2883
2951
|
remapWeatherPositionRegionIds(index, changes, mapping, operation.id);
|
|
2952
|
+
remapExternalScriptReferences(index, changes, mapping, operation.entity, operation.id);
|
|
2884
2953
|
if (operation.renameLocalisation ?? true)
|
|
2885
2954
|
renameLocalisationKeys(currentIndex(base, changes), changes, localisationKeys, operation.id);
|
|
2886
2955
|
}
|