hoi4-agent-tools 2.3.12 → 2.4.0
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 +15 -0
- package/README.md +4 -4
- package/dist/hoi4_agent_tools/focus/compact.js +39 -25
- package/dist/hoi4_agent_tools/gui/layout.js +12 -6
- package/dist/hoi4_agent_tools/gui/scenario.js +10 -0
- package/dist/hoi4_agent_tools/gui/studio.js +113 -8
- package/dist/hoi4_agent_tools/gui/validators.js +198 -11
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +13 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +25 -5
- package/docs/setup.md +1 -1
- package/package.json +13 -12
- 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 +57 -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
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- Added named value-driven scripted-GUI scenario matrices with deterministic SVG, PNG, JSON, and per-element visibility, movement, size, text, and frame-change evidence.
|
|
6
|
+
- Added scenario assertions for visible and hidden elements, background or panel containment, and exact label-to-control centering, with source-linked diagnostics across every supplied variant.
|
|
7
|
+
- Modelled native HOI4 button-text centering and added automatic diagnostics for visibly off-center labels and text or controls crossing a parent background boundary.
|
|
8
|
+
- Updated the production MCP SDK to 1.30.0, the official Inspector to 2.1.0, the Registry publisher to 1.8.1, and patched dependencies; the dependency audit reports no known vulnerabilities.
|
|
9
|
+
- Updated the Inspector harness for the current CLI entry point, result envelope, explicit environment forwarding, and longer connection startup budget.
|
|
10
|
+
- Generalized installed-game integration tests so mods without their own focus, event, or technology definitions still qualify and use the available vanilla-backed analysis surfaces.
|
|
11
|
+
|
|
12
|
+
## 2.3.13 - 2026-08-06
|
|
13
|
+
|
|
14
|
+
- Repacked sparse sibling fan-outs around their structural parent instead of preserving avoidable long prerequisite connectors.
|
|
15
|
+
- Moved exclusive descendant branches with their compacted sibling root while keeping shared convergence focuses anchored.
|
|
16
|
+
- Added direct branch-packing regressions and reverified the public 1,024-focus MCP rewrite workflow.
|
|
17
|
+
|
|
3
18
|
## 2.3.12 - 2026-08-05
|
|
4
19
|
|
|
5
20
|
- Made compact focus rewrites choose among preservation, row compression, linear-chain repair, sibling balancing, and combined strategies instead of forcing one aggressive transformation.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
7
|
- Focus trees: inspect structure and references, render layouts, create trees, and reorganize existing branches.
|
|
8
|
-
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render states; create or repair interface source.
|
|
8
|
+
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
|
|
9
9
|
- Maps: inspect provinces, states, regions, adjacency, supply, and railways; render layers; create and repair exact map data.
|
|
10
10
|
- Event chains: scan definitions and call sites, trace routes and state flow, lint references, render graphs, and compare revisions without editing event source.
|
|
11
11
|
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
@@ -13,7 +13,7 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
13
13
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
16
|
-
Requires Node.js 22 or 24.
|
|
16
|
+
Requires Node.js 22.19 or later in the Node 22 line, or Node.js 24.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
npm install --global hoi4-agent-tools@latest
|
|
@@ -38,7 +38,7 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
38
38
|
| `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
|
|
39
39
|
| `hoi4.focus_rewrite` | Create or update a focus tree. |
|
|
40
40
|
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
41
|
-
| `hoi4.gui_render` | Render GUI states and
|
|
41
|
+
| `hoi4.gui_render` | Render GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
|
|
42
42
|
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
43
43
|
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
|
|
44
44
|
| `hoi4.map_render` | Render map layers and overlays. |
|
|
@@ -70,7 +70,7 @@ Connecting and listing tools does not scan mod source. Compact tool schemas and
|
|
|
70
70
|
Ask your agent in normal task language. A typical workflow is inspect, render, rewrite, then inspect the result. The agent can call a raster tool when a pixel review is useful without paying that cost during every structural operation.
|
|
71
71
|
|
|
72
72
|
- Focus trees: "Create a complete national focus tree for this route specification," or "Compact this existing tree into a balanced, readable layout." Existing trees can use a plan-free compact reflow; new trees use a complete plan. See [Focus trees](docs/focus.md).
|
|
73
|
-
- Scripted GUIs: "Create a scripted GUI for this mechanic," or "Render
|
|
73
|
+
- Scripted GUIs: "Create a scripted GUI for this mechanic," or "Render every value-driven version of this window and fix hidden controls, off-center button text, background alignment, clipping, and click-region conflicts." See [Scripted GUIs](docs/gui.md).
|
|
74
74
|
- Maps: "Create a state from these exact provinces," or "Inspect this state and split these provinces while keeping supply and railway references valid." See [Maps](docs/map.md).
|
|
75
75
|
- Event chains: "Trace every route from this event and explain where its flags and variables change," or "Compare the workspace event graph with its previous revision and render the affected routes." See [Event chains](docs/events.md).
|
|
76
76
|
- Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
|
|
@@ -93,8 +93,14 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
|
|
|
93
93
|
function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
94
94
|
const cohorts = new Map();
|
|
95
95
|
const laneBases = compactLaneBases(plan);
|
|
96
|
+
const childIds = new Map();
|
|
96
97
|
for (const focus of plan.focuses) {
|
|
97
98
|
const parents = prerequisiteIds(plan, focus.id);
|
|
99
|
+
for (const parentId of parents) {
|
|
100
|
+
const children = childIds.get(parentId) ?? [];
|
|
101
|
+
children.push(focus.id);
|
|
102
|
+
childIds.set(parentId, children);
|
|
103
|
+
}
|
|
98
104
|
const key = JSON.stringify([
|
|
99
105
|
parents.length === 0 ? focusLaneId(plan, focus) : null,
|
|
100
106
|
parents,
|
|
@@ -108,36 +114,44 @@ function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
|
108
114
|
if (cohort.length < 2)
|
|
109
115
|
continue;
|
|
110
116
|
cohort.sort((left, right) => (preferredX.get(left) ?? 0) - (preferredX.get(right) ?? 0) || compareCodeUnits(left, right));
|
|
111
|
-
const coordinates = cohort.map((id) => preferredX.get(id) ?? 0);
|
|
112
|
-
const pairCount = Math.floor(coordinates.length / 2);
|
|
113
117
|
const focus = plan.focuses.find(({ id }) => id === cohort[0]);
|
|
114
118
|
if (focus === undefined)
|
|
115
119
|
continue;
|
|
116
|
-
const mirroredAt = (centerTwice) => {
|
|
117
|
-
const mirrored = [...coordinates];
|
|
118
|
-
for (let index = 0; index < pairCount; index += 1) {
|
|
119
|
-
const opposite = coordinates.length - 1 - index;
|
|
120
|
-
const left = Math.round(((coordinates[index] ?? 0) + centerTwice - (coordinates[opposite] ?? 0)) / 2);
|
|
121
|
-
mirrored[index] = left;
|
|
122
|
-
mirrored[opposite] = centerTwice - left;
|
|
123
|
-
}
|
|
124
|
-
if (coordinates.length % 2 === 1)
|
|
125
|
-
mirrored[pairCount] = Math.round(centerTwice / 2);
|
|
126
|
-
let maximumLeft = centerTwice / 2 - (coordinates.length % 2 === 1 ? 2 : 1);
|
|
127
|
-
for (let index = pairCount - 1; index >= 0; index -= 1) {
|
|
128
|
-
const opposite = coordinates.length - 1 - index;
|
|
129
|
-
const left = Math.min(mirrored[index] ?? maximumLeft, maximumLeft);
|
|
130
|
-
mirrored[index] = left;
|
|
131
|
-
mirrored[opposite] = centerTwice - left;
|
|
132
|
-
maximumLeft = left - COMPACT_SAME_ROW_SPACING;
|
|
133
|
-
}
|
|
134
|
-
return mirrored;
|
|
135
|
-
};
|
|
136
120
|
const anchorCenterTwice = 2 *
|
|
137
121
|
compactStructuralAnchorX(plan, focus, prerequisiteIds(plan, focus.id), preferredX, laneBases);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
122
|
+
const firstX = Math.round(anchorCenterTwice / 2 - ((cohort.length - 1) * COMPACT_SAME_ROW_SPACING) / 2);
|
|
123
|
+
const targetX = new Map(cohort.map((focusId, index) => [focusId, firstX + index * COMPACT_SAME_ROW_SPACING]));
|
|
124
|
+
const deltaX = new Map(cohort.map((focusId) => [
|
|
125
|
+
focusId,
|
|
126
|
+
(targetX.get(focusId) ?? 0) - (preferredX.get(focusId) ?? 0),
|
|
127
|
+
]));
|
|
128
|
+
const reachableBy = new Map();
|
|
129
|
+
for (const rootId of cohort) {
|
|
130
|
+
const pending = [rootId];
|
|
131
|
+
const visited = new Set();
|
|
132
|
+
while (pending.length > 0) {
|
|
133
|
+
const focusId = pending.pop();
|
|
134
|
+
if (focusId === undefined || visited.has(focusId))
|
|
135
|
+
continue;
|
|
136
|
+
visited.add(focusId);
|
|
137
|
+
const owners = reachableBy.get(focusId) ?? new Set();
|
|
138
|
+
owners.add(rootId);
|
|
139
|
+
reachableBy.set(focusId, owners);
|
|
140
|
+
pending.push(...(childIds.get(focusId) ?? []));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const [focusId, owners] of reachableBy) {
|
|
144
|
+
if (owners.size !== 1)
|
|
145
|
+
continue;
|
|
146
|
+
const ownerId = [...owners][0];
|
|
147
|
+
if (ownerId === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
const delta = deltaX.get(ownerId);
|
|
150
|
+
const current = preferredX.get(focusId);
|
|
151
|
+
if (delta === undefined || current === undefined)
|
|
152
|
+
continue;
|
|
153
|
+
preferredX.set(focusId, current + delta);
|
|
154
|
+
}
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
function straightenLinearChains(plan, preferredX, preferredY) {
|
|
@@ -384,18 +384,23 @@ function frameFor(element, sprite, scenario) {
|
|
|
384
384
|
: 0;
|
|
385
385
|
return Math.min(frameCount - 1, mapped);
|
|
386
386
|
}
|
|
387
|
-
function alignment(attributes) {
|
|
388
|
-
const
|
|
387
|
+
function alignment(attributes, buttonText) {
|
|
388
|
+
const explicitFormat = scalarString(property(attributes, 'format'))?.toLowerCase();
|
|
389
|
+
const format = explicitFormat ?? '';
|
|
389
390
|
const horizontalAlignment = format.includes('center')
|
|
390
391
|
? 'center'
|
|
391
392
|
: format.includes('right')
|
|
392
393
|
? 'right'
|
|
393
|
-
:
|
|
394
|
+
: buttonText && explicitFormat === undefined
|
|
395
|
+
? 'center'
|
|
396
|
+
: 'left';
|
|
394
397
|
const verticalAlignment = format.includes('bottom')
|
|
395
398
|
? 'bottom'
|
|
396
399
|
: format.includes('center')
|
|
397
400
|
? 'center'
|
|
398
|
-
:
|
|
401
|
+
: buttonText && explicitFormat === undefined
|
|
402
|
+
? 'center'
|
|
403
|
+
: 'top';
|
|
399
404
|
return { horizontalAlignment, verticalAlignment };
|
|
400
405
|
}
|
|
401
406
|
class GuiSceneWorkBudget {
|
|
@@ -523,7 +528,8 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
523
528
|
}
|
|
524
529
|
}
|
|
525
530
|
}
|
|
526
|
-
const
|
|
531
|
+
const rawButtonText = scalarString(property(definition.attributes, 'buttonText'));
|
|
532
|
+
const rawText = scalarString(property(definition.attributes, 'text')) ?? rawButtonText;
|
|
527
533
|
let text;
|
|
528
534
|
if (rawText !== undefined) {
|
|
529
535
|
const resolved = resolveTokenText(rawText, scenario, context.localisation, rowValues);
|
|
@@ -568,7 +574,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
568
574
|
measuredWidth,
|
|
569
575
|
measuredHeight,
|
|
570
576
|
metricSource: wrapped.metricSource,
|
|
571
|
-
...alignment(definition.attributes),
|
|
577
|
+
...alignment(definition.attributes, rawButtonText !== undefined),
|
|
572
578
|
...(fontName === undefined ? {} : { fontName }),
|
|
573
579
|
glyphLines,
|
|
574
580
|
overflowX: width > 0 && measuredWidth > width + 0.01,
|
|
@@ -4,6 +4,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
4
4
|
import { GUI_SCENARIO_MAX_KEYS, GUI_SCENARIO_MAX_LIST_KEYS, GUI_SCENARIO_MAX_ROWS, GUI_SCENARIO_MAX_STRING_CHARACTERS, } from './limits.js';
|
|
5
5
|
const scalarSchema = z.union([z.string().max(16_384), z.number(), z.boolean()]);
|
|
6
6
|
const objectSchema = z.record(z.string(), scalarSchema);
|
|
7
|
+
const elementNameSchema = z.string().min(1).max(256);
|
|
7
8
|
const previewStateSchema = z.enum([
|
|
8
9
|
'normal',
|
|
9
10
|
'hover',
|
|
@@ -124,6 +125,15 @@ const GuiPreviewScenarioBodySchema = z
|
|
|
124
125
|
scrollOffsets: z.record(z.string(), z.number().min(0)).default({}),
|
|
125
126
|
guiCosts: z.record(z.string(), z.number()).default({}),
|
|
126
127
|
scriptCosts: z.record(z.string(), z.number()).default({}),
|
|
128
|
+
expectations: z
|
|
129
|
+
.object({
|
|
130
|
+
visible: z.array(elementNameSchema).max(10_000).default([]),
|
|
131
|
+
hidden: z.array(elementNameSchema).max(10_000).default([]),
|
|
132
|
+
containedBy: z.record(elementNameSchema, elementNameSchema).default({}),
|
|
133
|
+
centeredOn: z.record(elementNameSchema, elementNameSchema).default({}),
|
|
134
|
+
})
|
|
135
|
+
.strict()
|
|
136
|
+
.default({ visible: [], hidden: [], containedBy: {}, centeredOn: {} }),
|
|
127
137
|
})
|
|
128
138
|
.strict()
|
|
129
139
|
.superRefine((scenario, context) => {
|
|
@@ -325,6 +325,72 @@ function safeSlug(value) {
|
|
|
325
325
|
function scenarioWith(base, patch) {
|
|
326
326
|
return parsePreviewScenario({ ...base, ...patch });
|
|
327
327
|
}
|
|
328
|
+
function assertUniqueScenarioIds(scenarios) {
|
|
329
|
+
const seen = new Set();
|
|
330
|
+
for (const scenario of scenarios) {
|
|
331
|
+
if (seen.has(scenario.id)) {
|
|
332
|
+
throw new ServiceError('GUI_SCENARIO_ID_DUPLICATE', `GUI scenario id must be unique within one request: ${scenario.id}`, { scenarioId: scenario.id });
|
|
333
|
+
}
|
|
334
|
+
seen.add(scenario.id);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function scenarioMatrixEvidence(scenes) {
|
|
338
|
+
const primary = scenes[0];
|
|
339
|
+
if (primary === undefined)
|
|
340
|
+
return { offline: true, scenarios: [], changes: [] };
|
|
341
|
+
const elementKey = (element) => `${element.sourceId}#${element.rowIndex ?? 0}`;
|
|
342
|
+
const primaryByKey = new Map(primary.elements.map((element) => [elementKey(element), element]));
|
|
343
|
+
return {
|
|
344
|
+
offline: true,
|
|
345
|
+
scenarios: scenes.map((scene) => ({
|
|
346
|
+
scenario: scene.scenario,
|
|
347
|
+
sourceRevision: scene.sourceRevision,
|
|
348
|
+
fidelity: scene.fidelity,
|
|
349
|
+
visibleElements: scene.elements
|
|
350
|
+
.filter(({ visible }) => visible)
|
|
351
|
+
.map(({ id, name }) => ({ id, name })),
|
|
352
|
+
})),
|
|
353
|
+
changes: scenes.slice(1).map((scene) => {
|
|
354
|
+
const currentByKey = new Map(scene.elements.map((element) => [elementKey(element), element]));
|
|
355
|
+
const keys = [...new Set([...primaryByKey.keys(), ...currentByKey.keys()])].sort((left, right) => compareCodeUnits(left, right));
|
|
356
|
+
const shown = [];
|
|
357
|
+
const hidden = [];
|
|
358
|
+
const moved = [];
|
|
359
|
+
const resized = [];
|
|
360
|
+
const textChanged = [];
|
|
361
|
+
const frameChanged = [];
|
|
362
|
+
for (const key of keys) {
|
|
363
|
+
const before = primaryByKey.get(key);
|
|
364
|
+
const after = currentByKey.get(key);
|
|
365
|
+
if (before?.visible !== true && after?.visible === true)
|
|
366
|
+
shown.push(after.name);
|
|
367
|
+
if (before?.visible === true && after?.visible !== true)
|
|
368
|
+
hidden.push(before.name);
|
|
369
|
+
if (before === undefined || after === undefined)
|
|
370
|
+
continue;
|
|
371
|
+
if (before.unclippedRect.x !== after.unclippedRect.x ||
|
|
372
|
+
before.unclippedRect.y !== after.unclippedRect.y)
|
|
373
|
+
moved.push(after.name);
|
|
374
|
+
if (before.unclippedRect.width !== after.unclippedRect.width ||
|
|
375
|
+
before.unclippedRect.height !== after.unclippedRect.height)
|
|
376
|
+
resized.push(after.name);
|
|
377
|
+
if (before.text?.text !== after.text?.text)
|
|
378
|
+
textChanged.push(after.name);
|
|
379
|
+
if (before.sprite?.frame !== after.sprite?.frame)
|
|
380
|
+
frameChanged.push(after.name);
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
scenarioId: scene.scenario.id,
|
|
384
|
+
shown,
|
|
385
|
+
hidden,
|
|
386
|
+
moved,
|
|
387
|
+
resized,
|
|
388
|
+
textChanged,
|
|
389
|
+
frameChanged,
|
|
390
|
+
};
|
|
391
|
+
}),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
328
394
|
function fullImage(render) {
|
|
329
395
|
const image = render.images.find(({ variant }) => variant === 'full') ?? render.images[0];
|
|
330
396
|
if (image === undefined)
|
|
@@ -460,6 +526,7 @@ export class ScriptedGuiStudio {
|
|
|
460
526
|
input.signal?.throwIfAborted();
|
|
461
527
|
const scenario = parsePreviewScenario(input.scenario);
|
|
462
528
|
const relatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
529
|
+
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
463
530
|
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [scenario.language, ...relatedScenarios.map(({ language }) => language)]);
|
|
464
531
|
const budget = new RenderBudget();
|
|
465
532
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget);
|
|
@@ -510,11 +577,37 @@ export class ScriptedGuiStudio {
|
|
|
510
577
|
const budget = new RenderBudget();
|
|
511
578
|
const workspace = this.resolver.get(input.workspaceId, input.principal);
|
|
512
579
|
const scenario = parsePreviewScenario(input.scenario);
|
|
580
|
+
const relatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
581
|
+
assertUniqueScenarioIds([scenario, ...relatedScenarios]);
|
|
513
582
|
const baselineScenario = parsePreviewScenario(input.comparisonScenario ?? { ...scenario, id: `${scenario.id}-comparison`, state: 'normal' });
|
|
514
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [
|
|
583
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [
|
|
584
|
+
scenario.language,
|
|
585
|
+
baselineScenario.language,
|
|
586
|
+
...relatedScenarios.map(({ language }) => language),
|
|
587
|
+
]);
|
|
515
588
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget);
|
|
516
589
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
517
590
|
const render = await renderGuiScene(scene, undefined, input.signal, budget);
|
|
591
|
+
const scenarioScenes = [scene];
|
|
592
|
+
const scenarioItems = [];
|
|
593
|
+
for (const variantScenario of [scenario, ...relatedScenarios]) {
|
|
594
|
+
input.signal?.throwIfAborted();
|
|
595
|
+
const variantScene = variantScenario.id === scenario.id
|
|
596
|
+
? scene
|
|
597
|
+
: await buildGuiScene(scanned.graph, scanned.files, input.windowName, variantScenario, catalog);
|
|
598
|
+
if (variantScene !== scene)
|
|
599
|
+
scenarioScenes.push(variantScene);
|
|
600
|
+
const variantRender = variantScene === scene
|
|
601
|
+
? render
|
|
602
|
+
: await renderGuiScene(variantScene, ['full'], input.signal, new RenderBudget());
|
|
603
|
+
const image = fullImage(variantRender);
|
|
604
|
+
scenarioItems.push({
|
|
605
|
+
label: variantScenario.description ?? variantScenario.id,
|
|
606
|
+
png: image.png,
|
|
607
|
+
width: image.width,
|
|
608
|
+
height: image.height,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
518
611
|
const stateScenes = [];
|
|
519
612
|
const stateItems = [];
|
|
520
613
|
for (const state of input.states ?? [...galleryStates]) {
|
|
@@ -556,21 +649,29 @@ export class ScriptedGuiStudio {
|
|
|
556
649
|
const baselineScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, baselineScenario, catalog);
|
|
557
650
|
const baselineRender = await renderGuiScene(baselineScene, ['full'], input.signal, budget);
|
|
558
651
|
const comparison = await compareGuiImages(fullImage(baselineRender).png, fullImage(render).png, budget, input.signal);
|
|
559
|
-
const validation = await validateGuiScene(scanned.graph, scene, scanned.files,
|
|
652
|
+
const validation = await validateGuiScene(scanned.graph, scene, scanned.files, scenarioScenes.slice(1), catalog);
|
|
560
653
|
const stateValidation = validateStateMatrix(stateScenes);
|
|
561
654
|
const resolutionValidation = validateResolutionDrift(resolutionScenes);
|
|
562
655
|
validation.diagnostics.push(...stateValidation.diagnostics, ...resolutionValidation.diagnostics);
|
|
563
656
|
validation.checks.push(...stateValidation.checks, ...resolutionValidation.checks);
|
|
564
657
|
const completeValidation = withGraphDiagnostics(scanned.graph, validation);
|
|
658
|
+
const scenarioGallerySvg = renderGallerySvg(`${input.windowName} scripted scenario matrix`, scenarioItems);
|
|
565
659
|
const stateGallerySvg = renderGallerySvg(`${input.windowName} state matrix`, stateItems);
|
|
566
660
|
const resolutionGallerySvg = renderGallerySvg(`${input.windowName} resolution and UI-scale matrix`, resolutionItems);
|
|
661
|
+
const scenarioGallerySize = galleryDimensions(scenarioItems);
|
|
567
662
|
const stateGallerySize = galleryDimensions(stateItems);
|
|
568
663
|
const resolutionGallerySize = galleryDimensions(resolutionItems);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
664
|
+
const galleryBudget = new RenderBudget();
|
|
665
|
+
galleryBudget.reserve(scenarioGallerySize.width, scenarioGallerySize.height, 'GUI scripted scenario gallery PNG');
|
|
666
|
+
galleryBudget.reserve(stateGallerySize.width, stateGallerySize.height, 'GUI state gallery PNG');
|
|
667
|
+
galleryBudget.reserve(resolutionGallerySize.width, resolutionGallerySize.height, 'GUI resolution gallery PNG');
|
|
668
|
+
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(scenarioGallerySvg)}`, 'GUI scripted scenario gallery SVG rasterization');
|
|
669
|
+
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(stateGallerySvg)}`, 'GUI state gallery SVG rasterization');
|
|
670
|
+
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(resolutionGallerySvg)}`, 'GUI resolution gallery SVG rasterization');
|
|
671
|
+
const [scenarioGalleryPng, stateGalleryPng, resolutionGalleryPng] = await Promise.all([
|
|
672
|
+
sharp(Buffer.from(scenarioGallerySvg), { limitInputPixels: RENDER_MAX_PIXELS })
|
|
673
|
+
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
674
|
+
.toBuffer(),
|
|
574
675
|
sharp(Buffer.from(stateGallerySvg), { limitInputPixels: RENDER_MAX_PIXELS })
|
|
575
676
|
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
576
677
|
.toBuffer(),
|
|
@@ -598,7 +699,10 @@ export class ScriptedGuiStudio {
|
|
|
598
699
|
add(`${slug}-scenario.json`, 'application/json', render.scenarioJson, 'gui-preview-scenario');
|
|
599
700
|
add(`${slug}-fidelity.json`, 'application/json', `${canonicalJson({ offline: true, fidelity: render.fidelity })}\n`, 'gui-fidelity-report');
|
|
600
701
|
add(`${slug}-source-graph.json`, 'application/json', `${canonicalJson(scanned.graph)}\n`, 'gui-source-graph');
|
|
601
|
-
add(`${slug}-validation.json`, 'application/json', `${canonicalJson(completeValidation)}\n`, 'gui-validation-report', [
|
|
702
|
+
add(`${slug}-validation.json`, 'application/json', `${canonicalJson(completeValidation)}\n`, 'gui-validation-report', [...scenarioScenes, ...stateScenes, ...resolutionScenes]);
|
|
703
|
+
add(`${slug}-scenario-matrix.svg`, 'image/svg+xml', scenarioGallerySvg, 'gui-scripted-scenario-matrix', scenarioScenes);
|
|
704
|
+
add(`${slug}-scenario-matrix.png`, 'image/png', scenarioGalleryPng, 'gui-scripted-scenario-matrix', scenarioScenes);
|
|
705
|
+
add(`${slug}-scenario-matrix.json`, 'application/json', `${canonicalJson(scenarioMatrixEvidence(scenarioScenes))}\n`, 'gui-scripted-scenario-matrix-json', scenarioScenes);
|
|
602
706
|
add(`${slug}-state-matrix.svg`, 'image/svg+xml', stateGallerySvg, 'gui-state-matrix', stateScenes);
|
|
603
707
|
add(`${slug}-state-matrix.png`, 'image/png', stateGalleryPng, 'gui-state-matrix', stateScenes);
|
|
604
708
|
add(`${slug}-state-matrix.json`, 'application/json', `${canonicalJson({ offline: true, scenarios: stateScenes.map(({ scenario: stateScenario, fidelity }) => ({ scenario: stateScenario, fidelity })) })}\n`, 'gui-state-matrix-json', stateScenes);
|
|
@@ -615,6 +719,7 @@ export class ScriptedGuiStudio {
|
|
|
615
719
|
artifacts: stored,
|
|
616
720
|
filesScanned: scanned.graph.filesScanned,
|
|
617
721
|
render,
|
|
722
|
+
scenarioScenes,
|
|
618
723
|
stateScenes,
|
|
619
724
|
resolutionScenes,
|
|
620
725
|
comparison,
|
|
@@ -96,6 +96,177 @@ function contains(outer, inner, tolerance = 0.01) {
|
|
|
96
96
|
inner.x + inner.width <= outer.x + outer.width + tolerance &&
|
|
97
97
|
inner.y + inner.height <= outer.y + outer.height + tolerance);
|
|
98
98
|
}
|
|
99
|
+
function visualTextBounds(element) {
|
|
100
|
+
const text = element.text;
|
|
101
|
+
if (text === undefined)
|
|
102
|
+
return element.unclippedRect;
|
|
103
|
+
const width = Math.min(element.unclippedRect.width, text.measuredWidth);
|
|
104
|
+
const height = Math.min(element.unclippedRect.height, text.measuredHeight);
|
|
105
|
+
const x = text.horizontalAlignment === 'center'
|
|
106
|
+
? element.unclippedRect.x + (element.unclippedRect.width - width) / 2
|
|
107
|
+
: text.horizontalAlignment === 'right'
|
|
108
|
+
? element.unclippedRect.x + element.unclippedRect.width - width
|
|
109
|
+
: element.unclippedRect.x;
|
|
110
|
+
const y = text.verticalAlignment === 'center'
|
|
111
|
+
? element.unclippedRect.y + (element.unclippedRect.height - height) / 2
|
|
112
|
+
: text.verticalAlignment === 'bottom'
|
|
113
|
+
? element.unclippedRect.y + element.unclippedRect.height - height
|
|
114
|
+
: element.unclippedRect.y;
|
|
115
|
+
return { x, y, width, height };
|
|
116
|
+
}
|
|
117
|
+
function centreDelta(inner, outer) {
|
|
118
|
+
return {
|
|
119
|
+
x: inner.x + inner.width / 2 - (outer.x + outer.width / 2),
|
|
120
|
+
y: inner.y + inner.height / 2 - (outer.y + outer.height / 2),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function elementMatches(element, selector) {
|
|
124
|
+
return element.name === selector || element.id === selector || element.sourceId === selector;
|
|
125
|
+
}
|
|
126
|
+
function expectedElements(scene, selector) {
|
|
127
|
+
return scene.elements.filter((element) => elementMatches(element, selector));
|
|
128
|
+
}
|
|
129
|
+
function validateScenarioExpectations(scene, diagnostics) {
|
|
130
|
+
const { expectations } = scene.scenario;
|
|
131
|
+
const missing = new Set();
|
|
132
|
+
const requireElements = (selector) => {
|
|
133
|
+
const matches = expectedElements(scene, selector);
|
|
134
|
+
if (matches.length === 0 && !missing.has(selector)) {
|
|
135
|
+
missing.add(selector);
|
|
136
|
+
diagnostics.push({
|
|
137
|
+
code: 'GUI_EXPECTED_ELEMENT_MISSING',
|
|
138
|
+
severity: 'error',
|
|
139
|
+
category: 'design',
|
|
140
|
+
message: `Scenario ${scene.scenario.id} expects GUI element ${selector}, but the window does not contain it.`,
|
|
141
|
+
details: { scenarioId: scene.scenario.id, element: selector },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return matches;
|
|
145
|
+
};
|
|
146
|
+
for (const selector of expectations.visible) {
|
|
147
|
+
const matches = requireElements(selector);
|
|
148
|
+
if (matches.length > 0 && !matches.some(({ visible }) => visible))
|
|
149
|
+
diagnostics.push({
|
|
150
|
+
code: 'GUI_EXPECTED_ELEMENT_HIDDEN',
|
|
151
|
+
severity: 'error',
|
|
152
|
+
category: 'design',
|
|
153
|
+
message: `Scenario ${scene.scenario.id} expects ${selector} to be visible, but every matching element is hidden.`,
|
|
154
|
+
details: { scenarioId: scene.scenario.id, element: selector },
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
for (const selector of expectations.hidden) {
|
|
158
|
+
const matches = requireElements(selector);
|
|
159
|
+
if (matches.some(({ visible }) => visible))
|
|
160
|
+
diagnostics.push({
|
|
161
|
+
code: 'GUI_EXPECTED_ELEMENT_VISIBLE',
|
|
162
|
+
severity: 'error',
|
|
163
|
+
category: 'design',
|
|
164
|
+
message: `Scenario ${scene.scenario.id} expects ${selector} to be hidden, but a matching element is visible.`,
|
|
165
|
+
details: { scenarioId: scene.scenario.id, element: selector },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
for (const [selector, ownerSelector] of Object.entries(expectations.containedBy)) {
|
|
169
|
+
const elements = requireElements(selector).filter(({ visible }) => visible);
|
|
170
|
+
const owners = requireElements(ownerSelector).filter(({ visible }) => visible);
|
|
171
|
+
for (const element of elements) {
|
|
172
|
+
if (owners.some((owner) => contains(owner.unclippedRect, element.unclippedRect, 0.5)))
|
|
173
|
+
continue;
|
|
174
|
+
diagnostics.push(issue('GUI_CONTENT_OUTSIDE_EXPECTED_BACKGROUND', 'error', 'layout', `Scenario ${scene.scenario.id} places ${selector} outside its expected background or panel ${ownerSelector}.`, element, {
|
|
175
|
+
scenarioId: scene.scenario.id,
|
|
176
|
+
element: selector,
|
|
177
|
+
expectedContainer: ownerSelector,
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const [selector, ownerSelector] of Object.entries(expectations.centeredOn)) {
|
|
182
|
+
const elements = requireElements(selector).filter(({ visible }) => visible);
|
|
183
|
+
const owners = requireElements(ownerSelector).filter(({ visible }) => visible);
|
|
184
|
+
for (const element of elements) {
|
|
185
|
+
const target = owners
|
|
186
|
+
.filter((owner) => intersection(owner.unclippedRect, element.unclippedRect) !== undefined)
|
|
187
|
+
.toSorted((left, right) => area(left.unclippedRect) - area(right.unclippedRect))[0];
|
|
188
|
+
if (target === undefined) {
|
|
189
|
+
diagnostics.push(issue('GUI_EXPECTED_CENTER_TARGET_MISSING', 'error', 'layout', `Scenario ${scene.scenario.id} cannot center ${selector} because it does not overlap visible target ${ownerSelector}.`, element, { scenarioId: scene.scenario.id, element: selector, target: ownerSelector }));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const delta = centreDelta(visualTextBounds(element), target.unclippedRect);
|
|
193
|
+
const toleranceX = Math.max(2, target.unclippedRect.width * 0.025);
|
|
194
|
+
const toleranceY = Math.max(2, target.unclippedRect.height * 0.08);
|
|
195
|
+
if (Math.abs(delta.x) <= toleranceX && Math.abs(delta.y) <= toleranceY)
|
|
196
|
+
continue;
|
|
197
|
+
diagnostics.push(issue('GUI_EXPECTED_CENTERING_MISMATCH', 'error', 'layout', `Scenario ${scene.scenario.id} offsets ${selector} from the center of ${ownerSelector} by ${delta.x.toFixed(2)}px horizontally and ${delta.y.toFixed(2)}px vertically.`, element, {
|
|
198
|
+
scenarioId: scene.scenario.id,
|
|
199
|
+
element: selector,
|
|
200
|
+
target: ownerSelector,
|
|
201
|
+
deltaX: delta.x,
|
|
202
|
+
deltaY: delta.y,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function validateButtonLabelsAndBackgrounds(scene, diagnostics) {
|
|
208
|
+
const visible = scene.elements.filter((element) => element.visible);
|
|
209
|
+
const byId = new Map(scene.elements.map((element) => [element.id, element]));
|
|
210
|
+
const buttons = visible.filter((element) => element.clickable && /button/iu.test(element.elementType));
|
|
211
|
+
const labels = visible.filter((element) => element.text !== undefined);
|
|
212
|
+
const inferredButtonByLabel = new Map();
|
|
213
|
+
const inferredLabelCountByButton = new Map();
|
|
214
|
+
for (const label of labels) {
|
|
215
|
+
if (buttons.some(({ id }) => id === label.id))
|
|
216
|
+
continue;
|
|
217
|
+
const parentButton = label.parentId === undefined ? undefined : buttons.find(({ id }) => id === label.parentId);
|
|
218
|
+
const overlayButton = buttons
|
|
219
|
+
.filter((button) => {
|
|
220
|
+
if (button.zIndex > label.zIndex || button.id === label.id)
|
|
221
|
+
return false;
|
|
222
|
+
const overlap = intersection(button.unclippedRect, label.unclippedRect);
|
|
223
|
+
return (overlap !== undefined && area(overlap) / Math.max(1, area(label.unclippedRect)) >= 0.6);
|
|
224
|
+
})
|
|
225
|
+
.toSorted((left, right) => area(left.unclippedRect) - area(right.unclippedRect))[0];
|
|
226
|
+
const inferredButton = parentButton ?? overlayButton;
|
|
227
|
+
if (inferredButton === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
inferredButtonByLabel.set(label.id, inferredButton);
|
|
230
|
+
inferredLabelCountByButton.set(inferredButton.id, (inferredLabelCountByButton.get(inferredButton.id) ?? 0) + 1);
|
|
231
|
+
}
|
|
232
|
+
for (const label of labels) {
|
|
233
|
+
const ownButton = buttons.find(({ id }) => id === label.id);
|
|
234
|
+
const inferredButton = inferredButtonByLabel.get(label.id);
|
|
235
|
+
const button = ownButton ??
|
|
236
|
+
(inferredButton !== undefined && inferredLabelCountByButton.get(inferredButton.id) === 1
|
|
237
|
+
? inferredButton
|
|
238
|
+
: undefined);
|
|
239
|
+
if (button !== undefined) {
|
|
240
|
+
const delta = centreDelta(visualTextBounds(label), button.unclippedRect);
|
|
241
|
+
const toleranceX = Math.max(2, button.unclippedRect.width * 0.025);
|
|
242
|
+
const toleranceY = Math.max(2, button.unclippedRect.height * 0.08);
|
|
243
|
+
if (Math.abs(delta.x) > toleranceX || Math.abs(delta.y) > toleranceY)
|
|
244
|
+
diagnostics.push(issue('GUI_BUTTON_LABEL_OFF_CENTER', 'warning', 'layout', `${label.name} is offset from button ${button.name} by ${delta.x.toFixed(2)}px horizontally and ${delta.y.toFixed(2)}px vertically in scenario ${scene.scenario.id}.`, label, {
|
|
245
|
+
scenarioId: scene.scenario.id,
|
|
246
|
+
label: label.name,
|
|
247
|
+
button: button.name,
|
|
248
|
+
deltaX: delta.x,
|
|
249
|
+
deltaY: delta.y,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
let parent = label.parentId === undefined ? undefined : byId.get(label.parentId);
|
|
253
|
+
while (parent !== undefined && parent.sprite === undefined)
|
|
254
|
+
parent = parent.parentId === undefined ? undefined : byId.get(parent.parentId);
|
|
255
|
+
if (parent !== undefined &&
|
|
256
|
+
!contains(parent.unclippedRect, label.unclippedRect, 0.5) &&
|
|
257
|
+
intersection(parent.unclippedRect, label.unclippedRect) !== undefined)
|
|
258
|
+
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${label.name} crosses the visible background boundary of ${parent.name} in scenario ${scene.scenario.id}.`, label, { scenarioId: scene.scenario.id, element: label.name, background: parent.name }));
|
|
259
|
+
}
|
|
260
|
+
for (const control of visible.filter((element) => element.clickable && element.text === undefined)) {
|
|
261
|
+
let parent = control.parentId === undefined ? undefined : byId.get(control.parentId);
|
|
262
|
+
while (parent !== undefined && parent.sprite === undefined)
|
|
263
|
+
parent = parent.parentId === undefined ? undefined : byId.get(parent.parentId);
|
|
264
|
+
if (parent !== undefined &&
|
|
265
|
+
!contains(parent.unclippedRect, control.unclippedRect, 0.5) &&
|
|
266
|
+
intersection(parent.unclippedRect, control.unclippedRect) !== undefined)
|
|
267
|
+
diagnostics.push(issue('GUI_CONTENT_CROSSES_BACKGROUND_EDGE', 'warning', 'layout', `${control.name} crosses the visible background boundary of ${parent.name} in scenario ${scene.scenario.id}.`, control, { scenarioId: scene.scenario.id, element: control.name, background: parent.name }));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
99
270
|
function sameRect(left, right) {
|
|
100
271
|
return (Math.abs(left.x - right.x) < 0.01 &&
|
|
101
272
|
Math.abs(left.y - right.y) < 0.01 &&
|
|
@@ -900,19 +1071,27 @@ export async function validateGuiScene(graph, scene, files, relatedScenes = [],
|
|
|
900
1071
|
};
|
|
901
1072
|
}
|
|
902
1073
|
const diagnostics = validationCollector();
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1074
|
+
for (const currentScene of [scene, ...relatedScenes]) {
|
|
1075
|
+
const sceneDiagnostics = validationCollector();
|
|
1076
|
+
const work = new GuiValidationWorkBudget();
|
|
1077
|
+
sceneDiagnostics.pushMany(currentScene.diagnostics);
|
|
1078
|
+
validateOverlapAndGeometry(currentScene, sceneDiagnostics, work);
|
|
1079
|
+
validateAlignmentAndSpacing(currentScene, sceneDiagnostics, work);
|
|
1080
|
+
validateButtonLabelsAndBackgrounds(currentScene, sceneDiagnostics);
|
|
1081
|
+
validateScenarioExpectations(currentScene, sceneDiagnostics);
|
|
1082
|
+
validateReferencesAndScript(graph, currentScene, sceneDiagnostics);
|
|
1083
|
+
validateScrollAndTabs(currentScene, sceneDiagnostics);
|
|
1084
|
+
diagnostics.pushMany(sceneDiagnostics.values().map((diagnostic) => ({
|
|
1085
|
+
...diagnostic,
|
|
1086
|
+
details: { ...diagnostic.details, scenarioId: currentScene.scenario.id },
|
|
1087
|
+
})));
|
|
1088
|
+
}
|
|
909
1089
|
const selectedSpriteNames = new Set();
|
|
910
|
-
for (const
|
|
911
|
-
|
|
912
|
-
|
|
1090
|
+
for (const currentScene of [scene, ...relatedScenes])
|
|
1091
|
+
for (const element of currentScene.elements)
|
|
1092
|
+
if (element.sprite !== undefined)
|
|
1093
|
+
selectedSpriteNames.add(element.sprite.spriteName);
|
|
913
1094
|
await validateGuiAnimationsInto(graph, files, selectedSpriteNames, catalog, diagnostics);
|
|
914
|
-
if (relatedScenes.length > 0)
|
|
915
|
-
validateResolutionDriftInto([scene, ...relatedScenes], diagnostics);
|
|
916
1095
|
return validationResult(diagnostics.values(), [
|
|
917
1096
|
'GUI_VALIDATION_DIAGNOSTICS_TRUNCATED',
|
|
918
1097
|
'GUI_VALIDATION_COMPARISON_BUDGET_BLOCKED',
|
|
@@ -922,6 +1101,14 @@ export async function validateGuiScene(graph, scene, files, relatedScenes = [],
|
|
|
922
1101
|
'GUI_TEXT_OVERFLOW',
|
|
923
1102
|
'GUI_INCONSISTENT_ALIGNMENT',
|
|
924
1103
|
'GUI_INCONSISTENT_SPACING',
|
|
1104
|
+
'GUI_BUTTON_LABEL_OFF_CENTER',
|
|
1105
|
+
'GUI_CONTENT_CROSSES_BACKGROUND_EDGE',
|
|
1106
|
+
'GUI_EXPECTED_ELEMENT_MISSING',
|
|
1107
|
+
'GUI_EXPECTED_ELEMENT_HIDDEN',
|
|
1108
|
+
'GUI_EXPECTED_ELEMENT_VISIBLE',
|
|
1109
|
+
'GUI_CONTENT_OUTSIDE_EXPECTED_BACKGROUND',
|
|
1110
|
+
'GUI_EXPECTED_CENTER_TARGET_MISSING',
|
|
1111
|
+
'GUI_EXPECTED_CENTERING_MISMATCH',
|
|
925
1112
|
'GUI_INVALID_SIZE',
|
|
926
1113
|
'GUI_CHILD_OUTSIDE_CLIPPED_PARENT',
|
|
927
1114
|
'GUI_CLICK_BOUNDS_MISMATCH',
|
|
@@ -49,6 +49,7 @@ const guiRenderOutputSchema = strictOperationResultSchema(z
|
|
|
49
49
|
.strict())
|
|
50
50
|
.max(64),
|
|
51
51
|
stateCount: nonNegativeIntegerSchema,
|
|
52
|
+
scenarioCount: nonNegativeIntegerSchema,
|
|
52
53
|
resolutionCount: nonNegativeIntegerSchema,
|
|
53
54
|
comparison: z
|
|
54
55
|
.object({ changedPixels: nonNegativeIntegerSchema, changedRatio: z.number().min(0).max(1) })
|
|
@@ -281,6 +282,9 @@ export function registerGuiTools(server, engine, context) {
|
|
|
281
282
|
...(uiScale === undefined ? {} : { uiScale }),
|
|
282
283
|
})),
|
|
283
284
|
}),
|
|
285
|
+
...(input.relatedScenarios === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { relatedScenarios: input.relatedScenarios }),
|
|
284
288
|
...(input.comparisonScenario === undefined
|
|
285
289
|
? {}
|
|
286
290
|
: { comparisonScenario: input.comparisonScenario }),
|
|
@@ -300,6 +304,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
300
304
|
height,
|
|
301
305
|
})),
|
|
302
306
|
stateCount: rendered.stateScenes.length,
|
|
307
|
+
scenarioCount: rendered.scenarioScenes.length,
|
|
303
308
|
resolutionCount: rendered.resolutionScenes.length,
|
|
304
309
|
comparison: {
|
|
305
310
|
changedPixels: rendered.comparison.changedPixels,
|
|
@@ -327,10 +332,11 @@ export function registerGuiTools(server, engine, context) {
|
|
|
327
332
|
.extend({
|
|
328
333
|
states: z.array(previewStateSchema).max(14).optional(),
|
|
329
334
|
resolutions: z.array(resolutionSchema).min(1).max(16).optional(),
|
|
335
|
+
relatedScenarios: z.array(compactGuiScenarioSchema).max(32).optional(),
|
|
330
336
|
comparisonScenario: compactGuiScenarioSchema.optional(),
|
|
331
337
|
})
|
|
332
338
|
.strict()
|
|
333
|
-
.superRefine(({ scenario, states, resolutions }, context) => {
|
|
339
|
+
.superRefine(({ scenario, states, resolutions, relatedScenarios }, context) => {
|
|
334
340
|
const budget = new RenderBudget();
|
|
335
341
|
try {
|
|
336
342
|
const stateCount = states?.length ?? 14;
|
|
@@ -345,6 +351,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
345
351
|
]) {
|
|
346
352
|
budget.reserve(resolution.width, resolution.height, 'GUI resolution variant');
|
|
347
353
|
}
|
|
354
|
+
const scenarioCount = 1 + (relatedScenarios?.length ?? 0);
|
|
348
355
|
const stateColumns = Math.min(3, Math.max(1, stateCount));
|
|
349
356
|
const stateRows = Math.max(1, Math.ceil(stateCount / stateColumns));
|
|
350
357
|
budget.reserve(stateColumns * 420, 46 + stateRows * 280, 'GUI state gallery');
|
|
@@ -352,6 +359,10 @@ export function registerGuiTools(server, engine, context) {
|
|
|
352
359
|
const resolutionColumns = Math.min(3, Math.max(1, resolutionCount));
|
|
353
360
|
const resolutionRows = Math.max(1, Math.ceil(resolutionCount / resolutionColumns));
|
|
354
361
|
budget.reserve(resolutionColumns * 420, 46 + resolutionRows * 280, 'GUI resolution gallery');
|
|
362
|
+
const scenarioBudget = new RenderBudget();
|
|
363
|
+
const scenarioColumns = Math.min(3, Math.max(1, scenarioCount));
|
|
364
|
+
const scenarioRows = Math.max(1, Math.ceil(scenarioCount / scenarioColumns));
|
|
365
|
+
scenarioBudget.reserve(scenarioColumns * 420, 46 + scenarioRows * 280, 'GUI scripted scenario gallery');
|
|
355
366
|
}
|
|
356
367
|
catch (error) {
|
|
357
368
|
if (error instanceof ServiceError) {
|
|
@@ -363,7 +374,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
363
374
|
});
|
|
364
375
|
server.registerTool('hoi4.gui_render', {
|
|
365
376
|
title: 'Render scripted GUI artifacts',
|
|
366
|
-
description: 'Render deterministic
|
|
377
|
+
description: 'Render deterministic GUI review artifacts, named value-driven scenario variants, generic states, resolutions, click regions, hierarchy, comparisons, and layout diagnostics.',
|
|
367
378
|
inputSchema: renderInput,
|
|
368
379
|
outputSchema: guiRenderOutputSchema,
|
|
369
380
|
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.
|
|
3
|
+
export const PACKAGE_VERSION = '2.4.0';
|
|
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/focus.md
CHANGED
|
@@ -30,7 +30,7 @@ For a layout-only cleanup of an existing national tree, call `hoi4.focus_rewrite
|
|
|
30
30
|
|
|
31
31
|
Compact mode imports the tree, resets authored presentation coordinates and lane bounds, and performs a deterministic reflow in the same rewrite call. It keeps prerequisites, mutual exclusions, availability, bypasses, rewards, icons, localisation, AI behavior, and route/lane membership unchanged.
|
|
32
32
|
|
|
33
|
-
The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode evaluates preservation, row repair, linear-chain repair, sibling balancing, and combined compression strategies, then chooses the cleanest valid result. It refuses invalid geometry such as overlapping or cramped focuses, bad parent order, rendered crossings, connectors through unrelated focuses, or an off-center canvas. Long connectors, fake detours, staircase or zigzag chains, and avoidable sibling imbalance are optimization priorities instead of independent rejection budgets.
|
|
33
|
+
The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode evaluates preservation, row repair, linear-chain repair, sibling balancing, and combined compression strategies, then chooses the cleanest valid result. Sparse direct sibling fan-outs are packed around their parent at the minimum readable spacing, with each exclusive descendant branch moving as a unit and shared convergence focuses staying centered. Dense branches remain wider only when their contents need the room. It refuses invalid geometry such as overlapping or cramped focuses, bad parent order, rendered crossings, connectors through unrelated focuses, or an off-center canvas. Long connectors, fake detours, staircase or zigzag chains, and avoidable sibling imbalance are optimization priorities instead of independent rejection budgets.
|
|
34
34
|
|
|
35
35
|
Use the default `layoutMode: "authored"` with a complete plan when deliberately designing coordinates or changing gameplay. Set `layoutMode: "compact"` with a complete plan when creating a large automatically arranged tree. Inspect and render calls remain useful before or after either rewrite mode, but a separate plan is not required for compact cleanup.
|
|
36
36
|
|
package/docs/gui.md
CHANGED
|
@@ -24,11 +24,31 @@ Rewrite once, then inspect and render the result.
|
|
|
24
24
|
|
|
25
25
|
1. Inspect the root window and its linked GUI, GFX, scripted-GUI, and localisation source.
|
|
26
26
|
2. Render the normal state and the states relevant to the task, such as hover, selected, disabled, warning, empty list, full list, minimum value, maximum value, or long text.
|
|
27
|
-
3.
|
|
28
|
-
4.
|
|
29
|
-
5.
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
3. Add `relatedScenarios` for every meaningful value-driven version of the window, such as open and closed panels, empty and populated lists, unlocked and locked actions, low and high values, and alternate scripted-GUI property results.
|
|
28
|
+
4. Check common resolutions and UI scales when positioning or clipping can change.
|
|
29
|
+
5. Fix the existing mod-owned file with targeted patches, a complete supported source replacement, or a structured helper replacement.
|
|
30
|
+
6. Rewrite once, then inspect and render the result.
|
|
31
|
+
|
|
32
|
+
Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
|
|
33
|
+
|
|
34
|
+
Preview scenarios can include exact layout expectations:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"id": "populated-unlocked",
|
|
39
|
+
"flags": { "panel_open": true },
|
|
40
|
+
"lists": { "target_list": [{ "id": 1, "label": "First target" }] },
|
|
41
|
+
"visibility": { "target_list": true, "confirm_button": true },
|
|
42
|
+
"expectations": {
|
|
43
|
+
"visible": ["target_list", "confirm_button"],
|
|
44
|
+
"hidden": ["empty_message"],
|
|
45
|
+
"containedBy": { "confirm_button": "main_panel" },
|
|
46
|
+
"centeredOn": { "confirm_label": "confirm_button" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Selectors accept an element name, instance ID, or source ID. Visibility expectations diagnose elements that are missing or in the wrong scripted version. `containedBy` diagnoses text or controls that leave their intended background or panel. `centeredOn` uses rendered glyph bounds, so it catches labels that occupy the right text box but are visibly off-center on the button.
|
|
32
52
|
|
|
33
53
|
Broad inspection indexes localisation actually referenced by GUI source and returns a connected workspace projection when the complete source graph is very large. The resource records full and returned node and edge counts, so an agent can identify the relevant window without loading unrelated vanilla UI into its prompt. Rendering remains targeted to the selected root, state, and resolution.
|
|
34
54
|
|
package/docs/setup.md
CHANGED
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.
|
|
4
|
+
"version": "2.4.0",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"hoi4-map"
|
|
33
33
|
],
|
|
34
34
|
"engines": {
|
|
35
|
-
"node": "^22.
|
|
35
|
+
"node": "^22.19.0 || ^24.0.0"
|
|
36
36
|
},
|
|
37
37
|
"files": [
|
|
38
38
|
"dist/",
|
|
@@ -95,19 +95,19 @@
|
|
|
95
95
|
"prepack": "npm run build"
|
|
96
96
|
},
|
|
97
97
|
"dependencies": {
|
|
98
|
-
"@fontsource-variable/roboto": "5.
|
|
99
|
-
"@modelcontextprotocol/sdk": "1.
|
|
98
|
+
"@fontsource-variable/roboto": "5.3.0",
|
|
99
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
100
100
|
"express": "5.2.1",
|
|
101
101
|
"fast-glob": "3.3.3",
|
|
102
102
|
"fontkit": "2.0.4",
|
|
103
103
|
"iconv-lite": "0.7.3",
|
|
104
|
-
"jose": "6.2.
|
|
104
|
+
"jose": "6.2.8",
|
|
105
105
|
"sharp": "0.35.3",
|
|
106
106
|
"zod": "4.4.3"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@eslint/js": "10.0.1",
|
|
110
|
-
"@modelcontextprotocol/inspector": "1.0
|
|
110
|
+
"@modelcontextprotocol/inspector": "2.1.0",
|
|
111
111
|
"@types/express": "5.0.6",
|
|
112
112
|
"@types/fontkit": "2.0.9",
|
|
113
113
|
"@types/node": "22.20.1",
|
|
@@ -115,22 +115,23 @@
|
|
|
115
115
|
"@vitest/coverage-v8": "4.1.10",
|
|
116
116
|
"ajv": "8.20.0",
|
|
117
117
|
"ajv-formats": "3.0.1",
|
|
118
|
-
"eslint": "10.
|
|
118
|
+
"eslint": "10.8.1",
|
|
119
119
|
"fast-check": "4.9.0",
|
|
120
120
|
"pixelmatch": "7.2.0",
|
|
121
121
|
"prettier": "3.9.6",
|
|
122
|
-
"tsx": "4.23.
|
|
122
|
+
"tsx": "4.23.11",
|
|
123
123
|
"typescript": "6.0.3",
|
|
124
|
-
"typescript-eslint": "8.
|
|
124
|
+
"typescript-eslint": "8.66.0",
|
|
125
125
|
"vitest": "4.1.10"
|
|
126
126
|
},
|
|
127
127
|
"overrides": {
|
|
128
|
-
"@hono/node-server": "2.0
|
|
128
|
+
"@hono/node-server": "2.1.0",
|
|
129
129
|
"brace-expansion": "5.0.9",
|
|
130
130
|
"fast-uri": "3.1.5",
|
|
131
|
-
"hono": "4.13.
|
|
131
|
+
"hono": "4.13.1",
|
|
132
|
+
"nanoid": "3.3.18",
|
|
132
133
|
"ip-address": "10.4.0",
|
|
133
|
-
"postcss": "8.5.
|
|
134
|
+
"postcss": "8.5.26",
|
|
134
135
|
"shell-quote": "1.10.0"
|
|
135
136
|
},
|
|
136
137
|
"publishConfig": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/schemas/gui-scenario.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -65,6 +65,9 @@
|
|
|
65
65
|
},
|
|
66
66
|
"scriptCosts": {
|
|
67
67
|
"$ref": "#/$defs/__schema24"
|
|
68
|
+
},
|
|
69
|
+
"expectations": {
|
|
70
|
+
"$ref": "#/$defs/__schema25"
|
|
68
71
|
}
|
|
69
72
|
},
|
|
70
73
|
"required": [
|
|
@@ -293,6 +296,59 @@
|
|
|
293
296
|
"additionalProperties": {
|
|
294
297
|
"type": "number"
|
|
295
298
|
}
|
|
299
|
+
},
|
|
300
|
+
"__schema25": {
|
|
301
|
+
"default": {
|
|
302
|
+
"visible": [],
|
|
303
|
+
"hidden": [],
|
|
304
|
+
"containedBy": {},
|
|
305
|
+
"centeredOn": {}
|
|
306
|
+
},
|
|
307
|
+
"type": "object",
|
|
308
|
+
"properties": {
|
|
309
|
+
"visible": {
|
|
310
|
+
"default": [],
|
|
311
|
+
"maxItems": 10000,
|
|
312
|
+
"type": "array",
|
|
313
|
+
"items": {
|
|
314
|
+
"$ref": "#/$defs/__schema26"
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
"hidden": {
|
|
318
|
+
"default": [],
|
|
319
|
+
"maxItems": 10000,
|
|
320
|
+
"type": "array",
|
|
321
|
+
"items": {
|
|
322
|
+
"$ref": "#/$defs/__schema26"
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
"containedBy": {
|
|
326
|
+
"default": {},
|
|
327
|
+
"type": "object",
|
|
328
|
+
"propertyNames": {
|
|
329
|
+
"$ref": "#/$defs/__schema26"
|
|
330
|
+
},
|
|
331
|
+
"additionalProperties": {
|
|
332
|
+
"$ref": "#/$defs/__schema26"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"centeredOn": {
|
|
336
|
+
"default": {},
|
|
337
|
+
"type": "object",
|
|
338
|
+
"propertyNames": {
|
|
339
|
+
"$ref": "#/$defs/__schema26"
|
|
340
|
+
},
|
|
341
|
+
"additionalProperties": {
|
|
342
|
+
"$ref": "#/$defs/__schema26"
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
"additionalProperties": false
|
|
347
|
+
},
|
|
348
|
+
"__schema26": {
|
|
349
|
+
"type": "string",
|
|
350
|
+
"minLength": 1,
|
|
351
|
+
"maxLength": 256
|
|
296
352
|
}
|
|
297
353
|
}
|
|
298
354
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.4.0/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.
|
|
6
|
+
"version": "2.4.0",
|
|
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.
|
|
16
|
+
"version": "2.4.0",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
|
19
19
|
}
|