hoi4-agent-tools 3.0.0 → 3.0.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 (47) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +3 -1
  3. package/dist/bin/stdio.js +4 -0
  4. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
  5. package/dist/hoi4_agent_tools/gui/assets.js +26 -14
  6. package/dist/hoi4_agent_tools/gui/renderer.js +4 -11
  7. package/dist/hoi4_agent_tools/gui/scenario.js +7 -1
  8. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +483 -0
  9. package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -1
  10. package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -5
  11. package/dist/hoi4_agent_tools/probability/service.js +48 -6
  12. package/dist/hoi4_agent_tools/technology/compare.js +6 -1
  13. package/dist/hoi4_agent_tools/technology/graph.js +67 -2
  14. package/dist/hoi4_agent_tools/technology/model.js +1 -1
  15. package/dist/hoi4_agent_tools/technology/queries.js +4 -0
  16. package/dist/hoi4_agent_tools/technology/render.js +73 -28
  17. package/dist/hoi4_agent_tools/technology/service.js +4 -1
  18. package/dist/hoi4_agent_tools/technology/source-analysis.js +102 -19
  19. package/dist/hoi4_agent_tools/version.js +1 -1
  20. package/docs/gui.md +2 -0
  21. package/docs/images/readme/event-chain-mcp.png +0 -0
  22. package/docs/images/readme/probability-mcp.png +0 -0
  23. package/docs/images/readme/technology-tree-mcp.png +0 -0
  24. package/docs/probability.md +3 -1
  25. package/docs/technology.md +1 -1
  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/event-feature-manifest.schema.json +1 -1
  31. package/schemas/focus-plan.schema.json +1 -1
  32. package/schemas/focus-planning-sidecar.schema.json +1 -1
  33. package/schemas/gui-animation-source.schema.json +1 -1
  34. package/schemas/gui-helper.schema.json +1 -1
  35. package/schemas/gui-scenario.schema.json +59 -55
  36. package/schemas/map-operation.schema.json +1 -1
  37. package/schemas/operation-result.schema.json +1 -1
  38. package/schemas/probability-analysis-result.schema.json +1 -1
  39. package/schemas/probability-compare-input.schema.json +1 -1
  40. package/schemas/probability-evaluate-input.schema.json +1 -1
  41. package/schemas/probability-inspect-input.schema.json +1 -1
  42. package/schemas/probability-render-input.schema.json +1 -1
  43. package/schemas/probability-scenario-set.schema.json +1 -1
  44. package/schemas/probability-sequence-input.schema.json +1 -1
  45. package/schemas/probability-simulate-input.schema.json +1 -1
  46. package/schemas/probability-sweep-input.schema.json +1 -1
  47. package/server.json +2 -2
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@
4
4
 
5
5
  No unreleased changes.
6
6
 
7
+ ## 3.0.2 - 2026-08-31
8
+
9
+ - Render enlarged HOI4 bitmap glyphs as pre-scaled atlas masks so VPS and Linux output stays crisp instead of depending on platform SVG interpolation.
10
+ - Apply bitmap-font face, border, and localisation colours with portable alpha masks instead of SVG colour-matrix filters that could be ignored by remote rasterizers.
11
+ - Clarify that GUI `uiScale` represents the in-game layout scale and is not an artifact zoom control.
12
+
13
+ ## 3.0.1 - 2026-08-31
14
+
15
+ - Render technology folders with the source GUI's actual small and regular item geometry, infer small items from equipment unlock behavior, and include horizontal or vertical year guides.
16
+ - Restore the opt-in `chaosx.focus_country_assets` and `chaosx.visual_revision` tools with direct and stdio discovery coverage.
17
+ - Infer unambiguous source-backed probability adapters for evaluate, sweep, simulation, and comparison calls, including calls that previously failed with `PROBABILITY_MANIFEST_REQUIRED` or `PROBABILITY_SURFACE_EMPTY` after a mismatched adapter selection.
18
+
7
19
  ## 3.0.0 - 2026-08-31
8
20
 
9
21
  - Promote the complete focus, GUI, map, event, technology, and probability MCP surface to the stable 3.0 release line after full synthetic, installed-game, external-workspace, transport, package, Inspector, and Registry qualification.
package/README.md CHANGED
@@ -72,7 +72,7 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
72
72
  | `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
73
73
  | `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
74
74
  | `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
75
- | `hoi4.tech_render` | Render source layouts, dependency paths, unlocks, grants, metadata, and asset coverage. |
75
+ | `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
76
76
  | `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
77
77
  | `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
78
78
  | `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
@@ -82,6 +82,8 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
82
82
  | `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
83
83
  | `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
84
84
 
85
+ Set `HOI4_AGENT_TOOLS_CHAOSX=1` on the server process to expose the optional `chaosx.focus_country_assets` and `chaosx.visual_revision` tools used by ChaosX workflows. They remain absent from the default public tool list.
86
+
85
87
  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>`.
86
88
 
87
89
  ## Coexistence with agent workflows
package/dist/bin/stdio.js CHANGED
@@ -8,6 +8,10 @@ async function main() {
8
8
  const engine = await createEngine();
9
9
  const context = {};
10
10
  const server = createMcpServer(engine, context);
11
+ if (process.env.HOI4_AGENT_TOOLS_CHAOSX === '1') {
12
+ const { registerChaosxTools } = await import('../hoi4_agent_tools/mcp/tools/chaosx.js');
13
+ registerChaosxTools(server, engine, context);
14
+ }
11
15
  server.server.onerror = (error) => {
12
16
  const fatalInputError = error instanceof StdioFrameLimitError || error instanceof StdioInvalidUtf8Error;
13
17
  const rejectedMessage = error instanceof StdioInvalidMessageError;
@@ -58,6 +58,18 @@ export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
58
58
  'events/**/*.txt',
59
59
  ]);
60
60
  }
61
+ /** Text sources needed to discover country flags and leader portraits. */
62
+ export function countryAssetDiscoveryPatterns(workspace) {
63
+ const roots = workspace.registration.roots;
64
+ return uniquePatterns([
65
+ 'history/countries/**/*.txt',
66
+ 'common/characters/**/*.txt',
67
+ 'common/scripted_effects/**/*.txt',
68
+ 'events/**/*.txt',
69
+ ...under(roots.interface, '**/*.gfx'),
70
+ ...under(roots.gfx, '**/*.gfx'),
71
+ ]);
72
+ }
61
73
  export function exactChangedFilePatterns(relativePaths) {
62
74
  return uniquePatterns(relativePaths.map((relativePath) => relativePath.replaceAll('\\', '/').replace(/^\.\//u, '')));
63
75
  }
@@ -664,7 +664,7 @@ export class GuiAssetCatalog {
664
664
  const pageFile = this.resolveBmFontPage(entry, pagePath ?? '', metric.page);
665
665
  const raster = pageFile === undefined
666
666
  ? undefined
667
- : await this.loadBmFontGlyph(entry, pageFile, metric, codePoint);
667
+ : await this.loadBmFontGlyph(entry, pageFile, metric, codePoint, scale);
668
668
  if (raster !== undefined && pageFile !== undefined) {
669
669
  pageHashes.add(pageFile.sha256);
670
670
  glyphs.push({
@@ -674,8 +674,8 @@ export class GuiAssetCatalog {
674
674
  ...(raster.borderDataUri === undefined ? {} : { borderDataUri: raster.borderDataUri }),
675
675
  x: penX + metric.xOffset * scale,
676
676
  y: metric.yOffset * scale,
677
- width: raster.width * scale,
678
- height: raster.height * scale,
677
+ width: raster.width * (scale > 1 ? 1 : scale),
678
+ height: raster.height * (scale > 1 ? 1 : scale),
679
679
  });
680
680
  }
681
681
  else if (metric.width > 0 &&
@@ -782,10 +782,13 @@ export class GuiAssetCatalog {
782
782
  this.metrics.set(key, null);
783
783
  return undefined;
784
784
  }
785
- loadBmFontGlyph(entry, pageFile, metric, codePoint) {
785
+ loadBmFontGlyph(entry, pageFile, metric, codePoint, scale) {
786
786
  if (metric.width <= 0 || metric.height <= 0)
787
787
  return Promise.resolve(undefined);
788
- const key = `${entry.sourceFile.sha256}:${pageFile.sha256}:${codePoint}:${metric.x}:${metric.y}:${metric.width}:${metric.height}`;
788
+ const rasterScale = scale > 1 ? scale : 1;
789
+ const targetWidth = Math.max(1, Math.round(metric.width * rasterScale));
790
+ const targetHeight = Math.max(1, Math.round(metric.height * rasterScale));
791
+ const key = `${entry.sourceFile.sha256}:${pageFile.sha256}:${codePoint}:${metric.x}:${metric.y}:${metric.width}:${metric.height}:${targetWidth}:${targetHeight}`;
789
792
  let promise = this.glyphRasters.get(key);
790
793
  if (promise === undefined) {
791
794
  promise = (async () => {
@@ -799,7 +802,7 @@ export class GuiAssetCatalog {
799
802
  metric.y + metric.height > raster.height)
800
803
  return undefined;
801
804
  this.budget.reserveRasterOperation(`gui-bmfont-glyph:${key}`, `GUI BMFont glyph rasterization U+${codePoint.toString(16).toUpperCase()}`);
802
- this.budget.reserve(metric.width, metric.height, `GUI BMFont glyph U+${codePoint.toString(16).toUpperCase()}`, { maximumPixels: RENDER_MAX_DECODED_PIXELS });
805
+ this.budget.reserve(targetWidth, targetHeight, `GUI BMFont glyph U+${codePoint.toString(16).toUpperCase()}`, { maximumPixels: RENDER_MAX_DECODED_PIXELS });
803
806
  const extracted = await sharp(raster.data, {
804
807
  raw: { width: raster.width, height: raster.height, channels: 4 },
805
808
  limitInputPixels: RENDER_MAX_DECODED_PIXELS,
@@ -919,12 +922,21 @@ export class GuiAssetCatalog {
919
922
  }
920
923
  }
921
924
  }
922
- const encodeMask = (pixels) => sharp(pixels, {
923
- raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
924
- limitInputPixels: RENDER_MAX_DECODED_PIXELS,
925
- })
926
- .png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
927
- .toBuffer();
925
+ const encodeMask = (pixels) => {
926
+ let pipeline = sharp(pixels, {
927
+ raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
928
+ limitInputPixels: RENDER_MAX_DECODED_PIXELS,
929
+ });
930
+ if (targetWidth !== extracted.info.width || targetHeight !== extracted.info.height) {
931
+ pipeline = pipeline.resize(targetWidth, targetHeight, {
932
+ fit: 'fill',
933
+ kernel: scale >= 1 ? sharp.kernel.nearest : sharp.kernel.lanczos3,
934
+ });
935
+ }
936
+ return pipeline
937
+ .png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
938
+ .toBuffer();
939
+ };
928
940
  const png = await encodeMask(facePixels);
929
941
  const borderPng = borderPixels === undefined ? undefined : await encodeMask(borderPixels);
930
942
  return {
@@ -932,8 +944,8 @@ export class GuiAssetCatalog {
932
944
  ...(borderPng === undefined
933
945
  ? {}
934
946
  : { borderDataUri: `data:image/png;base64,${borderPng.toString('base64')}` }),
935
- width: metric.width,
936
- height: metric.height,
947
+ width: targetWidth,
948
+ height: targetHeight,
937
949
  };
938
950
  })();
939
951
  this.glyphRasters.set(key, promise);
@@ -60,22 +60,15 @@ function sceneGlyphDefinitions(scene) {
60
60
  .map(([key, glyphPath]) => `<path id="${outlineDefinitionId(key)}" d="${glyphPath}"/>`),
61
61
  ...[...bitmaps.entries()]
62
62
  .sort(([left], [right]) => compareCodeUnits(left, right))
63
- .map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed"/>${glyph.borderDataUri === undefined ? '' : `<image id="${bitmapDefinitionId(key, 'border')}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.borderDataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed"/>`}`),
63
+ .map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed" style="image-rendering:pixelated"/>${glyph.borderDataUri === undefined ? '' : `<image id="${bitmapDefinitionId(key, 'border')}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.borderDataUri}" preserveAspectRatio="none" image-rendering="optimizeSpeed" style="image-rendering:pixelated"/>`}`),
64
64
  ].join('');
65
65
  }
66
66
  function bitmapTintMarkup(id, colour, glyphs, rect) {
67
67
  const match = /^#([0-9a-f]{6})([0-9a-f]{2})?$/iu.exec(colour);
68
68
  const rgb = match?.[1] ?? 'ffffff';
69
69
  const alpha = match?.[2] ?? 'ff';
70
- const channels = [
71
- Number.parseInt(rgb.slice(0, 2), 16) / 255,
72
- Number.parseInt(rgb.slice(2, 4), 16) / 255,
73
- Number.parseInt(rgb.slice(4, 6), 16) / 255,
74
- Number.parseInt(alpha, 16) / 255,
75
- ].map(finite);
76
- const [red, green, blue, opacity] = channels;
77
- const matrix = `${red} 0 0 0 0 0 ${green} 0 0 0 0 0 ${blue} 0 0 0 0 0 ${opacity} 0`;
78
- 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>`;
70
+ const opacity = finite(Number.parseInt(alpha, 16) / 255);
71
+ return `<defs><mask id="${id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" ${rectAttributes(rect)} style="mask-type:alpha"><g>${glyphs}</g></mask></defs><rect ${rectAttributes(rect)} fill="#${rgb}"${opacity >= 1 ? '' : ` fill-opacity="${opacity}"`} mask="url(#${id})"/>`;
79
72
  }
80
73
  function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale, bitmapLayer = 'face') {
81
74
  if (glyphLine.source === 'fontkit-path')
@@ -92,7 +85,7 @@ function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale, bitmap
92
85
  [glyph.x, glyph.y, glyph.width, glyph.height, glyphLine.baseline].every((value) => Math.abs(value - Math.round(value)) < 0.000_001);
93
86
  const x = originX + glyph.x * horizontalScale;
94
87
  const y = baseline + glyph.y - glyphLine.baseline;
95
- return `<use href="#${bitmapDefinitionId(glyph.key, bitmapLayer)}" transform="translate(${finite(nativePixelGrid ? Math.round(x) : x)} ${finite(nativePixelGrid ? Math.round(y) : y)}) scale(${finite(horizontalScale)} 1)"/>`;
88
+ return `<use href="#${bitmapDefinitionId(glyph.key, bitmapLayer)}" transform="translate(${finite(nativePixelGrid ? Math.round(x) : x)} ${finite(nativePixelGrid ? Math.round(y) : y)}) scale(${finite(horizontalScale)} 1)" image-rendering="optimizeSpeed" style="image-rendering:pixelated"/>`;
96
89
  })
97
90
  .join('');
98
91
  return '';
@@ -104,7 +104,13 @@ const GuiPreviewScenarioBodySchema = z
104
104
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
105
105
  description: z.string().max(500).optional(),
106
106
  resolution: GuiPreviewResolutionSchema.default({ width: 1920, height: 1080 }),
107
- uiScale: z.number().positive().min(0.25).max(4).default(1),
107
+ uiScale: z
108
+ .number()
109
+ .positive()
110
+ .min(0.25)
111
+ .max(4)
112
+ .default(1)
113
+ .describe('In-game UI scale. Use 1 for the normal 1920x1080 layout; this changes layout geometry and is not an output-image zoom.'),
108
114
  state: previewStateSchema.default('normal'),
109
115
  language: z
110
116
  .string()