hoi4-agent-tools 2.5.2 → 2.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 +13 -0
- package/README.md +5 -5
- package/dist/hoi4_agent_tools/core/index.js +22 -6
- package/dist/hoi4_agent_tools/focus/compact.js +103 -12
- package/dist/hoi4_agent_tools/focus/continuous-render.js +4 -10
- package/dist/hoi4_agent_tools/focus/layout.js +15 -7
- package/dist/hoi4_agent_tools/focus/render.js +2 -8
- package/dist/hoi4_agent_tools/gui/dds.js +5 -2
- package/dist/hoi4_agent_tools/gui/layout.js +1 -1
- package/dist/hoi4_agent_tools/gui/renderer.js +4 -16
- package/dist/hoi4_agent_tools/gui/source-graph.js +9 -8
- package/dist/hoi4_agent_tools/gui/studio.js +58 -4
- package/dist/hoi4_agent_tools/map/catalog.js +398 -0
- package/dist/hoi4_agent_tools/map/index.js +1 -0
- package/dist/hoi4_agent_tools/map/operations.js +337 -30
- package/dist/hoi4_agent_tools/map/render.js +40 -147
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +85 -9
- package/dist/hoi4_agent_tools/mcp/transports/http.js +4 -5
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +7 -7
- package/dist/hoi4_agent_tools/schemas/map.js +69 -2
- package/dist/hoi4_agent_tools/technology/render.js +65 -19
- package/dist/hoi4_agent_tools/technology/service.js +67 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +2 -0
- package/docs/map.md +60 -23
- package/docs/technology.md +1 -1
- package/package.json +3 -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/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/map-operation.schema.json +228 -74
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
No unreleased changes.
|
|
6
6
|
|
|
7
|
+
## 2.6.0 - 2026-08-22
|
|
8
|
+
|
|
9
|
+
- Added a shared complete map catalog with resolved names, source locations, geometry, memberships, networks, positions, buildings, ports, adjacency, and localisation evidence.
|
|
10
|
+
- Made map inspection return a full-map overview and a searchable, clickable HTML navigator with exact province hit testing and pixel/map coordinate lookup.
|
|
11
|
+
- Added compact state and province creation defaults, rectangle province geometry, and one-call province, state, and strategic-region ID swapping with connected map-reference and standard localisation updates.
|
|
12
|
+
|
|
13
|
+
## 2.5.3 - 2026-08-22
|
|
14
|
+
|
|
15
|
+
- Resolved and embedded technology icons through the complete mod, dependency, and installed-game load order, including vanilla sprites used by mod content.
|
|
16
|
+
- Recognized vanilla `SpriteType` capitalization consistently in the shared index and GUI source graph.
|
|
17
|
+
- Decoded uncompressed vanilla DDS textures whose header stores total linear size instead of row pitch.
|
|
18
|
+
- Added deterministic cross-root focus, scripted-GUI, and technology regressions plus an opt-in installed-game DDS render test.
|
|
19
|
+
|
|
7
20
|
## 2.5.2 - 2026-08-15
|
|
8
21
|
|
|
9
22
|
- Distinguished HOI4 small and large technology layouts in graph data and source-positioned folder renders, including exact 72×72 small items selected by `force_use_small_tech_layout`.
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
6
6
|
|
|
7
7
|
- Focus trees: inspect structure and references, render layouts, create trees, and reorganize existing branches.
|
|
8
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
|
-
- Maps: inspect provinces, states, regions, adjacency, supply, and
|
|
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
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.
|
|
12
12
|
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
@@ -40,9 +40,9 @@ Paste the printed Codex or generic global-install entry into your MCP client, th
|
|
|
40
40
|
| `hoi4.gui_inspect` | Read a scripted GUI and its linked assets and logic. |
|
|
41
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
|
-
| `hoi4.map_inspect` |
|
|
44
|
-
| `hoi4.map_render` | Render map layers and
|
|
45
|
-
| `hoi4.map_rewrite` | Create or update
|
|
43
|
+
| `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
|
|
44
|
+
| `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
|
|
45
|
+
| `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
|
|
46
46
|
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
47
47
|
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
48
48
|
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
@@ -71,7 +71,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
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
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
|
-
- Maps: "Create a state from these
|
|
74
|
+
- 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).
|
|
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).
|
|
77
77
|
- 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).
|
|
@@ -2,6 +2,22 @@ import { compareCodeUnits } from './canonical.js';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { sortDiagnostics } from './diagnostics.js';
|
|
4
4
|
import { parseClausewitz, parseLocalisation, assignments, childBlocks, firstScalar, locationFor, sourcePartialLimitDiagnostics, SOURCE_MAX_BYTES, SOURCE_MAX_NESTING, nodeLocation, } from './source/index.js';
|
|
5
|
+
const spriteDefinitionKeys = new Set([
|
|
6
|
+
'spritetype',
|
|
7
|
+
'textspritetype',
|
|
8
|
+
'frameanimatedspritetype',
|
|
9
|
+
'corneredtilespritetype',
|
|
10
|
+
'maskedshieldtype',
|
|
11
|
+
]);
|
|
12
|
+
function firstScalarInsensitiveNode(block, ...keys) {
|
|
13
|
+
const normalizedKeys = new Set(keys.map((key) => key.toLowerCase()));
|
|
14
|
+
for (const assignment of assignments(block)) {
|
|
15
|
+
if (assignment.value.type === 'scalar' &&
|
|
16
|
+
normalizedKeys.has(assignment.key.value.toLowerCase()))
|
|
17
|
+
return assignment.value;
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
5
21
|
const guiElementKeys = new Set([
|
|
6
22
|
'containerWindowType',
|
|
7
23
|
'windowType',
|
|
@@ -992,10 +1008,11 @@ export class SymbolIndex {
|
|
|
992
1008
|
metadata: {},
|
|
993
1009
|
});
|
|
994
1010
|
}
|
|
995
|
-
else if ((
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
const
|
|
1011
|
+
else if (sourcePath.endsWith('.gfx') &&
|
|
1012
|
+
spriteDefinitionKeys.has(key.toLowerCase()) &&
|
|
1013
|
+
firstScalarInsensitiveNode(child, 'name') !== undefined) {
|
|
1014
|
+
const name = firstScalarInsensitiveNode(child, 'name');
|
|
1015
|
+
const texture = firstScalarInsensitiveNode(child, 'texturefile', 'textureFile');
|
|
999
1016
|
this.addSymbol({
|
|
1000
1017
|
kind: 'sprite',
|
|
1001
1018
|
id: name.value,
|
|
@@ -1006,8 +1023,7 @@ export class SymbolIndex {
|
|
|
1006
1023
|
metadata: {
|
|
1007
1024
|
spriteType: key,
|
|
1008
1025
|
texture: texture?.value,
|
|
1009
|
-
frames: numeric(
|
|
1010
|
-
numeric(firstScalar(child, 'noofframes')?.value),
|
|
1026
|
+
frames: numeric(firstScalarInsensitiveNode(child, 'noOfFrames', 'noofframes')?.value),
|
|
1011
1027
|
},
|
|
1012
1028
|
});
|
|
1013
1029
|
if (texture !== undefined)
|
|
@@ -90,6 +90,51 @@ function compactStructuralAnchorX(plan, focus, parents, preferredX, laneBases) {
|
|
|
90
90
|
}
|
|
91
91
|
return laneBases.get(focusLaneId(plan, focus)) ?? 0;
|
|
92
92
|
}
|
|
93
|
+
function structuralRows(plan) {
|
|
94
|
+
const focusIds = new Set(plan.focuses.map(({ id }) => id));
|
|
95
|
+
const parentsByFocus = new Map(plan.focuses.map((focus) => [
|
|
96
|
+
focus.id,
|
|
97
|
+
[...new Set(prerequisiteIds(plan, focus.id).filter((parentId) => focusIds.has(parentId)))],
|
|
98
|
+
]));
|
|
99
|
+
const childrenByFocus = new Map();
|
|
100
|
+
const unresolvedParents = new Map();
|
|
101
|
+
for (const focus of plan.focuses) {
|
|
102
|
+
const parents = parentsByFocus.get(focus.id) ?? [];
|
|
103
|
+
unresolvedParents.set(focus.id, parents.length);
|
|
104
|
+
for (const parentId of parents) {
|
|
105
|
+
const children = childrenByFocus.get(parentId) ?? [];
|
|
106
|
+
children.push(focus.id);
|
|
107
|
+
childrenByFocus.set(parentId, children);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const children of childrenByFocus.values())
|
|
111
|
+
children.sort((left, right) => compareCodeUnits(left, right));
|
|
112
|
+
const rows = new Map();
|
|
113
|
+
const pending = [...plan.focuses]
|
|
114
|
+
.map(({ id }) => id)
|
|
115
|
+
.filter((focusId) => (unresolvedParents.get(focusId) ?? 0) === 0)
|
|
116
|
+
.sort((left, right) => compareCodeUnits(right, left));
|
|
117
|
+
for (const focusId of pending)
|
|
118
|
+
rows.set(focusId, 0);
|
|
119
|
+
let visited = 0;
|
|
120
|
+
while (pending.length > 0) {
|
|
121
|
+
const focusId = pending.pop();
|
|
122
|
+
if (focusId === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
visited += 1;
|
|
125
|
+
const nextRow = (rows.get(focusId) ?? 0) + 1;
|
|
126
|
+
for (const childId of childrenByFocus.get(focusId) ?? []) {
|
|
127
|
+
rows.set(childId, Math.max(rows.get(childId) ?? 0, nextRow));
|
|
128
|
+
const remaining = (unresolvedParents.get(childId) ?? 0) - 1;
|
|
129
|
+
unresolvedParents.set(childId, remaining);
|
|
130
|
+
if (remaining === 0)
|
|
131
|
+
pending.push(childId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// The layout engine reports dependency cycles. Keep authored row preferences
|
|
135
|
+
// for a cyclic graph instead of inventing a partial topological reflow.
|
|
136
|
+
return visited === plan.focuses.length ? rows : undefined;
|
|
137
|
+
}
|
|
93
138
|
function mirrorSiblingCohorts(plan, preferredX, preferredY) {
|
|
94
139
|
const cohorts = new Map();
|
|
95
140
|
const laneBases = compactLaneBases(plan);
|
|
@@ -182,7 +227,7 @@ function straightenLinearChains(plan, preferredX, preferredY) {
|
|
|
182
227
|
}
|
|
183
228
|
}
|
|
184
229
|
function compactCandidate(plan, layout, strategy) {
|
|
185
|
-
const { scale, compressRows, mirrorSiblings, straightenChains } = strategy;
|
|
230
|
+
const { scale, compressRows, structuralRows: useStructuralRows, mirrorSiblings, straightenChains, } = strategy;
|
|
186
231
|
const minimumX = Math.min(...layout.nodes.map(({ x }) => x));
|
|
187
232
|
const maximumX = Math.max(...layout.nodes.map(({ x }) => x));
|
|
188
233
|
const minimumY = Math.min(...layout.nodes.map(({ y }) => y));
|
|
@@ -194,11 +239,19 @@ function compactCandidate(plan, layout, strategy) {
|
|
|
194
239
|
node.id,
|
|
195
240
|
compressRows ? (compactRows.get(node.y) ?? node.y - minimumY) : node.y - minimumY,
|
|
196
241
|
]));
|
|
242
|
+
if (useStructuralRows) {
|
|
243
|
+
const normalizedRows = structuralRows(plan);
|
|
244
|
+
if (normalizedRows !== undefined) {
|
|
245
|
+
for (const [focusId, row] of normalizedRows)
|
|
246
|
+
preferredY.set(focusId, row);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
197
249
|
const rows = new Map();
|
|
198
250
|
for (const node of layout.nodes) {
|
|
199
|
-
const
|
|
251
|
+
const y = preferredY.get(node.id) ?? node.y;
|
|
252
|
+
const row = rows.get(y) ?? [];
|
|
200
253
|
row.push(node);
|
|
201
|
-
rows.set(
|
|
254
|
+
rows.set(y, row);
|
|
202
255
|
}
|
|
203
256
|
for (const row of rows.values()) {
|
|
204
257
|
row.sort((left, right) => left.x - right.x || compareCodeUnits(left.id, right.id));
|
|
@@ -249,12 +302,12 @@ function compactScore(layout) {
|
|
|
249
302
|
metrics.symmetry.offAnchorSiblingCohortCount,
|
|
250
303
|
metrics.symmetry.totalSiblingDeviation,
|
|
251
304
|
metrics.symmetry.totalSiblingAnchorDeviation,
|
|
252
|
-
metrics.bounds.rowCount,
|
|
253
|
-
metrics.bounds.columnCount,
|
|
254
305
|
metrics.connectors.maximumHorizontalSpan,
|
|
255
|
-
metrics.connectors.totalHorizontalSpan,
|
|
256
306
|
metrics.connectors.maximumManhattanSpan,
|
|
307
|
+
metrics.connectors.totalHorizontalSpan,
|
|
257
308
|
metrics.connectors.totalVerticalSpan,
|
|
309
|
+
metrics.bounds.rowCount,
|
|
310
|
+
metrics.bounds.columnCount,
|
|
258
311
|
layout.layoutHash,
|
|
259
312
|
];
|
|
260
313
|
}
|
|
@@ -281,14 +334,50 @@ function compactScales(focusCount) {
|
|
|
281
334
|
}
|
|
282
335
|
function compactStrategies(focusCount) {
|
|
283
336
|
const preservationStrategies = [
|
|
284
|
-
{
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
337
|
+
{
|
|
338
|
+
scale: 1,
|
|
339
|
+
compressRows: false,
|
|
340
|
+
structuralRows: false,
|
|
341
|
+
mirrorSiblings: false,
|
|
342
|
+
straightenChains: false,
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
scale: 1,
|
|
346
|
+
compressRows: true,
|
|
347
|
+
structuralRows: false,
|
|
348
|
+
mirrorSiblings: false,
|
|
349
|
+
straightenChains: false,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
scale: 1,
|
|
353
|
+
compressRows: false,
|
|
354
|
+
structuralRows: false,
|
|
355
|
+
mirrorSiblings: false,
|
|
356
|
+
straightenChains: true,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
scale: 1,
|
|
360
|
+
compressRows: false,
|
|
361
|
+
structuralRows: false,
|
|
362
|
+
mirrorSiblings: true,
|
|
363
|
+
straightenChains: false,
|
|
364
|
+
},
|
|
288
365
|
];
|
|
289
366
|
const compactStrategies = compactScales(focusCount).flatMap((scale) => [
|
|
290
|
-
{
|
|
291
|
-
|
|
367
|
+
{
|
|
368
|
+
scale,
|
|
369
|
+
compressRows: false,
|
|
370
|
+
structuralRows: false,
|
|
371
|
+
mirrorSiblings: true,
|
|
372
|
+
straightenChains: true,
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
scale,
|
|
376
|
+
compressRows: false,
|
|
377
|
+
structuralRows: true,
|
|
378
|
+
mirrorSiblings: true,
|
|
379
|
+
straightenChains: true,
|
|
380
|
+
},
|
|
292
381
|
]);
|
|
293
382
|
return [...preservationStrategies, ...compactStrategies];
|
|
294
383
|
}
|
|
@@ -307,6 +396,7 @@ export function compactFocusTreePlan(plan) {
|
|
|
307
396
|
? compactCandidate(plan, currentLayout, {
|
|
308
397
|
scale: 1,
|
|
309
398
|
compressRows: false,
|
|
399
|
+
structuralRows: false,
|
|
310
400
|
mirrorSiblings: false,
|
|
311
401
|
straightenChains: false,
|
|
312
402
|
})
|
|
@@ -358,6 +448,7 @@ export async function compactFocusTreePlanAsync(plan, options = {}) {
|
|
|
358
448
|
? compactCandidate(plan, currentLayout, {
|
|
359
449
|
scale: 1,
|
|
360
450
|
compressRows: false,
|
|
451
|
+
structuralRows: false,
|
|
361
452
|
mirrorSiblings: false,
|
|
362
453
|
straightenChains: false,
|
|
363
454
|
})
|
|
@@ -42,7 +42,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
42
42
|
const rows = Math.max(1, Math.ceil(plan.focuses.length / columns));
|
|
43
43
|
const usedColumns = Math.max(1, Math.min(columns, plan.focuses.length));
|
|
44
44
|
const width = padding * 2 + usedColumns * cardWidth + Math.max(0, usedColumns - 1) * gap;
|
|
45
|
-
const height = padding * 2 +
|
|
45
|
+
const height = padding * 2 + 52 + rows * cardHeight + Math.max(0, rows - 1) * gap;
|
|
46
46
|
if (options.rasterize !== false) {
|
|
47
47
|
budget.reserve(width, height, 'continuous focus palette PNG');
|
|
48
48
|
}
|
|
@@ -61,7 +61,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
61
61
|
const column = index % columns;
|
|
62
62
|
const row = Math.floor(index / columns);
|
|
63
63
|
const x = padding + column * (cardWidth + gap);
|
|
64
|
-
const y = padding +
|
|
64
|
+
const y = padding + 52 + row * (cardHeight + gap);
|
|
65
65
|
const entry = options.presentation?.entries[focus.id];
|
|
66
66
|
const iconSprite = focus.icons[0]?.sprite;
|
|
67
67
|
const icon = iconSprite === undefined ? undefined : options.presentation?.icons[iconSprite];
|
|
@@ -80,13 +80,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
80
80
|
weight: 700,
|
|
81
81
|
fill: '#f3f6f8',
|
|
82
82
|
});
|
|
83
|
-
const
|
|
84
|
-
x: padding,
|
|
85
|
-
y: padding + 27,
|
|
86
|
-
fontSize: 12,
|
|
87
|
-
fill: '#d7b65b',
|
|
88
|
-
});
|
|
89
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><title>${escapeXml(plan.id)} continuous focus palette</title><desc>Offline source-derived continuous focus palette; not an in-game screenshot.</desc><defs>${toolText.definitions()}</defs><rect width="${width}" height="${height}" fill="#101820"/>${heading}${notice}${cards.join('')}</svg>`;
|
|
83
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><title>${escapeXml(plan.id)} continuous focus palette</title><desc>${plan.focuses.length} continuous focuses.</desc><defs>${toolText.definitions()}</defs><rect width="${width}" height="${height}" fill="#101820"/>${heading}${cards.join('')}</svg>`;
|
|
90
84
|
const compiled = compileContinuousFocusPaletteWithSourceMap(plan);
|
|
91
85
|
const json = `${canonicalJson({
|
|
92
86
|
schemaVersion: 1,
|
|
@@ -122,7 +116,7 @@ export async function renderContinuousFocusPalette(plan, diagnostics, options =
|
|
|
122
116
|
},
|
|
123
117
|
renderProfile: options.renderProfile ?? {},
|
|
124
118
|
})}\n`;
|
|
125
|
-
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeXml(plan.id)} continuous focus palette</title><style>html{background:#101820;color:#f3f6f8;font:14px system-ui}body{margin:0;padding:24px}
|
|
119
|
+
const html = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>${escapeXml(plan.id)} continuous focus palette</title><style>html{background:#101820;color:#f3f6f8;font:14px system-ui}body{margin:0;padding:24px}svg{max-width:100%;height:auto;border:1px solid #385064}pre{white-space:pre-wrap}</style></head><body><h1>${escapeXml(plan.id)}</h1>${svg}<details><summary>Structured source model</summary><pre>${escapeXml(json)}</pre></details></body></html>`;
|
|
126
120
|
options.signal?.throwIfAborted();
|
|
127
121
|
let png = Buffer.alloc(0);
|
|
128
122
|
if (options.rasterize !== false) {
|
|
@@ -1529,6 +1529,7 @@ function siblingBatchConflicts(context, cohort, override) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
function* refineSiblingCohorts(context, edges, initial) {
|
|
1531
1531
|
let current = initial;
|
|
1532
|
+
let evaluatedEdges = edges;
|
|
1532
1533
|
for (let pass = 0; pass < 2; pass += 1) {
|
|
1533
1534
|
let accepted = false;
|
|
1534
1535
|
for (const [cohortIndex, cohort] of context.siblingCohorts.entries()) {
|
|
@@ -1558,7 +1559,7 @@ function* refineSiblingCohorts(context, edges, initial) {
|
|
|
1558
1559
|
continue;
|
|
1559
1560
|
if (siblingBatchConflicts(context, cohortIds, override))
|
|
1560
1561
|
continue;
|
|
1561
|
-
const candidateEdges = edgesWithXOverride(
|
|
1562
|
+
const candidateEdges = edgesWithXOverride(evaluatedEdges, override);
|
|
1562
1563
|
const crossingSummary = yield* connectorCrossings(candidateEdges, context);
|
|
1563
1564
|
if (crossingSummary.count > current.crossingCount)
|
|
1564
1565
|
continue;
|
|
@@ -1587,6 +1588,7 @@ function* refineSiblingCohorts(context, edges, initial) {
|
|
|
1587
1588
|
movePlacedFocusX(context, node, x);
|
|
1588
1589
|
}
|
|
1589
1590
|
current = best.score;
|
|
1591
|
+
evaluatedEdges = edgesWithXOverride(evaluatedEdges, best.override);
|
|
1590
1592
|
accepted = true;
|
|
1591
1593
|
}
|
|
1592
1594
|
if (!accepted)
|
|
@@ -1644,13 +1646,19 @@ const CONNECTOR_REFINEMENT_WORK_MAX = 7_000_000;
|
|
|
1644
1646
|
function* refineConnectorQualityWithinBudget(context) {
|
|
1645
1647
|
const maximumAcceptedMoves = context.aggressiveAestheticRepair ? 32 : 24;
|
|
1646
1648
|
const maximumCandidateNodes = context.aggressiveAestheticRepair ? 48 : 32;
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1649
|
+
let edges = yield* connectorEdges(context);
|
|
1650
|
+
let crossingSummary = yield* connectorCrossings(edges, context);
|
|
1651
|
+
let intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
|
|
1652
|
+
let symmetry = siblingQuality(context);
|
|
1651
1653
|
let current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
|
|
1652
|
-
if (context.aggressiveAestheticRepair)
|
|
1653
|
-
|
|
1654
|
+
if (context.aggressiveAestheticRepair) {
|
|
1655
|
+
yield* refineSiblingCohorts(context, edges, current);
|
|
1656
|
+
edges = yield* connectorEdges(context);
|
|
1657
|
+
crossingSummary = yield* connectorCrossings(edges, context);
|
|
1658
|
+
intersectionSummary = yield* connectorNodeIntersectionSummary(context, edges);
|
|
1659
|
+
symmetry = siblingQuality(context);
|
|
1660
|
+
current = connectorQualityScore(context, edges, crossingSummary.count, intersectionSummary.count, symmetry);
|
|
1661
|
+
}
|
|
1654
1662
|
const candidateIds = new Set(intersectionSummary.involvedFocusIds);
|
|
1655
1663
|
const longFocusIds = new Set();
|
|
1656
1664
|
for (const edge of edges.filter(longConnector)) {
|
|
@@ -216,7 +216,7 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
216
216
|
const parts = [
|
|
217
217
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${logicalWidth} ${logicalHeight}" role="img" aria-labelledby="title description">`,
|
|
218
218
|
`<title id="title">${escapeXml(plan.id)} focus tree</title>`,
|
|
219
|
-
`<desc id="description"
|
|
219
|
+
`<desc id="description">${plan.focuses.length} focuses.</desc>`,
|
|
220
220
|
'<defs>',
|
|
221
221
|
'__DETERMINISTIC_FONT_DEFINITIONS__',
|
|
222
222
|
'<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M 0 0 L 10 5 L 0 10 z" fill="#8da2bc"/></marker>',
|
|
@@ -224,12 +224,6 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
224
224
|
'</defs>',
|
|
225
225
|
'<rect width="100%" height="100%" fill="#111723"/>',
|
|
226
226
|
toolText.render(plan.id, { x: padding, y: 36, fontSize: 22, weight: 700, fill: '#e8edf5' }),
|
|
227
|
-
toolText.render(`Offline agent render · layout ${layout.layoutHash.slice(0, 12)}`, {
|
|
228
|
-
x: padding,
|
|
229
|
-
y: 58,
|
|
230
|
-
fontSize: 12,
|
|
231
|
-
fill: '#95a4b8',
|
|
232
|
-
}),
|
|
233
227
|
'<g id="prerequisites">',
|
|
234
228
|
];
|
|
235
229
|
for (const edge of prerequisiteEdges) {
|
|
@@ -403,7 +397,7 @@ function htmlDocument(plan, svg, graphJson, diagnostics) {
|
|
|
403
397
|
<body>
|
|
404
398
|
<main class="canvas" aria-label="Focus tree artifact">${svg}</main>
|
|
405
399
|
<aside class="panel">
|
|
406
|
-
<div class="controls"><h1>${escapeXml(plan.id)}</h1><
|
|
400
|
+
<div class="controls"><h1>${escapeXml(plan.id)}</h1><label>Find focus<input id="search" type="search" placeholder="ID, title, branch, or lane"></label></div>
|
|
407
401
|
<h2>Diagnostics</h2><ol>${diagnosticsHtml}</ol>
|
|
408
402
|
<p class="muted">Source revision: <code>${escapeXml(plan.provenance.sourceHash)}</code></p>
|
|
409
403
|
</aside>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RenderBudget, RENDER_MAX_DECODED_PIXELS } from '../core/render-budget.js';
|
|
2
2
|
import { sha256Bytes } from '../core/canonical.js';
|
|
3
3
|
const DDS_MAGIC = 0x2053_4444;
|
|
4
|
+
const DDSD_PITCH = 0x8;
|
|
4
5
|
const DDPF_FOURCC = 0x4;
|
|
5
6
|
const DDPF_RGB = 0x40;
|
|
6
7
|
function fourCc(bytes, offset) {
|
|
@@ -180,7 +181,9 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
180
181
|
budget.reserve(width, height, 'DDS texture decode', {
|
|
181
182
|
maximumPixels: RENDER_MAX_DECODED_PIXELS,
|
|
182
183
|
});
|
|
183
|
-
const
|
|
184
|
+
const headerFlags = bytes.readUInt32LE(8);
|
|
185
|
+
const pitchOrLinearSize = bytes.readUInt32LE(20);
|
|
186
|
+
const rowPitch = (headerFlags & DDSD_PITCH) === 0 ? 0 : pitchOrLinearSize;
|
|
184
187
|
const flags = bytes.readUInt32LE(80);
|
|
185
188
|
const code = fourCc(bytes, 84).toUpperCase();
|
|
186
189
|
if ((flags & DDPF_FOURCC) !== 0) {
|
|
@@ -236,7 +239,7 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
236
239
|
bytes.readUInt32LE(100),
|
|
237
240
|
bytes.readUInt32LE(104),
|
|
238
241
|
];
|
|
239
|
-
const output = decodeRgb(bytes, 128, width, height, bitCount,
|
|
242
|
+
const output = decodeRgb(bytes, 128, width, height, bitCount, rowPitch, masks);
|
|
240
243
|
const format = bitCount === 24 ? 'rgb24' : 'rgba32';
|
|
241
244
|
return output === undefined
|
|
242
245
|
? unsupported(format, 'The DDS RGB pixel data is truncated.')
|
|
@@ -750,7 +750,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
-
/** Build a deterministic
|
|
753
|
+
/** Build a deterministic GUI scene from the connected source graph and scenario. */
|
|
754
754
|
export async function buildGuiScene(graph, scannedFiles, windowName, scenario, catalog = new GuiAssetCatalog(graph, scannedFiles)) {
|
|
755
755
|
if (graph.nodes.length > GUI_GRAPH_MAX_NODES ||
|
|
756
756
|
graph.edges.length > GUI_GRAPH_MAX_EDGES ||
|
|
@@ -214,13 +214,7 @@ export function sceneToSvg(scene, variant) {
|
|
|
214
214
|
.map((element) => renderOverlay(element, variant, toolText))
|
|
215
215
|
.join('');
|
|
216
216
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
217
|
-
|
|
218
|
-
x: view.viewBox.x + 15,
|
|
219
|
-
y: view.viewBox.y + 22,
|
|
220
|
-
fontSize: 11,
|
|
221
|
-
fill: '#f1c75b',
|
|
222
|
-
});
|
|
223
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}<g><rect x="${finite(view.viewBox.x + 8)}" y="${finite(view.viewBox.y + 8)}" width="215" height="20" rx="3" fill="#05080c" fill-opacity="0.82"/>${banner}</g></svg>`;
|
|
217
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions.join('')}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}</svg>`;
|
|
224
218
|
}
|
|
225
219
|
async function cooperativeParts(values, render, signal) {
|
|
226
220
|
const output = [];
|
|
@@ -244,13 +238,7 @@ async function sceneToSvgCooperative(scene, variant, signal) {
|
|
|
244
238
|
const body = await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal);
|
|
245
239
|
const overlays = await cooperativeParts(scene.elements, (element) => renderOverlay(element, variant, toolText), signal);
|
|
246
240
|
const fidelity = escapeXml(canonicalJson(scene.fidelity));
|
|
247
|
-
|
|
248
|
-
x: view.viewBox.x + 15,
|
|
249
|
-
y: view.viewBox.y + 22,
|
|
250
|
-
fontSize: 11,
|
|
251
|
-
fill: '#f1c75b',
|
|
252
|
-
});
|
|
253
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}<g><rect x="${finite(view.viewBox.x + 8)}" y="${finite(view.viewBox.y + 8)}" width="215" height="20" rx="3" fill="#05080c" fill-opacity="0.82"/>${banner}</g></svg>`;
|
|
241
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${view.width}" height="${view.height}" viewBox="${finite(view.viewBox.x)} ${finite(view.viewBox.y)} ${finite(view.viewBox.width)} ${finite(view.viewBox.height)}"><metadata data-renderer="hoi4-agent-tools" data-mode="offline">${fidelity}</metadata><defs>${clipDefinitions}${sceneGlyphDefinitions(scene)}${toolText.definitions()}</defs><rect ${rectAttributes(view.viewBox)} fill="#17202a"/>${body}${overlays}</svg>`;
|
|
254
242
|
}
|
|
255
243
|
export function hierarchyToSvg(scene) {
|
|
256
244
|
const rows = scene.elements.toSorted((left, right) => left.depth - right.depth || left.zIndex - right.zIndex || compareCodeUnits(left.id, right.id));
|
|
@@ -283,7 +271,7 @@ export function hierarchyToSvg(scene) {
|
|
|
283
271
|
return `${parentLine}<circle cx="${x}" cy="${y - 8}" r="4" fill="${colour}"/>${nameText}${detailText}`;
|
|
284
272
|
})
|
|
285
273
|
.join('');
|
|
286
|
-
const heading = toolText.render(
|
|
274
|
+
const heading = toolText.render(`${scene.windowName} \u00b7 HIERARCHY`, {
|
|
287
275
|
x: 16,
|
|
288
276
|
y: 20,
|
|
289
277
|
fontSize: 12,
|
|
@@ -393,7 +381,7 @@ export function renderGallerySvg(title, items) {
|
|
|
393
381
|
return `<g>${toolText.render(item.label, { x, y: y + 14, fontSize: 12, fill: '#f1c75b' })}<image x="${x}" y="${y + 22}" width="${finite(item.width * scale)}" height="${finite(item.height * scale)}" href="data:image/png;base64,${item.png.toString('base64')}"/></g>`;
|
|
394
382
|
})
|
|
395
383
|
.join('');
|
|
396
|
-
const heading = toolText.render(
|
|
384
|
+
const heading = toolText.render(title, {
|
|
397
385
|
x: 12,
|
|
398
386
|
y: 24,
|
|
399
387
|
fontSize: 15,
|
|
@@ -31,12 +31,12 @@ const explicitGuiElementTypes = new Set([
|
|
|
31
31
|
'mapIconType',
|
|
32
32
|
]);
|
|
33
33
|
const spriteTypes = new Set([
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
34
|
+
'spritetype',
|
|
35
|
+
'textspritetype',
|
|
36
|
+
'frameanimatedspritetype',
|
|
37
|
+
'corneredtilespritetype',
|
|
38
|
+
'progressbartype',
|
|
39
|
+
'maskedshieldtype',
|
|
40
40
|
]);
|
|
41
41
|
const modelledElementAttributes = new Set([
|
|
42
42
|
'name',
|
|
@@ -330,7 +330,8 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
330
330
|
if (assignment.value.type !== 'block')
|
|
331
331
|
continue;
|
|
332
332
|
const child = assignment.value;
|
|
333
|
-
|
|
333
|
+
const spriteType = assignment.key.value.toLowerCase();
|
|
334
|
+
if (spriteTypes.has(spriteType)) {
|
|
334
335
|
const name = firstScalarInsensitive(child, 'name');
|
|
335
336
|
if (name !== undefined) {
|
|
336
337
|
const id = deterministicId('gui_sprite', { path: file.displayPath, name });
|
|
@@ -352,7 +353,7 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
352
353
|
...(texturePath === undefined ? {} : { texturePath }),
|
|
353
354
|
...(texturePath2 === undefined ? {} : { texturePath2 }),
|
|
354
355
|
frameCount: Math.trunc(numberScalar(child, 'noOfFrames', 'noofframes') ?? 1),
|
|
355
|
-
frameAnimated:
|
|
356
|
+
frameAnimated: spriteType === 'frameanimatedspritetype',
|
|
356
357
|
...(animationRateFps === undefined ? {} : { animationRateFps }),
|
|
357
358
|
...(looping === undefined ? {} : { looping }),
|
|
358
359
|
...(playOnShow === undefined ? {} : { playOnShow }),
|