hoi4-agent-tools 3.5.1 → 3.6.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 +7 -0
- package/README.md +43 -116
- package/dist/hoi4_agent_tools/gui/layout.js +237 -11
- package/dist/hoi4_agent_tools/gui/renderer.js +36 -3
- package/dist/hoi4_agent_tools/gui/scenario.js +18 -0
- package/dist/hoi4_agent_tools/gui/studio.js +73 -21
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +33 -18
- package/docs/examples.md +85 -0
- package/docs/gui.md +37 -0
- package/docs/setup.md +50 -26
- package/docs/tools.md +64 -0
- package/package.json +4 -2
- 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/decision-inspect-input.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 +139 -89
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.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/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.6.0 - 2026-09-27
|
|
6
|
+
|
|
7
|
+
- Preview immediate tooltip text from source localisation with declared position, width and padding, using the source tooltip sprite and font.
|
|
8
|
+
- Keep unknown GUI visibility and enablement separate from confirmed states, branch coverage and click regions.
|
|
9
|
+
- Identify inferred scrollbar gutter geometry as approximate.
|
|
10
|
+
- Provide a shorter setup guide, practical examples with real renders, and a separate tool reference; remove redundant planning and release-history documents.
|
|
11
|
+
|
|
5
12
|
## 3.5.1 - 2026-09-27
|
|
6
13
|
|
|
7
14
|
- Preserve installed-game and offline-wiki citations in compact reference context bundles, select question-relevant sections within required sources, and report sources omitted by the result limit separately from missing files.
|
package/README.md
CHANGED
|
@@ -1,137 +1,64 @@
|
|
|
1
1
|
# HOI4 Agent Tools
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Give your coding agent tools for Hearts of Iron IV modding.
|
|
4
|
+
Read mod files, find the right documentation, preview trees and interfaces, and make focused changes to focus trees, GUIs and maps.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Get started
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- Scripted GUIs: trace GUI, GFX, scripted-GUI, and localisation links; render value-driven variants, dynamic country flags, inline text icons, states, resolutions, hierarchy, and click regions; diagnose alignment, visibility, clipping, panel containment, and button-label centering; create or repair interface source.
|
|
9
|
-
- Maps: navigate the complete rendered map by ID or localised name; inspect provinces, states, regions, adjacency, supply, railways, and positions; create states and provinces; change IDs; and repair connected map data.
|
|
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
|
-
- Cross-system impact and decisions: trace definitions and consumers across source systems, evaluate declared actor and target scenarios, inspect costs and mission paths, and compare in-memory proposals.
|
|
12
|
-
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
13
|
-
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, bind special scope chains, enumerate dynamic target pools, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
14
|
-
- Local references: search and read cited offline wiki pages, installed game documentation, optional script documentation, and exact source symbols without loading whole manuals into the conversation.
|
|
8
|
+
Install [Node.js](https://nodejs.org/) 22.19+ in the Node 22 line, or Node.js 24, then run:
|
|
15
9
|
|
|
16
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install --global hoi4-agent-tools
|
|
12
|
+
hoi4-agent-tools-setup --print-client-config
|
|
13
|
+
```
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
Add the printed configuration to your MCP client and open your mod as its workspace.
|
|
16
|
+
Keep an installed copy of HOI4 available for vanilla examples, artwork and fonts.
|
|
17
|
+
The [setup guide](docs/setup.md) covers connecting your agent and choosing game or mod paths.
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
|
|
22
|
-
|  |  |
|
|
19
|
+
Then try:
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
26
|
-
|  |  |
|
|
21
|
+
> Render this mod's focus trees and show which prerequisites are alternatives.
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
> Find the installed documentation for `save_event_target_as`, read the relevant section, and show a vanilla example.
|
|
29
24
|
|
|
30
|
-
|
|
25
|
+
> Preview this scripted GUI at 100% and 125% UI scale and check for clipped text or overlapping controls.
|
|
31
26
|
|
|
32
|
-
|
|
27
|
+
## What you can do
|
|
33
28
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
29
|
+
| Task | Guide |
|
|
30
|
+
| -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
|
31
|
+
| Find the right wiki section, game documentation or source definition | [Local references](docs/reference.md) |
|
|
32
|
+
| Inspect, preview, create or reorganize a focus tree | [Focus trees](docs/focus.md) |
|
|
33
|
+
| Trace event chains, decisions, costs and dependencies | [Events](docs/events.md) · [Decisions](docs/analysis.md) |
|
|
34
|
+
| Preview technology and doctrine trees | [Technology trees](docs/technology.md) |
|
|
35
|
+
| Inspect and repair scripted interfaces | [Scripted GUIs](docs/gui.md) |
|
|
36
|
+
| Find states and provinces, preview maps and edit connected map data | [Maps](docs/map.md) |
|
|
37
|
+
| Compare AI weights and test declared mechanic scenarios | [AI analysis](docs/probability.md) · [Mechanic tests](docs/mechanics.md) |
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
40
|
-
|  |  |
|
|
39
|
+
## Real examples
|
|
41
40
|
|
|
42
|
-
These
|
|
41
|
+
These previews were rendered from Chaos Redux and installed HOI4 files.
|
|
42
|
+
Click an image to open it at full size.
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
| Fury focus tree | Vanilla Options interface |
|
|
45
|
+
| ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
46
|
+
| [](docs/images/comparisons/fury-mcp.png) | [](docs/images/comparisons/gui/options-video-mcp.png) |
|
|
45
47
|
|
|
46
|
-
|
|
48
|
+
Follow the [examples guide](docs/examples.md) for useful prompts and expected results.
|
|
49
|
+
See [focus and technology comparisons](docs/visual-comparisons.md) and [interface comparisons](docs/gui-comparisons.md) for supplied in-game captures beside the renders.
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
npm install --global hoi4-agent-tools@latest
|
|
50
|
-
```
|
|
51
|
+
## How it fits your workflow
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
startup_timeout_sec = 120
|
|
56
|
-
tool_timeout_sec = 600
|
|
57
|
-
```
|
|
53
|
+
The server works through your coding agent and follows the mod workspace you provide.
|
|
54
|
+
It keeps large renders and reports in linked files, and retrieves relevant documentation sections instead of whole manuals.
|
|
55
|
+
Your repository instructions and review process still apply.
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
| Tool | Purpose |
|
|
64
|
-
| --------------------------- | ----------------------------------------------------------------------------------------- |
|
|
65
|
-
| `hoi4.focus_inspect` | Read focus trees, continuous-focus placement, and structural or reference problems. |
|
|
66
|
-
| `hoi4.focus_render` | Produce fast HTML, SVG, JSON, and source-linked layout artifacts. |
|
|
67
|
-
| `hoi4.focus_raster` | Produce a high-fidelity PNG review with decoded source icons. |
|
|
68
|
-
| `hoi4.focus_rewrite` | Create or update a focus tree. |
|
|
69
|
-
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
70
|
-
| `hoi4.gui_render` | Render generated and explicit GUI scenarios, states, resolutions, and layout diagnostics. |
|
|
71
|
-
| `hoi4.gui_rewrite` | Create or update a GUI source package. |
|
|
72
|
-
| `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
|
|
73
|
-
| `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
|
|
74
|
-
| `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
|
|
75
|
-
| `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
|
|
76
|
-
| `hoi4.reference_search` | Search bounded local documentation sections. |
|
|
77
|
-
| `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
|
|
78
|
-
| `hoi4.source_lookup` | Find exact definitions, overrides, usages, and narrow source blocks. |
|
|
79
|
-
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
80
|
-
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
81
|
-
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
82
|
-
| `hoi4.impact_inspect` | Trace symbol and changed-file consumers across source systems and compare proposals. |
|
|
83
|
-
| `hoi4.decision_inspect` | Inventory and evaluate decisions or missions under declared scenarios and source changes. |
|
|
84
|
-
| `hoi4.mechanic_test` | Execute bounded source effects on a copied declared scenario and check assertions. |
|
|
85
|
-
| `hoi4.package_check` | Check declarative package definitions, calls, registrations, assets, and case links. |
|
|
86
|
-
| `hoi4.scenario_test` | Run named source and domain cases in resumable, revision-bound batches. |
|
|
87
|
-
| `hoi4.job_inspect` | Inspect durable background work or retrieve its completed tool result. |
|
|
88
|
-
| `hoi4.job_cancel` | Durably request cancellation of authorized background work. |
|
|
89
|
-
| `hoi4.tech_inspect` | Scan, trace, explain, lint, and assess technology and doctrine systems. |
|
|
90
|
-
| `hoi4.tech_render` | Render source layouts with real item sizes and year guides, plus dependencies and assets. |
|
|
91
|
-
| `hoi4.tech_compare` | Compare technology graphs, placements, references, diagnostics, and source overlays. |
|
|
92
|
-
| `hoi4.probability_inspect` | Locate weighted logic and discover compatible adapters, candidates, and required inputs. |
|
|
93
|
-
| `hoi4.probability_evaluate` | Evaluate exact values, probabilities, timing, bounds, and unresolved inputs. |
|
|
94
|
-
| `hoi4.probability_sweep` | Find sensitivity, breakpoints, cliffs, and rank reversals across declared ranges. |
|
|
95
|
-
| `hoi4.probability_simulate` | Run deterministic sampled analysis with confidence and convergence data. |
|
|
96
|
-
| `hoi4.probability_sequence` | Analyze declared recovery, caps, cooldowns, resets, timers, and terminal states. |
|
|
97
|
-
| `hoi4.probability_compare` | Attribute AI-weight and MTTH changes between real or proposed source. |
|
|
98
|
-
| `hoi4.probability_render` | Render cached rankings, matrices, timing, sensitivity, sequence, and comparisons. |
|
|
99
|
-
|
|
100
|
-
Set `HOI4_AGENT_TOOLS_CHAOSX=1` on the server process to expose the optional `chaosx.focus_country_assets` and `chaosx.visual_revision` tools used by ChaosX workflows. They remain absent from the default public tool list.
|
|
101
|
-
|
|
102
|
-
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>`.
|
|
103
|
-
|
|
104
|
-
Long-running domain calls advertise optional MCP task support. A task survives client disconnects and can be retrieved after reconnect; clients without task support keep the ordinary synchronous result contract. Native background rewrites require a stable caller-supplied `requestKey` so retries resolve to one durable mutation receipt. See [Persistent jobs and MCP tasks](docs/jobs.md).
|
|
105
|
-
|
|
106
|
-
## Coexistence with agent workflows
|
|
107
|
-
|
|
108
|
-
HOI4 Agent Tools provides HOI4 domain operations without replacing repository instructions such as `AGENTS.md`, skills, plans, or subagents. Its optional weighted-logic prompt helps an agent scope one probability analysis and then returns control to the normal workflow.
|
|
109
|
-
|
|
110
|
-
Connecting and listing tools does not scan mod source. Compact tool schemas and linked resources keep large diagnostics, renders, and diffs out of the agent's working context until needed. Event, impact, decision, technology, and probability tools analyze source without editing it; only `hoi4.*_rewrite` calls edit mod source.
|
|
111
|
-
|
|
112
|
-
## Create or clean content
|
|
113
|
-
|
|
114
|
-
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.
|
|
115
|
-
|
|
116
|
-
- 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).
|
|
117
|
-
- 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).
|
|
118
|
-
- Maps: "Render the whole map and find this state by name," "Create a state from these provinces," "Create a province inside this exact rectangle," or "Swap these state IDs and update connected references." See [Maps](docs/map.md).
|
|
119
|
-
- 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).
|
|
120
|
-
- Cross-system impact and decisions: "Find every direct and transitive consumer of this idea," or "Compare who can take this decision, what they pay, and its mission end paths for these actor and target scenarios." See [Cross-system impact and decisions](docs/analysis.md).
|
|
121
|
-
- Mechanics and packages: "Test this transfer's conservation and payment," or "Check every declared package link and resume its named scenario suite." See [Mechanic tests and scenario suites](docs/mechanics.md).
|
|
122
|
-
- 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).
|
|
123
|
-
- AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
|
|
124
|
-
- References: "Find the installed effect signature and an exact vanilla example," or "Show the pertinent wiki and installed documentation for this GUI task." See [Local references](docs/reference.md).
|
|
125
|
-
|
|
126
|
-
## HTTP
|
|
127
|
-
|
|
128
|
-
Use stdio for local MCP clients. For shared or remote deployments, see [HTTP](docs/http.md).
|
|
129
|
-
|
|
130
|
-
## Development
|
|
57
|
+
Inspection and rendering do not edit the mod.
|
|
58
|
+
The focus, GUI and map rewrite tools can change source files after checking the proposed edit.
|
|
59
|
+
The server does not launch or control the game; previews and source tests cannot prove live clicks, AI choices or engine timing.
|
|
131
60
|
|
|
132
|
-
|
|
133
|
-
npm ci
|
|
134
|
-
npm run check
|
|
135
|
-
```
|
|
61
|
+
[Documentation](docs/README.md) · [Tool reference](docs/tools.md) · [Contributing](CONTRIBUTING.md) · [Security](SECURITY.md)
|
|
136
62
|
|
|
137
|
-
|
|
63
|
+
Apache-2.0 licensed.
|
|
64
|
+
Hearts of Iron IV artwork belongs to its respective owners; this project is unaffiliated with Paradox Interactive.
|
|
@@ -5,7 +5,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { GuiAssetCatalog } from './assets.js';
|
|
6
6
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_ELEMENTS, GUI_GRAPH_MAX_NODES, GUI_SCENE_MAX_DEPTH, GUI_SCENE_MAX_ELEMENTS, GUI_SCENE_MAX_TEXT_CHARACTERS, GUI_SCENE_MAX_TEXT_LAYOUT_OPERATIONS, GUI_SCENE_MAX_WORK, GUI_TEXT_MAX_CHARACTERS, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES, } from './limits.js';
|
|
7
7
|
import { guiElementAttributeFidelity } from './source-graph.js';
|
|
8
|
-
import { emptyFidelityReport } from './types.js';
|
|
8
|
+
import { emptyFidelityReport, fidelityCategories } from './types.js';
|
|
9
9
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
10
10
|
import { evaluateGuiCondition, guiConditionScenario } from './scenario-conditions.js';
|
|
11
11
|
import { parseClausewitz } from '../core/source/index.js';
|
|
@@ -636,13 +636,29 @@ function refreshSceneClips(context) {
|
|
|
636
636
|
: geometry.appearanceVisible
|
|
637
637
|
? 'outside_clip'
|
|
638
638
|
: (element.visibilityReason ?? 'visibility_false');
|
|
639
|
+
element.visibilityStatus =
|
|
640
|
+
intersection === undefined
|
|
641
|
+
? 'hidden'
|
|
642
|
+
: element.visibilityStatusReason !== undefined
|
|
643
|
+
? 'unresolved'
|
|
644
|
+
: element.visible
|
|
645
|
+
? 'shown'
|
|
646
|
+
: 'hidden';
|
|
639
647
|
if (clickableTypes.test(element.elementType)) {
|
|
640
648
|
const reason = element.visible ? geometry.interactionBlock : 'hidden';
|
|
641
649
|
if (reason === undefined)
|
|
642
650
|
delete element.disabledReason;
|
|
643
651
|
else
|
|
644
652
|
element.disabledReason = reason;
|
|
645
|
-
element.
|
|
653
|
+
element.enablement =
|
|
654
|
+
geometry.interactionBlock !== undefined
|
|
655
|
+
? 'disabled'
|
|
656
|
+
: element.visibilityStatus === 'unresolved' || element.enablementReason !== undefined
|
|
657
|
+
? 'unresolved'
|
|
658
|
+
: reason === undefined
|
|
659
|
+
? 'enabled'
|
|
660
|
+
: 'disabled';
|
|
661
|
+
element.clickable = element.enablement === 'enabled';
|
|
646
662
|
}
|
|
647
663
|
}
|
|
648
664
|
}
|
|
@@ -1142,7 +1158,7 @@ async function layoutAttachedScrollbars(definition, scrollbars, owner, childOutp
|
|
|
1142
1158
|
vertical.rect.y = Math.max(verticalRect.y, horizontalRect.y + horizontalRect.height);
|
|
1143
1159
|
vertical.rect.height = Math.max(0, verticalRect.y + verticalRect.height - vertical.rect.y);
|
|
1144
1160
|
}
|
|
1145
|
-
addFidelity(context.fidelity, '
|
|
1161
|
+
addFidelity(context.fidelity, 'approximated', 'orthogonal_scrollbar_gutters', 'Visible horizontal and vertical scrollbars reserve their anchored cross-axis gutters instead of drawing their end buttons over the same corner.', definition);
|
|
1146
1162
|
}
|
|
1147
1163
|
for (const bar of barLayouts) {
|
|
1148
1164
|
await layoutElement({
|
|
@@ -1232,6 +1248,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1232
1248
|
if (height < 0)
|
|
1233
1249
|
height = Math.max(0, parentRect.height - localY + height);
|
|
1234
1250
|
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues);
|
|
1251
|
+
const unresolvedVisibilityReason = context.unresolvedElementVisibility.get(definition.name);
|
|
1252
|
+
const parentSceneElement = parentInstanceId === undefined ? undefined : context.instancesById.get(parentInstanceId);
|
|
1253
|
+
const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
|
|
1254
|
+
? parentSceneElement.visibilityStatusReason
|
|
1255
|
+
: undefined;
|
|
1235
1256
|
const declaredHidden = scalarBoolean(property(definition.attributes, 'hide', 'hidden')) ?? false;
|
|
1236
1257
|
const sourceVisible = scalarBoolean(property(definition.attributes, 'visible')) ?? !declaredHidden;
|
|
1237
1258
|
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
@@ -1240,6 +1261,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1240
1261
|
scenario.scriptedGui[`${definition.name}.enabled`] ??
|
|
1241
1262
|
context.constantElementEnabled[definition.name] ??
|
|
1242
1263
|
scalarBoolean(property(definition.attributes, 'enabled'));
|
|
1264
|
+
const unresolvedEnabledReason = context.unresolvedElementEnabled.get(definition.name);
|
|
1265
|
+
const explicitEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1266
|
+
scenario.values[`${definition.name}.enabled`] ??
|
|
1267
|
+
scenario.scriptedGui[`${definition.name}.enabled`];
|
|
1268
|
+
const enablementUnresolved = unresolvedEnabledReason !== undefined && typeof explicitEnabled !== 'boolean';
|
|
1243
1269
|
const requestedState = scenario.elementStates[definition.name] ?? scenario.state;
|
|
1244
1270
|
const state = scriptedEnabled === false ? 'disabled' : requestedState;
|
|
1245
1271
|
const backgroundValue = property(definition.attributes, 'background');
|
|
@@ -1595,6 +1621,12 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1595
1621
|
const clipped = clipRect !== undefined &&
|
|
1596
1622
|
(availableClip === undefined || !equalRect(unclippedRect, availableClip));
|
|
1597
1623
|
const visible = resolveVisibleAppearance && (availableClip !== undefined || inheritedClip === undefined);
|
|
1624
|
+
const visibilityStatusReason = explicitlyVisible === false
|
|
1625
|
+
? undefined
|
|
1626
|
+
: (parentUnresolvedVisibility ??
|
|
1627
|
+
(inheritedVisible && explicitlyVisible === undefined
|
|
1628
|
+
? unresolvedVisibilityReason
|
|
1629
|
+
: undefined));
|
|
1598
1630
|
const clickThrough = scalarBoolean(property(definition.attributes, 'clickThrough', 'alwaystransparent', 'allwaystransparent')) ?? false;
|
|
1599
1631
|
const interactive = clickableTypes.test(definition.elementType);
|
|
1600
1632
|
const interactionBlock = !interactive
|
|
@@ -1607,7 +1639,21 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1607
1639
|
? 'scenario_state'
|
|
1608
1640
|
: undefined;
|
|
1609
1641
|
const disabledReason = interactive && !visible ? 'hidden' : interactionBlock;
|
|
1610
|
-
const
|
|
1642
|
+
const visibilityStatus = availableClip === undefined && inheritedClip !== undefined
|
|
1643
|
+
? 'hidden'
|
|
1644
|
+
: visibilityStatusReason !== undefined
|
|
1645
|
+
? 'unresolved'
|
|
1646
|
+
: visible
|
|
1647
|
+
? 'shown'
|
|
1648
|
+
: 'hidden';
|
|
1649
|
+
const enablement = interactionBlock !== undefined
|
|
1650
|
+
? 'disabled'
|
|
1651
|
+
: visibilityStatus === 'unresolved' || enablementUnresolved
|
|
1652
|
+
? 'unresolved'
|
|
1653
|
+
: disabledReason === undefined
|
|
1654
|
+
? 'enabled'
|
|
1655
|
+
: 'disabled';
|
|
1656
|
+
const clickable = interactive && enablement === 'enabled';
|
|
1611
1657
|
const visibilityReason = visible
|
|
1612
1658
|
? 'visible'
|
|
1613
1659
|
: !inheritedVisible
|
|
@@ -1661,7 +1707,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1661
1707
|
zIndex: context.output.length,
|
|
1662
1708
|
visible,
|
|
1663
1709
|
visibilityReason,
|
|
1710
|
+
visibilityStatus,
|
|
1711
|
+
...(visibilityStatusReason === undefined ? {} : { visibilityStatusReason }),
|
|
1664
1712
|
clickable,
|
|
1713
|
+
...(interactive ? { enablement } : {}),
|
|
1714
|
+
...(interactive && enablementUnresolved ? { enablementReason: unresolvedEnabledReason } : {}),
|
|
1665
1715
|
...(disabledReason === undefined ? {} : { disabledReason }),
|
|
1666
1716
|
clickThrough,
|
|
1667
1717
|
rect: availableClip ?? { x: unclippedRect.x, y: unclippedRect.y, width: 0, height: 0 },
|
|
@@ -1977,6 +2027,8 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
1977
2027
|
const scriptedWindowVisibility = {};
|
|
1978
2028
|
const constantElementVisibility = {};
|
|
1979
2029
|
const constantElementEnabled = {};
|
|
2030
|
+
const unresolvedElementEnabled = new Map();
|
|
2031
|
+
const unresolvedElementVisibility = new Map();
|
|
1980
2032
|
const resolvedScriptedProperties = {};
|
|
1981
2033
|
const dynamicListsByName = new Map();
|
|
1982
2034
|
const countryFlagProperties = new Map();
|
|
@@ -2033,11 +2085,25 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2033
2085
|
...scenario.variables,
|
|
2034
2086
|
...scenario.values,
|
|
2035
2087
|
});
|
|
2036
|
-
if (resolved === undefined)
|
|
2037
|
-
continue;
|
|
2038
2088
|
const key = `${propertyDefinition.elementName}.${suffix}`;
|
|
2039
|
-
if (
|
|
2040
|
-
|
|
2089
|
+
if (suffix === 'visible' || suffix === 'enabled') {
|
|
2090
|
+
const booleanValue = scalarBoolean(resolved ?? expression);
|
|
2091
|
+
if (booleanValue === undefined) {
|
|
2092
|
+
const reason = `${scripted.name}.${key}: scenario has no boolean value for ${typeof expression === 'string' ? expression : JSON.stringify(expression)}.`;
|
|
2093
|
+
(suffix === 'visible' ? unresolvedElementVisibility : unresolvedElementEnabled).set(propertyDefinition.elementName, reason);
|
|
2094
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_scenario_property', reason);
|
|
2095
|
+
continue;
|
|
2096
|
+
}
|
|
2097
|
+
(suffix === 'visible' ? unresolvedElementVisibility : unresolvedElementEnabled).delete(propertyDefinition.elementName);
|
|
2098
|
+
if (scenario.values[key] === undefined)
|
|
2099
|
+
resolvedScriptedProperties[key] = booleanValue;
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
if (resolved === undefined)
|
|
2103
|
+
continue;
|
|
2104
|
+
if (scenario.values[key] === undefined)
|
|
2105
|
+
resolvedScriptedProperties[key] = resolved;
|
|
2106
|
+
}
|
|
2041
2107
|
addFidelity(fidelity, 'modelled', 'scripted_gui_scenario_property', `${key} resolved from a scenario runtime value.`);
|
|
2042
2108
|
}
|
|
2043
2109
|
}
|
|
@@ -2051,13 +2117,23 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2051
2117
|
? undefined
|
|
2052
2118
|
: evaluated.state === 'true';
|
|
2053
2119
|
if (result === undefined) {
|
|
2054
|
-
|
|
2120
|
+
if (trigger.name.endsWith('_visible'))
|
|
2121
|
+
unresolvedElementVisibility.set(trigger.elementName, `${scripted.name}.${trigger.name}: ${evaluated?.unresolved.map(({ message }) => message).join('; ') ?? 'condition requires more scenario facts'}`);
|
|
2122
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
2123
|
+
unresolvedElementEnabled.set(trigger.elementName, `${scripted.name}.${trigger.name}: ${evaluated?.unresolved.map(({ message }) => message).join('; ') ?? 'condition requires more scenario facts'}`);
|
|
2124
|
+
addFidelity(fidelity, 'unresolved', 'scripted_gui_trigger', unresolvedElementVisibility.get(trigger.elementName) ??
|
|
2125
|
+
unresolvedElementEnabled.get(trigger.elementName) ??
|
|
2126
|
+
`${scripted.name}.${trigger.name} requires more scenario facts.`);
|
|
2055
2127
|
continue;
|
|
2056
2128
|
}
|
|
2057
2129
|
if (trigger.name.endsWith('_visible'))
|
|
2058
2130
|
constantElementVisibility[trigger.elementName] = result;
|
|
2131
|
+
if (trigger.name.endsWith('_visible'))
|
|
2132
|
+
unresolvedElementVisibility.delete(trigger.elementName);
|
|
2059
2133
|
if (trigger.name.endsWith('_click_enabled'))
|
|
2060
2134
|
constantElementEnabled[trigger.elementName] = result;
|
|
2135
|
+
if (trigger.name.endsWith('_click_enabled'))
|
|
2136
|
+
unresolvedElementEnabled.delete(trigger.elementName);
|
|
2061
2137
|
addFidelity(fidelity, 'modelled', trigger.constantResult === undefined
|
|
2062
2138
|
? 'scripted_gui_trigger'
|
|
2063
2139
|
: 'scripted_gui_constant_trigger', `${trigger.name} resolves to ${result ? 'yes' : 'no'}.`);
|
|
@@ -2117,6 +2193,8 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2117
2193
|
dynamicListsByName,
|
|
2118
2194
|
countryFlagProperties,
|
|
2119
2195
|
constantElementEnabled,
|
|
2196
|
+
unresolvedElementEnabled,
|
|
2197
|
+
unresolvedElementVisibility,
|
|
2120
2198
|
spritesByName: new Map(graph.sprites.map((sprite) => [sprite.name.toLowerCase(), sprite])),
|
|
2121
2199
|
localisation,
|
|
2122
2200
|
output,
|
|
@@ -2171,11 +2249,158 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2171
2249
|
});
|
|
2172
2250
|
}
|
|
2173
2251
|
refreshSceneClips(context);
|
|
2252
|
+
let tooltipPreview;
|
|
2253
|
+
if (scenario.tooltip !== undefined) {
|
|
2254
|
+
const { target: targetToken, x, y, width, padding } = scenario.tooltip;
|
|
2255
|
+
const textWidth = width - 2 * padding;
|
|
2256
|
+
const skipTooltip = (code, message) => {
|
|
2257
|
+
diagnostics.push({ code, severity: 'warning', category: 'rendering', message });
|
|
2258
|
+
addFidelity(fidelity, 'unresolved', 'immediate_tooltip_preview', message);
|
|
2259
|
+
};
|
|
2260
|
+
const matches = output.filter(({ id, name }) => id === targetToken || name === targetToken);
|
|
2261
|
+
const target = matches[0];
|
|
2262
|
+
if (matches.length === 0)
|
|
2263
|
+
skipTooltip('GUI_TOOLTIP_TARGET_MISSING', `Tooltip target ${targetToken} is absent from the scene.`);
|
|
2264
|
+
else if (matches.length > 1)
|
|
2265
|
+
skipTooltip('GUI_TOOLTIP_TARGET_AMBIGUOUS', `Tooltip target ${targetToken} matches ${matches.length} scene elements; select an instance id.`);
|
|
2266
|
+
else if (target?.visibilityStatus === 'unresolved')
|
|
2267
|
+
skipTooltip('GUI_TOOLTIP_TARGET_UNRESOLVED', `Tooltip target ${targetToken} has unresolved visibility: ${target.visibilityStatusReason ?? 'more scenario facts are required'}.`);
|
|
2268
|
+
else if (target?.visible !== true)
|
|
2269
|
+
skipTooltip('GUI_TOOLTIP_TARGET_HIDDEN', `Tooltip target ${targetToken} is hidden in this preview.`);
|
|
2270
|
+
else {
|
|
2271
|
+
const source = elementsById.get(target.sourceId);
|
|
2272
|
+
const localisationKey = source === undefined ? undefined : scalarString(property(source.attributes, 'pdx_tooltip'));
|
|
2273
|
+
const localised = localisationKey === undefined
|
|
2274
|
+
? undefined
|
|
2275
|
+
: (scenario.localisation[localisationKey] ?? localisation.get(localisationKey));
|
|
2276
|
+
const tooltipSprite = graph.sprites.find(({ name }) => name.toLowerCase() === 'tooltip');
|
|
2277
|
+
const tooltipFont = catalog.fontDefinition('cg_16b');
|
|
2278
|
+
if (localisationKey === undefined)
|
|
2279
|
+
skipTooltip('GUI_TOOLTIP_KEY_MISSING', `Tooltip target ${targetToken} has no pdx_tooltip localisation key.`);
|
|
2280
|
+
else if (localised === undefined)
|
|
2281
|
+
skipTooltip('GUI_TOOLTIP_LOCALISATION_MISSING', `Tooltip localisation ${localisationKey} is unavailable in ${scenario.language}.`);
|
|
2282
|
+
else if (tooltipSprite === undefined)
|
|
2283
|
+
skipTooltip('GUI_TOOLTIP_SPRITE_MISSING', 'The declared ToolTip text sprite is unavailable.');
|
|
2284
|
+
else if (tooltipFont === undefined ||
|
|
2285
|
+
catalog.resolvedFontMetrics('cg_16b').source === 'approximation')
|
|
2286
|
+
skipTooltip('GUI_TOOLTIP_FONT_MISSING', 'The declared cg_16b tooltip font or its metrics are unavailable.');
|
|
2287
|
+
else if (x + width > scenario.resolution.width || y >= scenario.resolution.height)
|
|
2288
|
+
skipTooltip('GUI_TOOLTIP_BOUNDS', 'Caller-declared tooltip position and width exceed the viewport.');
|
|
2289
|
+
else {
|
|
2290
|
+
const spriteFrame = await catalog.loadSpriteFrame(tooltipSprite, 0);
|
|
2291
|
+
if (spriteFrame?.supported !== true || spriteFrame.dataUri === undefined)
|
|
2292
|
+
skipTooltip('GUI_TOOLTIP_SPRITE_MISSING', spriteFrame?.reason ?? 'The ToolTip texture is unavailable.');
|
|
2293
|
+
else {
|
|
2294
|
+
const missingEngineText = [];
|
|
2295
|
+
const suppliedText = replaceTextBounded(localised, /\[![^\]]+\]/gu, (match) => {
|
|
2296
|
+
const token = match[0];
|
|
2297
|
+
const supplied = scenario.values[token] ?? scenario.scriptedGui[token];
|
|
2298
|
+
if (typeof supplied === 'string' && supplied.length > 0)
|
|
2299
|
+
return supplied;
|
|
2300
|
+
missingEngineText.push(token);
|
|
2301
|
+
return token;
|
|
2302
|
+
}, 'GUI tooltip supplied effect text');
|
|
2303
|
+
const resolved = resolveTokenText(suppliedText, scenario, localisation);
|
|
2304
|
+
const unresolved = [
|
|
2305
|
+
...new Set([
|
|
2306
|
+
...missingEngineText,
|
|
2307
|
+
...resolved.unresolved,
|
|
2308
|
+
...[...resolved.text.matchAll(/\[![^\]]+\]/gu)].map(([token]) => token),
|
|
2309
|
+
]),
|
|
2310
|
+
];
|
|
2311
|
+
if (unresolved.length > 0)
|
|
2312
|
+
skipTooltip('GUI_TOOLTIP_TEXT_UNRESOLVED', `Tooltip ${localisationKey} requires supplied values for ${unresolved.join(', ')}.`);
|
|
2313
|
+
else {
|
|
2314
|
+
const tooltipTextDefinition = {
|
|
2315
|
+
id: `tooltip-preview:${target.id}:text`,
|
|
2316
|
+
name: `${target.name}_immediate_tooltip_text`,
|
|
2317
|
+
elementType: 'instantTextBoxType',
|
|
2318
|
+
sourcePath: source?.sourcePath ?? target.sourcePath,
|
|
2319
|
+
childIds: [],
|
|
2320
|
+
attributes: {
|
|
2321
|
+
position: { x: (x + padding) / baseScale, y: (y + padding) / baseScale },
|
|
2322
|
+
size: { width: textWidth / baseScale, height: 0 },
|
|
2323
|
+
text: suppliedText,
|
|
2324
|
+
font: 'cg_16b',
|
|
2325
|
+
multiline: true,
|
|
2326
|
+
fixedsize: true,
|
|
2327
|
+
},
|
|
2328
|
+
unsupportedAttributes: [],
|
|
2329
|
+
rawSource: '',
|
|
2330
|
+
definitionOrder: source?.definitionOrder ?? 0,
|
|
2331
|
+
};
|
|
2332
|
+
const tooltipContext = {
|
|
2333
|
+
...context,
|
|
2334
|
+
output: [],
|
|
2335
|
+
instancesById: new Map(),
|
|
2336
|
+
geometry: new Map(),
|
|
2337
|
+
fidelity: emptyFidelityReport(),
|
|
2338
|
+
diagnostics: [],
|
|
2339
|
+
work: new GuiSceneWorkBudget(),
|
|
2340
|
+
};
|
|
2341
|
+
await layoutElement(tooltipTextDefinition, viewport, viewport, baseScale, 0, tooltipContext);
|
|
2342
|
+
refreshSceneClips(tooltipContext);
|
|
2343
|
+
const textElement = tooltipContext.output[0];
|
|
2344
|
+
const popupText = textElement?.text;
|
|
2345
|
+
if (textElement === undefined ||
|
|
2346
|
+
popupText === undefined ||
|
|
2347
|
+
popupText.unresolvedTokens.length > 0 ||
|
|
2348
|
+
popupText.metricSource === 'approximation' ||
|
|
2349
|
+
popupText.glyphLines.some(({ source: glyphSource, missingGlyphs }) => glyphSource === 'deterministic-fallback' || missingGlyphs.length > 0) ||
|
|
2350
|
+
popupText.inlineIcons?.some(({ sprite }) => sprite?.supported !== true || sprite.dataUri === undefined))
|
|
2351
|
+
skipTooltip('GUI_TOOLTIP_ASSET_OR_TEXT_UNRESOLVED', `Tooltip ${localisationKey} has unresolved text, glyph, icon, or texture data.`);
|
|
2352
|
+
else if (popupText.overflowX ||
|
|
2353
|
+
popupText.overflowY ||
|
|
2354
|
+
y + popupText.measuredHeight + 2 * padding > scenario.resolution.height)
|
|
2355
|
+
skipTooltip('GUI_TOOLTIP_BOUNDS', 'Tooltip text does not fit the caller-declared inset or the padded layout exceeds the viewport.');
|
|
2356
|
+
else {
|
|
2357
|
+
const backgroundDefinition = {
|
|
2358
|
+
...tooltipTextDefinition,
|
|
2359
|
+
id: `tooltip-preview:${target.id}`,
|
|
2360
|
+
name: `${target.name}_immediate_tooltip`,
|
|
2361
|
+
elementType: 'iconType',
|
|
2362
|
+
attributes: {
|
|
2363
|
+
position: { x: x / baseScale, y: y / baseScale },
|
|
2364
|
+
size: {
|
|
2365
|
+
width: width / baseScale,
|
|
2366
|
+
height: (popupText.measuredHeight + 2 * padding) / baseScale,
|
|
2367
|
+
},
|
|
2368
|
+
spriteType: tooltipSprite.name,
|
|
2369
|
+
},
|
|
2370
|
+
};
|
|
2371
|
+
await layoutElement(backgroundDefinition, viewport, viewport, baseScale, 0, tooltipContext);
|
|
2372
|
+
const backgroundElement = tooltipContext.instancesById.get(backgroundDefinition.id);
|
|
2373
|
+
backgroundElement.zIndex = 0;
|
|
2374
|
+
textElement.zIndex = 1;
|
|
2375
|
+
textElement.depth = 1;
|
|
2376
|
+
textElement.parentId = backgroundElement.id;
|
|
2377
|
+
tooltipPreview = {
|
|
2378
|
+
targetId: target.id,
|
|
2379
|
+
localisationKey,
|
|
2380
|
+
placement: 'caller-declared',
|
|
2381
|
+
element: backgroundElement,
|
|
2382
|
+
textElement,
|
|
2383
|
+
};
|
|
2384
|
+
for (const category of fidelityCategories)
|
|
2385
|
+
fidelity[category].push(...tooltipContext.fidelity[category]);
|
|
2386
|
+
fidelity.ignored = fidelity.ignored.filter(({ field, elementId }) => field !== 'pdx_tooltip' || elementId !== target.sourceId);
|
|
2387
|
+
addFidelity(fidelity, 'approximated', 'immediate_tooltip_preview', `Immediate tooltip ${localisationKey} uses caller-declared viewport position, width, and ${padding}px padding with ToolTip and cg_16b; native placement, padding, and timing remain unverified.`);
|
|
2388
|
+
addFidelity(fidelity, 'approximated', 'tooltip_text_sprite_composition', 'ToolTip textSpriteType texture is stretched to the measured text height plus caller-declared padding; native frame composition is unverified.');
|
|
2389
|
+
addFidelity(fidelity, 'unsupported', 'pdx_tooltip_delayed', 'Delayed tooltip timing is not previewed.');
|
|
2390
|
+
addFidelity(fidelity, 'unsupported', 'click_to_front', 'Tooltip preview does not model click_to_front behavior.');
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2174
2397
|
output.sort((left, right) => left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
2175
|
-
const bounds = unionRects(output.flatMap((element) => {
|
|
2398
|
+
const bounds = unionRects([...output, ...(tooltipPreview === undefined ? [] : [tooltipPreview.element])].flatMap((element) => {
|
|
2176
2399
|
if (!element.visible)
|
|
2177
2400
|
return [];
|
|
2178
|
-
const painted = element.sprite === undefined && element.progressColours === undefined
|
|
2401
|
+
const painted = element.sprite === undefined && element.progressColours === undefined
|
|
2402
|
+
? []
|
|
2403
|
+
: [element.rect];
|
|
2179
2404
|
const text = element.text;
|
|
2180
2405
|
if (text !== undefined && text.text.length > 0) {
|
|
2181
2406
|
const rect = element.unclippedRect;
|
|
@@ -2209,6 +2434,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2209
2434
|
scenario,
|
|
2210
2435
|
resolution: scenario.resolution,
|
|
2211
2436
|
elements: output,
|
|
2437
|
+
...(tooltipPreview === undefined ? {} : { tooltipPreview }),
|
|
2212
2438
|
bounds,
|
|
2213
2439
|
fidelity,
|
|
2214
2440
|
diagnostics: sortDiagnostics(diagnostics),
|