hoi4-agent-tools 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +43 -116
  3. package/dist/hoi4_agent_tools/gui/layout.js +237 -11
  4. package/dist/hoi4_agent_tools/gui/renderer.js +36 -3
  5. package/dist/hoi4_agent_tools/gui/scenario.js +18 -0
  6. package/dist/hoi4_agent_tools/gui/studio.js +73 -21
  7. package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
  8. package/dist/hoi4_agent_tools/version.js +1 -1
  9. package/docs/README.md +33 -18
  10. package/docs/examples.md +85 -0
  11. package/docs/gui.md +37 -0
  12. package/docs/setup.md +50 -26
  13. package/docs/tools.md +64 -0
  14. package/package.json +4 -2
  15. package/schemas/configuration.schema.json +1 -1
  16. package/schemas/continuous-focus-palette.schema.json +1 -1
  17. package/schemas/custom-weighted-pool.schema.json +1 -1
  18. package/schemas/decision-inspect-input.schema.json +1 -1
  19. package/schemas/event-feature-manifest.schema.json +1 -1
  20. package/schemas/focus-plan.schema.json +1 -1
  21. package/schemas/focus-planning-sidecar.schema.json +1 -1
  22. package/schemas/gui-animation-source.schema.json +1 -1
  23. package/schemas/gui-helper.schema.json +1 -1
  24. package/schemas/gui-scenario.schema.json +139 -89
  25. package/schemas/helper-expansion-request.schema.json +1 -1
  26. package/schemas/helper-expansion-summary.schema.json +1 -1
  27. package/schemas/impact-inspect-input.schema.json +1 -1
  28. package/schemas/map-operation.schema.json +1 -1
  29. package/schemas/mechanic-test-input.schema.json +1 -1
  30. package/schemas/operation-result.schema.json +1 -1
  31. package/schemas/package-check-input.schema.json +1 -1
  32. package/schemas/package-manifest.schema.json +1 -1
  33. package/schemas/probability-analysis-result.schema.json +1 -1
  34. package/schemas/probability-compare-input.schema.json +1 -1
  35. package/schemas/probability-evaluate-input.schema.json +1 -1
  36. package/schemas/probability-inspect-input.schema.json +1 -1
  37. package/schemas/probability-render-input.schema.json +1 -1
  38. package/schemas/probability-scenario-set.schema.json +1 -1
  39. package/schemas/probability-sequence-input.schema.json +1 -1
  40. package/schemas/probability-simulate-input.schema.json +1 -1
  41. package/schemas/probability-sweep-input.schema.json +1 -1
  42. package/schemas/reference-context-input.schema.json +1 -1
  43. package/schemas/reference-read-input.schema.json +1 -1
  44. package/schemas/reference-search-input.schema.json +1 -1
  45. package/schemas/scenario-suite.schema.json +1 -1
  46. package/schemas/scenario-test-input.schema.json +1 -1
  47. package/schemas/shared-scenario.schema.json +1 -1
  48. package/schemas/source-lookup-input.schema.json +1 -1
  49. package/server.json +3 -3
@@ -39,7 +39,10 @@ function bitmapDefinitionId(key, layer = 'face') {
39
39
  function sceneGlyphDefinitions(scene) {
40
40
  const outlines = new Map();
41
41
  const bitmaps = new Map();
42
- for (const element of scene.elements) {
42
+ for (const element of [
43
+ ...scene.elements,
44
+ ...(scene.tooltipPreview === undefined ? [] : [scene.tooltipPreview.textElement]),
45
+ ]) {
43
46
  for (const line of element.text?.glyphLines ?? []) {
44
47
  for (const glyph of line.glyphs) {
45
48
  if (glyph.kind === 'outline')
@@ -400,6 +403,8 @@ function renderOverlay(element, variant, toolText) {
400
403
  if (rect.width <= 0 || rect.height <= 0)
401
404
  return '';
402
405
  if (variant === 'click-regions') {
406
+ if (element.enablement === 'unresolved')
407
+ return `<g><rect ${rectAttributes(rect)} fill="#ffb84d" fill-opacity="0.12" stroke="#ffb84d" stroke-width="2" stroke-dasharray="5 3"/><title>${escapeXml(`Potential click region: ${element.enablementReason ?? element.visibilityStatusReason ?? element.name}`)}</title>${toolText.render(`${element.name} (?)`, { x: rect.x + 3, y: rect.y + 13, fontSize: 11, fill: '#ffb84d' })}</g>`;
403
408
  if (!element.clickable)
404
409
  return '';
405
410
  return `<g><rect ${rectAttributes(rect)} fill="#00d4ff" fill-opacity="0.2" stroke="#00d4ff" stroke-width="2"/>${toolText.render(element.name, { x: rect.x + 3, y: rect.y + 13, fontSize: 11, fill: '#00d4ff' })}</g>`;
@@ -418,6 +423,13 @@ function renderOverlay(element, variant, toolText) {
418
423
  }
419
424
  return '';
420
425
  }
426
+ function renderTooltipPreview(scene, toolText) {
427
+ const tooltip = scene.tooltipPreview;
428
+ if (tooltip === undefined)
429
+ return '';
430
+ return (renderBaseElement(tooltip.element, undefined, toolText) +
431
+ renderBaseElement(tooltip.textElement, undefined, toolText));
432
+ }
421
433
  function viewFor(scene, variant) {
422
434
  if (variant !== 'cropped' || scene.bounds.width <= 0 || scene.bounds.height <= 0) {
423
435
  return {
@@ -453,7 +465,7 @@ export function sceneToSvg(scene, variant) {
453
465
  : [`<clipPath id="clip-${index}"><rect ${rectAttributes(element.clipRect)}/></clipPath>`]);
454
466
  const body = scene.elements
455
467
  .map((element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText))
456
- .join('');
468
+ .join('') + renderTooltipPreview(scene, toolText);
457
469
  const overlays = scene.elements
458
470
  .map((element) => renderOverlay(element, variant, toolText))
459
471
  .join('');
@@ -479,7 +491,7 @@ async function sceneToSvgCooperative(scene, variant, signal) {
479
491
  const clipDefinitions = await cooperativeParts(scene.elements, (element, index) => element.clipRect === undefined
480
492
  ? ''
481
493
  : `<clipPath id="clip-${index}"><rect ${rectAttributes(element.clipRect)}/></clipPath>`, signal);
482
- const body = await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal);
494
+ const body = (await cooperativeParts(scene.elements, (element, index) => renderBaseElement(element, element.clipRect === undefined ? undefined : `clip-${index}`, toolText), signal)) + renderTooltipPreview(scene, toolText);
483
495
  const overlays = await cooperativeParts(scene.elements, (element) => renderOverlay(element, variant, toolText), signal);
484
496
  const fidelity = escapeXml(canonicalJson(scene.fidelity));
485
497
  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>`;
@@ -526,6 +538,27 @@ export function hierarchyToSvg(scene) {
526
538
  function sceneLayoutEvidence(scene) {
527
539
  return {
528
540
  ...scene,
541
+ ...(scene.tooltipPreview === undefined
542
+ ? {}
543
+ : {
544
+ tooltipPreview: {
545
+ targetId: scene.tooltipPreview.targetId,
546
+ localisationKey: scene.tooltipPreview.localisationKey,
547
+ placement: scene.tooltipPreview.placement,
548
+ element: {
549
+ id: scene.tooltipPreview.element.id,
550
+ rect: scene.tooltipPreview.element.rect,
551
+ sprite: scene.tooltipPreview.element.sprite?.spriteName,
552
+ },
553
+ textElement: {
554
+ id: scene.tooltipPreview.textElement.id,
555
+ rect: scene.tooltipPreview.textElement.rect,
556
+ text: scene.tooltipPreview.textElement.text?.text,
557
+ lines: scene.tooltipPreview.textElement.text?.lines,
558
+ fontName: scene.tooltipPreview.textElement.text?.fontName,
559
+ },
560
+ },
561
+ }),
529
562
  elements: scene.elements.map((element) => ({
530
563
  ...element,
531
564
  ...(element.sprite === undefined
@@ -103,6 +103,24 @@ const GuiPreviewScenarioBodySchema = z
103
103
  .min(1)
104
104
  .max(128)
105
105
  .regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
106
+ tooltip: z
107
+ .object({
108
+ target: elementNameSchema,
109
+ x: z.number().nonnegative(),
110
+ y: z.number().nonnegative(),
111
+ width: z.number().positive().max(RENDER_MAX_DIMENSION),
112
+ padding: z
113
+ .number()
114
+ .nonnegative()
115
+ .max(RENDER_MAX_DIMENSION / 2)
116
+ .describe('Caller-declared inset on each side, in viewport pixels; no native padding is inferred.'),
117
+ })
118
+ .strict()
119
+ .refine(({ padding, width }) => padding * 2 < width, {
120
+ path: ['padding'],
121
+ message: 'Tooltip padding must leave a positive text width.',
122
+ })
123
+ .optional(),
106
124
  description: z.string().max(500).optional(),
107
125
  date: GuiScenarioDateSchema.optional(),
108
126
  controls: ConditionControlMapSchema.optional(),
@@ -343,14 +343,20 @@ function localisationIconSpriteCandidates(token) {
343
343
  const normalized = token.startsWith('GFX_') ? token : `GFX_${token}`;
344
344
  return [...new Set([normalized, `${normalized}_texticon`, `${normalized}_text_icon`])];
345
345
  }
346
- export function referencedAssetPatternsForWindow(graph, windowName, additionalSpriteNames = [], languages = ['l_english'], additionalTemplateNames = []) {
346
+ export function referencedAssetPatternsForWindow(graph, windowName, additionalSpriteNames = [], languages = ['l_english'], additionalTemplateNames = [], includeImmediateTooltip = false) {
347
347
  const spriteNames = new Set(additionalSpriteNames);
348
348
  const fontNames = new Set();
349
349
  const textValues = new Set();
350
+ if (includeImmediateTooltip) {
351
+ spriteNames.add('ToolTip');
352
+ fontNames.add('cg_16b');
353
+ }
350
354
  for (const element of selectedElements(graph, windowName, additionalTemplateNames)) {
351
355
  collectNamedAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), spriteNames);
352
356
  collectNamedAttributes(element.attributes, new Set(['font', 'buttonFont']), fontNames);
353
357
  collectNamedAttributes(element.attributes, new Set(['text', 'buttonText', 'context_aware_text']), textValues);
358
+ if (includeImmediateTooltip)
359
+ collectNamedAttributes(element.attributes, new Set(['pdx_tooltip']), textValues);
354
360
  }
355
361
  for (const localisation of graph.localisation) {
356
362
  if (textValues.has(localisation.key))
@@ -619,7 +625,7 @@ function assertUniqueScenarioIds(scenarios) {
619
625
  seen.add(scenario.id);
620
626
  }
621
627
  }
622
- function scenarioMatrixEvidence(graph, scenes) {
628
+ export function scenarioMatrixEvidence(graph, scenes) {
623
629
  const primary = scenes[0];
624
630
  if (primary === undefined)
625
631
  return { offline: true, scenarios: [], changes: [] };
@@ -664,16 +670,23 @@ function scenarioMatrixEvidence(graph, scenes) {
664
670
  if (matches.length === 0)
665
671
  return [];
666
672
  const shown = matches
667
- .filter(({ elements }) => elements.some(({ visible }) => visible))
673
+ .filter(({ elements }) => elements.every(({ visibilityStatus }) => visibilityStatus !== 'unresolved') &&
674
+ elements.some(({ visibilityStatus }) => visibilityStatus === 'shown'))
668
675
  .map(({ scene }) => scene.scenario.id);
669
676
  const hidden = matches
670
- .filter(({ elements }) => elements.every(({ visible }) => !visible))
677
+ .filter(({ elements }) => elements.every(({ visibilityStatus }) => visibilityStatus === 'hidden'))
678
+ .map(({ scene }) => scene.scenario.id);
679
+ const unresolvedVisibility = matches
680
+ .filter(({ elements }) => elements.some(({ visibilityStatus }) => visibilityStatus === 'unresolved'))
671
681
  .map(({ scene }) => scene.scenario.id);
672
682
  const enabled = matches
673
- .filter(({ elements }) => elements.some(({ clickable }) => clickable))
683
+ .filter(({ elements }) => elements.some(({ enablement }) => enablement === 'enabled'))
674
684
  .map(({ scene }) => scene.scenario.id);
675
685
  const disabled = matches
676
- .filter(({ elements }) => elements.every(({ clickable }) => !clickable))
686
+ .filter(({ elements }) => elements.every(({ enablement }) => enablement === 'disabled'))
687
+ .map(({ scene }) => scene.scenario.id);
688
+ const unresolved = matches
689
+ .filter(({ elements }) => elements.some(({ enablement }) => enablement === 'unresolved'))
677
690
  .map(({ scene }) => scene.scenario.id);
678
691
  return [
679
692
  {
@@ -682,10 +695,24 @@ function scenarioMatrixEvidence(graph, scenes) {
682
695
  ...new Set(matches.flatMap(({ elements }) => elements.map(({ sourcePath }) => sourcePath))),
683
696
  ].sort(compareCodeUnits),
684
697
  ...(visibilityNames.has(name)
685
- ? { visibility: { shown, hidden, covered: shown.length > 0 && hidden.length > 0 } }
698
+ ? {
699
+ visibility: {
700
+ shown,
701
+ hidden,
702
+ unresolved: unresolvedVisibility,
703
+ covered: shown.length > 0 && hidden.length > 0,
704
+ },
705
+ }
686
706
  : {}),
687
707
  ...(enabledNames.has(name)
688
- ? { enabled: { enabled, disabled, covered: enabled.length > 0 && disabled.length > 0 } }
708
+ ? {
709
+ enabled: {
710
+ enabled,
711
+ disabled,
712
+ unresolved,
713
+ covered: enabled.length > 0 && disabled.length > 0,
714
+ },
715
+ }
689
716
  : {}),
690
717
  },
691
718
  ];
@@ -736,13 +763,34 @@ function scenarioMatrixEvidence(graph, scenes) {
736
763
  })),
737
764
  visibleElements: scene.elements
738
765
  .filter(({ visible }) => visible)
739
- .map(({ id, name }) => ({ id, name })),
766
+ .map(({ id, name, visibilityStatus }) => ({ id, name, visibilityStatus })),
740
767
  hiddenElements: scene.elements
741
768
  .filter(({ visible }) => !visible)
742
- .map(({ id, name, visibilityReason }) => ({ id, name, reason: visibilityReason })),
769
+ .map(({ id, name, visibilityReason, visibilityStatus }) => ({
770
+ id,
771
+ name,
772
+ reason: visibilityReason,
773
+ visibilityStatus,
774
+ })),
775
+ unresolvedVisibility: scene.elements
776
+ .filter(({ visibilityStatus }) => visibilityStatus === 'unresolved')
777
+ .map(({ id, name, visible, visibilityStatusReason }) => ({
778
+ id,
779
+ name,
780
+ previewVisible: visible,
781
+ reason: visibilityStatusReason,
782
+ })),
743
783
  disabledElements: scene.elements
744
- .filter(({ disabledReason }) => disabledReason !== undefined)
784
+ .filter(({ disabledReason, enablement }) => disabledReason !== undefined && enablement === 'disabled')
745
785
  .map(({ id, name, disabledReason }) => ({ id, name, reason: disabledReason })),
786
+ unresolvedClickRegions: scene.elements
787
+ .filter(({ visible, enablement }) => visible && enablement === 'unresolved')
788
+ .map(({ id, name, rect, enablementReason }) => ({
789
+ id,
790
+ name,
791
+ rect,
792
+ reason: enablementReason,
793
+ })),
746
794
  })),
747
795
  changes: scenes.slice(1).map((scene) => {
748
796
  const currentByKey = new Map(scene.elements.map((element) => [elementKey(element), element]));
@@ -758,13 +806,13 @@ function scenarioMatrixEvidence(graph, scenes) {
758
806
  for (const key of keys) {
759
807
  const before = primaryByKey.get(key);
760
808
  const after = currentByKey.get(key);
761
- if (before?.visible !== true && after?.visible === true)
809
+ if (before?.visibilityStatus === 'hidden' && after?.visibilityStatus === 'shown')
762
810
  shown.push(after.name);
763
- if (before?.visible === true && after?.visible !== true)
811
+ if (before?.visibilityStatus === 'shown' && after?.visibilityStatus === 'hidden')
764
812
  hidden.push(before.name);
765
- if (before?.clickable === true && after?.clickable === false)
813
+ if (before?.enablement === 'enabled' && after?.enablement === 'disabled')
766
814
  disabled.push(after.name);
767
- if (before?.clickable === false && after?.clickable === true)
815
+ if (before?.enablement === 'disabled' && after?.enablement === 'enabled')
768
816
  enabled.push(after.name);
769
817
  if (before === undefined || after === undefined)
770
818
  continue;
@@ -924,7 +972,7 @@ export class ScriptedGuiStudio {
924
972
  const files = mergeScannedFiles(snapshot.files, localisationFiles);
925
973
  return this.graphForFiles(files, `workspace:${workspaceId}`);
926
974
  }
927
- async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = []) {
975
+ async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = [], includeImmediateTooltip = false) {
928
976
  const workspace = this.resolver.get(workspaceId, principal);
929
977
  const layoutPatterns = guiLayoutPatterns(workspace);
930
978
  const overlayLayouts = await this.engine.scan(workspaceId, {
@@ -955,7 +1003,7 @@ export class ScriptedGuiStudio {
955
1003
  const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}`, additionalLocalisationKeys);
956
1004
  assertTargetWindowAvailable(definitions.graph, windowName);
957
1005
  const referencedPatterns = [
958
- ...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames),
1006
+ ...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames, includeImmediateTooltip),
959
1007
  ...additionalAssetPatterns,
960
1008
  ];
961
1009
  const exactReferenced = referencedPatterns.length === 0
@@ -1000,7 +1048,7 @@ export class ScriptedGuiStudio {
1000
1048
  const generatedOptions = input.generatedScenarios === undefined
1001
1049
  ? undefined
1002
1050
  : parseGeneratedScenarioOptions(input.generatedScenarios);
1003
- const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]));
1051
+ const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]), [placeholderScenario, ...explicitRelatedScenarios].some(({ tooltip }) => tooltip !== undefined));
1004
1052
  const generatedScenarios = generatedOptions === undefined
1005
1053
  ? []
1006
1054
  : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
@@ -1030,7 +1078,7 @@ export class ScriptedGuiStudio {
1030
1078
  const budget = new RenderBudget();
1031
1079
  const beforeScenario = parsePreviewScenario(input.before);
1032
1080
  const afterScenario = parsePreviewScenario(input.after);
1033
- const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]), scenarioLocalisationKeys([beforeScenario, afterScenario]));
1081
+ const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]), scenarioLocalisationKeys([beforeScenario, afterScenario]), [beforeScenario, afterScenario].some(({ tooltip }) => tooltip !== undefined));
1034
1082
  const beforeCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, beforeScenario.language);
1035
1083
  const afterCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, afterScenario.language);
1036
1084
  const before = await buildGuiScene(scanned.graph, scanned.files, input.windowName, beforeScenario, beforeCatalog);
@@ -1091,7 +1139,11 @@ export class ScriptedGuiStudio {
1091
1139
  placeholderScenario,
1092
1140
  ...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
1093
1141
  ...explicitRelatedScenarios,
1094
- ]));
1142
+ ]), [
1143
+ placeholderScenario,
1144
+ ...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
1145
+ ...explicitRelatedScenarios,
1146
+ ].some(({ tooltip }) => tooltip !== undefined));
1095
1147
  const generatedScenarios = generatedOptions === undefined
1096
1148
  ? []
1097
1149
  : generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
@@ -1438,7 +1490,7 @@ export class ScriptedGuiStudio {
1438
1490
  const previewScenario = input.scenario === undefined ? undefined : parsePreviewScenario(input.scenario);
1439
1491
  const initiallyScanned = input.windowName === undefined
1440
1492
  ? await this.scan(input.workspaceId, input.principal, input.signal)
1441
- : await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]), scenarioLocalisationKeys([previewScenario]));
1493
+ : await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]), scenarioLocalisationKeys([previewScenario]), previewScenario.tooltip !== undefined);
1442
1494
  const exactTargets = await this.scanner.scan(workspace, {
1443
1495
  patterns: prepared.map(({ relativePath }) => relativePath),
1444
1496
  ...(input.signal === undefined ? {} : { signal: input.signal }),
@@ -1,2 +1,2 @@
1
1
  /** Shared agent-facing guidance for both MCP protocol generations. */
2
- export const SERVER_INSTRUCTIONS = 'Use hoi4.reference_context for local wiki and installed documentation citations, hoi4.reference_search and hoi4.reference_read for exact sections, and hoi4.source_lookup for indexed source examples. Use focus tools for focus trees, GUI tools for interfaces, and map tools for map data. Start unfamiliar event chains with hoi4.event_inspect. Event tools are read-only. Start cross-system source review with hoi4.impact_inspect and decision or mission scenarios with hoi4.decision_inspect. Test declared mechanic steps with hoi4.mechanic_test, package connections with hoi4.package_check, and named suites with hoi4.scenario_test. These analysis tools are read-only. Start technology and doctrine work with hoi4.tech_inspect. Technology tools are read-only. Start weighted AI, MTTH, random, and declared-pool analysis with hoi4.probability_inspect. Probability tools are read-only. Concurrent calls queue automatically and emit periodic progress when the client requests it. Long operations support negotiated MCP tasks; native background rewrites require a stable requestKey. Large evidence is linked as resources.';
2
+ export const SERVER_INSTRUCTIONS = 'Start with the matching inspect tool; use hoi4.probability_inspect for weights and timing. Find local documentation with hoi4.reference_context, hoi4.reference_search and hoi4.reference_read, and source definitions with hoi4.source_lookup. Inspection and rendering are read-only; rewrite tools edit mod source. Results describe offline source and scenario models, not observed game behavior. Unresolved states are not confirmed coverage. Read linked artifacts when summaries omit detail. Long operations support negotiated MCP tasks and hoi4.job_inspect/hoi4.job_cancel; background rewrites need a stable requestKey.';
@@ -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.5.1';
3
+ export const PACKAGE_VERSION = '3.6.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/README.md CHANGED
@@ -1,20 +1,35 @@
1
1
  # Documentation
2
2
 
3
- - [Setup](setup.md): install the server, configure HOI4 paths, and connect an MCP client.
4
- - [Local references](reference.md): retrieve cited offline wiki and installed documentation sections and exact source definitions.
5
- - [Event chains](events.md): inspect, trace, lint, render, and compare event chains.
6
- - [Cross-system impact and decisions](analysis.md): trace source consumers and evaluate decision or mission scenarios.
7
- - [Mechanic tests and scenario suites](mechanics.md): interpret declared effect steps, check package links, and run resumable cases.
8
- - [Technology trees](technology.md): inspect, trace, lint, render, and compare technologies and doctrines.
9
- - [Visual examples and comparison limits](visual-comparisons.md): real game captures beside MCP renders, with measured findings and limits.
10
- - [Interface examples and comparison limits](https://github.com/klimPaskov/hoi4-agent-tools/blob/main/docs/gui-comparisons.md): native-size vanilla and Chaos Redux windows, populated templates, scenarios, and provenance.
11
- - [Bounded helper expansion](helper-expansion.md): resume source-linked event and technology helper paths with exact coverage boundaries.
12
- - [Persistent jobs](jobs.md): run, inspect, cancel, and recover long operations.
13
- - [AI and MTTH analysis](probability.md): evaluate weighted logic, timing, uncertainty, sequences, and patches.
14
- - [Focus trees](focus.md): inspect, create, render, and clean national or continuous focus content.
15
- - [Scripted GUIs](gui.md): inspect linked UI source, render states, and create or repair GUIs.
16
- - [Maps](map.md): inspect and edit provinces, states, regions, adjacency, supply, and railways.
17
- - [HTTP](http.md): run the MCP server for shared or remote access.
18
- - [Development](development.md): build, test, and contribute.
19
-
20
- For vulnerability reporting, see the repository [Security Policy](../SECURITY.md).
3
+ Start with [Setup](setup.md), then try the [Examples](examples.md).
4
+ You can ask your coding agent for these tasks in ordinary language; you do not need to memorize tool names.
5
+
6
+ ## Choose a task
7
+
8
+ | I want to… | Read |
9
+ | ---------------------------------------------------- | ----------------------------------------- |
10
+ | Find a modding rule or a vanilla example | [Local references](reference.md) |
11
+ | Work on a focus tree | [Focus trees](focus.md) |
12
+ | Understand an event chain | [Events](events.md) |
13
+ | Trace dependencies or inspect decisions and missions | [Decisions and dependencies](analysis.md) |
14
+ | Preview a technology or doctrine tree | [Technology trees](technology.md) |
15
+ | Build or review an interface | [Scripted GUIs](gui.md) |
16
+ | Inspect or edit the map | [Maps](map.md) |
17
+ | Compare weights, chances or timing | [AI and probability](probability.md) |
18
+ | Test a mechanic or check a content package | [Mechanic tests](mechanics.md) |
19
+
20
+ ## See the results
21
+
22
+ - [Examples](examples.md): practical prompts with real renders.
23
+ - [Focus and technology comparisons](visual-comparisons.md): supplied game captures beside source renders.
24
+ - [Interface comparisons](gui-comparisons.md): vanilla and Chaos Redux windows.
25
+
26
+ ## Detailed reference
27
+
28
+ - [Tool reference](tools.md): tool names and what each one does.
29
+ - [Background jobs](jobs.md): inspect, cancel or retrieve long operations.
30
+ - [Large event and technology queries](helper-expansion.md): follow bounded results without losing coverage.
31
+ - [Remote hosting](http.md): share the server over HTTP.
32
+ - [Development](development.md): build, test and contribute.
33
+ - [Architecture decisions](adr/README.md): implementation rationale for contributors.
34
+
35
+ For vulnerability reporting, use the [security policy](../SECURITY.md).
@@ -0,0 +1,85 @@
1
+ # Examples
2
+
3
+ Start with a task you would normally give your coding agent.
4
+ The agent chooses the MCP tools and opens the resulting images or source references.
5
+ The renders below use actual Chaos Redux and installed HOI4 files; use names from your own mod when trying the prompts.
6
+
7
+ ## Find a rule without reading a whole wiki
8
+
9
+ > Find the installed documentation for `save_event_target_as`, read the relevant section, and show a vanilla usage. Keep the file and line references.
10
+
11
+ The reference tools find the section, read a small excerpt, and retain its source revision so the agent can return to it later.
12
+ In the [measured reference example](research/reference-benchmark.md), the search found the installed effect documentation first and a 12-line read supplied the command and supported scopes.
13
+ No wiki download or game launch was needed.
14
+
15
+ Use [Local references](reference.md) to configure an offline wiki snapshot or narrow a search.
16
+
17
+ ## Check alternative focus prerequisites
18
+
19
+ > Find Fury's focus tree. Render the branch leading to “The Last Neighbor Has Fallen” and show whether both preceding focuses are required or either one is enough.
20
+
21
+ ![Fury branch with dotted alternative prerequisites](images/comparisons/fury-or-mcp.png)
22
+
23
+ Dotted connectors mark alternatives within one prerequisite group; solid connectors mark separate required prerequisites.
24
+ The agent can link each finding to the source block before you change the route.
25
+ The [in-game comparison](visual-comparisons.md#focus-trees) shows the same branch and explains the presentation differences.
26
+
27
+ ## Review a large tree
28
+
29
+ > Render the Utopia Manifesto focus tree. Check its branch connections, alternative prerequisites, missing references and crowded areas before suggesting edits.
30
+
31
+ [![Utopia Manifesto focus tree](images/comparisons/utopia-mcp.png)](images/comparisons/utopia-mcp.png)
32
+
33
+ Open the image at full size for the complete layout.
34
+ This example contains 124 focuses in the recorded source version.
35
+ The [focus guide](focus.md) covers inspecting, rendering and reorganizing trees.
36
+
37
+ ## Preview technology layouts and artwork
38
+
39
+ > Render the infantry technology folder using this mod and its installed game assets. Check card sizes, icons, prerequisite links and missing placements.
40
+
41
+ [![Infantry technology folder](images/comparisons/infantry-mcp.png)](images/comparisons/infantry-mcp.png)
42
+
43
+ The preview follows the loaded technology and GUI definitions, including wide equipment cards.
44
+ The [technology comparisons](visual-comparisons.md#technology-folders) also show chemical and biological folders and explain why different source versions can produce different layouts.
45
+ See the [technology guide](technology.md) for grants, bonuses and doctrine paths.
46
+
47
+ ## Inspect an interface before editing it
48
+
49
+ > Find the Video options window in the installed interface files. Render it with labelled control regions, then check its text, spacing and clipping.
50
+
51
+ | Supplied in-game capture | MCP source preview |
52
+ | ------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
53
+ | ![Video options in game](images/comparisons/gui/options-video-ingame.png) | ![Video options preview](images/comparisons/gui/options-video-mcp.png) |
54
+
55
+ For a mod window, ask for the same view at 100% and 125% UI scale and supply the state values needed by its controls.
56
+ Unknown state values must stay unresolved; a visible button alone does not establish that it is enabled in the game.
57
+ The [interface gallery](gui-comparisons.md) includes Škoda Priority, Chaos Redux Settings, Event Log, Chaos Meter, decisions and occupation views.
58
+
59
+ ## Locate map data and plan an edit
60
+
61
+ > Find Brandenburg by name, show it on the map, and inspect its provinces, neighbours and railway connections. Show the affected files before changing anything.
62
+
63
+ [![Map rendered from installed source](images/comparisons/source/map-mcp.png)](images/comparisons/source/map-mcp.png)
64
+
65
+ After reviewing the selection, you can ask for a specific change such as moving named provinces into a state or updating a railway path.
66
+ The map tools check connected records and preserve dated state history when the operation can be represented safely.
67
+ See [Maps](map.md) for supported edits.
68
+
69
+ ## Trace an event or compare AI weights
70
+
71
+ > Trace this event's options and follow-up events. Point out unresolved calls and link each route to its source.
72
+
73
+ [![Chaos Redux event options](images/comparisons/source/event-chain-mcp.png)](images/comparisons/source/event-chain-mcp.png)
74
+
75
+ > Compare this decision's `ai_will_do` score before and after my change in peace and war. List missing conditions separately from known results.
76
+
77
+ Use [Events](events.md) for route graphs and [AI analysis](probability.md) for supported weights, chances and timing models.
78
+ A decision score is not automatically a percentage chance of an AI click.
79
+
80
+ ## About these examples
81
+
82
+ The images are source previews, not recordings of the server running the game.
83
+ Supplied game screenshots are identified separately and published with permission.
84
+ The [comparison pages](visual-comparisons.md) and [interface gallery](gui-comparisons.md) keep source details, reproducible inputs and known differences alongside the images.
85
+ The package does not include HOI4 or Chaos Redux source files or artwork.
package/docs/gui.md CHANGED
@@ -2,6 +2,43 @@
2
2
 
3
3
  Use `hoi4.gui_inspect`, `hoi4.gui_render`, and `hoi4.gui_rewrite` for `.gui`, `.gfx`, `common/scripted_guis`, localisation, sprites, fonts, and linked decision entry points.
4
4
 
5
+ ## Start with a preview
6
+
7
+ > Render this window at 100% and 125% UI scale. Show its control regions and list the state inputs needed to establish visibility and availability.
8
+
9
+ The [interface examples](gui-comparisons.md) show real vanilla and Chaos Redux windows.
10
+ Cyan click regions have confirmed availability under the supplied scenario.
11
+ Dashed amber regions are potential controls with unresolved visibility or enabling conditions; they do not count as confirmed branch coverage.
12
+ The scene retains the reason so the agent can supply the missing facts instead of guessing.
13
+ These are source-model results, not proof of native click execution.
14
+
15
+ ## Preview immediate tooltip text
16
+
17
+ An explicit tooltip request uses a visible element's `pdx_tooltip` localisation key:
18
+
19
+ ```json
20
+ {
21
+ "id": "close-tooltip",
22
+ "tooltip": {
23
+ "target": "close_button",
24
+ "x": 980,
25
+ "y": 100,
26
+ "width": 160,
27
+ "padding": 16
28
+ }
29
+ }
30
+ ```
31
+
32
+ The target can be an instance ID from inspection or a unique element name.
33
+ Position, width and padding are declared viewport pixels; height follows the measured text.
34
+ The preview uses the loaded `ToolTip` sprite and `cg_16b` font.
35
+ Missing or ambiguous targets, hidden or unresolved visibility, missing text/assets, and bounds errors produce diagnostics without a guessed popup.
36
+ A visibly disabled control can still have tooltip text.
37
+
38
+ Engine-generated `[!element_action]` and `[!element_action_enabled]` descriptions are not invented.
39
+ If their exact text is known, supply the exact bracketed token as a string key in `scenario.values` or `scenario.scriptedGui`; otherwise the tooltip remains unresolved.
40
+ The preview does not establish native automatic positioning, frame composition, delayed timing or `click_to_front` behavior.
41
+
5
42
  ## Create a GUI
6
43
 
7
44
  `hoi4.gui_rewrite` can work from supported source, targeted patches, or structured helpers. Source and helper rewrites may include one bounded text package: the main `.gui` plus additional interface `.gui`/`.gfx`, configured GFX `.gfx`, `common/scripted_guis/*.txt`, and localisation `.yml` files. The package is path-checked, parsed, linked, rendered, validated, and applied together in one rewrite. Existing text encodings are retained, while localisation is always written as UTF-8 with BOM.
package/docs/setup.md CHANGED
@@ -1,50 +1,74 @@
1
1
  # Setup
2
2
 
3
- For Codex, the supplied registration allows 120 seconds for startup and 600 seconds per tool call.
4
- Large source scans and GUI renders can take several minutes; keep selectors and scenario matrices bounded and use negotiated task support when available.
5
- `tool_timeout_sec` is the per-server tool deadline described in the [official configuration reference](https://developers.openai.com/ja-JP/docs/config-file/config-reference).
3
+ You need a coding agent that supports MCP and Node.js 22.19+ in the Node 22 line, or Node.js 24.
4
+ Keep an installed copy of Hearts of Iron IV available for vanilla references, artwork and fonts.
6
5
 
7
- HOI4 Agent Tools requires Node.js 22.19 or later in the Node 22 line, or Node.js 24.
8
-
9
- ## Install
6
+ ## 1. Install
10
7
 
11
8
  ```bash
12
9
  npm install --global hoi4-agent-tools
13
10
  ```
14
11
 
15
- Set the MCP process working directory to the mod being edited. The server creates its local workspace in memory, keeps generated artifacts in the per-user data directory, and starts without a config file. The installed game is detected when available for vanilla references; it is not required for source inspection or offline rendering.
12
+ Source inspection can work without an installed game.
13
+ Previews need the assets and fonts referenced by the mod; missing files are reported in the result.
14
+
15
+ ## 2. Connect your agent
16
+
17
+ ```bash
18
+ hoi4-agent-tools-setup --print-client-config
19
+ ```
20
+
21
+ Copy the printed `globalInstall` configuration into your MCP client, or use `codexTomlGlobal` for a TOML-based Codex configuration.
22
+ The printed command is correct for your platform.
23
+
24
+ Open the mod as your agent's workspace.
25
+ If your client does not supply workspace roots, set the MCP process working directory to that mod folder.
26
+ The server can then select the active mod without a separate configuration file.
27
+ It keeps indexes and generated images in your user data directory.
16
28
 
17
- When the MCP server starts with its working directory inside a mod, every focus, GUI, map, event, and technology call uses that mod automatically. Configured multi-mod or remote deployments can use explicit workspace selection.
29
+ ## 3. Try a small task
18
30
 
19
- Local startup accepts normal descriptor-based mods and sparse descriptor-less mods that contain standard HOI4 content folders. Empty roots need a `descriptor.mod` or an explicit configuration so the server does not guess an unrelated directory.
31
+ > Find the focus trees in this mod and render one of them.
20
32
 
21
- ## Optional persistent configuration
33
+ > Find the installed documentation for `save_event_target_as` and read only the relevant section.
22
34
 
23
- Use `--init` only when you need several mod roots, a fixed game path, or a shared/remote process:
35
+ Continue with the [examples guide](examples.md).
36
+ A mod normally has `descriptor.mod` or standard HOI4 content folders; an empty folder needs a descriptor or explicit configuration.
24
37
 
25
- ### Configure explicit paths
38
+ ## Choose game and mod paths
26
39
 
27
- Use explicit paths when discovery is incomplete or when you want to expose more than one mod root:
40
+ Use explicit paths if automatic discovery cannot find the game, or if you want to configure several mods:
28
41
 
29
42
  ```bash
30
- hoi4-agent-tools-setup --init --mod-root /projects/hoi4-mods --mod-root /workshop/hoi4-mods --game-root "/games/Hearts of Iron IV" --workspace-storage-root /var/lib/hoi4-agent-tools
43
+ hoi4-agent-tools-setup --init --mod-root "/projects/hoi4-mods" --game-root "/games/Hearts of Iron IV"
44
+ hoi4-agent-tools-setup --print-client-config
31
45
  ```
32
46
 
33
- - `--mod-root PATH` adds a parent folder containing mods. Repeat it for additional locations.
34
- - `--game-root PATH` selects the installed game folder used for vanilla references.
35
- - `--workspace-storage-root PATH` selects where indexes, renders, and other generated workspace files are stored.
36
- - `--config PATH` writes or reads a config at a custom path instead of the default per-user location.
47
+ `--mod-root` is a parent folder containing mod directories, not the game installation.
48
+ It can be repeated for several locations.
49
+ `--game-root` points to the installed game.
50
+ Use quoted Windows paths on Windows, for example `"C:/Program Files (x86)/Steam/steamapps/common/Hearts of Iron IV"`.
37
51
 
38
- The corresponding config fields are `modRoots`, `gameRoot`, and `workspaceStorageRoot`.
52
+ For a custom configuration location, add `--config PATH` to both setup commands.
53
+ `--workspace-storage-root PATH` chooses where generated indexes and images are saved.
54
+ Linked mod directories are not followed; configure their real parent directory instead.
39
55
 
40
- For a configured workspace, `wikiRoot` can select an absolute path to an offline Paradox Wiki snapshot and `scriptDocsRoot` can select an absolute path to a user-supplied generated documentation dump. Without these fields, the reference tools look for `paradox_wiki/` and `script_docs/` inside the mod. See [Local references](reference.md).
56
+ ## Add local reference material
41
57
 
42
- Linked directory entries are not followed. If a mod folder is a symlink or junction, configure the real parent directory as another `--mod-root`.
58
+ Place an offline wiki snapshot in `paradox_wiki/` inside the mod.
59
+ An optional generated documentation dump can go in `script_docs/`.
60
+ For a configured workspace, `wikiRoot` and `scriptDocsRoot` can point to other absolute directories.
61
+ The server reads these local files; it does not download the wiki.
62
+ See [Local references](reference.md) for source selection and missing-source reports.
43
63
 
44
- ## MCP client registration
64
+ ## Common problems
45
65
 
46
- ```bash
47
- hoi4-agent-tools-setup --print-client-config
48
- ```
66
+ | Problem | Check |
67
+ | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
68
+ | The agent cannot find a mod | Open the mod workspace, set the server working directory, or configure its real parent with `--mod-root`. |
69
+ | A render lacks icons or fonts | Confirm `--game-root` and the mod's asset paths. Read the missing-file findings. |
70
+ | A control's state is unresolved | Supply the scenario's required country, state, controller or variable inputs; do not assume a visible control is available. |
71
+ | A large query takes several minutes | Start with the specific tree, window or source area you need. Use [background jobs](jobs.md) when supported by your client. |
49
72
 
50
- Use the printed `globalInstall` or `codexTomlGlobal` entry. It is platform-correct and uses the default config without an environment variable. For a custom config, add `--config PATH` when printing the registration.
73
+ The supplied Codex registration allows 120 seconds for startup and 600 seconds per tool call.
74
+ For remote or shared deployments, use the [HTTP guide](http.md).