hoi4-agent-tools 2.8.11 → 3.0.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 +10 -0
- package/README.md +35 -10
- package/dist/bin/stdio.js +0 -4
- package/dist/hoi4_agent_tools/core/configuration.js +16 -3
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +10 -13
- package/dist/hoi4_agent_tools/core/engine.js +3 -0
- package/dist/hoi4_agent_tools/core/scanner.js +63 -7
- package/dist/hoi4_agent_tools/event/service.js +30 -23
- package/dist/hoi4_agent_tools/gui/assets.js +218 -0
- package/dist/hoi4_agent_tools/gui/layout.js +70 -9
- package/dist/hoi4_agent_tools/gui/renderer.js +80 -11
- package/dist/hoi4_agent_tools/gui/scenario-generator.js +36 -0
- package/dist/hoi4_agent_tools/gui/studio.js +90 -25
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +1 -1
- package/dist/hoi4_agent_tools/probability/service.js +33 -8
- package/dist/hoi4_agent_tools/schemas/probability.js +2 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/development.md +2 -0
- package/docs/gui.md +14 -3
- package/docs/images/readme/chaos-meter-ingame.png +0 -0
- package/docs/images/readme/chaos-meter-mcp.png +0 -0
- package/docs/images/readme/communist-insurgency-ingame.png +0 -0
- package/docs/images/readme/communist-insurgency-mcp.png +0 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/focus-tree-mcp.png +0 -0
- package/docs/images/readme/map-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/settings-advanced-mcp.png +0 -0
- package/docs/images/readme/settings-events-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
- package/package.json +3 -1
- package/schemas/configuration.schema.json +3 -3
- 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 +58 -39
- 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/dist/hoi4_agent_tools/mcp/tools/chaosx.js +0 -483
|
@@ -241,7 +241,7 @@ function basenameFallbackPatterns(exactPaths, exactFiles) {
|
|
|
241
241
|
.map(({ relativePath }) => relativePath.replaceAll('\\', '/').toLowerCase()));
|
|
242
242
|
return [
|
|
243
243
|
...new Set(exactPaths
|
|
244
|
-
.filter((candidate) => !resolved.has(candidate.toLowerCase()))
|
|
244
|
+
.filter((candidate) => !/[*?[\]{}!]/u.test(candidate) && !resolved.has(candidate.toLowerCase()))
|
|
245
245
|
.map((candidate) => `**/${path.posix.basename(candidate)}`)),
|
|
246
246
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
247
247
|
}
|
|
@@ -341,6 +341,22 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
341
341
|
if (textValues.has(localisation.key))
|
|
342
342
|
textValues.add(localisation.value);
|
|
343
343
|
}
|
|
344
|
+
const dynamicTextNames = new Set();
|
|
345
|
+
for (const value of textValues) {
|
|
346
|
+
for (const match of value.matchAll(/\[(?:\?[^\]]*\.)?([A-Za-z_][A-Za-z0-9_.:-]*)[^\]]*\]/gu)) {
|
|
347
|
+
const token = match[1];
|
|
348
|
+
if (token === undefined)
|
|
349
|
+
continue;
|
|
350
|
+
dynamicTextNames.add(token);
|
|
351
|
+
dynamicTextNames.add(token.slice(token.lastIndexOf('.') + 1));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const dynamicTextLocalisationKeys = new Set(graph.scriptedLocalisation
|
|
355
|
+
.filter(({ name }) => dynamicTextNames.has(name))
|
|
356
|
+
.flatMap(({ localisationKeys }) => localisationKeys));
|
|
357
|
+
for (const localisation of graph.localisation)
|
|
358
|
+
if (dynamicTextLocalisationKeys.has(localisation.key))
|
|
359
|
+
textValues.add(localisation.value);
|
|
344
360
|
for (const value of textValues) {
|
|
345
361
|
for (const token of localisationIconTokens(value)) {
|
|
346
362
|
for (const candidate of localisationIconSpriteCandidates(token))
|
|
@@ -372,6 +388,12 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
372
388
|
const match = /^\[([^\]]+)\]$/u.exec(image);
|
|
373
389
|
return match?.[1] === undefined ? [] : [match[1]];
|
|
374
390
|
}));
|
|
391
|
+
const usesCountryFlags = graph.scriptedGuis
|
|
392
|
+
.filter(({ name }) => relatedScriptedGuiNames.has(name))
|
|
393
|
+
.flatMap(({ propertyDefinitions }) => propertyDefinitions)
|
|
394
|
+
.some(({ attributes }) => Object.entries(attributes).some(([key, value]) => key.toLocaleLowerCase('en-US') === 'image' &&
|
|
395
|
+
typeof value === 'string' &&
|
|
396
|
+
/(?:^|\.)GetFlag\]$/u.test(value)));
|
|
375
397
|
for (const definition of graph.scriptedLocalisation) {
|
|
376
398
|
if (!dynamicImageTokens.has(definition.name))
|
|
377
399
|
continue;
|
|
@@ -417,17 +439,41 @@ export function referencedAssetPatternsForWindow(graph, windowName, additionalSp
|
|
|
417
439
|
...manifest.sourceFrames.map(({ path: framePath }) => framePath),
|
|
418
440
|
]),
|
|
419
441
|
];
|
|
420
|
-
return [...new Set(references.flatMap(referenceVariants))].sort((left, right) => compareCodeUnits(left, right));
|
|
421
|
-
}
|
|
422
|
-
function scenarioSpriteNames(scenarios) {
|
|
423
442
|
return [
|
|
424
|
-
...new Set(
|
|
425
|
-
.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
443
|
+
...new Set([
|
|
444
|
+
...references.flatMap(referenceVariants),
|
|
445
|
+
...(usesCountryFlags
|
|
446
|
+
? [
|
|
447
|
+
'gfx/flags/*.{bmp,dds,png,tga}',
|
|
448
|
+
'gfx/flags/medium/*.{bmp,dds,png,tga}',
|
|
449
|
+
'gfx/flags/small/*.{bmp,dds,png,tga}',
|
|
450
|
+
]
|
|
451
|
+
: []),
|
|
452
|
+
]),
|
|
429
453
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
430
454
|
}
|
|
455
|
+
function scenarioSpriteNames(scenarios, additionalText = []) {
|
|
456
|
+
const sprites = new Set();
|
|
457
|
+
const pending = [...scenarios, ...additionalText];
|
|
458
|
+
while (pending.length > 0) {
|
|
459
|
+
const value = pending.pop();
|
|
460
|
+
if (typeof value === 'string') {
|
|
461
|
+
if (/^GFX_[A-Za-z0-9_.:+-]+$/u.test(value))
|
|
462
|
+
sprites.add(value);
|
|
463
|
+
for (const token of localisationIconTokens(value))
|
|
464
|
+
for (const candidate of localisationIconSpriteCandidates(token))
|
|
465
|
+
sprites.add(candidate);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (value === null || typeof value !== 'object')
|
|
469
|
+
continue;
|
|
470
|
+
if (Array.isArray(value))
|
|
471
|
+
pending.push(...value);
|
|
472
|
+
else
|
|
473
|
+
pending.push(...Object.values(value));
|
|
474
|
+
}
|
|
475
|
+
return [...sprites].sort((left, right) => compareCodeUnits(left, right));
|
|
476
|
+
}
|
|
431
477
|
function bmFontPagePatterns(files) {
|
|
432
478
|
const references = files.flatMap((file) => {
|
|
433
479
|
if (!file.relativePath.toLowerCase().endsWith('.fnt') || file.shadowedBy !== undefined)
|
|
@@ -570,6 +616,14 @@ function fullImage(render) {
|
|
|
570
616
|
throw new Error('GUI render did not produce an image.');
|
|
571
617
|
return image;
|
|
572
618
|
}
|
|
619
|
+
function visualSceneKey(scene) {
|
|
620
|
+
const { id: _id, description: _description, ...scenario } = scene.scenario;
|
|
621
|
+
return hashCanonical({
|
|
622
|
+
windowName: scene.windowName,
|
|
623
|
+
sourceRevision: scene.sourceRevision,
|
|
624
|
+
scenario,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
573
627
|
export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes = [primaryScene], metadata = {}) {
|
|
574
628
|
const { scenario } = primaryScene;
|
|
575
629
|
const sourceEvidence = boundedSourceHashEvidence(graph.sourceHashes);
|
|
@@ -657,7 +711,7 @@ export class ScriptedGuiStudio {
|
|
|
657
711
|
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles)),
|
|
658
712
|
};
|
|
659
713
|
this.#graphCache.set(key, scanned);
|
|
660
|
-
while (this.#graphCache.size >
|
|
714
|
+
while (this.#graphCache.size > 1) {
|
|
661
715
|
const oldest = this.#graphCache.keys().next().value;
|
|
662
716
|
if (oldest === undefined)
|
|
663
717
|
break;
|
|
@@ -665,6 +719,10 @@ export class ScriptedGuiStudio {
|
|
|
665
719
|
}
|
|
666
720
|
return scanned;
|
|
667
721
|
}
|
|
722
|
+
/** Release parsed GUI graphs after a bounded batch job has finished using the studio. */
|
|
723
|
+
clearCaches() {
|
|
724
|
+
this.#graphCache.clear();
|
|
725
|
+
}
|
|
668
726
|
async scan(workspaceId, principal, signal, languages = ['l_english']) {
|
|
669
727
|
signal?.throwIfAborted();
|
|
670
728
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
@@ -746,10 +804,10 @@ export class ScriptedGuiStudio {
|
|
|
746
804
|
input.signal?.throwIfAborted();
|
|
747
805
|
const placeholderScenario = parsePreviewScenario(input.scenario);
|
|
748
806
|
const explicitRelatedScenarios = (input.relatedScenarios ?? []).map(parsePreviewScenario);
|
|
749
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios]));
|
|
750
807
|
const generatedOptions = input.generatedScenarios === undefined
|
|
751
808
|
? undefined
|
|
752
809
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
810
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples));
|
|
753
811
|
const generatedScenarios = generatedOptions === undefined
|
|
754
812
|
? []
|
|
755
813
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
@@ -817,6 +875,9 @@ export class ScriptedGuiStudio {
|
|
|
817
875
|
const requestedBaselineScenario = input.comparisonScenario === undefined
|
|
818
876
|
? undefined
|
|
819
877
|
: parsePreviewScenario(input.comparisonScenario);
|
|
878
|
+
const generatedOptions = input.generatedScenarios === undefined
|
|
879
|
+
? undefined
|
|
880
|
+
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
820
881
|
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [
|
|
821
882
|
placeholderScenario.language,
|
|
822
883
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario.language]),
|
|
@@ -825,10 +886,7 @@ export class ScriptedGuiStudio {
|
|
|
825
886
|
placeholderScenario,
|
|
826
887
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
827
888
|
...explicitRelatedScenarios,
|
|
828
|
-
]));
|
|
829
|
-
const generatedOptions = input.generatedScenarios === undefined
|
|
830
|
-
? undefined
|
|
831
|
-
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
889
|
+
], generatedOptions?.textSamples));
|
|
832
890
|
const generatedScenarios = generatedOptions === undefined
|
|
833
891
|
? []
|
|
834
892
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions);
|
|
@@ -846,6 +904,18 @@ export class ScriptedGuiStudio {
|
|
|
846
904
|
const catalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, scenario.language);
|
|
847
905
|
const scene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, scenario, catalog);
|
|
848
906
|
const render = await renderGuiScene(scene, undefined, input.signal, budget);
|
|
907
|
+
const fullRenderCache = new Map([
|
|
908
|
+
[visualSceneKey(scene), fullImage(render)],
|
|
909
|
+
]);
|
|
910
|
+
const renderFull = async (target) => {
|
|
911
|
+
const key = visualSceneKey(target);
|
|
912
|
+
const cached = fullRenderCache.get(key);
|
|
913
|
+
if (cached !== undefined)
|
|
914
|
+
return cached;
|
|
915
|
+
const image = fullImage(await renderGuiScene(target, ['full'], input.signal, budget));
|
|
916
|
+
fullRenderCache.set(key, image);
|
|
917
|
+
return image;
|
|
918
|
+
};
|
|
849
919
|
const scenarioScenes = [scene];
|
|
850
920
|
const scenarioItems = [];
|
|
851
921
|
for (const variantScenario of [scenario, ...relatedScenarios]) {
|
|
@@ -855,10 +925,7 @@ export class ScriptedGuiStudio {
|
|
|
855
925
|
: await buildGuiScene(scanned.graph, scanned.files, input.windowName, variantScenario, catalog);
|
|
856
926
|
if (variantScene !== scene)
|
|
857
927
|
scenarioScenes.push(variantScene);
|
|
858
|
-
const
|
|
859
|
-
? render
|
|
860
|
-
: await renderGuiScene(variantScene, ['full'], input.signal, new RenderBudget());
|
|
861
|
-
const image = fullImage(variantRender);
|
|
928
|
+
const image = variantScene === scene ? fullImage(render) : await renderFull(variantScene);
|
|
862
929
|
scenarioItems.push({
|
|
863
930
|
label: variantScenario.description ?? variantScenario.id,
|
|
864
931
|
png: image.png,
|
|
@@ -873,8 +940,7 @@ export class ScriptedGuiStudio {
|
|
|
873
940
|
const stateScenario = scenarioWith(scenario, { id: `${scenario.id}-${state}`, state });
|
|
874
941
|
const stateScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, stateScenario, catalog);
|
|
875
942
|
stateScenes.push(stateScene);
|
|
876
|
-
const
|
|
877
|
-
const image = fullImage(stateRender);
|
|
943
|
+
const image = await renderFull(stateScene);
|
|
878
944
|
stateItems.push({ label: state, png: image.png, width: image.width, height: image.height });
|
|
879
945
|
}
|
|
880
946
|
const resolutionScenes = [];
|
|
@@ -895,8 +961,7 @@ export class ScriptedGuiStudio {
|
|
|
895
961
|
});
|
|
896
962
|
const resolutionScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, resolutionScenario, catalog);
|
|
897
963
|
resolutionScenes.push(resolutionScene);
|
|
898
|
-
const
|
|
899
|
-
const image = fullImage(resolutionRender);
|
|
964
|
+
const image = await renderFull(resolutionScene);
|
|
900
965
|
resolutionItems.push({
|
|
901
966
|
label: `${resolution.width}×${resolution.height} · UI ${uiScale}`,
|
|
902
967
|
png: image.png,
|
|
@@ -905,8 +970,8 @@ export class ScriptedGuiStudio {
|
|
|
905
970
|
});
|
|
906
971
|
}
|
|
907
972
|
const baselineScene = await buildGuiScene(scanned.graph, scanned.files, input.windowName, baselineScenario, catalog);
|
|
908
|
-
const
|
|
909
|
-
const comparison = await compareGuiImages(
|
|
973
|
+
const baselineImage = await renderFull(baselineScene);
|
|
974
|
+
const comparison = await compareGuiImages(baselineImage.png, fullImage(render).png, budget, input.signal);
|
|
910
975
|
const renderedSourceIds = [
|
|
911
976
|
...scenarioScenes,
|
|
912
977
|
...stateScenes,
|
|
@@ -391,7 +391,7 @@ export function registerGuiTools(server, engine, context) {
|
|
|
391
391
|
});
|
|
392
392
|
server.registerTool('hoi4.gui_render', {
|
|
393
393
|
title: 'Render scripted GUI artifacts',
|
|
394
|
-
description: 'Render GUI states, resolutions, generated or explicit scenarios, hierarchy, comparisons, and diagnostics.',
|
|
394
|
+
description: 'Render GUI states, resolutions, generated or explicit scenarios, dynamic flags and text icons, hierarchy, comparisons, and diagnostics.',
|
|
395
395
|
inputSchema: renderInput,
|
|
396
396
|
outputSchema: guiRenderOutputSchema,
|
|
397
397
|
annotations: artifactProducing,
|
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { canonicalJson, compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
4
4
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
5
|
-
import { probabilityDomainScanPatterns } from '../core/domain-scan-patterns.js';
|
|
5
|
+
import { probabilityDomainScanPatterns, probabilitySharedDefinitionPatterns, } from '../core/domain-scan-patterns.js';
|
|
6
6
|
import { publicArtifactLink } from '../core/artifacts.js';
|
|
7
7
|
import { ServiceError } from '../core/result.js';
|
|
8
8
|
import { PACKAGE_VERSION } from '../version.js';
|
|
@@ -649,12 +649,19 @@ export class ProbabilityAnalyzer {
|
|
|
649
649
|
this.engine = engine;
|
|
650
650
|
this.state = analyzerState(engine);
|
|
651
651
|
}
|
|
652
|
-
async scan(context, sourcePaths = []) {
|
|
652
|
+
async scan(context, sourcePaths = [], emptyScope = 'domain') {
|
|
653
653
|
if (context.refresh === true)
|
|
654
654
|
this.engine.invalidate(context.workspaceId);
|
|
655
655
|
const workspace = this.engine.resolver.get(context.workspaceId, context.principal);
|
|
656
|
+
const selectedSourcePaths = sourcePaths.filter((sourcePath) => sourcePath !== undefined);
|
|
656
657
|
return this.engine.scan(context.workspaceId, {
|
|
657
|
-
patterns:
|
|
658
|
+
patterns: selectedSourcePaths.length > 0
|
|
659
|
+
? probabilityDomainScanPatterns(workspace, selectedSourcePaths)
|
|
660
|
+
: emptyScope === 'domain'
|
|
661
|
+
? probabilityDomainScanPatterns(workspace)
|
|
662
|
+
: emptyScope === 'shared'
|
|
663
|
+
? probabilitySharedDefinitionPatterns()
|
|
664
|
+
: [],
|
|
658
665
|
}, context.principal, context.signal);
|
|
659
666
|
}
|
|
660
667
|
async verifyGameVersion(context, adapter) {
|
|
@@ -714,6 +721,14 @@ export class ProbabilityAnalyzer {
|
|
|
714
721
|
return {
|
|
715
722
|
workspaceId: context.workspaceId,
|
|
716
723
|
workspaceIdentity: workspace.workspaceIdentity,
|
|
724
|
+
...(typeof extra.sourceScope === 'string'
|
|
725
|
+
? {
|
|
726
|
+
sourceScope: extra.sourceScope,
|
|
727
|
+
}
|
|
728
|
+
: {}),
|
|
729
|
+
...(Array.isArray(extra.sourcePaths) && extra.sourcePaths.length > 0
|
|
730
|
+
? { sourcePaths: extra.sourcePaths }
|
|
731
|
+
: {}),
|
|
717
732
|
sourceRevision: snapshot.revision,
|
|
718
733
|
sourceHash: surface.sourceHash,
|
|
719
734
|
scenarioHash,
|
|
@@ -840,7 +855,7 @@ export class ProbabilityAnalyzer {
|
|
|
840
855
|
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide either source or customPoolManifest for inspection');
|
|
841
856
|
if (customPoolManifest !== undefined && adapter !== 'custom_weighted_pool')
|
|
842
857
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
843
|
-
const snapshot = await this.scan(context, [source?.path]);
|
|
858
|
+
const snapshot = await this.scan(context, [source?.path], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
844
859
|
if (customPoolManifest !== undefined) {
|
|
845
860
|
const surface = customSurface(customPoolManifest, snapshot.revision);
|
|
846
861
|
const gameVersionVerification = await this.verifyGameVersion(context, 'custom_weighted_pool');
|
|
@@ -1030,7 +1045,7 @@ export class ProbabilityAnalyzer {
|
|
|
1030
1045
|
if (!usingManifest && request.adapter === 'custom_weighted_pool')
|
|
1031
1046
|
throw new ServiceError('PROBABILITY_MANIFEST_REQUIRED', 'custom_weighted_pool requires customPoolManifest');
|
|
1032
1047
|
const gameVersionVerification = await this.verifyGameVersion(request, request.adapter);
|
|
1033
|
-
const snapshot = await this.scan(request, [request.source?.path]);
|
|
1048
|
+
const snapshot = await this.scan(request, [request.source?.path], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1034
1049
|
const surface = usingManifest
|
|
1035
1050
|
? customSurface(request.customPoolManifest, snapshot.revision)
|
|
1036
1051
|
: discoverWeightedSurface(snapshot, request.adapter, request.source, request.candidatePool ?? []);
|
|
@@ -1039,6 +1054,14 @@ export class ProbabilityAnalyzer {
|
|
|
1039
1054
|
? request.customPoolManifest.candidates.map(({ id }) => id)
|
|
1040
1055
|
: (request.candidatePool ?? []), gameVersionVerification, {
|
|
1041
1056
|
operation,
|
|
1057
|
+
sourceScope: request.source?.path !== undefined
|
|
1058
|
+
? 'file'
|
|
1059
|
+
: request.source?.inlineClausewitz !== undefined
|
|
1060
|
+
? 'shared'
|
|
1061
|
+
: request.source !== undefined
|
|
1062
|
+
? 'domain'
|
|
1063
|
+
: 'none',
|
|
1064
|
+
sourcePaths: request.source?.path === undefined ? [] : [request.source.path],
|
|
1042
1065
|
horizonDays: request.horizonDays,
|
|
1043
1066
|
requestedMetrics: request.metrics,
|
|
1044
1067
|
acceptanceBands: request.acceptanceBands,
|
|
@@ -1167,7 +1190,7 @@ export class ProbabilityAnalyzer {
|
|
|
1167
1190
|
}
|
|
1168
1191
|
async sequence(request) {
|
|
1169
1192
|
request.signal?.throwIfAborted();
|
|
1170
|
-
const snapshot = await this.scan(request);
|
|
1193
|
+
const snapshot = await this.scan(request, [], 'none');
|
|
1171
1194
|
const surface = customSurface(request.customPoolManifest, snapshot.revision);
|
|
1172
1195
|
const gameVersionVerification = await this.verifyGameVersion(request, 'custom_weighted_pool');
|
|
1173
1196
|
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, request.customPoolManifest.candidates.map(({ id }) => id), gameVersionVerification, {
|
|
@@ -1334,8 +1357,10 @@ export class ProbabilityAnalyzer {
|
|
|
1334
1357
|
return this.store(stale, ['json'], false, request.signal, request.principal);
|
|
1335
1358
|
}
|
|
1336
1359
|
await this.verifyGameVersion(request, result.adapter.id);
|
|
1337
|
-
const snapshot =
|
|
1338
|
-
|
|
1360
|
+
const snapshot = result.metadata.sourceScope === undefined || result.metadata.sourceScope === 'none'
|
|
1361
|
+
? undefined
|
|
1362
|
+
: await this.scan(request, result.metadata.sourcePaths ?? [], result.metadata.sourceScope === 'shared' ? 'shared' : 'domain');
|
|
1363
|
+
if (snapshot !== undefined && snapshot.revision !== result.metadata.sourceRevision) {
|
|
1339
1364
|
const stale = {
|
|
1340
1365
|
...result,
|
|
1341
1366
|
status: 'stale',
|
|
@@ -737,6 +737,8 @@ const probabilityMetadataSchema = z
|
|
|
737
737
|
.object({
|
|
738
738
|
workspaceId: z.string(),
|
|
739
739
|
workspaceIdentity: z.string(),
|
|
740
|
+
sourceScope: z.enum(['file', 'shared', 'domain', 'none']).optional(),
|
|
741
|
+
sourcePaths: z.array(z.string()).max(100_000).optional(),
|
|
740
742
|
sourceRevision: z.string(),
|
|
741
743
|
sourceHash: z.string(),
|
|
742
744
|
scenarioHash: z.string(),
|
|
@@ -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 = '
|
|
3
|
+
export const PACKAGE_VERSION = '3.0.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/development.md
CHANGED
|
@@ -35,4 +35,6 @@ npm run test:local
|
|
|
35
35
|
|
|
36
36
|
Add `HOI4_DEPENDENCY_ROOTS` as the platform path-delimited list when the external mod has dependencies. Local tests remain read-only.
|
|
37
37
|
|
|
38
|
+
The installed-game qualification also parses every shipped bitmap-font descriptor, resolves every atlas page, shapes native and enlarged glyphs, verifies deterministic raster output, and checks HOI4 localisation colours across base fonts and language overrides.
|
|
39
|
+
|
|
38
40
|
See the package [Security Policy](../SECURITY.md) for private vulnerability reporting.
|
package/docs/gui.md
CHANGED
|
@@ -31,13 +31,13 @@ Rewrite once, then inspect and render the result.
|
|
|
31
31
|
|
|
32
32
|
Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
|
|
33
33
|
|
|
34
|
-
The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
|
|
34
|
+
The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, country flags, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
|
|
35
35
|
|
|
36
36
|
Bitmap text uses the language-appropriate HOI4 font definition and its real glyph atlas. Header, serif, compact, typewriter, black, inverted, and other declared faces keep their own glyph shapes, native metrics, face colour, border colour, language override, and font-specific `textcolors` palette. The renderer reads BMFont channel roles, separates face and outline coverage, and recognizes both vanilla black-on-white masks and conventional white-on-transparent custom masks. HOI4 localisation colour runs such as `§Y` and `§R` therefore colour the glyph face while retaining its sharp antialiasing and declared border instead of flattening the glyph into a blurred monochrome mask or switching to the tool font. The default language uses the base font rather than an unrelated language override, and the renderer recognizes installed-game atlases whose filename omits a page suffix declared by the matching `.fnt` file.
|
|
37
37
|
|
|
38
38
|
Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
|
|
39
39
|
|
|
40
|
-
Scripted-GUI composition follows `parent_scripted_gui` chains even when the linked child window is a top-level sibling in the `.gui` file. Literal `always = yes` and `always = no` visibility and click-enabled blocks are applied automatically. A scenario's `values` object supplies concrete runtime values in one place: variable names resolve numeric text, scripted-localisation names resolve dynamic labels, a progress-bar element name sets its fill value, and element keys ending in `.image`, `.frame`, `.x`, `.y`, `.visible`, or `.enabled` update that visual property. Scripted-GUI property expressions such as `image = "[GetMeterSprite]"` or `x = meter_fill_width` consume matching scenario values automatically. Dynamic lists instantiate their declared `entry_container` or `country_scope_entry_container`; each scenario row can select the country template with `countryScope`, choose an explicit `entryContainer`, and
|
|
40
|
+
Scripted-GUI composition follows `parent_scripted_gui` chains even when the linked child window is a top-level sibling in the `.gui` file. Literal `always = yes` and `always = no` visibility and click-enabled blocks are applied automatically. A scenario's `values` object supplies concrete runtime values in one place: variable names resolve numeric text, scripted-localisation names resolve dynamic labels, a progress-bar element name sets its fill value, and element keys ending in `.image`, `.frame`, `.x`, `.y`, `.visible`, or `.enabled` update that visual property. Scripted-GUI property expressions such as `image = "[GetMeterSprite]"` or `x = meter_fill_width` consume matching scenario values automatically. Dynamic text is parsed after scenario and scripted-localisation substitution, so a runtime string containing `£command_power` or `£[GetResourceIcon]` renders the resolved inline HOI4 text icon instead of literal marker text. Dynamic lists instantiate their declared `entry_container` or `country_scope_entry_container`; each scenario row can select the country template with `countryScope`, choose an explicit `entryContainer`, provide `countryTag` and `ideology` for `GetFlag`, and supply row-scoped text, image, frame, position, visibility, and enabled values. Masked country sprites use the actual mask and overlay dimensions, so custom flag sizes are discovered from GFX assets rather than assumed by the renderer.
|
|
41
41
|
|
|
42
42
|
`hoi4.gui_inspect` and `hoi4.gui_render` generate a plausible seeded runtime scenario by default. The generator follows the complete `parent_scripted_gui` surface, discovers numeric and text tokens in GUI localisation and scripted-GUI properties, chooses real scripted-localisation text and sprite candidates, applies simple source thresholds, fills meters within their declared ranges, populates dynamic lists with coherent rows, selects one overlapping tab pane, keeps active and idle controls mutually exclusive, and leaves values explicitly supplied by the caller unchanged. Normal controls and animation frames remain stable unless variation is explicitly requested. Every explicit `relatedScenarios` entry is rendered beside the generated view.
|
|
43
43
|
|
|
@@ -66,11 +66,22 @@ Preview scenarios can include exact layout expectations:
|
|
|
66
66
|
"values": {
|
|
67
67
|
"threat": 73,
|
|
68
68
|
"GetThreatLabel": "High",
|
|
69
|
+
"GetCostLine": "Cost £command_power 20",
|
|
69
70
|
"threat_meter": 73,
|
|
70
71
|
"confirm_button.enabled": true
|
|
71
72
|
},
|
|
72
73
|
"flags": { "panel_open": true },
|
|
73
|
-
"lists": {
|
|
74
|
+
"lists": {
|
|
75
|
+
"target_list": [
|
|
76
|
+
{
|
|
77
|
+
"id": 1,
|
|
78
|
+
"label": "France",
|
|
79
|
+
"countryScope": true,
|
|
80
|
+
"countryTag": "FRA",
|
|
81
|
+
"ideology": "democratic"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
},
|
|
74
85
|
"visibility": { "target_list": true, "confirm_button": true },
|
|
75
86
|
"expectations": {
|
|
76
87
|
"visible": ["target_list", "confirm_button"],
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
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": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"docs/map.md",
|
|
48
48
|
"docs/http.md",
|
|
49
49
|
"docs/development.md",
|
|
50
|
+
"docs/images/readme/",
|
|
50
51
|
"examples/clients/",
|
|
51
52
|
"examples/probability/",
|
|
52
53
|
"schemas/",
|
|
@@ -75,6 +76,7 @@
|
|
|
75
76
|
"test:local": "tsx scripts/run-local-tests.ts",
|
|
76
77
|
"test:coverage": "vitest run --coverage",
|
|
77
78
|
"benchmark": "node --expose-gc --import tsx scripts/benchmark.ts",
|
|
79
|
+
"docs:examples": "tsx scripts/generate-readme-examples.ts",
|
|
78
80
|
"fixtures:generate": "npm run fixtures:generate:focus && npm run fixtures:generate:gui && npm run fixtures:generate:map && npm run fixtures:generate:event && npm run fixtures:generate:technology && npm run fixtures:generate:probability",
|
|
79
81
|
"fixtures:generate:focus": "tsx scripts/generate-fixtures.ts",
|
|
80
82
|
"fixtures:generate:gui": "tsx scripts/generate-gui-fixture.ts",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -60,13 +60,13 @@
|
|
|
60
60
|
"minLength": 1
|
|
61
61
|
},
|
|
62
62
|
"__schema2": {
|
|
63
|
-
"default":
|
|
63
|
+
"default": 100000,
|
|
64
64
|
"type": "integer",
|
|
65
65
|
"minimum": 1,
|
|
66
66
|
"maximum": 1000000
|
|
67
67
|
},
|
|
68
68
|
"__schema3": {
|
|
69
|
-
"default":
|
|
69
|
+
"default": 268435456,
|
|
70
70
|
"type": "integer",
|
|
71
71
|
"minimum": 1048576,
|
|
72
72
|
"maximum": 9007199254740991
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/gui-animation-source.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/gui-helper.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/gui-scenario.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/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/map-operation.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"oneOf": [
|
|
5
5
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v3.0.0/schemas/operation-result.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|