hoi4-agent-tools 2.8.7 → 2.8.9

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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/hoi4_agent_tools/gui/assets.js +89 -13
  3. package/dist/hoi4_agent_tools/gui/layout.js +5 -5
  4. package/dist/hoi4_agent_tools/gui/renderer.js +26 -13
  5. package/dist/hoi4_agent_tools/gui/scenario-generator.js +70 -9
  6. package/dist/hoi4_agent_tools/gui/source-graph.js +39 -16
  7. package/dist/hoi4_agent_tools/version.js +1 -1
  8. package/docs/gui.md +3 -3
  9. package/package.json +1 -1
  10. package/schemas/configuration.schema.json +1 -1
  11. package/schemas/continuous-focus-palette.schema.json +1 -1
  12. package/schemas/custom-weighted-pool.schema.json +1 -1
  13. package/schemas/event-feature-manifest.schema.json +1 -1
  14. package/schemas/focus-plan.schema.json +1 -1
  15. package/schemas/focus-planning-sidecar.schema.json +1 -1
  16. package/schemas/gui-animation-source.schema.json +1 -1
  17. package/schemas/gui-helper.schema.json +1 -1
  18. package/schemas/gui-scenario.schema.json +1 -1
  19. package/schemas/map-operation.schema.json +1 -1
  20. package/schemas/operation-result.schema.json +1 -1
  21. package/schemas/probability-analysis-result.schema.json +1 -1
  22. package/schemas/probability-compare-input.schema.json +1 -1
  23. package/schemas/probability-evaluate-input.schema.json +1 -1
  24. package/schemas/probability-inspect-input.schema.json +1 -1
  25. package/schemas/probability-render-input.schema.json +1 -1
  26. package/schemas/probability-scenario-set.schema.json +1 -1
  27. package/schemas/probability-sequence-input.schema.json +1 -1
  28. package/schemas/probability-simulate-input.schema.json +1 -1
  29. package/schemas/probability-sweep-input.schema.json +1 -1
  30. 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.9 - 2026-08-29
8
+
9
+ - Decode BMFont channel roles so installed HOI4 atlases render their face and outline masks separately instead of losing localisation colours.
10
+ - Infer black-on-white and white-on-transparent glyph-mask polarity, preserving support for both vanilla fonts and custom mod fonts.
11
+
12
+ ## 2.8.8 - 2026-08-29
13
+
14
+ - Correct GUI font selection so mod definitions override lower-load-order game definitions, language-specific overrides only apply to their language, and extensionless WOFF assets resolve.
15
+ - Accept numeric-block and six-digit Paradox colors, normalize fractional text-color channels, and place color runs from kerning-aware measurements without clipping glyph overhangs.
16
+ - Make generated GUI scenarios favour ordinary values, respect literal numeric limits and progress ranges, and keep current/limit pairs coherent.
17
+
7
18
  ## 2.8.7 - 2026-08-28
8
19
 
9
20
  - Isolate stale, foreign, oversized, malformed, and address-mismatched artifact manifests so one bad generated record cannot block unrelated probability, event, GUI, or other MCP evidence routes.
@@ -43,6 +43,7 @@ export function parseBmFont(text) {
43
43
  let size = 16;
44
44
  let lineHeight = 16;
45
45
  let base;
46
+ let channelRoles;
46
47
  const pages = new Map();
47
48
  const characters = new Map();
48
49
  const kerning = new Map();
@@ -66,6 +67,9 @@ export function parseBmFont(text) {
66
67
  const fields = parseFields(line);
67
68
  lineHeight = typeof fields.lineHeight === 'number' ? fields.lineHeight : lineHeight;
68
69
  base = typeof fields.base === 'number' ? fields.base : base;
70
+ const roles = [fields.redChnl, fields.greenChnl, fields.blueChnl, fields.alphaChnl];
71
+ if (roles.every((role) => typeof role === 'number'))
72
+ channelRoles = roles;
69
73
  recognised = true;
70
74
  }
71
75
  else if (line.startsWith('page ')) {
@@ -137,6 +141,7 @@ export function parseBmFont(text) {
137
141
  pages: [...pages.entries()].sort(([left], [right]) => left - right).map(([, page]) => page),
138
142
  characters,
139
143
  kerning,
144
+ ...(channelRoles === undefined ? {} : { channelRoles }),
140
145
  }
141
146
  : undefined;
142
147
  }
@@ -156,6 +161,7 @@ export class GuiAssetCatalog {
156
161
  constructor(graph, scannedFiles, budget = new RenderBudget(), language = 'l_english') {
157
162
  this.graph = graph;
158
163
  this.budget = budget;
164
+ const sourceLoadOrder = new Map(scannedFiles.map((file) => [file.displayPath, file.loadOrder]));
159
165
  for (const file of scannedFiles
160
166
  .filter((candidate) => candidate.shadowedBy === undefined)
161
167
  .sort((left, right) => compareCodeUnits(left.relativePath, right.relativePath))) {
@@ -167,25 +173,38 @@ export class GuiAssetCatalog {
167
173
  this.basenames.set(basename, candidates);
168
174
  }
169
175
  const normalizedLanguage = language.toLocaleLowerCase('en-US');
170
- for (const definition of graph.fonts) {
176
+ const eligibleFonts = graph.fonts.filter((definition) => definition.languages.length === 0 ||
177
+ definition.languages.some((candidate) => candidate.toLocaleLowerCase('en-US') === normalizedLanguage));
178
+ const definitionPriority = (definition) => {
179
+ const languageMatch = definition.languages.some((candidate) => candidate.toLocaleLowerCase('en-US') === normalizedLanguage);
180
+ return [
181
+ languageMatch ? 1 : 0,
182
+ sourceLoadOrder.get(definition.sourcePath) ?? 0,
183
+ languageMatch === definition.override ? 1 : 0,
184
+ ];
185
+ };
186
+ const higherPriority = (left, right) => {
187
+ const leftPriority = definitionPriority(left);
188
+ const rightPriority = definitionPriority(right);
189
+ for (let index = 0; index < leftPriority.length; index += 1) {
190
+ if (leftPriority[index] !== rightPriority[index])
191
+ return leftPriority[index] > rightPriority[index];
192
+ }
193
+ return false;
194
+ };
195
+ for (const definition of eligibleFonts) {
171
196
  const key = definition.name.toLocaleLowerCase('en-US');
172
197
  const existing = this.fontDefinitions.get(key);
173
- const definitionMatches = definition.languages.some((candidate) => candidate.toLocaleLowerCase('en-US') === normalizedLanguage);
174
- const existingMatches = existing?.languages.some((candidate) => candidate.toLocaleLowerCase('en-US') === normalizedLanguage) ?? false;
175
- if (existing === undefined ||
176
- (definitionMatches && !existingMatches) ||
177
- (definitionMatches === existingMatches && existing.override && !definition.override) ||
178
- (definitionMatches === existingMatches &&
179
- existing.languages.length > 0 &&
180
- definition.languages.length === 0))
198
+ if (existing === undefined || higherPriority(definition, existing))
181
199
  this.fontDefinitions.set(key, definition);
182
200
  }
183
201
  const baseDefinitions = new Map();
184
- for (const definition of graph.fonts) {
202
+ for (const definition of eligibleFonts) {
185
203
  if (definition.override || definition.languages.length > 0)
186
204
  continue;
187
205
  const key = definition.name.toLocaleLowerCase('en-US');
188
- if (!baseDefinitions.has(key))
206
+ const existing = baseDefinitions.get(key);
207
+ if (existing === undefined || higherPriority(definition, existing))
189
208
  baseDefinitions.set(key, definition);
190
209
  }
191
210
  for (const [key, definition] of this.fontDefinitions) {
@@ -211,7 +230,14 @@ export class GuiAssetCatalog {
211
230
  const extension = path.posix.extname(normalized);
212
231
  const rasterExtensions = ['.png', '.bmp', '.tga', '.dds', '.svg'];
213
232
  const normalizedCandidates = extension.length === 0
214
- ? [normalized, `${normalized}.fnt`, `${normalized}.ttf`, `${normalized}.otf`]
233
+ ? [
234
+ normalized,
235
+ `${normalized}.fnt`,
236
+ `${normalized}.ttf`,
237
+ `${normalized}.otf`,
238
+ `${normalized}.woff`,
239
+ `${normalized}.woff2`,
240
+ ]
215
241
  : rasterExtensions.includes(extension)
216
242
  ? [
217
243
  normalized,
@@ -455,6 +481,7 @@ export class GuiAssetCatalog {
455
481
  kind: 'bitmap',
456
482
  key: `${entry.sourceFile.sha256}:${codePoint}:${Math.max(1, fontSize)}`,
457
483
  dataUri: raster.dataUri,
484
+ ...(raster.borderDataUri === undefined ? {} : { borderDataUri: raster.borderDataUri }),
458
485
  x: penX + metric.xOffset * scale,
459
486
  y: metric.yOffset * scale,
460
487
  width: raster.width * scale,
@@ -591,14 +618,63 @@ export class GuiAssetCatalog {
591
618
  .ensureAlpha()
592
619
  .raw()
593
620
  .toBuffer({ resolveWithObject: true });
594
- const png = await sharp(extracted.data, {
621
+ const channelRoles = entry.bmfont?.channelRoles;
622
+ let facePixels = extracted.data;
623
+ let borderPixels;
624
+ if (channelRoles?.some((role) => role === 0) === true) {
625
+ facePixels = Buffer.alloc(extracted.data.length);
626
+ borderPixels = channelRoles.some((role) => role === 1)
627
+ ? Buffer.alloc(extracted.data.length)
628
+ : undefined;
629
+ let backgroundSamples = 0;
630
+ let backgroundTotal = 0;
631
+ for (let offset = 0; offset < extracted.data.length; offset += 4) {
632
+ const borderCoverage = channelRoles.reduce((maximum, role, channel) => role === 1 ? Math.max(maximum, extracted.data[offset + channel] ?? 0) : maximum, 0);
633
+ if (borderCoverage !== 0)
634
+ continue;
635
+ for (let channel = 0; channel < 4; channel += 1) {
636
+ if (channelRoles[channel] !== 0)
637
+ continue;
638
+ backgroundTotal += extracted.data[offset + channel] ?? 0;
639
+ backgroundSamples += 1;
640
+ }
641
+ }
642
+ const invertedFace = backgroundSamples > 0 && backgroundTotal / backgroundSamples > 127;
643
+ for (let offset = 0; offset < extracted.data.length; offset += 4) {
644
+ let faceCoverage = 0;
645
+ let borderCoverage = 0;
646
+ for (let channel = 0; channel < 4; channel += 1) {
647
+ const sample = extracted.data[offset + channel] ?? 0;
648
+ if (channelRoles[channel] === 0)
649
+ faceCoverage = Math.max(faceCoverage, invertedFace ? 255 - sample : sample);
650
+ if (channelRoles[channel] === 1)
651
+ borderCoverage = Math.max(borderCoverage, sample);
652
+ }
653
+ facePixels[offset] = 255;
654
+ facePixels[offset + 1] = 255;
655
+ facePixels[offset + 2] = 255;
656
+ facePixels[offset + 3] = faceCoverage;
657
+ if (borderPixels !== undefined) {
658
+ borderPixels[offset] = 255;
659
+ borderPixels[offset + 1] = 255;
660
+ borderPixels[offset + 2] = 255;
661
+ borderPixels[offset + 3] = borderCoverage;
662
+ }
663
+ }
664
+ }
665
+ const encodeMask = (pixels) => sharp(pixels, {
595
666
  raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
596
667
  limitInputPixels: RENDER_MAX_DECODED_PIXELS,
597
668
  })
598
669
  .png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
599
670
  .toBuffer();
671
+ const png = await encodeMask(facePixels);
672
+ const borderPng = borderPixels === undefined ? undefined : await encodeMask(borderPixels);
600
673
  return {
601
674
  dataUri: `data:image/png;base64,${png.toString('base64')}`,
675
+ ...(borderPng === undefined
676
+ ? {}
677
+ : { borderDataUri: `data:image/png;base64,${borderPng.toString('base64')}` }),
602
678
  width: metric.width,
603
679
  height: metric.height,
604
680
  };
@@ -460,11 +460,9 @@ function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inl
460
460
  return [];
461
461
  const runs = [];
462
462
  let start = 0;
463
- let offsetX = 0;
464
463
  while (start < line.length) {
465
464
  const inlineIcon = inlineIcons.get(line[start] ?? '');
466
465
  if (inlineIcon !== undefined) {
467
- offsetX += inlineIcon.width;
468
466
  start += 1;
469
467
  continue;
470
468
  }
@@ -475,10 +473,12 @@ function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inl
475
473
  (colours[end] ?? defaultColour) === colour)
476
474
  end += 1;
477
475
  const text = line.slice(start, end);
478
- work.spendTextLayout('localisation colour-run measurement');
479
- const width = measureTextWithInlineIcons(catalog, fontName, text, fontSize, inlineIcons).width;
476
+ work.spendTextLayout('localisation colour-run prefix measurement');
477
+ const offsetX = measureTextWithInlineIcons(catalog, fontName, line.slice(0, start), fontSize, inlineIcons).width;
478
+ work.spendTextLayout('localisation colour-run end measurement');
479
+ const endX = measureTextWithInlineIcons(catalog, fontName, line.slice(0, end), fontSize, inlineIcons).width;
480
+ const width = Math.max(0, endX - offsetX);
480
481
  runs.push({ text, colour, offsetX, width });
481
- offsetX += width;
482
482
  start = end;
483
483
  }
484
484
  return runs;
@@ -33,8 +33,8 @@ function colourFor(value) {
33
33
  function outlineDefinitionId(key) {
34
34
  return `gui-font-outline-${sha256Bytes(key).slice(0, 20)}`;
35
35
  }
36
- function bitmapDefinitionId(key) {
37
- return `gui-font-bitmap-${sha256Bytes(key).slice(0, 20)}`;
36
+ function bitmapDefinitionId(key, layer = 'face') {
37
+ return `gui-font-bitmap-${layer === 'face' ? '' : 'border-'}${sha256Bytes(key).slice(0, 20)}`;
38
38
  }
39
39
  function sceneGlyphDefinitions(scene) {
40
40
  const outlines = new Map();
@@ -47,6 +47,7 @@ function sceneGlyphDefinitions(scene) {
47
47
  else
48
48
  bitmaps.set(glyph.key, {
49
49
  dataUri: glyph.dataUri,
50
+ ...(glyph.borderDataUri === undefined ? {} : { borderDataUri: glyph.borderDataUri }),
50
51
  width: glyph.width,
51
52
  height: glyph.height,
52
53
  });
@@ -59,7 +60,7 @@ function sceneGlyphDefinitions(scene) {
59
60
  .map(([key, glyphPath]) => `<path id="${outlineDefinitionId(key)}" d="${glyphPath}"/>`),
60
61
  ...[...bitmaps.entries()]
61
62
  .sort(([left], [right]) => compareCodeUnits(left, right))
62
- .map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none"/>`),
63
+ .map(([key, glyph]) => `<image id="${bitmapDefinitionId(key)}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.dataUri}" preserveAspectRatio="none"/>${glyph.borderDataUri === undefined ? '' : `<image id="${bitmapDefinitionId(key, 'border')}" width="${finite(glyph.width)}" height="${finite(glyph.height)}" href="${glyph.borderDataUri}" preserveAspectRatio="none"/>`}`),
63
64
  ].join('');
64
65
  }
65
66
  function bitmapTintMarkup(id, colour, glyphs, rect) {
@@ -76,7 +77,7 @@ function bitmapTintMarkup(id, colour, glyphs, rect) {
76
77
  const matrix = `${red} 0 0 0 0 0 ${green} 0 0 0 0 0 ${blue} 0 0 0 0 0 ${opacity} 0`;
77
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>`;
78
79
  }
79
- function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale) {
80
+ function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale, bitmapLayer = 'face') {
80
81
  if (glyphLine.source === 'fontkit-path')
81
82
  return glyphLine.glyphs
82
83
  .filter((glyph) => glyph.kind === 'outline')
@@ -85,7 +86,8 @@ function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale) {
85
86
  if (glyphLine.source === 'bmfont-atlas')
86
87
  return glyphLine.glyphs
87
88
  .filter((glyph) => glyph.kind === 'bitmap')
88
- .map((glyph) => `<use href="#${bitmapDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y - glyphLine.baseline)}) scale(${finite(horizontalScale)} 1)"/>`)
89
+ .filter((glyph) => bitmapLayer === 'face' || glyph.borderDataUri !== undefined)
90
+ .map((glyph) => `<use href="#${bitmapDefinitionId(glyph.key, bitmapLayer)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y - glyphLine.baseline)}) scale(${finite(horizontalScale)} 1)"/>`)
89
91
  .join('');
90
92
  return '';
91
93
  }
@@ -137,17 +139,22 @@ function renderText(element, toolText) {
137
139
  const key = `${element.id}:${index}:${runIndex}:${run.colour}`;
138
140
  const clipId = `gui-font-run-clip-${sha256Bytes(key).slice(0, 20)}`;
139
141
  const runX = originX + run.offsetX * horizontalScale;
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>`);
142
+ definitions.push(`<clipPath id="${clipId}"><rect x="${finite(runX - 0.25)}" y="${finite(lineTop - text.lineHeight)}" width="${finite(run.width * horizontalScale + 0.5)}" height="${finite(text.lineHeight * 3)}"/></clipPath>`);
141
143
  const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
142
144
  if (glyphLine.source === 'bmfont-atlas') {
143
- const maskId = `gui-font-mask-${sha256Bytes(key).slice(0, 20)}`;
145
+ const faceFilterId = `gui-font-face-${sha256Bytes(key).slice(0, 20)}`;
146
+ const borderFilterId = `gui-font-border-${sha256Bytes(key).slice(0, 20)}`;
144
147
  const runRect = {
145
148
  x: runX - 0.25,
146
- y: lineTop,
149
+ y: lineTop - text.lineHeight,
147
150
  width: run.width * horizontalScale + 0.5,
148
- height: text.lineHeight,
151
+ height: text.lineHeight * 3,
149
152
  };
150
- return `<g clip-path="url(#${clipId})" data-font-colour="${run.colour}">${bitmapTintMarkup(maskId, run.colour, glyphs, runRect)}</g>`;
153
+ const borderGlyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale, 'border');
154
+ const border = borderGlyphs.length === 0
155
+ ? ''
156
+ : bitmapTintMarkup(borderFilterId, text.borderColour ?? '#000000ff', borderGlyphs, runRect);
157
+ return `<g clip-path="url(#${clipId})" data-font-colour="${run.colour}">${border}${bitmapTintMarkup(faceFilterId, run.colour, glyphs, runRect)}</g>`;
151
158
  }
152
159
  return `<g clip-path="url(#${clipId})" fill="${run.colour}" stroke="${text.borderColour ?? '#12151a'}" data-font-colour="${run.colour}">${glyphs}</g>`;
153
160
  });
@@ -171,11 +178,17 @@ function renderText(element, toolText) {
171
178
  }
172
179
  if (glyphLine?.source === 'bmfont-atlas') {
173
180
  const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
174
- if (text.colour === undefined)
181
+ const borderGlyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale, 'border');
182
+ if (text.colour === undefined && borderGlyphs.length === 0)
175
183
  return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphs}${inlineIconMarkup}</g>`;
176
- const maskId = `gui-font-mask-${sha256Bytes(`${element.id}:${index}:${text.colour}`).slice(0, 20)}`;
184
+ const colour = text.colour ?? '#ffffffff';
185
+ const faceFilterId = `gui-font-face-${sha256Bytes(`${element.id}:${index}:${colour}`).slice(0, 20)}`;
186
+ const borderFilterId = `gui-font-border-${sha256Bytes(`${element.id}:${index}:${text.borderColour ?? '#000000ff'}`).slice(0, 20)}`;
177
187
  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>`;
188
+ const border = borderGlyphs.length === 0
189
+ ? ''
190
+ : bitmapTintMarkup(borderFilterId, text.borderColour ?? '#000000ff', borderGlyphs, lineRect);
191
+ return `<g data-font-sha256="${glyphLine.sourceHash}" data-font-colour="${colour}">${border}${bitmapTintMarkup(faceFilterId, colour, glyphs, lineRect)}${inlineIconMarkup}</g>`;
179
192
  }
180
193
  const visibleLine = line.replace(/[\uE000-\uF8FF]/gu, ' ');
181
194
  return `<g>${toolText.render(visibleLine, {
@@ -52,6 +52,9 @@ class ScenarioRandom {
52
52
  integer(minimum, maximum) {
53
53
  return Math.floor(this.next() * (maximum - minimum + 1)) + minimum;
54
54
  }
55
+ ordinary() {
56
+ return (this.next() + this.next() + this.next()) / 3;
57
+ }
55
58
  pick(values) {
56
59
  return values[Math.min(values.length - 1, Math.floor(this.next() * values.length))];
57
60
  }
@@ -182,18 +185,64 @@ function plausibleText(key, random, samples, scopedCountries) {
182
185
  function generatedNumber(options, random, minimum = options.numericMinimum, maximum = options.numericMaximum) {
183
186
  const boundedMinimum = Math.max(options.numericMinimum, minimum);
184
187
  const boundedMaximum = Math.min(options.numericMaximum, maximum);
185
- const low = Math.min(boundedMinimum, boundedMaximum);
186
- const high = Math.max(boundedMinimum, boundedMaximum);
187
- const value = low + random.next() * (high - low);
188
+ const [low, high] = boundedMinimum <= boundedMaximum
189
+ ? [boundedMinimum, boundedMaximum]
190
+ : maximum < options.numericMinimum
191
+ ? [maximum, maximum]
192
+ : [minimum, minimum];
193
+ const value = low + random.ordinary() * (high - low);
188
194
  return options.integerValues ? Math.round(value) : Math.round(value * 100) / 100;
189
195
  }
190
- function generatedNumberForKey(key, options, random) {
196
+ function sourceNumericConstraints(texts) {
197
+ const ranges = new Map();
198
+ const relationships = [];
199
+ const update = (key, range) => {
200
+ const current = ranges.get(key) ?? {};
201
+ const minimum = range.minimum === undefined
202
+ ? current.minimum
203
+ : current.minimum === undefined
204
+ ? range.minimum
205
+ : Math.max(current.minimum, range.minimum);
206
+ const maximum = range.maximum === undefined
207
+ ? current.maximum
208
+ : current.maximum === undefined
209
+ ? range.maximum
210
+ : Math.min(current.maximum, range.maximum);
211
+ ranges.set(key, {
212
+ ...(minimum === undefined ? {} : { minimum }),
213
+ ...(maximum === undefined ? {} : { maximum }),
214
+ });
215
+ };
216
+ const token = String.raw `\[\?([^\]|]+)(?:\|[^\]]+)?\]`;
217
+ for (const text of texts) {
218
+ for (const match of text.matchAll(new RegExp(`${token}\\s*(?:/|of)\\s*(-?(?:\\d+\\.?\\d*|\\.\\d+))`, 'giu'))) {
219
+ const key = match[1];
220
+ const maximum = Number(match[2]);
221
+ if (key !== undefined && Number.isFinite(maximum))
222
+ update(key, { maximum });
223
+ }
224
+ for (const match of text.matchAll(new RegExp(`(-?(?:\\d+\\.?\\d*|\\.\\d+))\\s*(?:-|to)\\s*${token}`, 'giu'))) {
225
+ const minimum = Number(match[1]);
226
+ const key = match[2];
227
+ if (key !== undefined && Number.isFinite(minimum))
228
+ update(key, { minimum });
229
+ }
230
+ for (const match of text.matchAll(new RegExp(`${token}\\s*(?:/|of)\\s*${token}`, 'giu'))) {
231
+ const value = match[1];
232
+ const limit = match[2];
233
+ if (value !== undefined && limit !== undefined)
234
+ relationships.push({ value, limit });
235
+ }
236
+ }
237
+ return { ranges, relationships };
238
+ }
239
+ function generatedNumberForKey(key, options, random, inferred = {}) {
191
240
  const lower = key.toLocaleLowerCase('en-US');
192
241
  if (/(?:ratio|fraction|progress)$/u.test(lower))
193
- return generatedNumber(options, random, 0, 1);
242
+ return generatedNumber(options, random, inferred.minimum ?? 0, inferred.maximum ?? 1);
194
243
  if (/(?:percent|percentage|support|stability|war_support|legitimacy|threat|charge)/u.test(lower))
195
- return generatedNumber(options, random, 0, 100);
196
- return generatedNumber(options, random);
244
+ return generatedNumber(options, random, inferred.minimum ?? 0, inferred.maximum ?? 100);
245
+ return generatedNumber(options, random, inferred.minimum ?? options.numericMinimum, inferred.maximum ?? options.numericMaximum);
197
246
  }
198
247
  function setIfMissing(target, key, value) {
199
248
  target[key] ??= value;
@@ -419,7 +468,9 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
419
468
  return [];
420
469
  const scripted = relatedScriptedGuis(graph, windowName);
421
470
  const elements = relevantElements(graph, windowName, scripted);
422
- const tokens = tokensIn(sourceTexts(graph, elements, base.language, scripted));
471
+ const relevantSourceTexts = sourceTexts(graph, elements, base.language, scripted);
472
+ const tokens = tokensIn(relevantSourceTexts);
473
+ const numericConstraints = sourceNumericConstraints(relevantSourceTexts);
423
474
  const imageTokens = new Set();
424
475
  const booleanTokens = new Set();
425
476
  const frameTokens = new Set();
@@ -482,7 +533,7 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
482
533
  const elementStates = { ...base.elementStates };
483
534
  const scopedCountries = new Map();
484
535
  for (const key of tokens.numeric)
485
- setIfMissing(values, key, generatedNumberForKey(key, options, random));
536
+ setIfMissing(values, key, generatedNumberForKey(key, options, random, numericConstraints.ranges.get(key)));
486
537
  for (const [elementName, { minimum, maximum }] of progressRanges) {
487
538
  const element = elements.find(({ name }) => name === elementName);
488
539
  const startValueToken = typeof element?.attributes.startValue === 'string'
@@ -507,6 +558,16 @@ export function generateGuiPreviewScenarios(graph, windowName, base, options) {
507
558
  if (typeof sourceValue === 'number')
508
559
  values[key] = sourceValue;
509
560
  }
561
+ for (const { value: valueKey, limit: limitKey } of numericConstraints.relationships) {
562
+ if (base.values[valueKey] !== undefined)
563
+ continue;
564
+ const value = values[valueKey];
565
+ const limit = values[limitKey];
566
+ if (typeof value !== 'number' || typeof limit !== 'number')
567
+ continue;
568
+ const minimum = Math.min(numericConstraints.ranges.get(valueKey)?.minimum ?? options.numericMinimum, limit);
569
+ values[valueKey] = Math.max(minimum, Math.min(value, limit));
570
+ }
510
571
  for (const key of tokens.textual)
511
572
  setIfMissing(values, key, plausibleText(key, random, options.textSamples, scopedCountries));
512
573
  const scriptedTextTemplates = new Map();
@@ -208,15 +208,44 @@ function boolScalar(block, ...keys) {
208
208
  return false;
209
209
  return undefined;
210
210
  }
211
+ function colourChannels(value) {
212
+ if (value === undefined)
213
+ return undefined;
214
+ const channels = value.entries
215
+ .filter((entry) => entry.type === 'scalar')
216
+ .map((entry) => Number(entry.value));
217
+ if (channels.length < 3 || channels.slice(0, 4).some((channel) => !Number.isFinite(channel)))
218
+ return undefined;
219
+ const normalized = channels.slice(0, 4).every((channel) => channel >= 0 && channel <= 1);
220
+ const bytes = channels
221
+ .slice(0, 4)
222
+ .map((channel) => Math.max(0, Math.min(255, Math.round(channel * (normalized ? 255 : 1)))));
223
+ while (bytes.length < 4)
224
+ bytes.push(255);
225
+ return `#${bytes.map((channel) => channel.toString(16).padStart(2, '0')).join('')}`;
226
+ }
211
227
  function paradoxColour(value) {
212
228
  if (value === undefined)
213
229
  return undefined;
214
- const match = /^0x([0-9a-f]{8})$/iu.exec(value.trim());
230
+ const match = /^0x([0-9a-f]{6}|[0-9a-f]{8})$/iu.exec(value.trim());
215
231
  if (match === null)
216
232
  return undefined;
233
+ if (match[1].length === 6)
234
+ return `#${match[1].toLowerCase()}ff`;
217
235
  const argb = match[1];
218
236
  return `#${argb.slice(2)}${argb.slice(0, 2)}`.toLowerCase();
219
237
  }
238
+ function colourFrom(block, ...keys) {
239
+ const normalized = new Set(keys.map((key) => key.toLowerCase()));
240
+ for (const assignment of assignments(block)) {
241
+ if (!normalized.has(assignment.key.value.toLowerCase()))
242
+ continue;
243
+ if (assignment.value.type === 'scalar')
244
+ return paradoxColour(assignment.value.value);
245
+ return colourChannels(assignment.value);
246
+ }
247
+ return undefined;
248
+ }
220
249
  function textColoursFrom(block) {
221
250
  const textColours = childBlocks(block, 'textcolors')[0];
222
251
  if (textColours === undefined)
@@ -225,15 +254,9 @@ function textColoursFrom(block) {
225
254
  for (const entry of assignments(textColours)) {
226
255
  if (entry.value.type !== 'block')
227
256
  continue;
228
- const channels = entry.value.entries
229
- .filter((channel) => channel.type === 'scalar')
230
- .map((channel) => Number(channel.value));
231
- if (channels.length < 3 || channels.slice(0, 3).some((channel) => !Number.isFinite(channel)))
232
- continue;
233
- const [red, green, blue] = channels.map((channel) => Math.max(0, Math.min(255, Math.round(channel))));
234
- result[entry.key.value] = `#${red.toString(16).padStart(2, '0')}${green
235
- .toString(16)
236
- .padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`;
257
+ const colour = colourChannels(entry.value);
258
+ if (colour !== undefined)
259
+ result[entry.key.value] = colour.slice(0, 7);
237
260
  }
238
261
  return result;
239
262
  }
@@ -462,8 +485,8 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
462
485
  addEdge(edges, 'contains', fileNodeId, id, true, {}, definition.location);
463
486
  }
464
487
  }
465
- else if (assignment.key.value === 'bitmapfont' ||
466
- assignment.key.value === 'bitmapfont_override') {
488
+ else if (assignment.key.value.toLowerCase() === 'bitmapfont' ||
489
+ assignment.key.value.toLowerCase() === 'bitmapfont_override') {
467
490
  const name = firstScalarInsensitive(child, 'name');
468
491
  if (name !== undefined) {
469
492
  const assetPaths = [firstScalarInsensitive(child, 'path')]
@@ -474,10 +497,10 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
474
497
  const languages = childBlocks(child, 'languages').flatMap((languageBlock) => languageBlock.entries
475
498
  .filter((entry) => entry.type === 'scalar')
476
499
  .map((entry) => entry.value));
477
- const colour = paradoxColour(firstScalarInsensitive(child, 'color', 'colour'));
478
- const borderColour = paradoxColour(firstScalarInsensitive(child, 'border_color', 'borderColor', 'border_colour'));
500
+ const colour = colourFrom(child, 'color', 'colour');
501
+ const borderColour = colourFrom(child, 'border_color', 'borderColor', 'border_colour');
479
502
  const textColours = textColoursFrom(child);
480
- const override = assignment.key.value === 'bitmapfont_override';
503
+ const override = assignment.key.value.toLowerCase() === 'bitmapfont_override';
481
504
  const id = deterministicId('gui_font', {
482
505
  path: file.displayPath,
483
506
  name,
@@ -519,7 +542,7 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
519
542
  addEdge(edges, 'contains', fileNodeId, id, true, {}, font.location);
520
543
  }
521
544
  }
522
- if (assignment.key.value === 'bitmapfonts') {
545
+ if (assignment.key.value.toLowerCase() === 'bitmapfonts') {
523
546
  Object.assign(globalTextColours, textColoursFrom(child));
524
547
  }
525
548
  walk(child);
@@ -1,6 +1,6 @@
1
1
  import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
2
2
  export const PACKAGE_NAME = 'hoi4-agent-tools';
3
- export const PACKAGE_VERSION = '2.8.7';
3
+ export const PACKAGE_VERSION = '2.8.9';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 1;
package/docs/gui.md CHANGED
@@ -33,7 +33,7 @@ Inspection checks missing assets and localisation, invalid sizes, overlap, clipp
33
33
 
34
34
  The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
35
35
 
36
- Bitmap text uses the language-appropriate HOI4 font definition and its real glyph atlas. Header, serif, compact, typewriter, black, inverted, and other declared faces keep their own glyph shapes, native metrics, face colour, border colour, language override, and font-specific `textcolors` palette. HOI4 localisation colour runs such as `§Y` and `§R` multiply the selected palette colour into the native glyph pixels, preserving the atlas's sharp antialiasing and dark shadow pixels instead of flattening the glyph into a blurred monochrome mask or switching to the tool font. The default language uses the base font rather than an unrelated language override, and the renderer recognizes installed-game atlases whose filename omits a page suffix declared by the matching `.fnt` file.
36
+ Bitmap text uses the language-appropriate HOI4 font definition and its real glyph atlas. Header, serif, compact, typewriter, black, inverted, and other declared faces keep their own glyph shapes, native metrics, face colour, border colour, language override, and font-specific `textcolors` palette. The renderer reads BMFont channel roles, separates face and outline coverage, and recognizes both vanilla black-on-white masks and conventional white-on-transparent custom masks. HOI4 localisation colour runs such as `§Y` and `§R` therefore colour the glyph face while retaining its sharp antialiasing and declared border instead of flattening the glyph into a blurred monochrome mask or switching to the tool font. The default language uses the base font rather than an unrelated language override, and the renderer recognizes installed-game atlases whose filename omits a page suffix declared by the matching `.fnt` file.
37
37
 
38
38
  Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
39
39
 
@@ -41,7 +41,7 @@ Scripted-GUI composition follows `parent_scripted_gui` chains even when the link
41
41
 
42
42
  `hoi4.gui_inspect` and `hoi4.gui_render` generate a plausible seeded runtime scenario by default. The generator follows the complete `parent_scripted_gui` surface, discovers numeric and text tokens in GUI localisation and scripted-GUI properties, chooses real scripted-localisation text and sprite candidates, applies simple source thresholds, fills meters within their declared ranges, populates dynamic lists with coherent rows, selects one overlapping tab pane, keeps active and idle controls mutually exclusive, and leaves values explicitly supplied by the caller unchanged. Normal controls and animation frames remain stable unless variation is explicitly requested. Every explicit `relatedScenarios` entry is rendered beside the generated view.
43
43
 
44
- Generation is reproducible. Set `generatedScenarios.seed` to any stable string for an exact rerun, use `count` for several plausible versions, or configure numeric bounds, integer or decimal values, list row bounds, text samples, visibility probability, and state variation. The generated view is the default. Set `enabled` to `false` when only the supplied placeholder scenario should render, or set `preservePlaceholder` to `true` to include the placeholder beside generated scenarios.
44
+ Generation is reproducible. Set `generatedScenarios.seed` to any stable string for an exact rerun, use `count` for several plausible versions, or configure numeric bounds, integer or decimal values, list row bounds, text samples, visibility probability, and state variation. Generated numeric values favour ordinary mid-range states, obey literal limits such as `[?capacity]/10`, keep displayed current/limit pairs coherent, and remain inside progress-bar ranges. The generated view is the default. Set `enabled` to `false` when only the supplied placeholder scenario should render, or set `preservePlaceholder` to `true` to include the placeholder beside generated scenarios.
45
45
 
46
46
  ```json
47
47
  {
@@ -95,4 +95,4 @@ The renderer does not run the game engine. Each render includes a fidelity repor
95
95
 
96
96
  The generated primary view replaces discovered numeric and text runtime tokens with plausible values. Set `generatedScenarios.preservePlaceholder` to `true`, or disable generation, when the same run should also show unresolved numeric values such as `[?variable|format]` as `[X]` and unresolved text-returning scripted or scoped localisation such as `[GetStatusText]`, `[FROM.GetName]`, and `[?leader_scope.GetName]` as `[dynamic_loc]`. Put `variable` or `GetStatusText` in the supplied scenario's `values` object when an exact value matters; explicit values always take precedence over generated ones and can drive the related meter or state-dependent controls. Supported HOI4 `§` localisation colour controls use the active font's palette and `§!` restores the face colour.
97
97
 
98
- A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous. Frame-sheet animation, nine-slice tiling, progress composition, masking, and the primary and secondary textures referenced by vanilla GFX definitions render offline. Engine shader programs, hardcoded controls, and dynamic values that are not supplied by a scenario remain fidelity-report entries; an unexecuted shader never hides its resolved base texture.
98
+ A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous. Frame-sheet animation, nine-slice tiling, progress composition, masking, and the primary and secondary textures referenced by vanilla GFX definitions render offline. Font selection follows mod/game load order and preview language, accepts Paradox hex and numeric color declarations, and uses the supplied HOI4 bitmap or outline font assets without requiring host font installation. Engine shader programs, hardcoded controls, and dynamic values that are not supplied by a scenario remain fidelity-report entries; an unexecuted shader never hides its resolved base texture.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hoi4-agent-tools",
3
3
  "mcpName": "io.github.klimPaskov/hoi4-agent-tools",
4
- "version": "2.8.7",
4
+ "version": "2.8.9",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
6
  "type": "module",
7
7
  "license": "Apache-2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/configuration.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/configuration.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/continuous-focus-palette.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/continuous-focus-palette.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/custom-weighted-pool.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/custom-weighted-pool.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/event-feature-manifest.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/event-feature-manifest.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/focus-plan.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/focus-plan.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/focus-planning-sidecar.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/focus-planning-sidecar.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/gui-animation-source.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/gui-animation-source.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/gui-helper.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/gui-helper.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/gui-scenario.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/gui-scenario.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/map-operation.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/map-operation.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "oneOf": [
5
5
  {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/operation-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/operation-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-analysis-result.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-analysis-result.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-compare-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-compare-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-evaluate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-evaluate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-inspect-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-inspect-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-render-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-render-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-scenario-set.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-scenario-set.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-sequence-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-sequence-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-simulate-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-simulate-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
@@ -1,5 +1,5 @@
1
1
  {
2
- "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.7/schemas/probability-sweep-input.schema.json",
2
+ "$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.9/schemas/probability-sweep-input.schema.json",
3
3
  "$schema": "https://json-schema.org/draft/2020-12/schema",
4
4
  "type": "object",
5
5
  "properties": {
package/server.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.klimPaskov/hoi4-agent-tools",
4
4
  "title": "HOI4 Agent Tools",
5
5
  "description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
6
- "version": "2.8.7",
6
+ "version": "2.8.9",
7
7
  "repository": {
8
8
  "url": "https://github.com/klimPaskov/hoi4-agent-tools",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "npm",
15
15
  "identifier": "hoi4-agent-tools",
16
- "version": "2.8.7",
16
+ "version": "2.8.9",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }