hoi4-agent-tools 2.3.13 → 2.5.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 +16 -0
- package/README.md +28 -28
- 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/focus.js +8 -1
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +13 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +52 -6
- package/dist/hoi4_agent_tools/probability/service.js +81 -7
- package/dist/hoi4_agent_tools/probability/source-analysis.js +83 -32
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +25 -5
- package/docs/probability.md +3 -1
- 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,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0 - 2026-08-09
|
|
4
|
+
|
|
5
|
+
- Added exact continuous-focus coordinates and linked palette IDs to compact national focus inspection results.
|
|
6
|
+
- Added source-first probability inspection that discovers compatible adapters, candidate counts, and selector examples without requiring the caller to guess a surface type.
|
|
7
|
+
- Changed empty or mismatched probability inspection into an actionable discovery result with a reason and suggested adapter while keeping evaluation, sweeps, and simulation strict.
|
|
8
|
+
- Prevented focus-tree blocks from being misclassified as decision or mission weighted surfaces.
|
|
9
|
+
|
|
10
|
+
## 2.4.0 - 2026-08-09
|
|
11
|
+
|
|
12
|
+
- Added named value-driven scripted-GUI scenario matrices with deterministic SVG, PNG, JSON, and per-element visibility, movement, size, text, and frame-change evidence.
|
|
13
|
+
- 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.
|
|
14
|
+
- Modelled native HOI4 button-text centering and added automatic diagnostics for visibly off-center labels and text or controls crossing a parent background boundary.
|
|
15
|
+
- 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.
|
|
16
|
+
- Updated the Inspector harness for the current CLI entry point, result envelope, explicit environment forwarding, and longer connection startup budget.
|
|
17
|
+
- 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.
|
|
18
|
+
|
|
3
19
|
## 2.3.13 - 2026-08-06
|
|
4
20
|
|
|
5
21
|
- Repacked sparse sibling fan-outs around their structural parent instead of preserving avoidable long prerequisite connectors.
|
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
|
|
@@ -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 and
|
|
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 states and
|
|
42
|
-
| `hoi4.gui_rewrite` | Create or update a GUI source package.
|
|
43
|
-
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data.
|
|
44
|
-
| `hoi4.map_render` | Render map layers and overlays.
|
|
45
|
-
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes.
|
|
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, candidates, 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 GUI scenario variants, states, resolutions, hierarchy, and layout diagnostics. |
|
|
42
|
+
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
43
|
+
| `hoi4.map_inspect` | Read map, state, province, region, supply, and railway data. |
|
|
44
|
+
| `hoi4.map_render` | Render map layers and overlays. |
|
|
45
|
+
| `hoi4.map_rewrite` | Create or update map data from an ordered list of exact changes. |
|
|
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
|
|
|
@@ -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).
|
|
@@ -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',
|
|
@@ -31,6 +31,11 @@ const focusInspectOutputSchema = strictOperationResultSchema(z
|
|
|
31
31
|
branchCount: nonNegativeIntegerSchema,
|
|
32
32
|
continuousPaletteCount: nonNegativeIntegerSchema,
|
|
33
33
|
continuousFocusCount: nonNegativeIntegerSchema,
|
|
34
|
+
continuousFocusPosition: z
|
|
35
|
+
.object({ x: z.number().int(), y: z.number().int() })
|
|
36
|
+
.strict()
|
|
37
|
+
.nullable(),
|
|
38
|
+
continuousFocusPaletteIds: z.array(z.string().max(256)).max(100),
|
|
34
39
|
resolvedTitleCount: nonNegativeIntegerSchema,
|
|
35
40
|
layoutHash: sha256Schema,
|
|
36
41
|
layoutDecisionCount: nonNegativeIntegerSchema,
|
|
@@ -601,7 +606,7 @@ export function registerFocusTools(server, engine, context) {
|
|
|
601
606
|
const workbench = new FocusWorkbench(engine.resolver, engine.transactions, engine.artifacts);
|
|
602
607
|
server.registerTool('hoi4.focus_inspect', {
|
|
603
608
|
title: 'Inspect focus trees',
|
|
604
|
-
description: 'Inspect national trees or continuous palettes for creation and cleanup, including complete plans, references, diagnostics, and stable layout decisions.',
|
|
609
|
+
description: 'Inspect national trees or continuous palettes for creation and cleanup, including continuous-focus placement, complete plans, references, diagnostics, and stable layout decisions.',
|
|
605
610
|
inputSchema: focusInspectInput,
|
|
606
611
|
outputSchema: focusInspectOutputSchema,
|
|
607
612
|
annotations: artifactProducing,
|
|
@@ -791,6 +796,8 @@ export function registerFocusTools(server, engine, context) {
|
|
|
791
796
|
branchCount: plan.branchGroups.length,
|
|
792
797
|
continuousPaletteCount: plan.continuousFocusPaletteIds.length,
|
|
793
798
|
continuousFocusCount: plan.continuousFocusIds.length,
|
|
799
|
+
continuousFocusPosition: plan.continuousFocusPosition ?? null,
|
|
800
|
+
continuousFocusPaletteIds: plan.continuousFocusPaletteIds.slice(0, 100),
|
|
794
801
|
resolvedTitleCount: plan.focuses.filter(({ id }) => presentation.entries[id]?.titleSourceLocation !== undefined).length,
|
|
795
802
|
layoutHash: layout.layoutHash,
|
|
796
803
|
layoutDecisionCount: layout.decisions.length,
|