hoi4-agent-tools 2.8.1 → 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.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/hoi4_agent_tools/core/artifacts.js +126 -4
  3. package/dist/hoi4_agent_tools/core/index.js +3 -1
  4. package/dist/hoi4_agent_tools/core/scanner.js +43 -2
  5. package/dist/hoi4_agent_tools/gui/assets.js +63 -14
  6. package/dist/hoi4_agent_tools/gui/inspection-artifact.js +85 -14
  7. package/dist/hoi4_agent_tools/gui/layout.js +175 -24
  8. package/dist/hoi4_agent_tools/gui/source-graph.js +105 -6
  9. package/dist/hoi4_agent_tools/gui/studio.js +166 -34
  10. package/dist/hoi4_agent_tools/gui/validators.js +47 -27
  11. package/dist/hoi4_agent_tools/mcp/server/progress.js +3 -0
  12. package/dist/hoi4_agent_tools/version.js +1 -1
  13. package/docs/gui.md +4 -0
  14. package/package.json +1 -1
  15. package/schemas/configuration.schema.json +1 -1
  16. package/schemas/continuous-focus-palette.schema.json +1 -1
  17. package/schemas/custom-weighted-pool.schema.json +1 -1
  18. package/schemas/event-feature-manifest.schema.json +1 -1
  19. package/schemas/focus-plan.schema.json +1 -1
  20. package/schemas/focus-planning-sidecar.schema.json +1 -1
  21. package/schemas/gui-animation-source.schema.json +1 -1
  22. package/schemas/gui-helper.schema.json +1 -1
  23. package/schemas/gui-scenario.schema.json +1 -1
  24. package/schemas/map-operation.schema.json +1 -1
  25. package/schemas/operation-result.schema.json +1 -1
  26. package/schemas/probability-analysis-result.schema.json +1 -1
  27. package/schemas/probability-compare-input.schema.json +1 -1
  28. package/schemas/probability-evaluate-input.schema.json +1 -1
  29. package/schemas/probability-inspect-input.schema.json +1 -1
  30. package/schemas/probability-render-input.schema.json +1 -1
  31. package/schemas/probability-scenario-set.schema.json +1 -1
  32. package/schemas/probability-sequence-input.schema.json +1 -1
  33. package/schemas/probability-simulate-input.schema.json +1 -1
  34. package/schemas/probability-sweep-input.schema.json +1 -1
  35. 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, /\[\?([A-Za-z0-9_.-]+)(?:\|[^\]]+)?\]/gu, (match) => {
216
+ text = replaceTextBounded(text, /\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match) => {
217
217
  const key = match[1] ?? '';
218
- const replacement = rowValues?.[key] ?? scenario.variables[key] ?? scenario.scriptedGui[key];
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 localX = scaledNumber(position === undefined ? undefined : property(position, 'x'), parentRect.width, parentScale) ?? 0;
595
- const localY = scaledNumber(position === undefined ? undefined : property(position, 'y'), parentRect.height, parentScale) ?? 0;
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 spriteName = scalarString(property(definition.attributes, 'spriteType', 'quadTextureSprite')) ??
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 explicitlyVisible = scenario.visibility[definition.name] ?? scenario.visibility[definition.id];
821
- const visible = explicitlyVisible ?? (availableClip !== undefined || inheritedClip === undefined);
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 clickable = clickableTypes.test(definition.elementType) && !clickThrough;
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
- if (rows !== undefined &&
915
- childDefinitions.length > 0 &&
916
- /(?:grid|listbox|scroll)/iu.test(definition.elementType)) {
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
- let scriptedWindowVisible = true;
1002
- for (const scripted of graph.scriptedGuis.filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName)) {
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
- scriptedWindowVisible &&= mockedVisibility;
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 = scriptedWindowVisible
1016
- ? scenario
1017
- : { ...scenario, visibility: { ...scenario.visibility, [root.name]: false } };
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: { kind: font.kind, assetPaths },
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 triggers = namedAssignments(block, 'triggers').map(({ key }) => key.value);
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 dynamicLists = namedAssignments(block, 'dynamic_lists').map(({ key }) => key.value);
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(fonts.map((font) => [font.name.toLocaleLowerCase('en-US'), font]));
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' || value.trim().length === 0)
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' && value.trim().length > 0)
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
  }