hoi4-agent-tools 2.5.1 → 2.5.3
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 +18 -0
- 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/render.js +1 -1
- 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/technology/compare.js +1 -0
- package/dist/hoi4_agent_tools/technology/render.js +96 -34
- package/dist/hoi4_agent_tools/technology/service.js +67 -3
- package/dist/hoi4_agent_tools/technology/source-analysis.js +19 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +2 -0
- 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 +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
|
@@ -433,7 +433,7 @@ function htmlDocument(title, png, json) {
|
|
|
433
433
|
.replaceAll('<', '<')
|
|
434
434
|
.replaceAll('>', '>');
|
|
435
435
|
const escapedJson = json.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
436
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.controls{display:flex;gap:8px;align-items:center;margin:12px 0}button{background:#263241;color:#edf2f7;border:1px solid #526173;border-radius:4px;padding:6px 10px}.viewport{height:70vh;overflow:hidden;border:1px solid #48515c;background:#080b0f;position:relative;touch-action:none}.viewport img{image-rendering:pixelated;transform-origin:0 0;position:absolute;max-width:none;cursor:grab;user-select:none}.viewport img.dragging{cursor:grabbing}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${escapedTitle}</h1><
|
|
436
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.controls{display:flex;gap:8px;align-items:center;margin:12px 0}button{background:#263241;color:#edf2f7;border:1px solid #526173;border-radius:4px;padding:6px 10px}.viewport{height:70vh;overflow:hidden;border:1px solid #48515c;background:#080b0f;position:relative;touch-action:none}.viewport img{image-rendering:pixelated;transform-origin:0 0;position:absolute;max-width:none;cursor:grab;user-select:none}.viewport img.dragging{cursor:grabbing}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${escapedTitle}</h1><div class="controls"><button id="out" type="button">-</button><button id="reset" type="button">Reset</button><button id="in" type="button">+</button><output id="zoom">100%</output></div><div class="viewport" id="viewport"><img id="map" draggable="false" alt="${escapedTitle}" src="data:image/png;base64,${png.toString('base64')}"></div><details><summary>Render metadata</summary><pre>${escapedJson}</pre></details><script>(()=>{const image=document.getElementById('map'),viewport=document.getElementById('viewport'),label=document.getElementById('zoom');let scale=1,x=0,y=0,drag=false,lastX=0,lastY=0;const draw=()=>{image.style.transform='translate('+x+'px,'+y+'px) scale('+scale+')';label.value=Math.round(scale*100)+'%'};const zoom=(factor,cx=viewport.clientWidth/2,cy=viewport.clientHeight/2)=>{const next=Math.min(16,Math.max(.1,scale*factor));x=cx-(cx-x)*(next/scale);y=cy-(cy-y)*(next/scale);scale=next;draw()};document.getElementById('in').onclick=()=>zoom(1.25);document.getElementById('out').onclick=()=>zoom(.8);document.getElementById('reset').onclick=()=>{scale=1;x=0;y=0;draw()};viewport.addEventListener('wheel',event=>{event.preventDefault();const bounds=viewport.getBoundingClientRect();zoom(event.deltaY<0?1.15:.87,event.clientX-bounds.left,event.clientY-bounds.top)},{passive:false});image.addEventListener('pointerdown',event=>{drag=true;lastX=event.clientX;lastY=event.clientY;image.setPointerCapture(event.pointerId);image.classList.add('dragging')});image.addEventListener('pointermove',event=>{if(!drag)return;x+=event.clientX-lastX;y+=event.clientY-lastY;lastX=event.clientX;lastY=event.clientY;draw()});image.addEventListener('pointerup',()=>{drag=false;image.classList.remove('dragging')});draw()})();</script></body></html>`;
|
|
437
437
|
}
|
|
438
438
|
async function encodePng(raw, width, height, scale) {
|
|
439
439
|
assertRenderDimensions(width, height, 'map Sharp source raster');
|
|
@@ -4,9 +4,8 @@ import { TextDecoder } from 'node:util';
|
|
|
4
4
|
import express, {} from 'express';
|
|
5
5
|
import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
|
|
6
6
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { isInitializeRequest, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { canonicalJson } from '../../core/canonical.js';
|
|
9
|
-
import { MCP_PROTOCOL_VERSION } from '../../version.js';
|
|
10
9
|
import { authenticate, bearerChallenge, HttpAuthError, protectedResourceMetadata, validateHttpDeployment, } from '../security/auth.js';
|
|
11
10
|
import { BoundedEventStore, SharedEventStoreBudget } from './event-store.js';
|
|
12
11
|
import { FinalProtocolTransport } from './protocol-gate.js';
|
|
@@ -296,15 +295,15 @@ function requireParsedMcpJsonBody(request, response, next) {
|
|
|
296
295
|
}
|
|
297
296
|
function rejectInvalidProtocolHeader(request, response, required) {
|
|
298
297
|
const value = request.headers['mcp-protocol-version'];
|
|
299
|
-
if (value ===
|
|
298
|
+
if (typeof value === 'string' && SUPPORTED_PROTOCOL_VERSIONS.includes(value))
|
|
300
299
|
return false;
|
|
301
300
|
if (value === undefined && !required)
|
|
302
301
|
return false;
|
|
303
302
|
if (value === undefined) {
|
|
304
|
-
jsonRpcError(response, 400, `Missing MCP-Protocol-Version header; this server
|
|
303
|
+
jsonRpcError(response, 400, `Missing MCP-Protocol-Version header; this server supports ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}`);
|
|
305
304
|
return true;
|
|
306
305
|
}
|
|
307
|
-
jsonRpcError(response, 400, `Unsupported MCP protocol version; this server
|
|
306
|
+
jsonRpcError(response, 400, `Unsupported MCP protocol version; this server supports ${SUPPORTED_PROTOCOL_VERSIONS.join(', ')}`);
|
|
308
307
|
return true;
|
|
309
308
|
}
|
|
310
309
|
function sendAuthError(response, configuration, error) {
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { MCP_PROTOCOL_VERSION } from '../../version.js';
|
|
1
|
+
import { isInitializeRequest, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
2
|
const unsupportedVersionSentinel = 'hoi4-agent-tools-final-only';
|
|
4
3
|
function gateInitializeVersion(message) {
|
|
5
|
-
if (!isInitializeRequest(message) ||
|
|
4
|
+
if (!isInitializeRequest(message) ||
|
|
5
|
+
SUPPORTED_PROTOCOL_VERSIONS.includes(message.params.protocolVersion)) {
|
|
6
6
|
return message;
|
|
7
7
|
}
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// Unknown revisions cannot be negotiated. Rewrite them to an unsupported sentinel so the
|
|
9
|
+
// SDK applies its documented fallback response (the latest supported revision) instead of
|
|
10
|
+
// claiming unimplemented feature gates.
|
|
11
11
|
return {
|
|
12
12
|
...message,
|
|
13
13
|
params: { ...message.params, protocolVersion: unsupportedVersionSentinel },
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
|
-
/**
|
|
16
|
+
/** Let the SDK negotiate any revision it supports and fall back to the latest for unknown ones. */
|
|
17
17
|
export class FinalProtocolTransport {
|
|
18
18
|
inner;
|
|
19
19
|
onclose = () => undefined;
|
|
@@ -92,6 +92,7 @@ function technologyMetadata(technology) {
|
|
|
92
92
|
researchCost: technology.researchCost ?? null,
|
|
93
93
|
doctrineName: technology.doctrineName ?? null,
|
|
94
94
|
hidden: technology.hidden,
|
|
95
|
+
layoutSize: technology.layoutSize,
|
|
95
96
|
folders: technology.folders,
|
|
96
97
|
categories: technology.categories,
|
|
97
98
|
tags: technology.tags,
|
|
@@ -5,8 +5,10 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { DeterministicSvgTextRenderer } from '../core/svg-text.js';
|
|
6
6
|
import { traceTechnology } from './queries.js';
|
|
7
7
|
const MAX_RENDER_NODES = 2_000;
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const LARGE_NODE_WIDTH = 183;
|
|
9
|
+
const LARGE_NODE_HEIGHT = 84;
|
|
10
|
+
const SMALL_NODE_WIDTH = 72;
|
|
11
|
+
const SMALL_NODE_HEIGHT = 72;
|
|
10
12
|
const GAP_X = 42;
|
|
11
13
|
const GAP_Y = 42;
|
|
12
14
|
const GENERATED_NODE_WIDTH = 180;
|
|
@@ -44,6 +46,8 @@ function technologyNode(graph, technologyId, placement) {
|
|
|
44
46
|
kind: technology.kind,
|
|
45
47
|
sourcePath: technology.source.path,
|
|
46
48
|
sourceLine: technology.source.location.start.line,
|
|
49
|
+
layoutSize: technology.layoutSize,
|
|
50
|
+
iconSprite: technology.icon.sprite,
|
|
47
51
|
...(placement === undefined ? {} : { placement }),
|
|
48
52
|
};
|
|
49
53
|
}
|
|
@@ -149,6 +153,7 @@ function viewSelection(graph, options) {
|
|
|
149
153
|
kind: 'doctrine',
|
|
150
154
|
sourcePath: definition.source.path,
|
|
151
155
|
sourceLine: definition.source.location.start.line,
|
|
156
|
+
...(definition.icon === undefined ? {} : { iconSprite: definition.icon.sprite }),
|
|
152
157
|
});
|
|
153
158
|
for (const definition of graph.doctrineDefinitions) {
|
|
154
159
|
const from = `doctrine:${definition.kind}:${definition.id}`;
|
|
@@ -200,6 +205,7 @@ function viewSelection(graph, options) {
|
|
|
200
205
|
kind: 'doctrine',
|
|
201
206
|
sourcePath: doctrine.source.path,
|
|
202
207
|
sourceLine: doctrine.source.location.start.line,
|
|
208
|
+
...(doctrine.icon === undefined ? {} : { iconSprite: doctrine.icon.sprite }),
|
|
203
209
|
});
|
|
204
210
|
for (const targetId of doctrine.exclusiveIds) {
|
|
205
211
|
const target = graph.doctrineDefinitions.find(({ id }) => id === targetId);
|
|
@@ -212,7 +218,11 @@ function viewSelection(graph, options) {
|
|
|
212
218
|
kind: target === undefined ? 'unresolved' : 'doctrine',
|
|
213
219
|
...(target === undefined
|
|
214
220
|
? {}
|
|
215
|
-
: {
|
|
221
|
+
: {
|
|
222
|
+
sourcePath: target.source.path,
|
|
223
|
+
sourceLine: target.source.location.start.line,
|
|
224
|
+
...(target.icon === undefined ? {} : { iconSprite: target.icon.sprite }),
|
|
225
|
+
}),
|
|
216
226
|
});
|
|
217
227
|
edges.push({ id: `${from}:xor:${to}`, from, to, kind: 'exclusive' });
|
|
218
228
|
}
|
|
@@ -415,17 +425,22 @@ function issueKey(issue) {
|
|
|
415
425
|
return sha256Bytes(canonicalJson({ code: issue.code, details: issue.details })).slice(0, 20);
|
|
416
426
|
}
|
|
417
427
|
function layoutFolder(nodes) {
|
|
428
|
+
const dimensions = (node) => node.layoutSize === 'small'
|
|
429
|
+
? { width: SMALL_NODE_WIDTH, height: SMALL_NODE_HEIGHT }
|
|
430
|
+
: { width: LARGE_NODE_WIDTH, height: LARGE_NODE_HEIGHT };
|
|
418
431
|
const sourcePixel = nodes.filter(({ placement }) => placement?.pixelX !== undefined && placement.pixelY !== undefined);
|
|
419
432
|
if (sourcePixel.length === nodes.length && sourcePixel.length > 0) {
|
|
420
433
|
const minimumX = Math.min(...sourcePixel.map(({ placement }) => placement.pixelX));
|
|
421
434
|
const minimumY = Math.min(...sourcePixel.map(({ placement }) => placement.pixelY));
|
|
422
|
-
return sourcePixel.map((node) =>
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
435
|
+
return sourcePixel.map((node) => {
|
|
436
|
+
const size = dimensions(node);
|
|
437
|
+
return {
|
|
438
|
+
...node,
|
|
439
|
+
x: PADDING + node.placement.pixelX - minimumX,
|
|
440
|
+
y: HEADER_HEIGHT + PADDING + node.placement.pixelY - minimumY,
|
|
441
|
+
...size,
|
|
442
|
+
};
|
|
443
|
+
});
|
|
429
444
|
}
|
|
430
445
|
const branchRoots = [
|
|
431
446
|
...new Set(nodes.map(({ placement }) => placement?.branchRootId ?? '<unresolved>')),
|
|
@@ -434,9 +449,12 @@ function layoutFolder(nodes) {
|
|
|
434
449
|
return nodes.map((node, index) => {
|
|
435
450
|
const placement = node.placement;
|
|
436
451
|
const lane = lanes.get(placement?.branchRootId ?? '<unresolved>') ?? 0;
|
|
437
|
-
const
|
|
438
|
-
const
|
|
439
|
-
|
|
452
|
+
const size = dimensions(node);
|
|
453
|
+
const x = PADDING + lane * 680 + (placement?.x ?? index % 3) * (LARGE_NODE_WIDTH + GAP_X);
|
|
454
|
+
const y = HEADER_HEIGHT +
|
|
455
|
+
PADDING +
|
|
456
|
+
(placement?.y ?? Math.floor(index / 3)) * (LARGE_NODE_HEIGHT + GAP_Y);
|
|
457
|
+
return { ...node, x, y, ...size };
|
|
440
458
|
});
|
|
441
459
|
}
|
|
442
460
|
function layoutGenerated(nodes, edges) {
|
|
@@ -526,9 +544,9 @@ function colours(kind) {
|
|
|
526
544
|
removed: { fill: '#4a2828', stroke: '#ef7d7d' },
|
|
527
545
|
}[kind];
|
|
528
546
|
}
|
|
529
|
-
function svgFor(titleText, positioned, edges, sourceAccurate) {
|
|
530
|
-
const maximumX = Math.max(PADDING +
|
|
531
|
-
const maximumY = Math.max(HEADER_HEIGHT +
|
|
547
|
+
function svgFor(titleText, positioned, edges, sourceAccurate, iconDataUris) {
|
|
548
|
+
const maximumX = Math.max(PADDING + LARGE_NODE_WIDTH, ...positioned.map(({ x, width }) => x + width));
|
|
549
|
+
const maximumY = Math.max(HEADER_HEIGHT + LARGE_NODE_HEIGHT, ...positioned.map(({ y, height }) => y + height));
|
|
532
550
|
const width = Math.ceil(maximumX + PADDING);
|
|
533
551
|
const height = Math.ceil(maximumY + PADDING);
|
|
534
552
|
if (width > 16_384 || height > 16_384 || width * height > RENDER_MAX_PIXELS)
|
|
@@ -554,24 +572,46 @@ function svgFor(titleText, positioned, edges, sourceAccurate) {
|
|
|
554
572
|
});
|
|
555
573
|
const nodeSvg = positioned.map((node) => {
|
|
556
574
|
const colour = colours(node.kind);
|
|
557
|
-
const
|
|
575
|
+
const small = node.layoutSize === 'small';
|
|
576
|
+
const iconDataUri = node.iconSprite === undefined ? undefined : iconDataUris[node.iconSprite];
|
|
577
|
+
const compactIcon = iconDataUri !== undefined && small && node.width <= SMALL_NODE_WIDTH;
|
|
578
|
+
const iconSize = iconDataUri === undefined
|
|
579
|
+
? 0
|
|
580
|
+
: compactIcon
|
|
581
|
+
? Math.max(1, Math.min(node.width - 12, node.height - 12))
|
|
582
|
+
: Math.max(1, Math.min(64, node.height - 16, Math.floor(node.width * 0.36)));
|
|
583
|
+
const textInset = iconDataUri === undefined ? (small ? 6 : 12) : iconSize + 18;
|
|
584
|
+
const availableTextWidth = Math.max(1, node.width - textInset - (small ? 6 : 12));
|
|
585
|
+
const labelLimit = small ? 12 : 34;
|
|
586
|
+
const label = node.label.length > labelLimit
|
|
587
|
+
? `${node.label.slice(0, Math.max(1, labelLimit - 1))}…`
|
|
588
|
+
: node.label;
|
|
558
589
|
const subtitle = node.subtitle.length > 48 ? `${node.subtitle.slice(0, 45)}…` : node.subtitle;
|
|
559
|
-
const labelSvg =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
590
|
+
const labelSvg = compactIcon
|
|
591
|
+
? ''
|
|
592
|
+
: text.render(label, {
|
|
593
|
+
x: node.x + textInset,
|
|
594
|
+
y: node.y + (small ? 27 : 30),
|
|
595
|
+
fontSize: small ? 9 : 15,
|
|
596
|
+
fill: '#f1f5f8',
|
|
597
|
+
weight: 600,
|
|
598
|
+
targetWidth: Math.min(availableTextWidth, Math.max(1, text.measure(label, small ? 9 : 15))),
|
|
599
|
+
});
|
|
600
|
+
const subtitleSvg = compactIcon
|
|
601
|
+
? ''
|
|
602
|
+
: text.render(small ? 'SMALL' : subtitle, {
|
|
603
|
+
x: node.x + textInset,
|
|
604
|
+
y: node.y + (small ? 51 : 58),
|
|
605
|
+
fontSize: small ? 8 : 11,
|
|
606
|
+
fill: '#b8c4ce',
|
|
607
|
+
targetWidth: Math.min(availableTextWidth, Math.max(1, text.measure(small ? 'SMALL' : subtitle, small ? 8 : 11))),
|
|
608
|
+
});
|
|
609
|
+
const iconSvg = iconDataUri === undefined
|
|
610
|
+
? ''
|
|
611
|
+
: `<image href="${escapeXml(iconDataUri)}" x="${node.x + (compactIcon ? 6 : 8)}" y="${node.y + (node.height - iconSize) / 2}" width="${iconSize}" height="${iconSize}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
612
|
+
const layoutAttribute = node.layoutSize === undefined ? '' : ` data-layout-size="${node.layoutSize}"`;
|
|
613
|
+
const iconAttribute = node.iconSprite === undefined ? '' : ` data-icon-sprite="${escapeXml(node.iconSprite)}"`;
|
|
614
|
+
return `<g data-node-id="${escapeXml(node.id)}"${layoutAttribute}${iconAttribute}${node.sourcePath === undefined ? '' : ` data-source-path="${escapeXml(node.sourcePath)}" data-source-line="${node.sourceLine ?? 1}"`}><rect x="${node.x}" y="${node.y}" width="${node.width}" height="${node.height}" rx="8" fill="${colour.fill}" stroke="${colour.stroke}" stroke-width="2"/><title>${escapeXml(`${node.id}${node.layoutSize === undefined ? '' : ` · ${node.layoutSize} layout`}${node.sourcePath === undefined ? '' : ` · ${node.sourcePath}:${node.sourceLine ?? 1}`}`)}</title>${iconSvg}${labelSvg}${subtitleSvg}</g>`;
|
|
575
615
|
});
|
|
576
616
|
const title = text.render(titleText, {
|
|
577
617
|
x: PADDING,
|
|
@@ -600,7 +640,13 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
600
640
|
const positioned = selection.sourceAccurate
|
|
601
641
|
? layoutFolder(retained)
|
|
602
642
|
: layoutGenerated(retained, edges);
|
|
603
|
-
const
|
|
643
|
+
const iconDataUris = options.iconDataUris ?? {};
|
|
644
|
+
const requestedIconSprites = [
|
|
645
|
+
...new Set(retained.flatMap(({ iconSprite }) => (iconSprite === undefined ? [] : [iconSprite]))),
|
|
646
|
+
].sort(compareCodeUnits);
|
|
647
|
+
const renderedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] !== undefined);
|
|
648
|
+
const unresolvedIconSprites = requestedIconSprites.filter((sprite) => iconDataUris[sprite] === undefined);
|
|
649
|
+
const rendered = svgFor(selection.title, positioned, edges, selection.sourceAccurate, iconDataUris);
|
|
604
650
|
const budget = options.budget ?? new RenderBudget();
|
|
605
651
|
budget.reserve(rendered.width, rendered.height, `technology ${options.view} render`);
|
|
606
652
|
budget.reserveRasterOperation(`technology-render:${graph.revision}:${options.view}:${sha256Bytes(rendered.svg)}`, `technology ${options.view} rasterization`);
|
|
@@ -615,6 +661,11 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
615
661
|
nodes: retained,
|
|
616
662
|
edges,
|
|
617
663
|
omittedNodeCount: Math.max(0, selection.nodes.length - retained.length),
|
|
664
|
+
iconCoverage: {
|
|
665
|
+
requested: requestedIconSprites.length,
|
|
666
|
+
rendered: renderedIconSprites.length,
|
|
667
|
+
unresolvedSprites: unresolvedIconSprites,
|
|
668
|
+
},
|
|
618
669
|
render: { width: rendered.width, height: rendered.height },
|
|
619
670
|
})}\n`;
|
|
620
671
|
const html = options.includeHtml === true ? htmlFor(selection.title, rendered.svg, json) : undefined;
|
|
@@ -623,6 +674,8 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
623
674
|
graphRevision: graph.revision,
|
|
624
675
|
selectedIds: retained.map(({ id }) => id),
|
|
625
676
|
omittedNodeCount: Math.max(0, selection.nodes.length - retained.length),
|
|
677
|
+
renderedIconCount: renderedIconSprites.length,
|
|
678
|
+
unresolvedIconSprites,
|
|
626
679
|
sourceAccurate: selection.sourceAccurate,
|
|
627
680
|
generatedAnalysisLayout: !selection.sourceAccurate,
|
|
628
681
|
width: rendered.width,
|
|
@@ -639,3 +692,12 @@ export async function renderTechnologyGraph(graph, options) {
|
|
|
639
692
|
},
|
|
640
693
|
};
|
|
641
694
|
}
|
|
695
|
+
/** Return only the sprite names that the bounded render selection will display. */
|
|
696
|
+
export function technologyRenderIconSprites(graph, options) {
|
|
697
|
+
const maximum = Math.max(1, Math.min(options.maxNodes ?? 600, MAX_RENDER_NODES));
|
|
698
|
+
return [
|
|
699
|
+
...new Set(viewSelection(graph, options)
|
|
700
|
+
.nodes.slice(0, maximum)
|
|
701
|
+
.flatMap(({ iconSprite }) => (iconSprite === undefined ? [] : [iconSprite]))),
|
|
702
|
+
].sort(compareCodeUnits);
|
|
703
|
+
}
|
|
@@ -6,10 +6,12 @@ import { ServiceError } from '../core/result.js';
|
|
|
6
6
|
import { SOURCE_MAX_BYTES } from '../core/source/index.js';
|
|
7
7
|
import { isPortablePathSegment } from '../core/workspace.js';
|
|
8
8
|
import { PACKAGE_VERSION } from '../version.js';
|
|
9
|
+
import { GuiAssetCatalog } from '../gui/assets.js';
|
|
10
|
+
import { buildGuiSourceGraph } from '../gui/source-graph.js';
|
|
9
11
|
import { compareTechnologyGraphs } from './compare.js';
|
|
10
12
|
import { buildTechnologyGraph, technologyAssetPatterns } from './graph.js';
|
|
11
13
|
import { analyzeTechnologyImpact, discoverTechnologyFolders, explainTechnology, inspectTechnologyUnlocks, lintTechnologyGraph, technologyBonusCoverage, technologyScanReport, traceTechnology, } from './queries.js';
|
|
12
|
-
import { renderTechnologyGraph, } from './render.js';
|
|
14
|
+
import { renderTechnologyGraph, technologyRenderIconSprites, } from './render.js';
|
|
13
15
|
const FRAGMENT_CACHE_ENTRIES = 8_192;
|
|
14
16
|
const FRAGMENT_CACHE_BYTES = 134_217_728;
|
|
15
17
|
const PROPOSED_SOURCE_FILES = 128;
|
|
@@ -80,10 +82,54 @@ function technologyScanPatterns(workspace) {
|
|
|
80
82
|
'history/countries/**/*.txt',
|
|
81
83
|
...under(roots.focus, '**/*.txt'),
|
|
82
84
|
...under(roots.interface, '**/*.gui'),
|
|
85
|
+
...under(roots.interface, '**/*.gfx'),
|
|
86
|
+
...under(roots.gfx, '**/*.gfx'),
|
|
83
87
|
...under(roots.localisation, 'english/**/*.{yml,yaml}'),
|
|
84
88
|
...under(roots.localisation, '*.{yml,yaml}'),
|
|
85
89
|
].sort(compareCodeUnits);
|
|
86
90
|
}
|
|
91
|
+
function activeTechnologySprite(spriteName, spritePath, sprites) {
|
|
92
|
+
return sprites.find((sprite) => sprite.name === spriteName && (spritePath === undefined || sprite.sourcePath === spritePath));
|
|
93
|
+
}
|
|
94
|
+
async function resolveTechnologyIconDataUris(cached, requestedSprites, budget, signal) {
|
|
95
|
+
const icons = [
|
|
96
|
+
...cached.graph.technologies.map(({ icon }) => icon),
|
|
97
|
+
...cached.graph.doctrineDefinitions.flatMap(({ icon }) => icon ?? []),
|
|
98
|
+
].filter(({ sprite }) => requestedSprites.has(sprite));
|
|
99
|
+
const spriteSourcePaths = new Set(icons.flatMap(({ spritePath }) => (spritePath === undefined ? [] : [spritePath])));
|
|
100
|
+
const spriteFiles = cached.snapshot.files.filter(({ displayPath }) => spriteSourcePaths.has(displayPath));
|
|
101
|
+
if (spriteFiles.length === 0)
|
|
102
|
+
return {};
|
|
103
|
+
const graph = buildGuiSourceGraph(spriteFiles, cached.snapshot.index);
|
|
104
|
+
const files = new Map();
|
|
105
|
+
for (const file of [...spriteFiles, ...cached.assetFiles])
|
|
106
|
+
files.set(file.displayPath, file);
|
|
107
|
+
const catalog = new GuiAssetCatalog(graph, [...files.values()], budget);
|
|
108
|
+
const unique = new Map();
|
|
109
|
+
for (const icon of icons)
|
|
110
|
+
if (!unique.has(icon.sprite))
|
|
111
|
+
unique.set(icon.sprite, icon);
|
|
112
|
+
const entries = [...unique.entries()].sort(([left], [right]) => compareCodeUnits(left, right));
|
|
113
|
+
const dataUris = {};
|
|
114
|
+
let cursor = 0;
|
|
115
|
+
await Promise.all(Array.from({ length: Math.min(8, entries.length) }, async () => {
|
|
116
|
+
while (cursor < entries.length) {
|
|
117
|
+
signal?.throwIfAborted();
|
|
118
|
+
const entry = entries[cursor];
|
|
119
|
+
cursor += 1;
|
|
120
|
+
if (entry === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
const [spriteName, icon] = entry;
|
|
123
|
+
const sprite = activeTechnologySprite(spriteName, icon.spritePath, graph.sprites);
|
|
124
|
+
if (sprite === undefined)
|
|
125
|
+
continue;
|
|
126
|
+
const frame = await catalog.loadSpriteFrame(sprite, 0);
|
|
127
|
+
if (frame?.supported === true && frame.dataUri !== undefined)
|
|
128
|
+
dataUris[spriteName] = frame.dataUri;
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
return Object.fromEntries(Object.entries(dataUris).sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
132
|
+
}
|
|
87
133
|
function technologyAnalysisMode(workspace, snapshot) {
|
|
88
134
|
return workspace.gameRoot !== undefined || snapshot.files.length > FOCUSED_GRAPH_FILE_THRESHOLD
|
|
89
135
|
? 'focused'
|
|
@@ -462,6 +508,9 @@ export class TechnologyTreeViewer {
|
|
|
462
508
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
463
509
|
});
|
|
464
510
|
const budget = new RenderBudget();
|
|
511
|
+
const cached = this.#state.current.get(input.workspaceId);
|
|
512
|
+
if (cached?.graph.revision !== graph.revision)
|
|
513
|
+
throw new ServiceError('TECH_RENDER_SOURCE_MISSING', 'Technology render source inventory is unavailable for the current graph');
|
|
465
514
|
const renderOptions = {
|
|
466
515
|
view: input.view,
|
|
467
516
|
maxNodes: input.maxNodes ?? 1_000,
|
|
@@ -473,13 +522,23 @@ export class TechnologyTreeViewer {
|
|
|
473
522
|
...(input.targetId === undefined ? {} : { targetId: input.targetId }),
|
|
474
523
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
475
524
|
};
|
|
476
|
-
const render = await renderTechnologyGraph(graph, renderOptions);
|
|
477
|
-
const focused = [];
|
|
478
525
|
const focusedFolders = input.view === 'dependencies' && input.folderId === undefined
|
|
479
526
|
? graph.folders
|
|
480
527
|
.filter((folder) => graph.placements.some(({ folderId }) => folderId === folder.id))
|
|
481
528
|
.slice(0, FOCUSED_RENDER_LIMIT)
|
|
482
529
|
: [];
|
|
530
|
+
const requestedIconSprites = new Set([
|
|
531
|
+
...technologyRenderIconSprites(graph, renderOptions),
|
|
532
|
+
...focusedFolders.flatMap(({ id }) => technologyRenderIconSprites(graph, {
|
|
533
|
+
view: 'folder',
|
|
534
|
+
folderId: id,
|
|
535
|
+
maxNodes: 1_000,
|
|
536
|
+
})),
|
|
537
|
+
]);
|
|
538
|
+
const iconDataUris = await resolveTechnologyIconDataUris(cached, requestedIconSprites, budget, input.signal);
|
|
539
|
+
renderOptions.iconDataUris = iconDataUris;
|
|
540
|
+
const render = await renderTechnologyGraph(graph, renderOptions);
|
|
541
|
+
const focused = [];
|
|
483
542
|
for (const folder of focusedFolders) {
|
|
484
543
|
input.signal?.throwIfAborted();
|
|
485
544
|
focused.push(await renderTechnologyGraph(graph, {
|
|
@@ -487,6 +546,7 @@ export class TechnologyTreeViewer {
|
|
|
487
546
|
folderId: folder.id,
|
|
488
547
|
maxNodes: 1_000,
|
|
489
548
|
includeHtml: false,
|
|
549
|
+
iconDataUris,
|
|
490
550
|
budget,
|
|
491
551
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
492
552
|
}));
|
|
@@ -516,6 +576,8 @@ export class TechnologyTreeViewer {
|
|
|
516
576
|
omittedNodeCount: render.omittedNodeCount,
|
|
517
577
|
sourceAccurate: render.sourceAccurate,
|
|
518
578
|
generatedAnalysisLayout: render.generatedAnalysisLayout,
|
|
579
|
+
renderedIconCount: render.renderedIconCount,
|
|
580
|
+
unresolvedIconSprites: render.unresolvedIconSprites,
|
|
519
581
|
hashes: render.hashes,
|
|
520
582
|
},
|
|
521
583
|
focusedFolders: focused.map((bundle, index) => ({
|
|
@@ -523,6 +585,8 @@ export class TechnologyTreeViewer {
|
|
|
523
585
|
folderId: focusedFolders[index]?.id,
|
|
524
586
|
selectedIds: bundle.selectedIds,
|
|
525
587
|
omittedNodeCount: bundle.omittedNodeCount,
|
|
588
|
+
renderedIconCount: bundle.renderedIconCount,
|
|
589
|
+
unresolvedIconSprites: bundle.unresolvedIconSprites,
|
|
526
590
|
hashes: bundle.hashes,
|
|
527
591
|
})),
|
|
528
592
|
focusedFolderCoverage: {
|
|
@@ -348,6 +348,13 @@ function parseTechnologyDefinitions(file, document, fragment) {
|
|
|
348
348
|
.map(({ key }) => key.value)
|
|
349
349
|
.filter((key) => !technologyStructuralFields.has(key) && !key.startsWith('@'))),
|
|
350
350
|
].sort(compareCodeUnits);
|
|
351
|
+
const layoutExpression = firstValue(block, 'force_use_small_tech_layout');
|
|
352
|
+
const smallLayout = booleanValue(layoutExpression);
|
|
353
|
+
const layoutSize = layoutExpression === undefined || smallLayout === false
|
|
354
|
+
? 'large'
|
|
355
|
+
: smallLayout === true
|
|
356
|
+
? 'small'
|
|
357
|
+
: 'unknown';
|
|
351
358
|
const unsupportedFields = fragment.placements
|
|
352
359
|
.filter(({ technologyId, xExpression, yExpression }) => technologyId === id && (xExpression !== undefined || yExpression !== undefined))
|
|
353
360
|
.map((placement) => ({
|
|
@@ -355,7 +362,17 @@ function parseTechnologyDefinitions(file, document, fragment) {
|
|
|
355
362
|
expression: `${placement.xExpression ?? placement.x ?? '?'}:${placement.yExpression ?? placement.y ?? '?'}`,
|
|
356
363
|
location: placement.location,
|
|
357
364
|
reason: 'The source coordinate expression is not a resolvable file-local numeric constant',
|
|
358
|
-
}))
|
|
365
|
+
}))
|
|
366
|
+
.concat(layoutSize === 'unknown'
|
|
367
|
+
? [
|
|
368
|
+
{
|
|
369
|
+
field: 'force_use_small_tech_layout',
|
|
370
|
+
expression: layoutExpression,
|
|
371
|
+
location: nodeLocation(document, block, id),
|
|
372
|
+
reason: 'The technology layout-size expression is not a literal yes or no',
|
|
373
|
+
},
|
|
374
|
+
]
|
|
375
|
+
: []);
|
|
359
376
|
const doctrine = booleanValue(firstValue(block, 'doctrine')) === true;
|
|
360
377
|
fragment.technologies.push({
|
|
361
378
|
id,
|
|
@@ -372,6 +389,7 @@ function parseTechnologyDefinitions(file, document, fragment) {
|
|
|
372
389
|
? {}
|
|
373
390
|
: { doctrineName: firstValue(block, 'doctrine_name') }),
|
|
374
391
|
hidden: hiddenStatus(block),
|
|
392
|
+
layoutSize,
|
|
375
393
|
folders: [...new Set(folders)].sort(compareCodeUnits),
|
|
376
394
|
categories: [...new Set(categories)].sort(compareCodeUnits),
|
|
377
395
|
tags: [...new Set(tags)].sort(compareCodeUnits),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '2.5.
|
|
3
|
+
export const PACKAGE_VERSION = '2.5.3';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/focus.md
CHANGED
|
@@ -30,7 +30,7 @@ For a layout-only cleanup of an existing national tree, call `hoi4.focus_rewrite
|
|
|
30
30
|
|
|
31
31
|
Compact mode imports the tree, resets authored presentation coordinates and lane bounds, and performs a deterministic reflow in the same rewrite call. It keeps prerequisites, mutual exclusions, availability, bypasses, rewards, icons, localisation, AI behavior, and route/lane membership unchanged.
|
|
32
32
|
|
|
33
|
-
The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode evaluates preservation, row repair, linear-chain repair, sibling balancing, and combined compression strategies, then chooses the cleanest valid result. Sparse direct sibling fan-outs are packed around their parent at the minimum readable spacing, with each exclusive descendant branch moving as a unit and shared convergence focuses staying centered. Dense branches remain wider only when their contents need the room. It refuses invalid geometry such as overlapping or cramped focuses, bad parent order, rendered crossings, connectors through unrelated focuses, or an off-center canvas. Long connectors, fake detours, staircase or zigzag chains, and avoidable sibling imbalance are optimization priorities instead of independent rejection budgets.
|
|
33
|
+
The rewrite measures canvas bounds, same-row spacing, the same curved connector paths used by the renderer, connector lengths, connector-node intersections, branch balance, overall centering, and topology-aware fake-complexity patterns. Compact mode evaluates preservation, occupied-row repair, prerequisite-depth reflow, linear-chain repair, sibling balancing, and combined compression strategies, then chooses the cleanest valid result. Equivalent branches are aligned by dependency depth even when their authored rows differ, and connector shape is prioritized before raw canvas dimensions so a shorter canvas cannot win by introducing unnecessary horizontal detours. Sparse direct sibling fan-outs are packed around their parent at the minimum readable spacing, with each exclusive descendant branch moving as a unit and shared convergence focuses staying centered. Dense branches remain wider only when their contents need the room. It refuses invalid geometry such as overlapping or cramped focuses, bad parent order, rendered crossings, connectors through unrelated focuses, or an off-center canvas. Long connectors, fake detours, staircase or zigzag chains, and avoidable sibling imbalance are optimization priorities instead of independent rejection budgets.
|
|
34
34
|
|
|
35
35
|
Use the default `layoutMode: "authored"` with a complete plan when deliberately designing coordinates or changing gameplay. Set `layoutMode: "compact"` with a complete plan when creating a large automatically arranged tree. Inspect and render calls remain useful before or after either rewrite mode, but a separate plan is not required for compact cleanup.
|
|
36
36
|
|
package/docs/gui.md
CHANGED
|
@@ -52,6 +52,8 @@ Selectors accept an element name, instance ID, or source ID. Visibility expectat
|
|
|
52
52
|
|
|
53
53
|
Broad inspection indexes localisation actually referenced by GUI source and returns a connected workspace projection when the complete source graph is very large. The resource records full and returned node and edge counts, so an agent can identify the relevant window without loading unrelated vanilla UI into its prompt. Rendering remains targeted to the selected root, state, and resolution.
|
|
54
54
|
|
|
55
|
+
GUI rewrites keep complete validation evidence in a linked JSON resource and retain the highest-priority findings in the transaction result. Large surrounding interface inventories therefore do not turn a small scalar patch into a transaction structure-limit error.
|
|
56
|
+
|
|
55
57
|
## Offline preview limits
|
|
56
58
|
|
|
57
59
|
The renderer does not run the game engine. Each render includes a fidelity report that separates fields it models from fields it approximates, ignores, cannot resolve, or does not support. Treat that report as part of the review.
|
package/docs/technology.md
CHANGED
|
@@ -51,7 +51,7 @@ Game-backed and otherwise large workspaces automatically use focused analysis mo
|
|
|
51
51
|
|
|
52
52
|
`hoi4.tech_render` returns authoritative JSON with deterministic SVG and PNG resources; set `includeHtml` when a bundled static report is useful. Available views are `summary`, `folder`, `dependencies`, `technology`, `doctrine`, `exclusive`, `memberships`, `bonuses`, `grants`, `unlocks`, `metadata`, `assets`, and `unresolved`.
|
|
53
53
|
|
|
54
|
-
Folder renders use the actual folder assignments, gridbox geometry,
|
|
54
|
+
Folder renders use the actual folder assignments, gridbox geometry, technology coordinates, and HOI4 item size found in source. Technologies with `force_use_small_tech_layout = yes` render as compact icon items; normal technologies render as large items with their icon and label. Sprite declarations and texture files resolve through the active load order across the mod, configured dependencies, and the detected or configured game installation, so vanilla icons used by a mod appear in SVG, PNG, and HTML artifacts. The authoritative JSON and SVG expose `layoutSize`, rendered-icon coverage, and unresolved sprite names without embedding image data in JSON. Dependency and other semantic views are explicitly labelled as generated analysis layouts.
|
|
55
55
|
|
|
56
56
|
Example folder render:
|
|
57
57
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoi4-agent-tools",
|
|
3
3
|
"mcpName": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
|
-
"version": "2.5.
|
|
4
|
+
"version": "2.5.3",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
"format": "prettier --write .",
|
|
70
70
|
"format:check": "prettier --check .",
|
|
71
71
|
"test": "vitest run",
|
|
72
|
+
"test:all": "vitest run --shard=1/8 && vitest run --shard=2/8 && vitest run --shard=3/8 && vitest run --shard=4/8 && vitest run --shard=5/8 && vitest run --shard=6/8 && vitest run --shard=7/8 && vitest run --shard=8/8",
|
|
72
73
|
"test:unit": "vitest run tests/unit",
|
|
73
74
|
"test:integration": "vitest run tests/integration",
|
|
74
75
|
"test:local": "tsx scripts/run-local-tests.ts",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"release:order:verify": "tsx scripts/verify-npm-release-order.ts",
|
|
92
93
|
"pack:check": "tsx scripts/check-package.ts",
|
|
93
94
|
"inspector": "tsx scripts/run-inspector.ts",
|
|
94
|
-
"check": "npm run format:check && npm run lint && npm run typecheck && npm run test && npm run fixtures:check && npm run schemas:check && npm run build && npm run pack:check && npm run registry:validate",
|
|
95
|
+
"check": "npm run format:check && npm run lint && npm run typecheck && npm run test:all && npm run fixtures:check && npm run schemas:check && npm run build && npm run pack:check && npm run registry:validate",
|
|
95
96
|
"prepack": "npm run build"
|
|
96
97
|
},
|
|
97
98
|
"dependencies": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/gui-animation-source.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.5.3/schemas/gui-helper.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|