hoi4-agent-tools 4.0.3 → 4.0.5

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 (64) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +5 -3
  3. package/dist/hoi4_agent_tools/core/condition-evaluator.js +5 -1
  4. package/dist/hoi4_agent_tools/core/index.js +11 -0
  5. package/dist/hoi4_agent_tools/core/job-executor.js +3 -1
  6. package/dist/hoi4_agent_tools/core/job-worker-host.js +1 -1
  7. package/dist/hoi4_agent_tools/core/render-progress.js +7 -0
  8. package/dist/hoi4_agent_tools/gui/layout.js +76 -33
  9. package/dist/hoi4_agent_tools/gui/native-state.js +40 -1
  10. package/dist/hoi4_agent_tools/gui/scenario-generator.js +3 -3
  11. package/dist/hoi4_agent_tools/gui/source-graph.js +67 -32
  12. package/dist/hoi4_agent_tools/gui/studio.js +101 -32
  13. package/dist/hoi4_agent_tools/gui/tool-operations.js +8 -2
  14. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +2 -1
  15. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +3 -1
  16. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +2 -0
  17. package/dist/hoi4_agent_tools/mcp/server/progress.js +12 -0
  18. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +2 -2
  19. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +5 -2
  20. package/dist/hoi4_agent_tools/technology/job-operations.js +1 -0
  21. package/dist/hoi4_agent_tools/technology/service.js +18 -37
  22. package/dist/hoi4_agent_tools/version.js +1 -1
  23. package/docs/gui.md +14 -2
  24. package/docs/technology.md +2 -0
  25. package/npm-shrinkwrap.json +2 -2
  26. package/package.json +1 -1
  27. package/schemas/configuration.schema.json +1 -1
  28. package/schemas/continuous-focus-palette.schema.json +1 -1
  29. package/schemas/custom-weighted-pool.schema.json +1 -1
  30. package/schemas/decision-inspect-input.schema.json +1 -1
  31. package/schemas/error-log-input.schema.json +1 -1
  32. package/schemas/event-feature-manifest.schema.json +1 -1
  33. package/schemas/focus-plan.schema.json +1 -1
  34. package/schemas/focus-planning-sidecar.schema.json +1 -1
  35. package/schemas/gui-animation-source.schema.json +1 -1
  36. package/schemas/gui-helper.schema.json +1 -1
  37. package/schemas/gui-scenario.schema.json +1 -1
  38. package/schemas/helper-expansion-request.schema.json +1 -1
  39. package/schemas/helper-expansion-summary.schema.json +1 -1
  40. package/schemas/impact-inspect-input.schema.json +1 -1
  41. package/schemas/map-operation.schema.json +1 -1
  42. package/schemas/mechanic-test-input.schema.json +1 -1
  43. package/schemas/mod-index-input.schema.json +1 -1
  44. package/schemas/operation-result.schema.json +1 -1
  45. package/schemas/package-check-input.schema.json +1 -1
  46. package/schemas/package-manifest.schema.json +1 -1
  47. package/schemas/probability-analysis-result.schema.json +1 -1
  48. package/schemas/probability-compare-input.schema.json +1 -1
  49. package/schemas/probability-evaluate-input.schema.json +1 -1
  50. package/schemas/probability-inspect-input.schema.json +1 -1
  51. package/schemas/probability-render-input.schema.json +1 -1
  52. package/schemas/probability-scenario-set.schema.json +1 -1
  53. package/schemas/probability-sequence-input.schema.json +1 -1
  54. package/schemas/probability-simulate-input.schema.json +1 -1
  55. package/schemas/probability-sweep-input.schema.json +1 -1
  56. package/schemas/reference-context-input.schema.json +1 -1
  57. package/schemas/reference-read-input.schema.json +1 -1
  58. package/schemas/reference-search-input.schema.json +1 -1
  59. package/schemas/scenario-suite.schema.json +1 -1
  60. package/schemas/scenario-test-input.schema.json +1 -1
  61. package/schemas/script-validate-input.schema.json +1 -1
  62. package/schemas/shared-scenario.schema.json +1 -1
  63. package/schemas/source-lookup-input.schema.json +1 -1
  64. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,24 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 4.0.5 - 2026-10-14
6
+
7
+ - Keep child anchors independent of parent `origo`, so centered native windows place their controls inside the frame.
8
+ - Preview division and HQ branches separately and close native selector overlays by default. Existing scenario values and visibility bindings select panels explicitly.
9
+ - Preserve dropdown button and expanded-panel boundaries, including closed visibility, settled positions, backgrounds, clipping and attached scrollbars.
10
+ - Render background-free scroll containers using their declared viewport bounds; retain findings for explicitly disabled clipping and invalid boundaries or references.
11
+ - Resolve public scrollbar templates despite unrelated private namesakes. Public tool schemas and dependency versions are unchanged.
12
+
13
+ ## 4.0.4 - 2026-10-14
14
+
15
+ - Preview individual GUI windows without redundant full symbol indexes, workspace-wide asset fallback or unbound country-flag inventories; reuse condition definitions across render variants.
16
+ - Index technology sources once across layers and scope folder rendering to definitions, layouts and unlock targets. Full inspection retains inbound grants and references.
17
+ - Report render phases and include the last phase in worker deadline errors. Existing parsing, raster-memory, canonical-path and stale-source limits remain enforced.
18
+ - Resolve transitive localisation `$KEY$` references across files and bounded scenario substitutions; retain diagnostics for cycles and missing runtime values.
19
+ - Honour numeric flag presence in scalar visibility checks, matching valued-flag super-event localisation conditions.
20
+ - Preserve supplied GUI scenarios by default; exploratory `generatedScenarios` is now opt-in. Explicit state/resolution matrices and public schema shapes are unchanged.
21
+ - Retain regression coverage for hidden sub-windows, timed animated-sprite frames, 16-bit DDS portraits, declared font colours, worker recovery and render-memory ceilings.
22
+
5
23
  ## 4.0.3 - 2026-10-12
6
24
 
7
25
  - Publish the README with the supplied HOI4 MCP Server thumbnail and direct links to the demo video on YouTube.
@@ -31,11 +31,13 @@ export class ClausewitzEvaluationDefinitions {
31
31
  scriptedTriggers = new Map();
32
32
  localConstants = new Map();
33
33
  static build(snapshot) {
34
- const cached = definitionCache.get(snapshot);
34
+ const cached = definitionCache.get(snapshot.files);
35
35
  if (cached !== undefined)
36
36
  return cached;
37
37
  const result = new ClausewitzEvaluationDefinitions();
38
- for (const file of snapshot.files.filter(({ shadowedBy, relativePath }) => shadowedBy === undefined && relativePath.toLowerCase().endsWith('.txt'))) {
38
+ for (const file of snapshot.files.filter(({ shadowedBy, relativePath, bytes }) => shadowedBy === undefined &&
39
+ relativePath.toLowerCase().endsWith('.txt') &&
40
+ bytes.includes(0x40))) {
39
41
  const document = parseClausewitz(file.bytes, file.displayPath);
40
42
  const locals = new Map();
41
43
  for (const assignment of scalarAssignments(document.root)) {
@@ -84,7 +86,7 @@ export class ClausewitzEvaluationDefinitions {
84
86
  });
85
87
  }
86
88
  }
87
- definitionCache.set(snapshot, result);
89
+ definitionCache.set(snapshot.files, result);
88
90
  return result;
89
91
  }
90
92
  }
@@ -286,7 +286,11 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
286
286
  : { state: value ? 'true' : 'false', unresolved: [] };
287
287
  }
288
288
  if (key === 'has_country_flag' || key === 'has_global_flag' || key === 'has_state_flag') {
289
- const explicit = bool(scopeStateValue(scenario, `flag.${right}`, scopeContext));
289
+ const stored = key === 'has_global_flag'
290
+ ? (scopeStateValue(scenario, `global_flag.${right}`, scopeContext) ??
291
+ scopeStateValue(scenario, `flag.${right}`, scopeContext))
292
+ : scopeStateValue(scenario, `flag.${right}`, scopeContext);
293
+ const explicit = typeof stored === 'number' && Number.isFinite(stored) ? true : bool(stored);
290
294
  if (explicit !== undefined)
291
295
  return { state: explicit ? 'true' : 'false', unresolved: [] };
292
296
  const flags = key === 'has_global_flag' && scenario.globalFlags !== undefined
@@ -383,6 +383,17 @@ export class SymbolIndex {
383
383
  },
384
384
  });
385
385
  }
386
+ /** Record parser coverage without allocating symbols for a domain that builds its own graph. */
387
+ recordSourceCoverage(file, diagnostics) {
388
+ const limits = sourcePartialLimitDiagnostics(diagnostics);
389
+ if (limits.length > 0) {
390
+ this.recordSkippedSource(file, limits);
391
+ return false;
392
+ }
393
+ if (isWorkspaceOwnedSource(file) && file.shadowedBy === undefined)
394
+ this.addDiagnostics(diagnostics);
395
+ return true;
396
+ }
386
397
  addSymbol(record) {
387
398
  if (this.symbols.length >= INDEX_RECORD_LIMIT) {
388
399
  this.#complete = false;
@@ -106,10 +106,11 @@ export class JobExecutor {
106
106
  const controller = new AbortController();
107
107
  const watchStop = new AbortController();
108
108
  let watchFailure;
109
+ let lastProgressMessage = claimed.record.progress?.message;
109
110
  const deadlineTimer = options.deadlineMs === undefined || operation.mutation
110
111
  ? undefined
111
112
  : setTimeout(() => {
112
- watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED', 'The job ran past its deadline and was stopped; narrow its selector or limits, or raise jobDeadlineSeconds');
113
+ watchFailure = new ServiceError('JOB_DEADLINE_EXCEEDED', `The job ran past its deadline and was stopped${lastProgressMessage === undefined ? '' : ` during: ${lastProgressMessage.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`);
113
114
  controller.abort(watchFailure);
114
115
  }, Math.max(0, options.deadlineMs - Date.now()));
115
116
  const watch = (async () => {
@@ -136,6 +137,7 @@ export class JobExecutor {
136
137
  signal: controller.signal,
137
138
  checkpoint: claimed.record.checkpoint,
138
139
  progress: async (progress) => {
140
+ lastProgressMessage = progress.message;
139
141
  await this.jobs.store.updateOwned(scope, id, owner.token, { progress }, controller.signal);
140
142
  },
141
143
  saveCheckpoint: async (checkpoint) => {
@@ -309,7 +309,7 @@ export class JobWorkerHost {
309
309
  await stopChild(child, this.stoppedByHost);
310
310
  await this.jobs.failInterrupted(workspaceId, id, {
311
311
  code: 'JOB_DEADLINE_EXCEEDED',
312
- message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped; narrow its selector or limits, or raise jobDeadlineSeconds`,
312
+ message: `The job ran longer than ${jobDeadlineSeconds} seconds and was stopped${record.progress === undefined ? '' : ` during: ${record.progress.message.slice(0, 160)}`}; narrow its selector or limits, or raise jobDeadlineSeconds`,
313
313
  }, principal);
314
314
  return;
315
315
  }
@@ -0,0 +1,7 @@
1
+ export async function renderStage(input, phase) {
2
+ input.signal?.throwIfAborted();
3
+ await new Promise((resolve) => setImmediate(resolve));
4
+ input.signal?.throwIfAborted();
5
+ await input.progress?.(phase);
6
+ input.signal?.throwIfAborted();
7
+ }
@@ -219,15 +219,20 @@ function resolveTokenText(value, scenario, localisation, rowValues) {
219
219
  /^[A-Za-z0-9_.-]+$/u.test(value) &&
220
220
  (value.includes('_') || value === value.toUpperCase()))
221
221
  missingLocalisation = true;
222
- text = replaceTextBounded(text, /\$([A-Za-z0-9_.-]+)\$/gu, (match) => {
222
+ const expandTokens = (input, stack, depth) => replaceTextBounded(input, /\$([A-Za-z0-9_.-]+)\$/gu, (match) => {
223
223
  const key = match[1] ?? '';
224
- const replacement = scenario.localisation[key] ?? localisation.get(key);
225
- if (replacement === undefined) {
226
- unresolved.push(`$${key}$`);
227
- return `$${key}$`;
224
+ const replacement = scenario.localisation[key] ??
225
+ localisation.get(key) ??
226
+ rowValues?.[key] ??
227
+ scenario.values[key] ??
228
+ scenario.variables[key];
229
+ if (replacement === undefined || stack.has(key) || depth >= 16) {
230
+ unresolved.push(match[0]);
231
+ return match[0];
228
232
  }
229
- return replacement;
233
+ return expandTokens(String(replacement), new Set([...stack, key]), depth + 1);
230
234
  }, 'GUI localisation substitution');
235
+ text = expandTokens(text, new Set([value]), 0);
231
236
  text = replaceTextBounded(text, /\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu, (match) => {
232
237
  const key = match[1] ?? '';
233
238
  const shortKey = key.slice(key.lastIndexOf('.') + 1);
@@ -624,11 +629,12 @@ function explicitElementVisibility(definition, scenario, rowValues) {
624
629
  visibilityValue(scripted));
625
630
  }
626
631
  /** Source visibility: `visible` and `show` mean shown when true, `hide` and `hidden` when false. */
627
- function declaredVisibility(attributes) {
632
+ function declaredVisibility(attributes, fallback = true) {
628
633
  const visible = scalarBoolean(property(attributes, 'visible', 'show'));
629
634
  if (visible !== undefined)
630
635
  return visible;
631
- return !(scalarBoolean(property(attributes, 'hide', 'hidden')) ?? false);
636
+ const hidden = scalarBoolean(property(attributes, 'hide', 'hidden'));
637
+ return hidden === undefined ? fallback : !hidden;
632
638
  }
633
639
  function clippedByChain(chain) {
634
640
  let result;
@@ -689,7 +695,7 @@ function refreshSceneClips(context) {
689
695
  }
690
696
  }
691
697
  function attachedScrollbars(definition, context) {
692
- if (!/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType))
698
+ if (!/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType))
693
699
  return [];
694
700
  const result = [];
695
701
  const add = (candidate, horizontal, reference) => {
@@ -713,8 +719,10 @@ function attachedScrollbars(definition, context) {
713
719
  for (const horizontal of [false, true]) {
714
720
  const name = scalarString(property(definition.attributes, horizontal ? 'horizontalScrollbar' : 'verticalScrollbar'));
715
721
  if (name !== undefined) {
716
- const candidate = context.elementsByName.get(name);
717
- add(candidate?.parentId === undefined ? candidate : undefined, horizontal, name);
722
+ const candidate = context.elementsByNameCandidates
723
+ .get(name)
724
+ ?.find((element) => element.parentId === undefined);
725
+ add(candidate, horizontal, name);
718
726
  }
719
727
  }
720
728
  for (const id of definition.childIds) {
@@ -1213,10 +1221,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1213
1221
  const clipChain = inheritedClipChain ?? (inheritedClip === undefined ? [] : [inheritedClip]);
1214
1222
  const localScale = scalarNumber(property(definition.attributes, 'scale'), 1) ?? 1;
1215
1223
  const scale = parentScale * localScale;
1216
- const shownPosition = depth === 0 ? objectProperty(property(definition.attributes, 'show_position')) : undefined;
1224
+ const expandedWindow = definition.elementType.toLowerCase() === 'expandedwindow';
1225
+ const shownPosition = depth === 0 || expandedWindow
1226
+ ? objectProperty(property(definition.attributes, 'show_position'))
1227
+ : undefined;
1217
1228
  const position = shownPosition ?? objectProperty(property(definition.attributes, 'position'));
1218
1229
  if (shownPosition !== undefined) {
1219
- addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested root window uses its declared settled show position.', definition);
1230
+ addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested window uses its declared settled show position.', definition);
1220
1231
  }
1221
1232
  const size = objectProperty(property(definition.attributes, 'size'));
1222
1233
  const scriptedX = rowValues?.[`${definition.name}.x`] ??
@@ -1272,13 +1283,19 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1272
1283
  width = Math.max(0, parentRect.width - localX + width);
1273
1284
  if (height < 0)
1274
1285
  height = Math.max(0, parentRect.height - localY + height);
1275
- const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues);
1286
+ const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues) ??
1287
+ context.nativeVisibility.get(definition.id);
1276
1288
  const unresolvedVisibilityReason = context.unresolvedElementVisibility.get(definition.name);
1277
1289
  const parentSceneElement = parentInstanceId === undefined ? undefined : context.instancesById.get(parentInstanceId);
1278
1290
  const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
1279
1291
  ? parentSceneElement.visibilityStatusReason
1280
1292
  : undefined;
1281
- const sourceVisible = declaredVisibility(definition.attributes);
1293
+ const dropdown = definition.parentId === undefined ? undefined : context.elementsById.get(definition.parentId);
1294
+ const sourceVisible = declaredVisibility(definition.attributes, !expandedWindow ||
1295
+ depth === 0 ||
1296
+ visibilityValue(rowValues?.[`${dropdown?.name}.expanded`] ?? scenario.values[`${dropdown?.name}.expanded`]) === true);
1297
+ if (expandedWindow)
1298
+ addFidelity(fidelity, 'modelled', 'native_dropdown', 'Dropdown expansion is closed by default; bind <dropdown>.expanded or explicit panel visibility to show its settled position.', definition);
1282
1299
  const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
1283
1300
  const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
1284
1301
  scenario.values[`${definition.name}.enabled`] ??
@@ -1614,12 +1631,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1614
1631
  else if (origoAxes.vertical === 'bottom')
1615
1632
  y -= height;
1616
1633
  const unclippedRect = { x, y, width: Math.max(0, width), height: Math.max(0, height) };
1617
- const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
1618
- (backgrounds.length > 0 && /(?:containerwindow|windowtype)/iu.test(definition.elementType));
1619
1634
  let scrollbars = attachedScrollbars(definition, context);
1620
- if (scrollbars.length > 0 && (!ownClipping || backgrounds.length === 0)) {
1621
- diagnostics.push(diagnostic('GUI_SCROLLBAR_CONTAINER_BOUNDARY_MISSING', 'warning', `Container ${definition.name} needs a background and clipping for attached scrolling.`, definition));
1622
- addFidelity(fidelity, 'unsupported', 'attached_scrollbar', 'The container has no active clipped background boundary.', definition);
1635
+ const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
1636
+ ((backgrounds.length > 0 || scrollbars.length > 0) &&
1637
+ /(?:containerwindow|windowtype|expandedwindow)/iu.test(definition.elementType));
1638
+ if (scrollbars.length > 0 && (!ownClipping || width <= 0 || height <= 0)) {
1639
+ diagnostics.push(diagnostic('GUI_SCROLLBAR_CONTAINER_BOUNDARY_MISSING', 'warning', `Container ${definition.name} needs a positive-sized clipping boundary for attached scrolling.`, definition));
1640
+ addFidelity(fidelity, 'unsupported', 'attached_scrollbar', 'The container has no active clipping boundary.', definition);
1623
1641
  scrollbars = [];
1624
1642
  }
1625
1643
  const margin = scrollbars.length === 0 ? undefined : objectProperty(property(definition.attributes, 'margin'));
@@ -1830,10 +1848,12 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1830
1848
  const childDefinitions = await scrollbarChildren(definition, definition.childIds
1831
1849
  .map((id) => context.elementsById.get(id))
1832
1850
  .filter((element) => element !== undefined)
1833
- .filter((element) => !/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType) ||
1851
+ .filter((element) => !/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType) ||
1834
1852
  element.elementType.toLowerCase() !== 'extendedscrollbartype')
1835
- .sort((left, right) => (scalarNumber(property(left.attributes, 'priority'), 0) ?? 0) -
1836
- (scalarNumber(property(right.attributes, 'priority'), 0) ?? 0) ||
1853
+ .sort((left, right) => Number(context.nativeForeground.has(left.id)) -
1854
+ Number(context.nativeForeground.has(right.id)) ||
1855
+ (scalarNumber(property(left.attributes, 'priority'), 0) ?? 0) -
1856
+ (scalarNumber(property(right.attributes, 'priority'), 0) ?? 0) ||
1837
1857
  left.definitionOrder - right.definitionOrder), unclippedRect, scale, context, rowValues);
1838
1858
  const rowList = rowValues?.[`${definition.name}.list`];
1839
1859
  let rows = (typeof rowList === 'string' ? scenario.lists[rowList] : undefined) ??
@@ -1903,7 +1923,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1903
1923
  ...(slotWidth === undefined ? {} : { width: slotWidth }),
1904
1924
  ...(slotHeight === undefined ? {} : { height: slotHeight }),
1905
1925
  };
1906
- await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, origo, resolveVisibleAppearance, childClipChain);
1926
+ await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
1907
1927
  const rendered = context.instancesById.get(`${child.id}${rowSuffix(index)}`);
1908
1928
  rowHeight = Math.max(rowHeight, rendered?.unclippedRect.height ?? 0);
1909
1929
  }
@@ -1955,7 +1975,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1955
1975
  ...(slotWidth === undefined ? {} : { width: slotWidth }),
1956
1976
  ...(slotHeight === undefined ? {} : { height: slotHeight }),
1957
1977
  };
1958
- await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, origo, resolveVisibleAppearance, childClipChain);
1978
+ await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
1959
1979
  const rendered = context.instancesById.get(`${template.id}${rowSuffix(index)}`);
1960
1980
  rowY += Math.max(slotHeight ?? 0, rendered?.unclippedRect.height ?? 0) + spacingY;
1961
1981
  renderedRows += 1;
@@ -1965,7 +1985,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
1965
1985
  else {
1966
1986
  for (const child of childDefinitions) {
1967
1987
  context.work.spend('nested child expansion');
1968
- await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, origo, resolveVisibleAppearance, childClipChain);
1988
+ await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
1969
1989
  }
1970
1990
  }
1971
1991
  const childOutput = context.output.slice(firstChildOutput);
@@ -2026,7 +2046,9 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
2026
2046
  const localisation = new Map(graph.localisation
2027
2047
  .filter((entry) => entry.language.toLowerCase() === scenario.language.toLowerCase() ||
2028
2048
  entry.language.toLowerCase() === `l_${scenario.language.toLowerCase()}`)
2029
- .map((entry) => [entry.key, entry.value]));
2049
+ // Graph localisation is ordered with the active (highest-priority) definition first.
2050
+ .map((entry) => [entry.key, entry.value])
2051
+ .reverse());
2030
2052
  if (localisation.size === 0) {
2031
2053
  for (const entry of graph.localisation)
2032
2054
  if (!localisation.has(entry.key))
@@ -2058,7 +2080,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
2058
2080
  const countryFlagProperties = new Map();
2059
2081
  const conditionDefinitions = relevantScriptedGuiNames.size === 0
2060
2082
  ? undefined
2061
- : ClausewitzEvaluationDefinitions.build({ files: [...scannedFiles] });
2083
+ : ClausewitzEvaluationDefinitions.build({ files: scannedFiles });
2062
2084
  const conditionScenario = guiConditionScenario(scenario);
2063
2085
  for (const scripted of graph.scriptedGuis.filter(({ name }) => relevantScriptedGuiNames.has(name))) {
2064
2086
  const source = scannedFiles.find(({ displayPath }) => displayPath === scripted.sourcePath);
@@ -2188,10 +2210,14 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
2188
2210
  if (native !== undefined) {
2189
2211
  for (const note of native.notes)
2190
2212
  addFidelity(fidelity, 'modelled', 'native_window_state', note);
2191
- const { sources } = native.state;
2192
- if (native.state.tag === undefined ||
2193
- sources.faction === 'default' ||
2194
- sources.balanceOfPower === 'default')
2213
+ for (const note of native.approximations ?? [])
2214
+ addFidelity(fidelity, 'approximated', 'native_window_state', note);
2215
+ const sources = native.state?.sources;
2216
+ if (native.state !== undefined &&
2217
+ sources !== undefined &&
2218
+ (native.state.tag === undefined ||
2219
+ sources.faction === 'default' ||
2220
+ sources.balanceOfPower === 'default'))
2195
2221
  addFidelity(fidelity, 'approximated', 'native_window_state', native.state.tag === undefined
2196
2222
  ? 'The scenario names no country, so the window shows the state of a country in no faction and without a balance of power; set country.tag or the country faction, balanceOfPower and ideology facts.'
2197
2223
  : `No history for ${native.state.tag} was available, so its faction and balance of power default to none; set country.faction, country.balanceOfPower and country.ideology to override.`);
@@ -2204,7 +2230,6 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
2204
2230
  ...scenario.values,
2205
2231
  },
2206
2232
  visibility: {
2207
- ...(native?.visibility ?? {}),
2208
2233
  ...scriptedWindowVisibility,
2209
2234
  ...constantElementVisibility,
2210
2235
  ...scenario.visibility,
@@ -2222,6 +2247,24 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
2222
2247
  elementsByNameCandidates.set(element.name, candidates);
2223
2248
  }
2224
2249
  const context = {
2250
+ nativeForeground: new Set(graph.elements
2251
+ .filter((element) => element.parentId === root.id && native?.foreground?.includes(element.name))
2252
+ .map(({ id }) => id)),
2253
+ nativeVisibility: new Map(graph.elements.flatMap((element) => {
2254
+ const visible = native?.visibility[element.name];
2255
+ if (visible === undefined)
2256
+ return [];
2257
+ // The designer reuses names (including division_title) inside selector panels.
2258
+ // Its branch decisions apply to root controls, not identically named descendants.
2259
+ if (windowName === 'countrydivisiondesignerview' && element.parentId !== root.id) {
2260
+ const parent = element.parentId === undefined ? undefined : elementsById.get(element.parentId);
2261
+ if (element.name !== 'hq_badge' ||
2262
+ parent?.name !== 'template_emblem_slot' ||
2263
+ parent.parentId !== root.id)
2264
+ return [];
2265
+ }
2266
+ return [[element.id, visible]];
2267
+ })),
2225
2268
  graph,
2226
2269
  scenario: layoutScenario,
2227
2270
  catalog,
@@ -205,13 +205,52 @@ export function resolveNativeCountryState(scenario, files) {
205
205
  }
206
206
  /** Whether any native decision can apply to this window, so the country state is worth resolving. */
207
207
  export function windowHasNativeState(windowName, elementNames) {
208
- return (NATIVE_BRANCHES[windowName] !== undefined ||
208
+ return (windowName === 'countrydivisiondesignerview' ||
209
+ NATIVE_BRANCHES[windowName] !== undefined ||
209
210
  Object.keys(IDEOLOGY_FRAME_ICONS).some((name) => elementNames.has(name)));
210
211
  }
211
212
  /** The engine-selected branches and frames for a scene, below any explicit scenario input. */
212
213
  export function nativeWindowState(windowName, elementNames, scenario, files) {
213
214
  if (!windowHasNativeState(windowName, elementNames))
214
215
  return undefined;
216
+ if (windowName === 'countrydivisiondesignerview') {
217
+ const hq = scenario.values['countrydivisiondesignerview.hq'] === true;
218
+ const panel = scenario.values['countrydivisiondesignerview.panel'];
219
+ const visibility = {};
220
+ for (const name of ['hq_overlay_images', 'subunits_hq', 'hq_view', 'hq_title', 'hq_badge'])
221
+ if (elementNames.has(name))
222
+ visibility[name] = hq;
223
+ for (const name of [
224
+ 'subunits',
225
+ 'non_hq_view',
226
+ 'division_title',
227
+ 'division_composition_label',
228
+ 'combat_width_view',
229
+ ])
230
+ if (elementNames.has(name))
231
+ visibility[name] = !hq;
232
+ const selectors = [
233
+ 'designerregimentswindow',
234
+ 'division_equipment_view',
235
+ 'div_templ_custom_icon_view',
236
+ 'div_template_select_model',
237
+ 'niche_icon_selector',
238
+ ];
239
+ for (const name of selectors)
240
+ if (elementNames.has(name))
241
+ visibility[name] = panel === name;
242
+ return {
243
+ visibility,
244
+ values: {},
245
+ foreground: selectors,
246
+ notes: [
247
+ `Division designer uses the ${hq ? 'HQ' : 'division'} branch and only the explicitly selected native selector panel.`,
248
+ ],
249
+ approximations: [
250
+ 'Native division state is a preview convention: defaults to a division with selectors closed. Set values countrydivisiondesignerview.hq and countrydivisiondesignerview.panel, or explicit element visibility. Engine-populated rows and statistics require scenario bindings.',
251
+ ],
252
+ };
253
+ }
215
254
  const state = resolveNativeCountryState(scenario, files);
216
255
  const visibility = {};
217
256
  const values = {};
@@ -3,7 +3,7 @@ import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.j
3
3
  import { evaluateGuiCondition, guiConditionScenario, guiConditionVariables, guiExplicitConditionValues, } from './scenario-conditions.js';
4
4
  import { parsePreviewScenario } from './scenario.js';
5
5
  const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
6
- const countryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
6
+ export const generatedCountryTags = ['GER', 'FRA', 'POL', 'ITA', 'BRA', 'RAJ', 'CAN'];
7
7
  const countryIdeologies = [
8
8
  'fascism',
9
9
  'democratic',
@@ -435,7 +435,7 @@ function generatedRows(count, random, options) {
435
435
  return Array.from({ length: count }, (_unused, index) => {
436
436
  const countryIndex = (countryOffset + index) % countryNames.length;
437
437
  const name = countryNames[countryIndex];
438
- const tag = countryTags[countryIndex];
438
+ const tag = generatedCountryTags[countryIndex];
439
439
  const ideology = countryIdeologies[countryIndex];
440
440
  const value = generatedNumber(options, random);
441
441
  return {
@@ -587,7 +587,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options, de
587
587
  const elementStates = { ...base.elementStates };
588
588
  const scopedCountries = new Map();
589
589
  const generatedCountryIndex = random.integer(0, countryNames.length - 1);
590
- const generatedCountryTag = countryTags[generatedCountryIndex];
590
+ const generatedCountryTag = generatedCountryTags[generatedCountryIndex];
591
591
  for (const key of tokens.numeric)
592
592
  setIfMissing(values, key, generatedNumberForKey(key, options, random, numericConstraints.ranges.get(key)));
593
593
  for (const [elementName, { minimum, maximum }] of progressRanges) {