hoi4-agent-tools 2.8.0 → 2.8.2
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 +12 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +126 -4
- package/dist/hoi4_agent_tools/core/index.js +3 -1
- package/dist/hoi4_agent_tools/core/scanner.js +43 -2
- package/dist/hoi4_agent_tools/gui/assets.js +63 -14
- package/dist/hoi4_agent_tools/gui/inspection-artifact.js +85 -14
- package/dist/hoi4_agent_tools/gui/layout.js +175 -24
- package/dist/hoi4_agent_tools/gui/source-graph.js +105 -6
- package/dist/hoi4_agent_tools/gui/studio.js +166 -34
- package/dist/hoi4_agent_tools/gui/validators.js +47 -27
- package/dist/hoi4_agent_tools/mcp/server/create.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/progress.js +60 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +15 -15
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +6 -0
- 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/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/map-operation.schema.json +1 -1
- package/schemas/operation-result.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/server.json +2 -2
|
@@ -213,9 +213,15 @@ function resolveTokenText(value, scenario, localisation, rowValues) {
|
|
|
213
213
|
}
|
|
214
214
|
return replacement;
|
|
215
215
|
}, 'GUI localisation substitution');
|
|
216
|
-
text = replaceTextBounded(text, /\[\?([
|
|
216
|
+
text = replaceTextBounded(text, /\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match) => {
|
|
217
217
|
const key = match[1] ?? '';
|
|
218
|
-
const
|
|
218
|
+
const shortKey = key.slice(key.lastIndexOf('.') + 1);
|
|
219
|
+
const replacement = rowValues?.[key] ??
|
|
220
|
+
rowValues?.[shortKey] ??
|
|
221
|
+
scenario.variables[key] ??
|
|
222
|
+
scenario.variables[shortKey] ??
|
|
223
|
+
scenario.scriptedGui[key] ??
|
|
224
|
+
scenario.scriptedGui[shortKey];
|
|
219
225
|
if (replacement === undefined) {
|
|
220
226
|
unresolved.push(match[0]);
|
|
221
227
|
return /(?:^|\.)Get[A-Za-z0-9_]+$/u.test(key)
|
|
@@ -454,11 +460,14 @@ function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inl
|
|
|
454
460
|
}
|
|
455
461
|
return runs;
|
|
456
462
|
}
|
|
457
|
-
function frameFor(element, sprite, scenario) {
|
|
463
|
+
function frameFor(element, sprite, scenario, rowValues) {
|
|
458
464
|
const frameCount = Math.max(1, sprite.frameCount);
|
|
459
465
|
const selected = scenario.selectedFrames[element.name] ?? scenario.selectedFrames[sprite.name];
|
|
460
466
|
if (selected !== undefined)
|
|
461
467
|
return Math.min(frameCount - 1, selected);
|
|
468
|
+
const scriptedFrame = rowValues?.[`${element.name}.frame`] ?? scenario.scriptedGui[`${element.name}.frame`];
|
|
469
|
+
if (typeof scriptedFrame === 'number' && Number.isFinite(scriptedFrame))
|
|
470
|
+
return Math.min(frameCount - 1, Math.max(0, Math.trunc(scriptedFrame > 0 ? scriptedFrame - 1 : scriptedFrame)));
|
|
462
471
|
const explicit = scalarNumber(property(element.attributes, 'frame'), frameCount);
|
|
463
472
|
if (explicit !== undefined)
|
|
464
473
|
return Math.min(frameCount - 1, Math.max(0, Math.trunc(explicit > 0 ? explicit - 1 : explicit)));
|
|
@@ -583,7 +592,7 @@ class GuiSceneWorkBudget {
|
|
|
583
592
|
this.textLayoutOperations += amount;
|
|
584
593
|
}
|
|
585
594
|
}
|
|
586
|
-
async function layoutElement(definition, parentRect, inheritedClip, parentScale, depth, context, instanceSuffix = '', rowIndex, rowValues, parentInstanceId, inheritedOrientation = 'upper_left') {
|
|
595
|
+
async function layoutElement(definition, parentRect, inheritedClip, parentScale, depth, context, instanceSuffix = '', rowIndex, rowValues, parentInstanceId, inheritedOrientation = 'upper_left', inheritedVisible = true) {
|
|
587
596
|
context.work.admitElement(depth);
|
|
588
597
|
const { scenario, catalog, fidelity, diagnostics } = context;
|
|
589
598
|
const instanceId = instanceSuffix.length === 0 ? definition.id : `${definition.id}${instanceSuffix}`;
|
|
@@ -591,12 +600,27 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
591
600
|
const scale = parentScale * localScale;
|
|
592
601
|
const position = objectProperty(property(definition.attributes, 'position'));
|
|
593
602
|
const size = objectProperty(property(definition.attributes, 'size'));
|
|
594
|
-
const
|
|
595
|
-
const
|
|
603
|
+
const scriptedX = rowValues?.[`${definition.name}.x`] ?? scenario.scriptedGui[`${definition.name}.x`];
|
|
604
|
+
const scriptedY = rowValues?.[`${definition.name}.y`] ?? scenario.scriptedGui[`${definition.name}.y`];
|
|
605
|
+
const localX = (typeof scriptedX === 'number' && Number.isFinite(scriptedX)
|
|
606
|
+
? scriptedX * parentScale
|
|
607
|
+
: undefined) ??
|
|
608
|
+
scaledNumber(position === undefined ? undefined : property(position, 'x'), parentRect.width, parentScale) ??
|
|
609
|
+
0;
|
|
610
|
+
const localY = (typeof scriptedY === 'number' && Number.isFinite(scriptedY)
|
|
611
|
+
? scriptedY * parentScale
|
|
612
|
+
: undefined) ??
|
|
613
|
+
scaledNumber(position === undefined ? undefined : property(position, 'y'), parentRect.height, parentScale) ??
|
|
614
|
+
0;
|
|
596
615
|
let width = scaledNumber(size === undefined ? undefined : property(size, 'width', 'x'), parentRect.width, scale) ?? 0;
|
|
597
616
|
let height = scaledNumber(size === undefined ? undefined : property(size, 'height', 'y'), parentRect.height, scale) ?? 0;
|
|
598
617
|
const background = objectProperty(property(definition.attributes, 'background'));
|
|
599
|
-
const
|
|
618
|
+
const scriptedImage = rowValues?.[`${definition.name}.image`] ??
|
|
619
|
+
rowValues?.[definition.name] ??
|
|
620
|
+
scenario.scriptedGui[`${definition.name}.image`] ??
|
|
621
|
+
scenario.scriptedGui[definition.name];
|
|
622
|
+
const spriteName = (typeof scriptedImage === 'string' && scriptedImage.length > 0 ? scriptedImage : undefined) ??
|
|
623
|
+
scalarString(property(definition.attributes, 'spriteType', 'quadTextureSprite')) ??
|
|
600
624
|
scalarString(background === undefined
|
|
601
625
|
? undefined
|
|
602
626
|
: property(background, 'spriteType', 'quadTextureSprite'));
|
|
@@ -632,7 +656,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
632
656
|
}
|
|
633
657
|
if (partialAppearance.length > 0)
|
|
634
658
|
diagnostics.push(diagnostic('GUI_SPRITE_RENDER_PARTIAL', 'warning', `Element ${definition.name} has a partial sprite appearance: ${partialAppearance.join(' ')}`, definition));
|
|
635
|
-
const frame = frameFor(definition, spriteDefinition, scenario);
|
|
659
|
+
const frame = frameFor(definition, spriteDefinition, scenario, rowValues);
|
|
636
660
|
sprite = await catalog.loadSpriteFrame(spriteDefinition, frame);
|
|
637
661
|
if (spriteDefinition.texturePath2 !== undefined)
|
|
638
662
|
secondarySprite = await catalog.loadSecondarySpriteFrame(spriteDefinition, frame);
|
|
@@ -676,6 +700,10 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
676
700
|
}
|
|
677
701
|
}
|
|
678
702
|
}
|
|
703
|
+
if (typeof scriptedImage === 'string' && scriptedImage.length > 0)
|
|
704
|
+
addFidelity(fidelity, 'modelled', 'scripted_gui_image', `Scenario property selected ${scriptedImage} for ${definition.name}.`, definition);
|
|
705
|
+
if (typeof scriptedX === 'number' || typeof scriptedY === 'number')
|
|
706
|
+
addFidelity(fidelity, 'modelled', 'scripted_gui_position', `Scenario properties positioned ${definition.name} at ${localX},${localY} before anchoring.`, definition);
|
|
679
707
|
const rawButtonText = scalarString(property(definition.attributes, 'buttonText'));
|
|
680
708
|
const rawText = scalarString(property(definition.attributes, 'text')) ?? rawButtonText;
|
|
681
709
|
let text;
|
|
@@ -817,17 +845,24 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
817
845
|
const clipRect = inheritedClip;
|
|
818
846
|
const clipped = clipRect !== undefined &&
|
|
819
847
|
(availableClip === undefined || !equalRect(unclippedRect, availableClip));
|
|
820
|
-
const
|
|
821
|
-
const
|
|
848
|
+
const scriptedVisible = rowValues?.[`${definition.name}.visible`] ?? scenario.scriptedGui[`${definition.name}.visible`];
|
|
849
|
+
const explicitlyVisible = scenario.visibility[definition.name] ??
|
|
850
|
+
scenario.visibility[definition.id] ??
|
|
851
|
+
(typeof scriptedVisible === 'boolean' ? scriptedVisible : undefined);
|
|
852
|
+
const visible = inheritedVisible &&
|
|
853
|
+
(explicitlyVisible ?? (availableClip !== undefined || inheritedClip === undefined));
|
|
822
854
|
const clickThrough = scalarBoolean(property(definition.attributes, 'clickThrough', 'alwaystransparent', 'allwaystransparent')) ?? false;
|
|
823
|
-
const
|
|
855
|
+
const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
856
|
+
scenario.scriptedGui[`${definition.name}.enabled`] ??
|
|
857
|
+
context.constantElementEnabled[definition.name];
|
|
858
|
+
const clickable = clickableTypes.test(definition.elementType) && !clickThrough && scriptedEnabled !== false;
|
|
824
859
|
const state = scenario.elementStates[definition.name] ?? scenario.state;
|
|
825
860
|
const zPriority = scalarNumber(property(definition.attributes, 'priority'), 0) ?? 0;
|
|
826
861
|
let progressRatio;
|
|
827
862
|
if (/progressbar/iu.test(definition.elementType)) {
|
|
828
863
|
const minimum = scalarNumber(property(definition.attributes, 'minValue'), 1) ?? 0;
|
|
829
864
|
const maximum = scalarNumber(property(definition.attributes, 'maxValue'), 1) ?? 100;
|
|
830
|
-
const scriptedValue = scenario.scriptedGui[definition.name];
|
|
865
|
+
const scriptedValue = rowValues?.[definition.name] ?? scenario.scriptedGui[definition.name];
|
|
831
866
|
let value = typeof scriptedValue === 'number'
|
|
832
867
|
? scriptedValue
|
|
833
868
|
: (scalarNumber(property(definition.attributes, 'startValue'), maximum) ?? minimum);
|
|
@@ -911,9 +946,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
911
946
|
rows = [];
|
|
912
947
|
if (state === 'full-list' && rows === undefined)
|
|
913
948
|
rows = Array.from({ length: 12 }, (_unused, index) => ({ index }));
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
949
|
+
const dynamicList = context.dynamicListsByName.get(definition.name);
|
|
950
|
+
const listElement = /(?:grid|listbox|scroll)/iu.test(definition.elementType);
|
|
951
|
+
const slotSize = objectProperty(property(definition.attributes, 'slotsize'));
|
|
952
|
+
const slotHeight = scaledNumber(slotSize === undefined ? undefined : property(slotSize, 'height', 'y'), height, scale);
|
|
953
|
+
if (rows !== undefined && childDefinitions.length > 0 && listElement) {
|
|
917
954
|
const spacingValue = objectProperty(property(definition.attributes, 'spacing'));
|
|
918
955
|
const spacingY = scalarNumber(spacingValue === undefined
|
|
919
956
|
? property(definition.attributes, 'spacing')
|
|
@@ -930,18 +967,50 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
930
967
|
...childParentRect,
|
|
931
968
|
y: childParentRect.y + rowY - originalY * scale,
|
|
932
969
|
};
|
|
933
|
-
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, `${instanceSuffix}#row-${index}`, index, row, instanceId, origo);
|
|
970
|
+
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, `${instanceSuffix}#row-${index}`, index, row, instanceId, origo, visible);
|
|
934
971
|
const rendered = context.instancesById.get(`${child.id}${instanceSuffix}#row-${index}`);
|
|
935
972
|
rowHeight = Math.max(rowHeight, rendered?.unclippedRect.height ?? 0);
|
|
936
973
|
}
|
|
937
|
-
rowY += rowHeight + spacingY * scale;
|
|
974
|
+
rowY += (slotHeight ?? rowHeight) + spacingY * scale;
|
|
938
975
|
}
|
|
939
976
|
addFidelity(fidelity, 'modelled', 'scroll_rows', `Expanded ${rows.length} scenario rows for ${definition.name}.`, definition);
|
|
940
977
|
}
|
|
978
|
+
else if (rows !== undefined && dynamicList !== undefined && listElement) {
|
|
979
|
+
let rowY = 0;
|
|
980
|
+
let renderedRows = 0;
|
|
981
|
+
for (const [index, row] of rows.entries()) {
|
|
982
|
+
context.work.spend('scripted GUI dynamic-list row expansion');
|
|
983
|
+
const explicitTemplate = row.entryContainer;
|
|
984
|
+
const templateName = (typeof explicitTemplate === 'string' && explicitTemplate.length > 0
|
|
985
|
+
? explicitTemplate
|
|
986
|
+
: undefined) ??
|
|
987
|
+
(row.countryScope === true
|
|
988
|
+
? (dynamicList.countryScopeEntryContainer ?? dynamicList.entryContainer)
|
|
989
|
+
: (dynamicList.entryContainer ?? dynamicList.countryScopeEntryContainer));
|
|
990
|
+
const template = templateName === undefined ? undefined : context.elementsByName.get(templateName);
|
|
991
|
+
if (template === undefined) {
|
|
992
|
+
diagnostics.push(diagnostic('GUI_DYNAMIC_LIST_TEMPLATE_MISSING', 'error', templateName === undefined
|
|
993
|
+
? `Dynamic list ${definition.name} has no entry container for scenario row ${index}.`
|
|
994
|
+
: `Dynamic list ${definition.name} cannot resolve entry container ${templateName}.`, definition));
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const templatePosition = objectProperty(property(template.attributes, 'position'));
|
|
998
|
+
const originalY = scalarNumber(templatePosition === undefined ? undefined : property(templatePosition, 'y'), height) ?? 0;
|
|
999
|
+
const shiftedParent = {
|
|
1000
|
+
...childParentRect,
|
|
1001
|
+
y: childParentRect.y + rowY - originalY * scale,
|
|
1002
|
+
};
|
|
1003
|
+
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, `${instanceSuffix}#row-${index}`, index, row, instanceId, origo, visible);
|
|
1004
|
+
const rendered = context.instancesById.get(`${template.id}${instanceSuffix}#row-${index}`);
|
|
1005
|
+
rowY += slotHeight ?? rendered?.unclippedRect.height ?? 0;
|
|
1006
|
+
renderedRows += 1;
|
|
1007
|
+
}
|
|
1008
|
+
addFidelity(fidelity, renderedRows === rows.length ? 'modelled' : 'missing', 'scripted_gui_dynamic_list', `Expanded ${renderedRows}/${rows.length} scenario rows for ${definition.name} through its scripted-GUI entry containers.`, definition);
|
|
1009
|
+
}
|
|
941
1010
|
else {
|
|
942
1011
|
for (const child of childDefinitions) {
|
|
943
1012
|
context.work.spend('nested child expansion');
|
|
944
|
-
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, origo);
|
|
1013
|
+
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, origo, visible);
|
|
945
1014
|
}
|
|
946
1015
|
}
|
|
947
1016
|
}
|
|
@@ -998,24 +1067,65 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
998
1067
|
localisation.set(entry.key, entry.value);
|
|
999
1068
|
addFidelity(fidelity, 'approximated', 'language', `No exact ${scenario.language} bucket; used available localisation entries.`);
|
|
1000
1069
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1070
|
+
const relevantScriptedGuiNames = new Set(graph.scriptedGuis
|
|
1071
|
+
.filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName)
|
|
1072
|
+
.map(({ name }) => name));
|
|
1073
|
+
let addedRelatedScriptedGui = true;
|
|
1074
|
+
while (addedRelatedScriptedGui) {
|
|
1075
|
+
addedRelatedScriptedGui = false;
|
|
1076
|
+
for (const definition of graph.scriptedGuis) {
|
|
1077
|
+
if (definition.parentScriptedGui === undefined ||
|
|
1078
|
+
!relevantScriptedGuiNames.has(definition.parentScriptedGui) ||
|
|
1079
|
+
relevantScriptedGuiNames.has(definition.name))
|
|
1080
|
+
continue;
|
|
1081
|
+
relevantScriptedGuiNames.add(definition.name);
|
|
1082
|
+
addedRelatedScriptedGui = true;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const scriptedWindowVisibility = {};
|
|
1086
|
+
const constantElementVisibility = {};
|
|
1087
|
+
const constantElementEnabled = {};
|
|
1088
|
+
const dynamicListsByName = new Map();
|
|
1089
|
+
for (const scripted of graph.scriptedGuis.filter(({ name }) => relevantScriptedGuiNames.has(name))) {
|
|
1090
|
+
for (const dynamicList of scripted.dynamicListDefinitions)
|
|
1091
|
+
dynamicListsByName.set(dynamicList.name, dynamicList);
|
|
1092
|
+
for (const trigger of scripted.triggerDefinitions) {
|
|
1093
|
+
if (trigger.constantResult === undefined)
|
|
1094
|
+
continue;
|
|
1095
|
+
if (trigger.name.endsWith('_visible'))
|
|
1096
|
+
constantElementVisibility[trigger.elementName] = trigger.constantResult;
|
|
1097
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
1098
|
+
constantElementEnabled[trigger.elementName] = trigger.constantResult;
|
|
1099
|
+
addFidelity(fidelity, 'modelled', 'scripted_gui_constant_trigger', `${trigger.name} resolves to ${trigger.constantResult ? 'yes' : 'no'}.`);
|
|
1100
|
+
}
|
|
1003
1101
|
addFidelity(fidelity, 'modelled', 'scripted_gui_context', `${scripted.name} uses ${scripted.contextType ?? '<unspecified>'} context.`);
|
|
1004
1102
|
if (scripted.visibleExpression === undefined)
|
|
1005
1103
|
continue;
|
|
1006
1104
|
const mockedVisibility = scenario.visibility[scripted.name] ?? scenario.scriptedGui[`${scripted.name}.visible`];
|
|
1007
1105
|
if (typeof mockedVisibility === 'boolean') {
|
|
1008
|
-
|
|
1106
|
+
if (scripted.windowName !== undefined)
|
|
1107
|
+
scriptedWindowVisibility[scripted.windowName] =
|
|
1108
|
+
(scriptedWindowVisibility[scripted.windowName] ?? true) && mockedVisibility;
|
|
1009
1109
|
addFidelity(fidelity, 'modelled', 'scripted_gui_visibility', `${scripted.name}.visible was supplied by the preview scenario.`);
|
|
1010
1110
|
}
|
|
1011
1111
|
else {
|
|
1012
1112
|
addFidelity(fidelity, 'unresolved', 'scripted_gui_visibility', `${scripted.name}.visible requires an explicit scenario mock; the offline renderer leaves it visible.`);
|
|
1013
1113
|
}
|
|
1014
1114
|
}
|
|
1015
|
-
const layoutScenario =
|
|
1016
|
-
|
|
1017
|
-
|
|
1115
|
+
const layoutScenario = {
|
|
1116
|
+
...scenario,
|
|
1117
|
+
visibility: {
|
|
1118
|
+
...scriptedWindowVisibility,
|
|
1119
|
+
...constantElementVisibility,
|
|
1120
|
+
...scenario.visibility,
|
|
1121
|
+
},
|
|
1122
|
+
};
|
|
1018
1123
|
const output = [];
|
|
1124
|
+
const elementsByName = new Map();
|
|
1125
|
+
for (const element of [...graph.elements].sort((left, right) => compareCodeUnits(right.sourcePath, left.sourcePath) ||
|
|
1126
|
+
right.definitionOrder - left.definitionOrder))
|
|
1127
|
+
if (!elementsByName.has(element.name))
|
|
1128
|
+
elementsByName.set(element.name, element);
|
|
1019
1129
|
const context = {
|
|
1020
1130
|
graph,
|
|
1021
1131
|
scenario: layoutScenario,
|
|
@@ -1023,6 +1133,9 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1023
1133
|
fidelity,
|
|
1024
1134
|
diagnostics,
|
|
1025
1135
|
elementsById,
|
|
1136
|
+
elementsByName,
|
|
1137
|
+
dynamicListsByName,
|
|
1138
|
+
constantElementEnabled,
|
|
1026
1139
|
spritesByName: new Map(graph.sprites.map((sprite) => [sprite.name.toLowerCase(), sprite])),
|
|
1027
1140
|
localisation,
|
|
1028
1141
|
output,
|
|
@@ -1037,6 +1150,44 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1037
1150
|
height: scenario.resolution.height,
|
|
1038
1151
|
};
|
|
1039
1152
|
await layoutElement(root, viewport, viewport, baseScale, 0, context);
|
|
1153
|
+
const scriptedByName = new Map(graph.scriptedGuis.map((scripted) => [scripted.name, scripted]));
|
|
1154
|
+
const pendingAttached = graph.scriptedGuis
|
|
1155
|
+
.filter(({ name, parentScriptedGui, windowName: scriptedWindowName }) => relevantScriptedGuiNames.has(name) &&
|
|
1156
|
+
parentScriptedGui !== undefined &&
|
|
1157
|
+
scriptedWindowName !== undefined &&
|
|
1158
|
+
scriptedWindowName !== windowName)
|
|
1159
|
+
.sort((left, right) => compareCodeUnits(left.name, right.name));
|
|
1160
|
+
let attachmentProgress = true;
|
|
1161
|
+
while (pendingAttached.length > 0 && attachmentProgress) {
|
|
1162
|
+
attachmentProgress = false;
|
|
1163
|
+
for (let index = pendingAttached.length - 1; index >= 0; index -= 1) {
|
|
1164
|
+
const scripted = pendingAttached[index];
|
|
1165
|
+
const parentScripted = scripted.parentScriptedGui === undefined
|
|
1166
|
+
? undefined
|
|
1167
|
+
: scriptedByName.get(scripted.parentScriptedGui);
|
|
1168
|
+
const parentWindowName = parentScripted?.windowName;
|
|
1169
|
+
const parentDefinition = parentWindowName === undefined ? undefined : elementsByName.get(parentWindowName);
|
|
1170
|
+
const parentScene = parentDefinition === undefined ? undefined : context.instancesById.get(parentDefinition.id);
|
|
1171
|
+
const attachedDefinition = scripted.windowName === undefined ? undefined : elementsByName.get(scripted.windowName);
|
|
1172
|
+
if (parentScene === undefined || attachedDefinition === undefined)
|
|
1173
|
+
continue;
|
|
1174
|
+
if (!context.instancesById.has(attachedDefinition.id)) {
|
|
1175
|
+
await layoutElement(attachedDefinition, parentScene.unclippedRect, parentScene.unclippedRect, parentScene.scale, parentScene.depth + 1, context, '', undefined, undefined, parentScene.id, 'upper_left', parentScene.visible);
|
|
1176
|
+
addFidelity(fidelity, 'modelled', 'parent_scripted_gui_attachment', `${scripted.windowName} is attached to ${parentWindowName} through ${scripted.parentScriptedGui}.`, attachedDefinition);
|
|
1177
|
+
}
|
|
1178
|
+
pendingAttached.splice(index, 1);
|
|
1179
|
+
attachmentProgress = true;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
for (const scripted of pendingAttached) {
|
|
1183
|
+
diagnostics.push({
|
|
1184
|
+
code: 'GUI_PARENT_SCRIPTED_WINDOW_UNRESOLVED',
|
|
1185
|
+
severity: 'error',
|
|
1186
|
+
category: 'reference',
|
|
1187
|
+
message: `Could not attach ${scripted.windowName ?? '<missing window>'} through parent scripted GUI ${scripted.parentScriptedGui ?? '<missing parent>'}.`,
|
|
1188
|
+
...(scripted.location === undefined ? {} : { location: scripted.location }),
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1040
1191
|
output.sort((left, right) => left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
1041
1192
|
const bounds = unionRects(output.filter(({ visible }) => visible).map(({ unclippedRect }) => unclippedRect));
|
|
1042
1193
|
return {
|
|
@@ -63,6 +63,8 @@ const modelledElementAttributes = new Set([
|
|
|
63
63
|
'allwaystransparent',
|
|
64
64
|
'clickThrough',
|
|
65
65
|
'spacing',
|
|
66
|
+
'slotsize',
|
|
67
|
+
'max_slots_horizontal',
|
|
66
68
|
'priority',
|
|
67
69
|
'minValue',
|
|
68
70
|
'maxValue',
|
|
@@ -420,6 +422,9 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
420
422
|
.concat(childBlocks(child, 'fontfiles').flatMap((fontFiles) => fontFiles.entries
|
|
421
423
|
.filter((entry) => entry.type === 'scalar')
|
|
422
424
|
.map((entry) => entry.value)));
|
|
425
|
+
const languages = childBlocks(child, 'languages').flatMap((languageBlock) => languageBlock.entries
|
|
426
|
+
.filter((entry) => entry.type === 'scalar')
|
|
427
|
+
.map((entry) => entry.value));
|
|
423
428
|
const id = deterministicId('gui_font', { path: file.displayPath, name });
|
|
424
429
|
const font = {
|
|
425
430
|
id,
|
|
@@ -427,6 +432,8 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
427
432
|
sourcePath: file.displayPath,
|
|
428
433
|
location: nodeLocation(document, assignment, name),
|
|
429
434
|
kind: 'bitmapfont',
|
|
435
|
+
override: assignment.key.value === 'bitmapfont_override',
|
|
436
|
+
languages,
|
|
430
437
|
assetPaths,
|
|
431
438
|
rawSource: raw(document, assignment),
|
|
432
439
|
};
|
|
@@ -437,7 +444,12 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
437
444
|
name,
|
|
438
445
|
path: file.displayPath,
|
|
439
446
|
...(font.location === undefined ? {} : { location: font.location }),
|
|
440
|
-
metadata: {
|
|
447
|
+
metadata: {
|
|
448
|
+
kind: font.kind,
|
|
449
|
+
override: font.override,
|
|
450
|
+
languages,
|
|
451
|
+
assetPaths,
|
|
452
|
+
},
|
|
441
453
|
});
|
|
442
454
|
addEdge(edges, 'contains', fileNodeId, id, true, {}, font.location);
|
|
443
455
|
}
|
|
@@ -476,6 +488,21 @@ function directEffectCosts(block) {
|
|
|
476
488
|
}
|
|
477
489
|
return costs;
|
|
478
490
|
}
|
|
491
|
+
function constantTriggerResult(value) {
|
|
492
|
+
if (value.type !== 'block')
|
|
493
|
+
return undefined;
|
|
494
|
+
const entries = assignments(value);
|
|
495
|
+
if (entries.length !== 1 || entries[0]?.key.value.toLowerCase() !== 'always')
|
|
496
|
+
return undefined;
|
|
497
|
+
const result = entries[0].value;
|
|
498
|
+
if (result.type !== 'scalar')
|
|
499
|
+
return undefined;
|
|
500
|
+
if (result.value.toLowerCase() === 'yes')
|
|
501
|
+
return true;
|
|
502
|
+
if (result.value.toLowerCase() === 'no')
|
|
503
|
+
return false;
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
479
506
|
function indexScriptedGuis(document, file, fileNodeId, nodes, edges, scriptedGuis) {
|
|
480
507
|
for (const root of childBlocks(document.root, 'scripted_gui')) {
|
|
481
508
|
for (const assignment of assignments(root)) {
|
|
@@ -493,9 +520,34 @@ function indexScriptedGuis(document, file, fileNodeId, nodes, edges, scriptedGui
|
|
|
493
520
|
location: nodeLocation(document, effect, effect.key.value),
|
|
494
521
|
}));
|
|
495
522
|
const effects = effectDefinitions.map(({ name: effect }) => effect);
|
|
496
|
-
const
|
|
523
|
+
const triggerDefinitions = namedAssignments(block, 'triggers').map((trigger) => {
|
|
524
|
+
const constantResult = constantTriggerResult(trigger.value);
|
|
525
|
+
return {
|
|
526
|
+
name: trigger.key.value,
|
|
527
|
+
elementName: actionElementName(trigger.key.value),
|
|
528
|
+
...(constantResult === undefined ? {} : { constantResult }),
|
|
529
|
+
rawSource: raw(document, trigger),
|
|
530
|
+
location: nodeLocation(document, trigger, trigger.key.value),
|
|
531
|
+
};
|
|
532
|
+
});
|
|
533
|
+
const triggers = triggerDefinitions.map(({ name: trigger }) => trigger);
|
|
497
534
|
const properties = namedAssignments(block, 'properties').map(({ key }) => key.value);
|
|
498
|
-
const
|
|
535
|
+
const dynamicListDefinitions = namedAssignments(block, 'dynamic_lists').map((dynamicList) => {
|
|
536
|
+
const entryContainer = dynamicList.value.type === 'block'
|
|
537
|
+
? firstScalarInsensitive(dynamicList.value, 'entry_container')
|
|
538
|
+
: undefined;
|
|
539
|
+
const countryScopeEntryContainer = dynamicList.value.type === 'block'
|
|
540
|
+
? firstScalarInsensitive(dynamicList.value, 'country_scope_entry_container')
|
|
541
|
+
: undefined;
|
|
542
|
+
return {
|
|
543
|
+
name: dynamicList.key.value,
|
|
544
|
+
...(entryContainer === undefined ? {} : { entryContainer }),
|
|
545
|
+
...(countryScopeEntryContainer === undefined ? {} : { countryScopeEntryContainer }),
|
|
546
|
+
rawSource: raw(document, dynamicList),
|
|
547
|
+
location: nodeLocation(document, dynamicList, dynamicList.key.value),
|
|
548
|
+
};
|
|
549
|
+
});
|
|
550
|
+
const dynamicLists = dynamicListDefinitions.map(({ name: dynamicList }) => dynamicList);
|
|
499
551
|
const aiWeights = namedAssignments(block, 'ai_weights').map(({ key }) => key.value);
|
|
500
552
|
const contextType = firstScalarInsensitive(block, 'context_type');
|
|
501
553
|
const windowName = firstScalarInsensitive(block, 'window_name');
|
|
@@ -517,8 +569,10 @@ function indexScriptedGuis(document, file, fileNodeId, nodes, edges, scriptedGui
|
|
|
517
569
|
effects: effects.sort((a, b) => compareCodeUnits(a, b)),
|
|
518
570
|
effectDefinitions: effectDefinitions.sort((a, b) => compareCodeUnits(a.name, b.name)),
|
|
519
571
|
triggers: triggers.sort((a, b) => compareCodeUnits(a, b)),
|
|
572
|
+
triggerDefinitions: triggerDefinitions.sort((a, b) => compareCodeUnits(a.name, b.name)),
|
|
520
573
|
properties: properties.sort((a, b) => compareCodeUnits(a, b)),
|
|
521
574
|
dynamicLists: dynamicLists.sort((a, b) => compareCodeUnits(a, b)),
|
|
575
|
+
dynamicListDefinitions: dynamicListDefinitions.sort((left, right) => compareCodeUnits(left.name, right.name)),
|
|
522
576
|
aiWeights: aiWeights.sort((a, b) => compareCodeUnits(a, b)),
|
|
523
577
|
aiEnabled: childBlocks(block, 'ai_enabled').length > 0 || childBlocks(block, 'ai_check').length > 0,
|
|
524
578
|
rawSource: raw(document, assignment),
|
|
@@ -533,8 +587,23 @@ function indexScriptedGuis(document, file, fileNodeId, nodes, edges, scriptedGui
|
|
|
533
587
|
metadata: {
|
|
534
588
|
effects: definition.effects,
|
|
535
589
|
triggers: definition.triggers,
|
|
590
|
+
constantTriggers: definition.triggerDefinitions.flatMap(({ name: trigger, elementName, constantResult }) => constantResult === undefined ? [] : [{ trigger, elementName, constantResult }]),
|
|
536
591
|
properties: definition.properties,
|
|
537
592
|
dynamicLists: definition.dynamicLists,
|
|
593
|
+
dynamicListEntryContainers: definition.dynamicListDefinitions.flatMap(({ name: dynamicList, entryContainer, countryScopeEntryContainer }) => [
|
|
594
|
+
...(entryContainer === undefined
|
|
595
|
+
? []
|
|
596
|
+
: [{ dynamicList, scope: 'generic', container: entryContainer }]),
|
|
597
|
+
...(countryScopeEntryContainer === undefined
|
|
598
|
+
? []
|
|
599
|
+
: [
|
|
600
|
+
{
|
|
601
|
+
dynamicList,
|
|
602
|
+
scope: 'country',
|
|
603
|
+
container: countryScopeEntryContainer,
|
|
604
|
+
},
|
|
605
|
+
]),
|
|
606
|
+
]),
|
|
538
607
|
aiWeights: definition.aiWeights,
|
|
539
608
|
aiEnabled: definition.aiEnabled,
|
|
540
609
|
},
|
|
@@ -718,7 +787,15 @@ function indexDecisionEntries(document, file, nodes, pendingDecisionEdges) {
|
|
|
718
787
|
}
|
|
719
788
|
function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scriptedLocalisation, localisation, textureNodes, fontAssetNodes, pendingDecisionEdges) {
|
|
720
789
|
const spriteByName = new Map(sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
|
|
721
|
-
const fontByName = new Map(
|
|
790
|
+
const fontByName = new Map();
|
|
791
|
+
for (const font of fonts) {
|
|
792
|
+
const key = font.name.toLocaleLowerCase('en-US');
|
|
793
|
+
const existing = fontByName.get(key);
|
|
794
|
+
if (existing === undefined ||
|
|
795
|
+
(existing.override && !font.override) ||
|
|
796
|
+
(existing.languages.length > 0 && font.languages.length === 0))
|
|
797
|
+
fontByName.set(key, font);
|
|
798
|
+
}
|
|
722
799
|
const elementByName = new Map();
|
|
723
800
|
for (const element of [...elements].sort((a, b) => a.definitionOrder - b.definitionOrder)) {
|
|
724
801
|
if (!elementByName.has(element.name))
|
|
@@ -778,7 +855,9 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
778
855
|
}
|
|
779
856
|
for (const key of localisationAttributeNames) {
|
|
780
857
|
const value = element.attributes[key];
|
|
781
|
-
if (typeof value !== 'string' ||
|
|
858
|
+
if (typeof value !== 'string' ||
|
|
859
|
+
value.trim().length === 0 ||
|
|
860
|
+
!isStaticGuiLocalisationReference(value))
|
|
782
861
|
continue;
|
|
783
862
|
const entry = localisationByKey.get(value);
|
|
784
863
|
addEdge(edges, 'uses_localisation', element.id, entry === undefined ? `localisation:${value}` : deterministicId('gui_loc', entry), entry !== undefined, { field: key, key: value }, element.location);
|
|
@@ -824,6 +903,17 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
824
903
|
const element = elementByName.get(property);
|
|
825
904
|
addEdge(edges, 'property_target', gui.id, element?.id ?? `gui_element:${property}`, element !== undefined, { property }, gui.location);
|
|
826
905
|
}
|
|
906
|
+
for (const dynamicList of gui.dynamicListDefinitions) {
|
|
907
|
+
for (const [scope, containerName] of [
|
|
908
|
+
['generic', dynamicList.entryContainer],
|
|
909
|
+
['country', dynamicList.countryScopeEntryContainer],
|
|
910
|
+
]) {
|
|
911
|
+
if (containerName === undefined)
|
|
912
|
+
continue;
|
|
913
|
+
const element = elementByName.get(containerName);
|
|
914
|
+
addEdge(edges, 'dynamic_list_template', gui.id, element?.id ?? `gui_element:${containerName}`, element !== undefined, { dynamicList: dynamicList.name, scope, containerName }, dynamicList.location);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
827
917
|
}
|
|
828
918
|
for (const pending of pendingDecisionEdges) {
|
|
829
919
|
const target = scriptedByName.get(pending.target);
|
|
@@ -837,12 +927,18 @@ const localisationAttributeNames = [
|
|
|
837
927
|
'pdx_tooltip_delayed',
|
|
838
928
|
'hint_tag',
|
|
839
929
|
];
|
|
930
|
+
function isStaticGuiLocalisationReference(value) {
|
|
931
|
+
const trimmed = value.trim();
|
|
932
|
+
return !(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
933
|
+
}
|
|
840
934
|
function referencedGuiLocalisationKeys(elements, scriptedLocalisation) {
|
|
841
935
|
const keys = new Set(scriptedLocalisation.flatMap(({ localisationKeys }) => localisationKeys));
|
|
842
936
|
for (const element of elements) {
|
|
843
937
|
for (const attribute of localisationAttributeNames) {
|
|
844
938
|
const value = element.attributes[attribute];
|
|
845
|
-
if (typeof value === 'string' &&
|
|
939
|
+
if (typeof value === 'string' &&
|
|
940
|
+
value.trim().length > 0 &&
|
|
941
|
+
isStaticGuiLocalisationReference(value))
|
|
846
942
|
keys.add(value);
|
|
847
943
|
}
|
|
848
944
|
}
|
|
@@ -862,6 +958,7 @@ function skippedInventoryCouldResolve(edge, sharedIndex) {
|
|
|
862
958
|
case 'button_effect':
|
|
863
959
|
case 'button_trigger':
|
|
864
960
|
case 'property_target':
|
|
961
|
+
case 'dynamic_list_template':
|
|
865
962
|
return sharedIndex.hasSkippedSourceForKind('gui_element');
|
|
866
963
|
case 'parent_scripted_gui':
|
|
867
964
|
case 'decision_category_entry':
|
|
@@ -1016,6 +1113,8 @@ export function buildGuiSourceGraph(files, sharedIndex) {
|
|
|
1016
1113
|
name: path.posix.basename(normalized, '.fnt'),
|
|
1017
1114
|
sourcePath: file.displayPath,
|
|
1018
1115
|
kind: 'bmfont',
|
|
1116
|
+
override: false,
|
|
1117
|
+
languages: [],
|
|
1019
1118
|
assetPaths: [file.relativePath],
|
|
1020
1119
|
}, 'font');
|
|
1021
1120
|
}
|