hoi4-agent-tools 2.8.4 → 2.8.6

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 (37) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +25 -25
  3. package/dist/hoi4_agent_tools/core/artifacts.js +14 -6
  4. package/dist/hoi4_agent_tools/gui/assets.js +9 -1
  5. package/dist/hoi4_agent_tools/gui/index.js +1 -0
  6. package/dist/hoi4_agent_tools/gui/layout.js +30 -18
  7. package/dist/hoi4_agent_tools/gui/renderer.js +23 -15
  8. package/dist/hoi4_agent_tools/gui/scenario-generator.js +556 -0
  9. package/dist/hoi4_agent_tools/gui/scenario.js +44 -0
  10. package/dist/hoi4_agent_tools/gui/source-graph.js +84 -9
  11. package/dist/hoi4_agent_tools/gui/studio.js +94 -16
  12. package/dist/hoi4_agent_tools/gui/validators.js +6 -1
  13. package/dist/hoi4_agent_tools/mcp/tools/gui.js +23 -6
  14. package/dist/hoi4_agent_tools/version.js +1 -1
  15. package/docs/gui.md +22 -3
  16. package/package.json +1 -1
  17. package/schemas/configuration.schema.json +1 -1
  18. package/schemas/continuous-focus-palette.schema.json +1 -1
  19. package/schemas/custom-weighted-pool.schema.json +1 -1
  20. package/schemas/event-feature-manifest.schema.json +1 -1
  21. package/schemas/focus-plan.schema.json +1 -1
  22. package/schemas/focus-planning-sidecar.schema.json +1 -1
  23. package/schemas/gui-animation-source.schema.json +1 -1
  24. package/schemas/gui-helper.schema.json +1 -1
  25. package/schemas/gui-scenario.schema.json +1 -1
  26. package/schemas/map-operation.schema.json +1 -1
  27. package/schemas/operation-result.schema.json +1 -1
  28. package/schemas/probability-analysis-result.schema.json +1 -1
  29. package/schemas/probability-compare-input.schema.json +1 -1
  30. package/schemas/probability-evaluate-input.schema.json +1 -1
  31. package/schemas/probability-inspect-input.schema.json +1 -1
  32. package/schemas/probability-render-input.schema.json +1 -1
  33. package/schemas/probability-scenario-set.schema.json +1 -1
  34. package/schemas/probability-sequence-input.schema.json +1 -1
  35. package/schemas/probability-simulate-input.schema.json +1 -1
  36. package/schemas/probability-sweep-input.schema.json +1 -1
  37. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@
4
4
 
5
5
  No unreleased changes.
6
6
 
7
+ ## 2.8.6 - 2026-08-28
8
+
9
+ - Generate source-aware scripted-GUI scenarios from complete parent/child surfaces, real scripted-localisation candidates, simple threshold conditions, coherent tab states, scoped values, meter ranges, and dynamic-list rows while keeping explicit scenario values authoritative.
10
+ - Preserve native HOI4 bitmap-font antialiasing and shadow pixels when applying face and localisation colours, including global and per-font `textcolors` palettes.
11
+ - Prevent stale workspace artifacts and irrelevant language font atlases from blocking GUI calls, resolve alternate vanilla raster extensions and repeated path separators, and keep placeholder scenarios opt-in so their intentionally unresolved state does not pollute normal validation.
12
+
13
+ ## 2.8.5 - 2026-08-28
14
+
15
+ - Render bitmap-font face and localisation colours through alpha masks, including black source atlases, and load exact per-font HOI4 `textcolors` palettes.
16
+ - Generate configurable seeded GUI scenarios with plausible dynamic text, numeric values, meters, lists, visibility, control states, and animation frames while retaining explicit values and the unresolved placeholder view.
17
+
7
18
  ## 2.8.4 - 2026-08-28
8
19
 
9
20
  - Render each declared HOI4 font face with its own glyph atlas, native metrics, face colour, border colour, and inherited language-override styling.
package/README.md CHANGED
@@ -31,31 +31,31 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
31
31
 
32
32
  ## Tools
33
33
 
34
- | Tool | Purpose |
35
- | --------------------------- | ---------------------------------------------------------------------------------------- |
36
- | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
37
- | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
38
- | `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
39
- | `hoi4.focus_rewrite` | Create or update a focus tree. |
40
- | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
41
- | `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
42
- | `hoi4.gui_rewrite` | Create or update a GUI source package. |
43
- | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
44
- | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
45
- | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
46
- | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
47
- | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
48
- | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
49
- | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
50
- | `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
51
- | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
52
- | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
53
- | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
54
- | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
55
- | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
56
- | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
57
- | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
58
- | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
34
+ | Tool | Purpose |
35
+ | --------------------------- | ----------------------------------------------------------------------------------------- |
36
+ | `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
37
+ | `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
38
+ | `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
39
+ | `hoi4.focus_rewrite` | Create or update a focus tree. |
40
+ | `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
41
+ | `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
42
+ | `hoi4.gui_rewrite` | Create or update a GUI source package. |
43
+ | `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
44
+ | `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
45
+ | `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
46
+ | `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
47
+ | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
48
+ | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
49
+ | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
50
+ | `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
51
+ | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
52
+ | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
53
+ | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
54
+ | `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
55
+ | `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
56
+ | `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
57
+ | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
58
+ | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
59
59
 
60
60
  Large outputs are linked `hoi4-agent://` resources. For resources over 1 MiB, follow the `continuationUri` returned in `_meta` until it is `null`; clients may also request byte ranges with `?offset=<bytes>&length=<bytes>`.
61
61
 
@@ -131,9 +131,14 @@ export function boundedSourceHashEvidence(sourceHashes) {
131
131
  };
132
132
  }
133
133
  function assertManifestIntegrity(manifest, expected, workspace) {
134
+ assertManifestAddressIntegrity(manifest, expected);
134
135
  if (manifest.workspaceIdentity !== workspace.workspaceIdentity ||
135
- manifest.ownerIdentity !== workspace.ownerIdentity ||
136
- manifest.sha256 !== expected.sha256 ||
136
+ manifest.ownerIdentity !== workspace.ownerIdentity) {
137
+ throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest does not match its immutable address');
138
+ }
139
+ }
140
+ function assertManifestAddressIntegrity(manifest, expected) {
141
+ if (manifest.sha256 !== expected.sha256 ||
137
142
  manifest.provenanceHash !== expected.provenanceHash ||
138
143
  manifest.name !== expected.name ||
139
144
  provenanceHashFor(manifest) !== expected.provenanceHash) {
@@ -712,11 +717,11 @@ export class ArtifactStore {
712
717
  if (child.name !== `${manifest.name}.${manifest.provenanceHash}.manifest.json`) {
713
718
  throw new ServiceError('ARTIFACT_MANIFEST_INTEGRITY_FAILED', 'Artifact provenance manifest filename does not match its contents');
714
719
  }
715
- assertManifestIntegrity(manifest, {
720
+ assertManifestAddressIntegrity(manifest, {
716
721
  sha256: path.basename(directory),
717
722
  provenanceHash: manifest.provenanceHash,
718
723
  name: manifest.name,
719
- }, workspace);
724
+ });
720
725
  const targetPath = await containedGeneratedPath(workspace.artifactRoot, path.relative(workspace.artifactRoot, path.join(directory, manifest.name)));
721
726
  let targetBytes = 0;
722
727
  try {
@@ -728,6 +733,8 @@ export class ArtifactStore {
728
733
  }
729
734
  retained.push({
730
735
  manifest,
736
+ staleIdentity: manifest.workspaceIdentity !== workspace.workspaceIdentity ||
737
+ manifest.ownerIdentity !== workspace.ownerIdentity,
731
738
  manifestPath: candidate,
732
739
  manifestBytes: metadata.size,
733
740
  modifiedAt: metadata.mtimeMs,
@@ -789,7 +796,8 @@ export class ArtifactStore {
789
796
  }
790
797
  };
791
798
  await reclaimDebris(await containedGeneratedPath(workspace.artifactRoot));
792
- retained.sort((left, right) => left.modifiedAt - right.modifiedAt ||
799
+ retained.sort((left, right) => Number(right.staleIdentity) - Number(left.staleIdentity) ||
800
+ left.modifiedAt - right.modifiedAt ||
793
801
  compareCodeUnits(left.manifestPath, right.manifestPath));
794
802
  const current = { ...usage };
795
803
  for (const candidate of retained) {
@@ -822,7 +830,7 @@ export class ArtifactStore {
822
830
  if (error.code !== 'ENOENT')
823
831
  throw error;
824
832
  }
825
- const verificationPrefix = `${workspace.workspaceIdentity}\0${workspace.ownerIdentity}\0${candidate.targetPath}\0`;
833
+ const verificationPrefix = `${candidate.manifest.workspaceIdentity}\0${candidate.manifest.ownerIdentity}\0${candidate.targetPath}\0`;
826
834
  for (const key of this.#verifiedContent.keys()) {
827
835
  if (key.startsWith(verificationPrefix))
828
836
  this.#verifiedContent.delete(key);
@@ -202,6 +202,7 @@ export class GuiAssetCatalog {
202
202
  ...(definition.borderColour === undefined && base.borderColour !== undefined
203
203
  ? { borderColour: base.borderColour }
204
204
  : {}),
205
+ textColours: { ...base.textColours, ...definition.textColours },
205
206
  });
206
207
  }
207
208
  }
@@ -582,11 +583,18 @@ export class GuiAssetCatalog {
582
583
  return undefined;
583
584
  this.budget.reserveRasterOperation(`gui-bmfont-glyph:${key}`, `GUI BMFont glyph rasterization U+${codePoint.toString(16).toUpperCase()}`);
584
585
  this.budget.reserve(metric.width, metric.height, `GUI BMFont glyph U+${codePoint.toString(16).toUpperCase()}`, { maximumPixels: RENDER_MAX_DECODED_PIXELS });
585
- const png = await sharp(raster.data, {
586
+ const extracted = await sharp(raster.data, {
586
587
  raw: { width: raster.width, height: raster.height, channels: 4 },
587
588
  limitInputPixels: RENDER_MAX_DECODED_PIXELS,
588
589
  })
589
590
  .extract({ left: metric.x, top: metric.y, width: metric.width, height: metric.height })
591
+ .ensureAlpha()
592
+ .raw()
593
+ .toBuffer({ resolveWithObject: true });
594
+ const png = await sharp(extracted.data, {
595
+ raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
596
+ limitInputPixels: RENDER_MAX_DECODED_PIXELS,
597
+ })
590
598
  .png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
591
599
  .toBuffer();
592
600
  return {
@@ -6,6 +6,7 @@ export * from './layout.js';
6
6
  export * from './limits.js';
7
7
  export * from './renderer.js';
8
8
  export * from './scenario.js';
9
+ export * from './scenario-generator.js';
9
10
  export * from './source-graph.js';
10
11
  export * from './source-patch.js';
11
12
  export * from './studio.js';
@@ -26,23 +26,32 @@ function scalarString(value) {
26
26
  }
27
27
  const defaultTextColour = '#f5f2e8';
28
28
  const hoi4TextColours = {
29
- B: '#4aa3df',
30
- C: '#55d6d6',
31
- G: '#56b870',
32
- H: '#e7b454',
33
- L: '#9aa7b3',
34
- M: '#d16bd4',
35
- O: '#e58b3c',
36
- R: '#e05a5a',
37
- T: '#4bc6b9',
38
- W: defaultTextColour,
39
- Y: '#f1c75b',
40
- b: '#4aa3df',
41
- g: '#87939e',
29
+ C: '#23ceff',
30
+ L: '#c3b091',
31
+ W: '#ffffff',
32
+ B: '#0000ff',
33
+ G: '#009f03',
34
+ R: '#ff3232',
35
+ b: '#000000',
36
+ g: '#b0b0b0',
37
+ Y: '#ffbd00',
38
+ H: '#ffbd00',
39
+ T: '#ffffff',
40
+ O: '#ff7019',
41
+ '0': '#cb00cb',
42
+ '1': '#8078d3',
43
+ '2': '#5170f3',
44
+ '3': '#518fdc',
45
+ '4': '#5abee7',
46
+ '5': '#3fb5c2',
47
+ '6': '#77ccba',
48
+ '7': '#99d199',
49
+ '8': '#d1d175',
50
+ '9': '#d1a675',
42
51
  };
43
52
  const inlineIconMarkerStart = 0xe000;
44
53
  const inlineIconMarkerEnd = 0xf8ff;
45
- function visibleHoiText(value) {
54
+ function visibleHoiText(value, fontTextColours = {}) {
46
55
  const characters = [];
47
56
  const colours = [];
48
57
  const inlineIcons = [];
@@ -53,7 +62,7 @@ function visibleHoiText(value) {
53
62
  const next = value[index + 1];
54
63
  if (character === '\u00a7' && next !== undefined && /^[A-Za-z0-9!]$/u.test(next)) {
55
64
  hasColourMarkup = true;
56
- colour = next === '!' ? undefined : hoi4TextColours[next];
65
+ colour = next === '!' ? undefined : (fontTextColours[next] ?? hoi4TextColours[next]);
57
66
  index += 1;
58
67
  continue;
59
68
  }
@@ -739,11 +748,14 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
739
748
  displayText = `${displayText} — ${displayText} — ${displayText}`;
740
749
  if (state === 'missing-localisation')
741
750
  displayText = `\u00a7R${rawText}_MISSING\u00a7!`;
742
- const visibleText = visibleHoiText(displayText);
743
- displayText = visibleText.text;
744
- context.work.admitText(displayText, `GUI text for ${definition.name}`);
745
751
  const fontName = scalarString(property(definition.attributes, 'font', 'buttonFont'));
746
752
  const fontDefinition = fontName === undefined ? undefined : catalog.fontDefinition(fontName);
753
+ const visibleText = visibleHoiText(displayText, {
754
+ ...context.graph.textColours,
755
+ ...fontDefinition?.textColours,
756
+ });
757
+ displayText = visibleText.text;
758
+ context.work.admitText(displayText, `GUI text for ${definition.name}`);
747
759
  const resolvedFontMetrics = catalog.resolvedFontMetrics(fontName);
748
760
  const explicitFontSize = scalarNumber(property(definition.attributes, 'fontSize'), 16);
749
761
  const fontSize = explicitFontSize ??
@@ -62,17 +62,19 @@ function sceneGlyphDefinitions(scene) {
62
62
  .map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none"/>`),
63
63
  ].join('');
64
64
  }
65
- function colourMatrix(colour) {
65
+ function bitmapTintMarkup(id, colour, glyphs, rect) {
66
66
  const match = /^#([0-9a-f]{6})([0-9a-f]{2})?$/iu.exec(colour);
67
- if (match === null)
68
- return '1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0';
69
- const rgb = match[1];
70
- const alpha = match[2] ?? 'ff';
71
- const component = (offset) => Number.parseInt(rgb.slice(offset, offset + 2), 16) / 255;
72
- return `${finite(component(0))} 0 0 0 0 0 ${finite(component(2))} 0 0 0 0 0 ${finite(component(4))} 0 0 0 0 0 ${finite(Number.parseInt(alpha, 16) / 255)} 0`;
73
- }
74
- function bitmapTintDefinition(id, colour) {
75
- return `<filter id="${id}" color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${colourMatrix(colour)}"/></filter>`;
67
+ const rgb = match?.[1] ?? 'ffffff';
68
+ const alpha = match?.[2] ?? 'ff';
69
+ const channels = [
70
+ Number.parseInt(rgb.slice(0, 2), 16) / 255,
71
+ Number.parseInt(rgb.slice(2, 4), 16) / 255,
72
+ Number.parseInt(rgb.slice(4, 6), 16) / 255,
73
+ Number.parseInt(alpha, 16) / 255,
74
+ ].map(finite);
75
+ const [red, green, blue, opacity] = channels;
76
+ const matrix = `${red} 0 0 0 0 0 ${green} 0 0 0 0 0 ${blue} 0 0 0 0 0 ${opacity} 0`;
77
+ return `<defs><filter id="${id}" filterUnits="userSpaceOnUse" ${rectAttributes(rect)} color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${matrix}"/></filter></defs><g filter="url(#${id})">${glyphs}</g>`;
76
78
  }
77
79
  function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale) {
78
80
  if (glyphLine.source === 'fontkit-path')
@@ -138,9 +140,14 @@ function renderText(element, toolText) {
138
140
  definitions.push(`<clipPath id="${clipId}"><rect x="${finite(runX - 0.25)}" y="${finite(lineTop)}" width="${finite(run.width * horizontalScale + 0.5)}" height="${finite(text.lineHeight)}"/></clipPath>`);
139
141
  const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
140
142
  if (glyphLine.source === 'bmfont-atlas') {
141
- const filterId = `gui-font-tint-${sha256Bytes(key).slice(0, 20)}`;
142
- definitions.push(bitmapTintDefinition(filterId, run.colour));
143
- return `<g clip-path="url(#${clipId})" filter="url(#${filterId})" data-font-colour="${run.colour}">${glyphs}</g>`;
143
+ const maskId = `gui-font-mask-${sha256Bytes(key).slice(0, 20)}`;
144
+ const runRect = {
145
+ x: runX - 0.25,
146
+ y: lineTop,
147
+ width: run.width * horizontalScale + 0.5,
148
+ height: text.lineHeight,
149
+ };
150
+ return `<g clip-path="url(#${clipId})" data-font-colour="${run.colour}">${bitmapTintMarkup(maskId, run.colour, glyphs, runRect)}</g>`;
144
151
  }
145
152
  return `<g clip-path="url(#${clipId})" fill="${run.colour}" stroke="${text.borderColour ?? '#12151a'}" data-font-colour="${run.colour}">${glyphs}</g>`;
146
153
  });
@@ -166,8 +173,9 @@ function renderText(element, toolText) {
166
173
  const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
167
174
  if (text.colour === undefined)
168
175
  return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphs}${inlineIconMarkup}</g>`;
169
- const filterId = `gui-font-tint-${sha256Bytes(`${element.id}:${index}:${text.colour}`).slice(0, 20)}`;
170
- return `<g data-font-sha256="${glyphLine.sourceHash}" data-font-colour="${text.colour}"><defs>${bitmapTintDefinition(filterId, text.colour)}</defs><g filter="url(#${filterId})">${glyphs}</g>${inlineIconMarkup}</g>`;
176
+ const maskId = `gui-font-mask-${sha256Bytes(`${element.id}:${index}:${text.colour}`).slice(0, 20)}`;
177
+ const lineRect = { x: originX, y: lineTop, width, height: text.lineHeight };
178
+ return `<g data-font-sha256="${glyphLine.sourceHash}" data-font-colour="${text.colour}">${bitmapTintMarkup(maskId, text.colour, glyphs, lineRect)}${inlineIconMarkup}</g>`;
171
179
  }
172
180
  const visibleLine = line.replace(/[\uE000-\uF8FF]/gu, ' ');
173
181
  return `<g>${toolText.render(visibleLine, {