hoi4-agent-tools 2.8.3 → 2.8.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.
- package/CHANGELOG.md +10 -0
- package/README.md +25 -25
- package/dist/hoi4_agent_tools/gui/assets.js +38 -1
- package/dist/hoi4_agent_tools/gui/index.js +1 -0
- package/dist/hoi4_agent_tools/gui/layout.js +37 -21
- package/dist/hoi4_agent_tools/gui/renderer.js +50 -11
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +223 -0
- package/dist/hoi4_agent_tools/gui/scenario.js +44 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +38 -0
- package/dist/hoi4_agent_tools/gui/studio.js +49 -12
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +23 -6
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +22 -3
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 2.8.5 - 2026-08-28
|
|
8
|
+
|
|
9
|
+
- Render bitmap-font face and localisation colours through alpha masks, including black source atlases, and load exact per-font HOI4 `textcolors` palettes.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
12
|
+
## 2.8.4 - 2026-08-28
|
|
13
|
+
|
|
14
|
+
- Render each declared HOI4 font face with its own glyph atlas, native metrics, face colour, border colour, and inherited language-override styling.
|
|
15
|
+
- Keep native HOI4 bitmap or outline glyphs when localisation colour markup changes part of a line instead of replacing the line with the generic tool font.
|
|
16
|
+
|
|
7
17
|
## 2.8.3 - 2026-08-27
|
|
8
18
|
|
|
9
19
|
- Centered native button labels when HOI4 GUI source uses `text =` instead of `buttonText =`, matching the in-game horizontal and vertical alignment inside button frames.
|
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
|
|
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
|
|
|
@@ -180,6 +180,31 @@ export class GuiAssetCatalog {
|
|
|
180
180
|
definition.languages.length === 0))
|
|
181
181
|
this.fontDefinitions.set(key, definition);
|
|
182
182
|
}
|
|
183
|
+
const baseDefinitions = new Map();
|
|
184
|
+
for (const definition of graph.fonts) {
|
|
185
|
+
if (definition.override || definition.languages.length > 0)
|
|
186
|
+
continue;
|
|
187
|
+
const key = definition.name.toLocaleLowerCase('en-US');
|
|
188
|
+
if (!baseDefinitions.has(key))
|
|
189
|
+
baseDefinitions.set(key, definition);
|
|
190
|
+
}
|
|
191
|
+
for (const [key, definition] of this.fontDefinitions) {
|
|
192
|
+
if (!definition.override)
|
|
193
|
+
continue;
|
|
194
|
+
const base = baseDefinitions.get(key);
|
|
195
|
+
if (base === undefined)
|
|
196
|
+
continue;
|
|
197
|
+
this.fontDefinitions.set(key, {
|
|
198
|
+
...definition,
|
|
199
|
+
...(definition.colour === undefined && base.colour !== undefined
|
|
200
|
+
? { colour: base.colour }
|
|
201
|
+
: {}),
|
|
202
|
+
...(definition.borderColour === undefined && base.borderColour !== undefined
|
|
203
|
+
? { borderColour: base.borderColour }
|
|
204
|
+
: {}),
|
|
205
|
+
textColours: { ...base.textColours, ...definition.textColours },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
183
208
|
}
|
|
184
209
|
resolveFile(assetPath, relativeTo) {
|
|
185
210
|
const normalized = normalizeAssetPath(assetPath);
|
|
@@ -558,11 +583,23 @@ export class GuiAssetCatalog {
|
|
|
558
583
|
return undefined;
|
|
559
584
|
this.budget.reserveRasterOperation(`gui-bmfont-glyph:${key}`, `GUI BMFont glyph rasterization U+${codePoint.toString(16).toUpperCase()}`);
|
|
560
585
|
this.budget.reserve(metric.width, metric.height, `GUI BMFont glyph U+${codePoint.toString(16).toUpperCase()}`, { maximumPixels: RENDER_MAX_DECODED_PIXELS });
|
|
561
|
-
const
|
|
586
|
+
const extracted = await sharp(raster.data, {
|
|
562
587
|
raw: { width: raster.width, height: raster.height, channels: 4 },
|
|
563
588
|
limitInputPixels: RENDER_MAX_DECODED_PIXELS,
|
|
564
589
|
})
|
|
565
590
|
.extract({ left: metric.x, top: metric.y, width: metric.width, height: metric.height })
|
|
591
|
+
.ensureAlpha()
|
|
592
|
+
.raw()
|
|
593
|
+
.toBuffer({ resolveWithObject: true });
|
|
594
|
+
for (let index = 0; index < extracted.data.length; index += 4) {
|
|
595
|
+
extracted.data[index] = 255;
|
|
596
|
+
extracted.data[index + 1] = 255;
|
|
597
|
+
extracted.data[index + 2] = 255;
|
|
598
|
+
}
|
|
599
|
+
const png = await sharp(extracted.data, {
|
|
600
|
+
raw: { width: extracted.info.width, height: extracted.info.height, channels: 4 },
|
|
601
|
+
limitInputPixels: RENDER_MAX_DECODED_PIXELS,
|
|
602
|
+
})
|
|
566
603
|
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
567
604
|
.toBuffer();
|
|
568
605
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
}
|
|
@@ -446,7 +455,7 @@ function wrapText(catalog, fontName, text, colours, fontSize, maximumWidth, work
|
|
|
446
455
|
missingGlyphs: [...missingGlyphs].sort((a, b) => a - b),
|
|
447
456
|
};
|
|
448
457
|
}
|
|
449
|
-
function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inlineIcons = new Map()) {
|
|
458
|
+
function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inlineIcons = new Map(), defaultColour = defaultTextColour) {
|
|
450
459
|
if (!colours.some((colour) => colour !== undefined) || line.length === 0)
|
|
451
460
|
return [];
|
|
452
461
|
const runs = [];
|
|
@@ -459,11 +468,11 @@ function colourRunsForLine(catalog, fontName, line, colours, fontSize, work, inl
|
|
|
459
468
|
start += 1;
|
|
460
469
|
continue;
|
|
461
470
|
}
|
|
462
|
-
const colour = colours[start] ??
|
|
471
|
+
const colour = colours[start] ?? defaultColour;
|
|
463
472
|
let end = start + 1;
|
|
464
473
|
while (end < line.length &&
|
|
465
474
|
!inlineIcons.has(line[end] ?? '') &&
|
|
466
|
-
(colours[end] ??
|
|
475
|
+
(colours[end] ?? defaultColour) === colour)
|
|
467
476
|
end += 1;
|
|
468
477
|
const text = line.slice(start, end);
|
|
469
478
|
work.spendTextLayout('localisation colour-run measurement');
|
|
@@ -739,10 +748,11 @@ 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
|
|
751
|
+
const fontName = scalarString(property(definition.attributes, 'font', 'buttonFont'));
|
|
752
|
+
const fontDefinition = fontName === undefined ? undefined : catalog.fontDefinition(fontName);
|
|
753
|
+
const visibleText = visibleHoiText(displayText, fontDefinition?.textColours);
|
|
743
754
|
displayText = visibleText.text;
|
|
744
755
|
context.work.admitText(displayText, `GUI text for ${definition.name}`);
|
|
745
|
-
const fontName = scalarString(property(definition.attributes, 'font', 'buttonFont'));
|
|
746
756
|
const resolvedFontMetrics = catalog.resolvedFontMetrics(fontName);
|
|
747
757
|
const explicitFontSize = scalarNumber(property(definition.attributes, 'fontSize'), 16);
|
|
748
758
|
const fontSize = explicitFontSize ??
|
|
@@ -792,7 +802,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
792
802
|
inlineIcons.push(...shaped.icons);
|
|
793
803
|
}
|
|
794
804
|
const colourRuns = visibleText.hasColourMarkup
|
|
795
|
-
? wrapped.lines.map((line, index) => colourRunsForLine(catalog, fontName, line, wrapped.colourLines[index] ?? [], fontSize * scale, context.work, resolvedInlineIcons))
|
|
805
|
+
? wrapped.lines.map((line, index) => colourRunsForLine(catalog, fontName, line, wrapped.colourLines[index] ?? [], fontSize * scale, context.work, resolvedInlineIcons, fontDefinition?.colour ?? defaultTextColour))
|
|
796
806
|
: undefined;
|
|
797
807
|
const maximumLineWidth = wrapped.widths.reduce((maximum, value) => Math.max(maximum, value), 0);
|
|
798
808
|
if (width === 0)
|
|
@@ -812,6 +822,10 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
812
822
|
metricSource: wrapped.metricSource,
|
|
813
823
|
...alignment(definition.attributes, embeddedButtonText),
|
|
814
824
|
...(fontName === undefined ? {} : { fontName }),
|
|
825
|
+
...(fontDefinition?.colour === undefined ? {} : { colour: fontDefinition.colour }),
|
|
826
|
+
...(fontDefinition?.borderColour === undefined
|
|
827
|
+
? {}
|
|
828
|
+
: { borderColour: fontDefinition.borderColour }),
|
|
815
829
|
glyphLines,
|
|
816
830
|
overflowX: width > 0 && measuredWidth > width + 0.01,
|
|
817
831
|
overflowY: height > 0 && measuredHeight > height + 0.01,
|
|
@@ -824,6 +838,8 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
824
838
|
addFidelity(fidelity, 'approximated', 'font_metrics', `No supplied font metrics for ${fontName ?? '<default>'}.`, definition);
|
|
825
839
|
else
|
|
826
840
|
addFidelity(fidelity, 'modelled', 'font_metrics', `${wrapped.metricSource} metrics for ${fontName ?? '<default>'}.`, definition);
|
|
841
|
+
if (fontDefinition?.colour !== undefined || fontDefinition?.borderColour !== undefined)
|
|
842
|
+
addFidelity(fidelity, 'modelled', 'font_definition_style', `Font ${fontDefinition.name} uses its declared face and colour styling.`, definition);
|
|
827
843
|
const missingGlyphs = [
|
|
828
844
|
...new Set([...wrapped.missingGlyphs, ...glyphLines.flatMap((line) => line.missingGlyphs)]),
|
|
829
845
|
].slice(0, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES);
|
|
@@ -62,6 +62,22 @@ 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 bitmapTintMarkup(id, colour, glyphs, rect) {
|
|
66
|
+
return `<defs><mask id="${id}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" style="mask-type:alpha" ${rectAttributes(rect)}><g>${glyphs}</g></mask></defs><rect ${rectAttributes(rect)} fill="${colour}" mask="url(#${id})"/>`;
|
|
67
|
+
}
|
|
68
|
+
function actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale) {
|
|
69
|
+
if (glyphLine.source === 'fontkit-path')
|
|
70
|
+
return glyphLine.glyphs
|
|
71
|
+
.filter((glyph) => glyph.kind === 'outline')
|
|
72
|
+
.map((glyph) => `<use href="#${outlineDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y)}) scale(${finite(glyph.scale * horizontalScale)} ${finite(-glyph.scale)})"/>`)
|
|
73
|
+
.join('');
|
|
74
|
+
if (glyphLine.source === 'bmfont-atlas')
|
|
75
|
+
return glyphLine.glyphs
|
|
76
|
+
.filter((glyph) => glyph.kind === 'bitmap')
|
|
77
|
+
.map((glyph) => `<use href="#${bitmapDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y - glyphLine.baseline)}) scale(${finite(horizontalScale)} 1)"/>`)
|
|
78
|
+
.join('');
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
65
81
|
function renderText(element, toolText) {
|
|
66
82
|
const text = element.text;
|
|
67
83
|
if (text === undefined || text.lines.length === 0)
|
|
@@ -102,6 +118,30 @@ function renderText(element, toolText) {
|
|
|
102
118
|
})
|
|
103
119
|
.join('');
|
|
104
120
|
const colourRuns = text.colourRuns?.[index] ?? [];
|
|
121
|
+
if (colourRuns.length > 0 &&
|
|
122
|
+
glyphLine !== undefined &&
|
|
123
|
+
glyphLine.source !== 'deterministic-fallback') {
|
|
124
|
+
const definitions = [];
|
|
125
|
+
const renderedRuns = colourRuns.map((run, runIndex) => {
|
|
126
|
+
const key = `${element.id}:${index}:${runIndex}:${run.colour}`;
|
|
127
|
+
const clipId = `gui-font-run-clip-${sha256Bytes(key).slice(0, 20)}`;
|
|
128
|
+
const runX = originX + run.offsetX * horizontalScale;
|
|
129
|
+
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>`);
|
|
130
|
+
const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
|
|
131
|
+
if (glyphLine.source === 'bmfont-atlas') {
|
|
132
|
+
const maskId = `gui-font-mask-${sha256Bytes(key).slice(0, 20)}`;
|
|
133
|
+
const runRect = {
|
|
134
|
+
x: runX - 0.25,
|
|
135
|
+
y: lineTop,
|
|
136
|
+
width: run.width * horizontalScale + 0.5,
|
|
137
|
+
height: text.lineHeight,
|
|
138
|
+
};
|
|
139
|
+
return `<g clip-path="url(#${clipId})" data-font-colour="${run.colour}">${bitmapTintMarkup(maskId, run.colour, glyphs, runRect)}</g>`;
|
|
140
|
+
}
|
|
141
|
+
return `<g clip-path="url(#${clipId})" fill="${run.colour}" stroke="${text.borderColour ?? '#12151a'}" data-font-colour="${run.colour}">${glyphs}</g>`;
|
|
142
|
+
});
|
|
143
|
+
return `<g data-hoi4-colour-runs="true" data-font-sha256="${glyphLine.sourceHash}"><defs>${definitions.join('')}</defs>${renderedRuns.join('')}${inlineIconMarkup}</g>`;
|
|
144
|
+
}
|
|
105
145
|
if (colourRuns.length > 0) {
|
|
106
146
|
return `<g data-hoi4-colour-runs="true">${colourRuns
|
|
107
147
|
.map((run) => toolText.render(run.text, {
|
|
@@ -109,31 +149,30 @@ function renderText(element, toolText) {
|
|
|
109
149
|
y: baseline,
|
|
110
150
|
fontSize: text.fontSize,
|
|
111
151
|
fill: run.colour,
|
|
112
|
-
stroke: '#12151a',
|
|
152
|
+
stroke: text.borderColour ?? '#12151a',
|
|
113
153
|
strokeWidth: 0.6,
|
|
114
154
|
...(run.width > 0 ? { targetWidth: run.width * horizontalScale } : {}),
|
|
115
155
|
}))
|
|
116
156
|
.join('')}${inlineIconMarkup}</g>`;
|
|
117
157
|
}
|
|
118
158
|
if (glyphLine?.source === 'fontkit-path') {
|
|
119
|
-
return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphLine
|
|
120
|
-
.filter((glyph) => glyph.kind === 'outline')
|
|
121
|
-
.map((glyph) => `<use href="#${outlineDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y)}) scale(${finite(glyph.scale * horizontalScale)} ${finite(-glyph.scale)})"/>`)
|
|
122
|
-
.join('')}${inlineIconMarkup}</g>`;
|
|
159
|
+
return `<g data-font-sha256="${glyphLine.sourceHash}" fill="${text.colour ?? '#f5f2e8'}" stroke="${text.borderColour ?? '#12151a'}" data-font-colour="${text.colour ?? '#f5f2e8'}">${actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale)}${inlineIconMarkup}</g>`;
|
|
123
160
|
}
|
|
124
161
|
if (glyphLine?.source === 'bmfont-atlas') {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
162
|
+
const glyphs = actualGlyphMarkup(glyphLine, originX, baseline, horizontalScale);
|
|
163
|
+
if (text.colour === undefined)
|
|
164
|
+
return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphs}${inlineIconMarkup}</g>`;
|
|
165
|
+
const maskId = `gui-font-mask-${sha256Bytes(`${element.id}:${index}:${text.colour}`).slice(0, 20)}`;
|
|
166
|
+
const lineRect = { x: originX, y: lineTop, width, height: text.lineHeight };
|
|
167
|
+
return `<g data-font-sha256="${glyphLine.sourceHash}" data-font-colour="${text.colour}">${bitmapTintMarkup(maskId, text.colour, glyphs, lineRect)}${inlineIconMarkup}</g>`;
|
|
129
168
|
}
|
|
130
169
|
const visibleLine = line.replace(/[\uE000-\uF8FF]/gu, ' ');
|
|
131
170
|
return `<g>${toolText.render(visibleLine, {
|
|
132
171
|
x: originX,
|
|
133
172
|
y: baseline,
|
|
134
173
|
fontSize: text.fontSize,
|
|
135
|
-
fill: '#f5f2e8',
|
|
136
|
-
stroke: '#12151a',
|
|
174
|
+
fill: text.colour ?? '#f5f2e8',
|
|
175
|
+
stroke: text.borderColour ?? '#12151a',
|
|
137
176
|
strokeWidth: 0.6,
|
|
138
177
|
...(width > 0 ? { targetWidth: width } : {}),
|
|
139
178
|
})}${inlineIconMarkup}</g>`;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { sha256Bytes } from '../core/canonical.js';
|
|
2
|
+
import { parsePreviewScenario } from './scenario.js';
|
|
3
|
+
const countryNames = ['Germany', 'France', 'Poland', 'Italy', 'Brazil', 'India', 'Canada'];
|
|
4
|
+
const countryAdjectives = [
|
|
5
|
+
'German',
|
|
6
|
+
'French',
|
|
7
|
+
'Polish',
|
|
8
|
+
'Italian',
|
|
9
|
+
'Brazilian',
|
|
10
|
+
'Indian',
|
|
11
|
+
'Canadian',
|
|
12
|
+
];
|
|
13
|
+
const leaderNames = ['Anna Keller', 'Michael Byrne', 'Elena Rossi', 'Jan Kowalski', 'Rafael Silva'];
|
|
14
|
+
const variedStates = [
|
|
15
|
+
'normal',
|
|
16
|
+
'selected',
|
|
17
|
+
'active',
|
|
18
|
+
'warning',
|
|
19
|
+
'disabled',
|
|
20
|
+
'completed',
|
|
21
|
+
];
|
|
22
|
+
class ScenarioRandom {
|
|
23
|
+
#state;
|
|
24
|
+
constructor(seed) {
|
|
25
|
+
this.#state = Number.parseInt(sha256Bytes(seed).slice(0, 8), 16) || 0x6d2b_79f5;
|
|
26
|
+
}
|
|
27
|
+
next() {
|
|
28
|
+
this.#state = (this.#state + 0x6d2b_79f5) >>> 0;
|
|
29
|
+
let value = this.#state;
|
|
30
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
31
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
32
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4_294_967_296;
|
|
33
|
+
}
|
|
34
|
+
boolean(probability) {
|
|
35
|
+
return this.next() < probability;
|
|
36
|
+
}
|
|
37
|
+
integer(minimum, maximum) {
|
|
38
|
+
return Math.floor(this.next() * (maximum - minimum + 1)) + minimum;
|
|
39
|
+
}
|
|
40
|
+
pick(values) {
|
|
41
|
+
return values[Math.min(values.length - 1, Math.floor(this.next() * values.length))];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function relevantElements(graph, windowName) {
|
|
45
|
+
const root = graph.elements.find((element) => element.parentId === undefined && element.name === windowName);
|
|
46
|
+
if (root === undefined)
|
|
47
|
+
return graph.elements.filter(({ name }) => name === windowName);
|
|
48
|
+
const byId = new Map(graph.elements.map((element) => [element.id, element]));
|
|
49
|
+
const pending = [root.id];
|
|
50
|
+
const selected = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
while (pending.length > 0) {
|
|
53
|
+
const id = pending.pop();
|
|
54
|
+
if (seen.has(id))
|
|
55
|
+
continue;
|
|
56
|
+
seen.add(id);
|
|
57
|
+
const element = byId.get(id);
|
|
58
|
+
if (element === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
selected.push(element);
|
|
61
|
+
pending.push(...element.childIds);
|
|
62
|
+
}
|
|
63
|
+
return selected;
|
|
64
|
+
}
|
|
65
|
+
function stringsIn(value) {
|
|
66
|
+
if (typeof value === 'string')
|
|
67
|
+
return [value];
|
|
68
|
+
if (typeof value !== 'object')
|
|
69
|
+
return [];
|
|
70
|
+
return Object.values(value).flatMap(stringsIn);
|
|
71
|
+
}
|
|
72
|
+
function localisationText(graph, language, key) {
|
|
73
|
+
return graph.localisation.find((entry) => entry.language === language && entry.key === key)
|
|
74
|
+
?.value;
|
|
75
|
+
}
|
|
76
|
+
function sourceTexts(graph, elements, language, scripted) {
|
|
77
|
+
const values = elements.flatMap((element) => Object.values(element.attributes).flatMap(stringsIn));
|
|
78
|
+
const propertyValues = scripted.flatMap((definition) => definition.propertyDefinitions.flatMap((property) => Object.values(property.attributes).flatMap(stringsIn)));
|
|
79
|
+
return [...values, ...propertyValues].flatMap((value) => [
|
|
80
|
+
value,
|
|
81
|
+
localisationText(graph, language, value) ?? '',
|
|
82
|
+
]);
|
|
83
|
+
}
|
|
84
|
+
function dynamicTokens(texts) {
|
|
85
|
+
const numeric = new Set();
|
|
86
|
+
const textual = new Set();
|
|
87
|
+
for (const text of texts) {
|
|
88
|
+
for (const match of text.matchAll(/\[\?([^\]|]+)(?:\|[^\]]+)?\]/gu)) {
|
|
89
|
+
const key = match[1] ?? '';
|
|
90
|
+
if (/(?:^|\.)Get[A-Za-z0-9_]+$/u.test(key))
|
|
91
|
+
textual.add(key);
|
|
92
|
+
else
|
|
93
|
+
numeric.add(key);
|
|
94
|
+
}
|
|
95
|
+
for (const match of text.matchAll(/\[([A-Za-z][A-Za-z0-9_.:-]+)\]/gu)) {
|
|
96
|
+
const key = match[1] ?? '';
|
|
97
|
+
if (key !== 'X' && key !== 'dynamic_loc')
|
|
98
|
+
textual.add(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
numeric: [...numeric].sort(),
|
|
103
|
+
textual: [...textual].sort(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function plausibleText(key, random, samples) {
|
|
107
|
+
const lower = key.toLocaleLowerCase('en-US');
|
|
108
|
+
if (lower.includes('adjective'))
|
|
109
|
+
return random.pick(countryAdjectives);
|
|
110
|
+
if (lower.includes('leader') || lower.includes('character'))
|
|
111
|
+
return random.pick(leaderNames);
|
|
112
|
+
if (lower.includes('country') || lower.endsWith('getname') || lower.includes('.getname'))
|
|
113
|
+
return random.pick(countryNames);
|
|
114
|
+
return random.pick(samples);
|
|
115
|
+
}
|
|
116
|
+
function generatedNumber(options, random) {
|
|
117
|
+
const value = options.numericMinimum + random.next() * (options.numericMaximum - options.numericMinimum);
|
|
118
|
+
return options.integerValues ? Math.round(value) : Math.round(value * 100) / 100;
|
|
119
|
+
}
|
|
120
|
+
function setIfMissing(target, key, value) {
|
|
121
|
+
target[key] ??= value;
|
|
122
|
+
const shortKey = key.slice(key.lastIndexOf('.') + 1);
|
|
123
|
+
target[shortKey] ??= value;
|
|
124
|
+
}
|
|
125
|
+
function generatedRows(count, random, options) {
|
|
126
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
127
|
+
const name = plausibleText('GetName', random, options.textSamples);
|
|
128
|
+
const value = generatedNumber(options, random);
|
|
129
|
+
return {
|
|
130
|
+
index: index + 1,
|
|
131
|
+
id: index + 1,
|
|
132
|
+
label: name,
|
|
133
|
+
name,
|
|
134
|
+
GetName: name,
|
|
135
|
+
value,
|
|
136
|
+
amount: value,
|
|
137
|
+
percentage: Math.max(0, Math.min(100, value)),
|
|
138
|
+
selected: index === 0,
|
|
139
|
+
enabled: random.boolean(options.trueProbability),
|
|
140
|
+
};
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function generateGuiPreviewScenarios(graph, windowName, base, options) {
|
|
144
|
+
if (!options.enabled)
|
|
145
|
+
return [];
|
|
146
|
+
const elements = relevantElements(graph, windowName);
|
|
147
|
+
const scripted = graph.scriptedGuis.filter((definition) => definition.windowName === windowName || definition.parentWindowName === windowName);
|
|
148
|
+
const tokens = dynamicTokens(sourceTexts(graph, elements, base.language, scripted));
|
|
149
|
+
const dynamicLists = [...new Set(scripted.flatMap(({ dynamicLists }) => dynamicLists))].sort();
|
|
150
|
+
const visibilityTargets = new Set(scripted.flatMap((definition) => definition.propertyDefinitions
|
|
151
|
+
.filter((property) => Object.keys(property.attributes).some((key) => key.toLocaleLowerCase('en-US') === 'visible'))
|
|
152
|
+
.map(({ elementName }) => elementName)));
|
|
153
|
+
const enabledTargets = new Set(scripted.flatMap((definition) => [
|
|
154
|
+
...definition.triggerDefinitions.map(({ elementName }) => elementName),
|
|
155
|
+
...definition.propertyDefinitions
|
|
156
|
+
.filter((property) => Object.keys(property.attributes).some((key) => key.toLocaleLowerCase('en-US') === 'enabled'))
|
|
157
|
+
.map(({ elementName }) => elementName),
|
|
158
|
+
]));
|
|
159
|
+
const spritesByName = new Map(graph.sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
|
|
160
|
+
const sourceSeed = options.seed === 'auto' ? `${windowName}:${base.id}:${hashSourceGraph(graph)}` : options.seed;
|
|
161
|
+
return Array.from({ length: options.count }, (_unused, scenarioIndex) => {
|
|
162
|
+
const random = new ScenarioRandom(`${sourceSeed}:${scenarioIndex}`);
|
|
163
|
+
const values = { ...base.values };
|
|
164
|
+
const lists = Object.fromEntries(Object.entries(base.lists).map(([key, rows]) => [key, rows.map((row) => ({ ...row }))]));
|
|
165
|
+
const visibility = { ...base.visibility };
|
|
166
|
+
const elementStates = { ...base.elementStates };
|
|
167
|
+
const selectedFrames = { ...base.selectedFrames };
|
|
168
|
+
for (const key of tokens.numeric)
|
|
169
|
+
setIfMissing(values, key, generatedNumber(options, random));
|
|
170
|
+
for (const key of tokens.textual)
|
|
171
|
+
setIfMissing(values, key, plausibleText(key, random, options.textSamples));
|
|
172
|
+
for (const element of elements) {
|
|
173
|
+
if (values[element.name] === undefined && /progressbar/iu.test(element.elementType))
|
|
174
|
+
values[element.name] = generatedNumber(options, random);
|
|
175
|
+
if (visibilityTargets.has(element.name) && visibility[element.name] === undefined)
|
|
176
|
+
visibility[element.name] =
|
|
177
|
+
options.visibility === 'show-all' || random.boolean(options.trueProbability);
|
|
178
|
+
elementStates[element.name] ??= enabledTargets.has(element.name)
|
|
179
|
+
? random.boolean(options.trueProbability)
|
|
180
|
+
? options.elementStates === 'normal'
|
|
181
|
+
? 'normal'
|
|
182
|
+
: random.pick(variedStates)
|
|
183
|
+
: 'disabled'
|
|
184
|
+
: options.elementStates === 'normal'
|
|
185
|
+
? 'normal'
|
|
186
|
+
: random.pick(variedStates);
|
|
187
|
+
const spriteName = [
|
|
188
|
+
element.attributes.spriteType,
|
|
189
|
+
element.attributes.quadTextureSprite,
|
|
190
|
+
element.attributes.background,
|
|
191
|
+
].find((candidate) => typeof candidate === 'string');
|
|
192
|
+
const frameCount = spriteName === undefined
|
|
193
|
+
? 1
|
|
194
|
+
: (spritesByName.get(spriteName.toLocaleLowerCase('en-US'))?.frameCount ?? 1);
|
|
195
|
+
if (Number.isFinite(frameCount) &&
|
|
196
|
+
frameCount > 1 &&
|
|
197
|
+
selectedFrames[element.name] === undefined)
|
|
198
|
+
selectedFrames[element.name] = random.integer(0, Math.trunc(frameCount) - 1);
|
|
199
|
+
}
|
|
200
|
+
for (const listName of dynamicLists) {
|
|
201
|
+
if (lists[listName] !== undefined)
|
|
202
|
+
continue;
|
|
203
|
+
const count = random.integer(options.listRowsMinimum, options.listRowsMaximum);
|
|
204
|
+
lists[listName] = generatedRows(count, random, options);
|
|
205
|
+
}
|
|
206
|
+
return parsePreviewScenario({
|
|
207
|
+
...base,
|
|
208
|
+
id: `${base.id}-${options.idPrefix}-${scenarioIndex + 1}`,
|
|
209
|
+
description: `Generated plausible scenario ${scenarioIndex + 1} (seed ${sourceSeed})`,
|
|
210
|
+
values,
|
|
211
|
+
lists,
|
|
212
|
+
visibility,
|
|
213
|
+
elementStates,
|
|
214
|
+
selectedFrames,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function hashSourceGraph(graph) {
|
|
219
|
+
return sha256Bytes(Object.entries(graph.sourceHashes)
|
|
220
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
221
|
+
.map(([path, hash]) => `${path}:${hash}`)
|
|
222
|
+
.join('\n'));
|
|
223
|
+
}
|
|
@@ -193,9 +193,53 @@ export const GuiPreviewScenarioSchema = z.preprocess((input) => {
|
|
|
193
193
|
assertGuiScenarioInputBudget(input);
|
|
194
194
|
return input;
|
|
195
195
|
}, GuiPreviewScenarioBodySchema);
|
|
196
|
+
export const GuiGeneratedScenarioOptionsSchema = z
|
|
197
|
+
.object({
|
|
198
|
+
enabled: z.boolean().default(true),
|
|
199
|
+
count: z.number().int().min(1).max(16).default(1),
|
|
200
|
+
seed: z.string().min(1).max(256).default('auto'),
|
|
201
|
+
idPrefix: z
|
|
202
|
+
.string()
|
|
203
|
+
.min(1)
|
|
204
|
+
.max(64)
|
|
205
|
+
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
|
|
206
|
+
.default('generated'),
|
|
207
|
+
preservePlaceholder: z.boolean().default(true),
|
|
208
|
+
numericMinimum: z.number().min(-1_000_000_000).max(1_000_000_000).default(0),
|
|
209
|
+
numericMaximum: z.number().min(-1_000_000_000).max(1_000_000_000).default(100),
|
|
210
|
+
integerValues: z.boolean().default(true),
|
|
211
|
+
listRowsMinimum: z.number().int().min(0).max(100).default(1),
|
|
212
|
+
listRowsMaximum: z.number().int().min(0).max(100).default(5),
|
|
213
|
+
trueProbability: z.number().min(0).max(1).default(0.7),
|
|
214
|
+
visibility: z.enum(['show-all', 'varied']).default('varied'),
|
|
215
|
+
elementStates: z.enum(['normal', 'varied']).default('normal'),
|
|
216
|
+
textSamples: z
|
|
217
|
+
.array(z.string().min(1).max(128))
|
|
218
|
+
.min(1)
|
|
219
|
+
.max(64)
|
|
220
|
+
.default(['Operational', 'Ready', 'Stable', 'Available', 'In Progress']),
|
|
221
|
+
})
|
|
222
|
+
.strict()
|
|
223
|
+
.superRefine((options, context) => {
|
|
224
|
+
if (options.numericMaximum < options.numericMinimum)
|
|
225
|
+
context.addIssue({
|
|
226
|
+
code: 'custom',
|
|
227
|
+
path: ['numericMaximum'],
|
|
228
|
+
message: 'numericMaximum must be greater than or equal to numericMinimum',
|
|
229
|
+
});
|
|
230
|
+
if (options.listRowsMaximum < options.listRowsMinimum)
|
|
231
|
+
context.addIssue({
|
|
232
|
+
code: 'custom',
|
|
233
|
+
path: ['listRowsMaximum'],
|
|
234
|
+
message: 'listRowsMaximum must be greater than or equal to listRowsMinimum',
|
|
235
|
+
});
|
|
236
|
+
});
|
|
196
237
|
export function parsePreviewScenario(input) {
|
|
197
238
|
return GuiPreviewScenarioSchema.parse(input);
|
|
198
239
|
}
|
|
199
240
|
export function defaultPreviewScenario(id = 'default') {
|
|
200
241
|
return parsePreviewScenario({ id });
|
|
201
242
|
}
|
|
243
|
+
export function parseGeneratedScenarioOptions(input) {
|
|
244
|
+
return GuiGeneratedScenarioOptionsSchema.parse(input);
|
|
245
|
+
}
|
|
@@ -188,6 +188,35 @@ function boolScalar(block, ...keys) {
|
|
|
188
188
|
return false;
|
|
189
189
|
return undefined;
|
|
190
190
|
}
|
|
191
|
+
function paradoxColour(value) {
|
|
192
|
+
if (value === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
const match = /^0x([0-9a-f]{8})$/iu.exec(value.trim());
|
|
195
|
+
if (match === null)
|
|
196
|
+
return undefined;
|
|
197
|
+
const argb = match[1];
|
|
198
|
+
return `#${argb.slice(2)}${argb.slice(0, 2)}`.toLowerCase();
|
|
199
|
+
}
|
|
200
|
+
function textColoursFrom(block) {
|
|
201
|
+
const textColours = childBlocks(block, 'textcolors')[0];
|
|
202
|
+
if (textColours === undefined)
|
|
203
|
+
return {};
|
|
204
|
+
const result = {};
|
|
205
|
+
for (const entry of assignments(textColours)) {
|
|
206
|
+
if (entry.value.type !== 'block')
|
|
207
|
+
continue;
|
|
208
|
+
const channels = entry.value.entries
|
|
209
|
+
.filter((channel) => channel.type === 'scalar')
|
|
210
|
+
.map((channel) => Number(channel.value));
|
|
211
|
+
if (channels.length < 3 || channels.slice(0, 3).some((channel) => !Number.isFinite(channel)))
|
|
212
|
+
continue;
|
|
213
|
+
const [red, green, blue] = channels.map((channel) => Math.max(0, Math.min(255, Math.round(channel))));
|
|
214
|
+
result[entry.key.value] = `#${red.toString(16).padStart(2, '0')}${green
|
|
215
|
+
.toString(16)
|
|
216
|
+
.padStart(2, '0')}${blue.toString(16).padStart(2, '0')}`;
|
|
217
|
+
}
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
191
220
|
function sizeFrom(block) {
|
|
192
221
|
const size = childBlocks(block, 'size')[0];
|
|
193
222
|
if (size === undefined)
|
|
@@ -425,6 +454,9 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
425
454
|
const languages = childBlocks(child, 'languages').flatMap((languageBlock) => languageBlock.entries
|
|
426
455
|
.filter((entry) => entry.type === 'scalar')
|
|
427
456
|
.map((entry) => entry.value));
|
|
457
|
+
const colour = paradoxColour(firstScalarInsensitive(child, 'color', 'colour'));
|
|
458
|
+
const borderColour = paradoxColour(firstScalarInsensitive(child, 'border_color', 'borderColor', 'border_colour'));
|
|
459
|
+
const textColours = textColoursFrom(child);
|
|
428
460
|
const id = deterministicId('gui_font', { path: file.displayPath, name });
|
|
429
461
|
const font = {
|
|
430
462
|
id,
|
|
@@ -435,6 +467,9 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
435
467
|
override: assignment.key.value === 'bitmapfont_override',
|
|
436
468
|
languages,
|
|
437
469
|
assetPaths,
|
|
470
|
+
...(colour === undefined ? {} : { colour }),
|
|
471
|
+
...(borderColour === undefined ? {} : { borderColour }),
|
|
472
|
+
...(Object.keys(textColours).length === 0 ? {} : { textColours }),
|
|
438
473
|
rawSource: raw(document, assignment),
|
|
439
474
|
};
|
|
440
475
|
addDomainEntry(fonts, font, 'font');
|
|
@@ -449,6 +484,9 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
449
484
|
override: font.override,
|
|
450
485
|
languages,
|
|
451
486
|
assetPaths,
|
|
487
|
+
...(colour === undefined ? {} : { colour }),
|
|
488
|
+
...(borderColour === undefined ? {} : { borderColour }),
|
|
489
|
+
...(Object.keys(textColours).length === 0 ? {} : { textColours }),
|
|
452
490
|
},
|
|
453
491
|
});
|
|
454
492
|
addEdge(edges, 'contains', fileNodeId, id, true, {}, font.location);
|
|
@@ -19,7 +19,8 @@ import { projectGuiGraphForArtifact } from './inspection-artifact.js';
|
|
|
19
19
|
import { buildGuiScene } from './layout.js';
|
|
20
20
|
import { GUI_VALIDATION_MAX_DIAGNOSTICS } from './limits.js';
|
|
21
21
|
import { compareGuiImages, galleryDimensions, renderGallerySvg, renderGuiScene, } from './renderer.js';
|
|
22
|
-
import {
|
|
22
|
+
import { generateGuiPreviewScenarios } from './scenario-generator.js';
|
|
23
|
+
import { parseGeneratedScenarioOptions, parsePreviewScenario } from './scenario.js';
|
|
23
24
|
import { buildGuiSourceGraph } from './source-graph.js';
|
|
24
25
|
import { assertGuiSourcePatchesSafe } from './source-patch.js';
|
|
25
26
|
import { validateGuiScene, validateResolutionDrift, validateStateMatrix } from './validators.js';
|
|
@@ -702,10 +703,24 @@ export class ScriptedGuiStudio {
|
|
|
702
703
|
}
|
|
703
704
|
async lint(input) {
|
|
704
705
|
input.signal?.throwIfAborted();
|
|
705
|
-
const
|
|
706
|
-
const
|
|
706
|
+
const placeholderScenario = parsePreviewScenario(input.scenario);
|
|
707
|
+
const explicitRelatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
708
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios]));
|
|
709
|
+
const generatedOptions = input.generatedScenarios === undefined
|
|
710
|
+
? undefined
|
|
711
|
+
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
712
|
+
const generatedScenarios = generatedOptions === undefined
|
|
713
|
+
? []
|
|
714
|
+
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
715
|
+
const scenario = generatedScenarios[0] ?? placeholderScenario;
|
|
716
|
+
const relatedScenarios = [
|
|
717
|
+
...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
|
|
718
|
+
? [placeholderScenario]
|
|
719
|
+
: []),
|
|
720
|
+
...generatedScenarios.slice(1),
|
|
721
|
+
...explicitRelatedScenarios,
|
|
722
|
+
];
|
|
707
723
|
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
708
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [scenario.language, ...relatedScenarios.map(({ language }) => language)], scenarioSpriteNames([scenario, ...relatedScenarios]));
|
|
709
724
|
const budget = new RenderBudget();
|
|
710
725
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
711
726
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
@@ -756,15 +771,37 @@ export class ScriptedGuiStudio {
|
|
|
756
771
|
input.signal?.throwIfAborted();
|
|
757
772
|
const budget = new RenderBudget();
|
|
758
773
|
const workspace = this.resolver.get(input.workspaceId, input.principal);
|
|
759
|
-
const
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
774
|
+
const placeholderScenario = parsePreviewScenario(input.scenario);
|
|
775
|
+
const explicitRelatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
776
|
+
const requestedBaselineScenario = input.comparisonScenario === undefined
|
|
777
|
+
? undefined
|
|
778
|
+
: parsePreviewScenario(input.comparisonScenario);
|
|
763
779
|
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
...
|
|
767
|
-
], scenarioSpriteNames([
|
|
780
|
+
placeholderScenario.language,
|
|
781
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario.language]),
|
|
782
|
+
...explicitRelatedScenarios.map(({ language }) => language),
|
|
783
|
+
], scenarioSpriteNames([
|
|
784
|
+
placeholderScenario,
|
|
785
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
786
|
+
...explicitRelatedScenarios,
|
|
787
|
+
]));
|
|
788
|
+
const generatedOptions = input.generatedScenarios === undefined
|
|
789
|
+
? undefined
|
|
790
|
+
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
791
|
+
const generatedScenarios = generatedOptions === undefined
|
|
792
|
+
? []
|
|
793
|
+
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
794
|
+
const scenario = generatedScenarios[0] ?? placeholderScenario;
|
|
795
|
+
const relatedScenarios = [
|
|
796
|
+
...(generatedOptions?.preservePlaceholder === true && generatedScenarios.length > 0
|
|
797
|
+
? [placeholderScenario]
|
|
798
|
+
: []),
|
|
799
|
+
...generatedScenarios.slice(1),
|
|
800
|
+
...explicitRelatedScenarios,
|
|
801
|
+
];
|
|
802
|
+
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
803
|
+
const baselineScenario = requestedBaselineScenario ??
|
|
804
|
+
parsePreviewScenario({ ...scenario, id: `${scenario.id}-comparison`, state: 'normal' });
|
|
768
805
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
769
806
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
770
807
|
const render = await renderGuiScene(scene, undefined, input.signal, budget);
|
|
@@ -4,7 +4,7 @@ import { canonicalJson, compareCodeUnits, hashCanonical } from '../../core/canon
|
|
|
4
4
|
import { renderDimensionViolation, RenderBudget, RENDER_MAX_DIMENSION, } from '../../core/render-budget.js';
|
|
5
5
|
import { emptyServiceResult, ServiceError } from '../../core/result.js';
|
|
6
6
|
import { SOURCE_MAX_BYTES } from '../../core/source/index.js';
|
|
7
|
-
import { GuiHelperDocumentSchema, GuiPreviewScenarioSchema, GUI_TEXT_PACKAGE_MAX_FILES, ScriptedGuiStudio, compileGuiHelpers, } from '../../gui/index.js';
|
|
7
|
+
import { GuiHelperDocumentSchema, GuiGeneratedScenarioOptionsSchema, GuiPreviewScenarioSchema, GUI_TEXT_PACKAGE_MAX_FILES, ScriptedGuiStudio, compileGuiHelpers, } from '../../gui/index.js';
|
|
8
8
|
import { encodeGuiInspectionArtifact, projectGuiGraphForArtifact, } from '../../gui/inspection-artifact.js';
|
|
9
9
|
import { workspaceIdSchema, workspaceRelativePathSchema } from '../../schemas/common.js';
|
|
10
10
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
@@ -92,6 +92,7 @@ const previewStateSchema = z.enum([
|
|
|
92
92
|
'missing-localisation',
|
|
93
93
|
]);
|
|
94
94
|
const compactGuiScenarioSchema = compactValidatedInputSchema(GuiPreviewScenarioSchema, `Complete GUI preview scenario: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/gui.md`);
|
|
95
|
+
const compactGeneratedScenarioOptionsSchema = compactValidatedInputSchema(GuiGeneratedScenarioOptionsSchema, `Options: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/gui.md`);
|
|
95
96
|
const compactGuiHelperSchema = compactValidatedInputSchema(GuiHelperDocumentSchema, `Complete GUI helper document: https://github.com/klimPaskov/hoi4-agent-tools/blob/v${PACKAGE_VERSION}/docs/gui.md`);
|
|
96
97
|
const guiBaseInput = z
|
|
97
98
|
.object({
|
|
@@ -106,6 +107,7 @@ const guiInspectInput = z
|
|
|
106
107
|
windowName: z.string().min(1).max(256).optional(),
|
|
107
108
|
scenario: compactGuiScenarioSchema.optional(),
|
|
108
109
|
relatedScenarios: z.array(compactGuiScenarioSchema).max(32).optional(),
|
|
110
|
+
generatedScenarios: compactGeneratedScenarioOptionsSchema.optional(),
|
|
109
111
|
})
|
|
110
112
|
.strict()
|
|
111
113
|
.superRefine((value, context) => {
|
|
@@ -122,6 +124,13 @@ const guiInspectInput = z
|
|
|
122
124
|
message: 'relatedScenarios requires a window scenario',
|
|
123
125
|
});
|
|
124
126
|
}
|
|
127
|
+
if (value.generatedScenarios !== undefined && value.scenario === undefined) {
|
|
128
|
+
context.addIssue({
|
|
129
|
+
code: 'custom',
|
|
130
|
+
path: ['generatedScenarios'],
|
|
131
|
+
message: 'generatedScenarios requires a window scenario',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
125
134
|
});
|
|
126
135
|
const resolutionSchema = z
|
|
127
136
|
.object({
|
|
@@ -147,11 +156,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
147
156
|
const studio = new ScriptedGuiStudio(engine);
|
|
148
157
|
server.registerTool('hoi4.gui_inspect', {
|
|
149
158
|
title: 'Inspect scripted GUI',
|
|
150
|
-
description: 'Inspect GUI
|
|
159
|
+
description: 'Inspect GUI sources and scenarios.',
|
|
151
160
|
inputSchema: guiInspectInput,
|
|
152
161
|
outputSchema: guiScanOutputSchema,
|
|
153
162
|
annotations: artifactProducing,
|
|
154
|
-
}, async ({ workspaceId: requestedWorkspaceId, windowName, scenario, relatedScenarios }, extra) => {
|
|
163
|
+
}, async ({ workspaceId: requestedWorkspaceId, windowName, scenario, relatedScenarios, generatedScenarios, }, extra) => {
|
|
155
164
|
const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
|
|
156
165
|
try {
|
|
157
166
|
const progress = progressReporter(extra);
|
|
@@ -166,6 +175,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
166
175
|
windowName,
|
|
167
176
|
scenario,
|
|
168
177
|
...(relatedScenarios === undefined ? {} : { relatedScenarios }),
|
|
178
|
+
generatedScenarios: generatedScenarios ?? {},
|
|
169
179
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
170
180
|
signal: progress.signal,
|
|
171
181
|
});
|
|
@@ -285,6 +295,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
285
295
|
...(input.relatedScenarios === undefined
|
|
286
296
|
? {}
|
|
287
297
|
: { relatedScenarios: input.relatedScenarios }),
|
|
298
|
+
generatedScenarios: input.generatedScenarios ?? {},
|
|
288
299
|
...(input.comparisonScenario === undefined
|
|
289
300
|
? {}
|
|
290
301
|
: { comparisonScenario: input.comparisonScenario }),
|
|
@@ -333,10 +344,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
333
344
|
states: z.array(previewStateSchema).max(14).optional(),
|
|
334
345
|
resolutions: z.array(resolutionSchema).min(1).max(16).optional(),
|
|
335
346
|
relatedScenarios: z.array(compactGuiScenarioSchema).max(32).optional(),
|
|
347
|
+
generatedScenarios: compactGeneratedScenarioOptionsSchema.optional(),
|
|
336
348
|
comparisonScenario: compactGuiScenarioSchema.optional(),
|
|
337
349
|
})
|
|
338
350
|
.strict()
|
|
339
|
-
.superRefine(({ scenario, states, resolutions, relatedScenarios }, context) => {
|
|
351
|
+
.superRefine(({ scenario, states, resolutions, relatedScenarios, generatedScenarios }, context) => {
|
|
340
352
|
const budget = new RenderBudget();
|
|
341
353
|
try {
|
|
342
354
|
const stateCount = states?.length ?? 14;
|
|
@@ -351,7 +363,12 @@ export function registerGuiTools(server, engine, context) {
|
|
|
351
363
|
]) {
|
|
352
364
|
budget.reserve(resolution.width, resolution.height, 'GUI resolution variant');
|
|
353
365
|
}
|
|
354
|
-
const
|
|
366
|
+
const generatedCount = generatedScenarios?.enabled === false ? 0 : (generatedScenarios?.count ?? 1);
|
|
367
|
+
const scenarioCount = generatedCount === 0
|
|
368
|
+
? 1 + (relatedScenarios?.length ?? 0)
|
|
369
|
+
: generatedCount +
|
|
370
|
+
(generatedScenarios?.preservePlaceholder === false ? 0 : 1) +
|
|
371
|
+
(relatedScenarios?.length ?? 0);
|
|
355
372
|
const stateColumns = Math.min(3, Math.max(1, stateCount));
|
|
356
373
|
const stateRows = Math.max(1, Math.ceil(stateCount / stateColumns));
|
|
357
374
|
budget.reserve(stateColumns * 420, 46 + stateRows * 280, 'GUI state gallery');
|
|
@@ -374,7 +391,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
374
391
|
});
|
|
375
392
|
server.registerTool('hoi4.gui_render', {
|
|
376
393
|
title: 'Render scripted GUI artifacts',
|
|
377
|
-
description: 'Render
|
|
394
|
+
description: 'Render GUI states, resolutions, generated or explicit scenarios, hierarchy, comparisons, and diagnostics.',
|
|
378
395
|
inputSchema: renderInput,
|
|
379
396
|
outputSchema: guiRenderOutputSchema,
|
|
380
397
|
annotations: artifactProducing,
|
|
@@ -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.
|
|
3
|
+
export const PACKAGE_VERSION = '2.8.5';
|
|
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,12 +33,31 @@ 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. 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. HOI4 localisation colour runs such as `§Y` and `§R` tint those same native glyph masks with the exact palette colour instead of 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
|
|
|
40
40
|
Scripted-GUI composition follows `parent_scripted_gui` chains even when the linked child window is a top-level sibling in the `.gui` file. Literal `always = yes` and `always = no` visibility and click-enabled blocks are applied automatically. A scenario's `values` object supplies concrete runtime values in one place: variable names resolve numeric text, scripted-localisation names resolve dynamic labels, a progress-bar element name sets its fill value, and element keys ending in `.image`, `.frame`, `.x`, `.y`, `.visible`, or `.enabled` update that visual property. Scripted-GUI property expressions such as `image = "[GetMeterSprite]"` or `x = meter_fill_width` consume matching scenario values automatically. Dynamic lists instantiate their declared `entry_container` or `country_scope_entry_container`; each scenario row can select the country template with `countryScope`, choose an explicit `entryContainer`, and provide row-scoped text, image, frame, position, visibility, and enabled values.
|
|
41
41
|
|
|
42
|
+
`hoi4.gui_inspect` and `hoi4.gui_render` generate a plausible seeded runtime scenario by default. The generator discovers numeric and text tokens in the selected window, localisation, and scripted-GUI properties; fills meters; populates dynamic lists; chooses animated frames; varies condition-controlled visibility and enabled controls; and leaves values explicitly supplied by the caller unchanged. The same artifact gallery retains the unresolved placeholder scenario and every explicit `relatedScenarios` entry, so an agent can compare plausible data, exact authored cases, and `[dynamic_loc]` or `[X]` fallbacks in one result.
|
|
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. Set `enabled` to `false` when only the supplied scenario should render, and set `preservePlaceholder` to `false` when the fallback view is not needed.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"generatedScenarios": {
|
|
49
|
+
"seed": "chaos-meter-review",
|
|
50
|
+
"count": 3,
|
|
51
|
+
"numericMinimum": 0,
|
|
52
|
+
"numericMaximum": 100,
|
|
53
|
+
"listRowsMinimum": 2,
|
|
54
|
+
"listRowsMaximum": 8,
|
|
55
|
+
"trueProbability": 0.75,
|
|
56
|
+
"textSamples": ["Stable", "Escalating", "Critical"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
42
61
|
Preview scenarios can include exact layout expectations:
|
|
43
62
|
|
|
44
63
|
```json
|
|
@@ -70,10 +89,10 @@ GUI rewrites keep complete validation evidence in a linked JSON resource and ret
|
|
|
70
89
|
|
|
71
90
|
Large inspections, renders, and rewrites send periodic MCP progress heartbeats while a service stage is still running. Clients that request progress can reset their idle request timer from these notifications; there is no 180-second GUI operation limit in the server, and cancellation still follows the request signal.
|
|
72
91
|
|
|
73
|
-
##
|
|
92
|
+
## Scenario fidelity
|
|
74
93
|
|
|
75
94
|
The renderer does not run the game engine. Each render includes a fidelity report that separates fields it models from fields it approximates, ignores, cannot resolve, or does not support. Treat that report as part of the review.
|
|
76
95
|
|
|
77
|
-
|
|
96
|
+
The generated primary view replaces discovered numeric and text runtime tokens with plausible values. The retained fallback view renders 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.
|
|
78
97
|
|
|
79
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.
|
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.
|
|
4
|
+
"version": "2.8.5",
|
|
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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.8.5/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.
|
|
6
|
+
"version": "2.8.5",
|
|
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.
|
|
16
|
+
"version": "2.8.5",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|