hoi4-agent-tools 3.5.0 → 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 (53) hide show
  1. package/CHANGELOG.md +14 -1
  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/reference/service.js +31 -14
  9. package/dist/hoi4_agent_tools/schemas/reference.js +1 -0
  10. package/dist/hoi4_agent_tools/version.js +1 -1
  11. package/docs/README.md +33 -18
  12. package/docs/examples.md +85 -0
  13. package/docs/gui.md +37 -0
  14. package/docs/reference.md +1 -1
  15. package/docs/research/reference-benchmark.md +14 -0
  16. package/docs/setup.md +50 -26
  17. package/docs/tools.md +64 -0
  18. package/package.json +4 -2
  19. package/schemas/configuration.schema.json +1 -1
  20. package/schemas/continuous-focus-palette.schema.json +1 -1
  21. package/schemas/custom-weighted-pool.schema.json +1 -1
  22. package/schemas/decision-inspect-input.schema.json +1 -1
  23. package/schemas/event-feature-manifest.schema.json +1 -1
  24. package/schemas/focus-plan.schema.json +1 -1
  25. package/schemas/focus-planning-sidecar.schema.json +1 -1
  26. package/schemas/gui-animation-source.schema.json +1 -1
  27. package/schemas/gui-helper.schema.json +1 -1
  28. package/schemas/gui-scenario.schema.json +139 -89
  29. package/schemas/helper-expansion-request.schema.json +1 -1
  30. package/schemas/helper-expansion-summary.schema.json +1 -1
  31. package/schemas/impact-inspect-input.schema.json +1 -1
  32. package/schemas/map-operation.schema.json +1 -1
  33. package/schemas/mechanic-test-input.schema.json +1 -1
  34. package/schemas/operation-result.schema.json +1 -1
  35. package/schemas/package-check-input.schema.json +1 -1
  36. package/schemas/package-manifest.schema.json +1 -1
  37. package/schemas/probability-analysis-result.schema.json +1 -1
  38. package/schemas/probability-compare-input.schema.json +1 -1
  39. package/schemas/probability-evaluate-input.schema.json +1 -1
  40. package/schemas/probability-inspect-input.schema.json +1 -1
  41. package/schemas/probability-render-input.schema.json +1 -1
  42. package/schemas/probability-scenario-set.schema.json +1 -1
  43. package/schemas/probability-sequence-input.schema.json +1 -1
  44. package/schemas/probability-simulate-input.schema.json +1 -1
  45. package/schemas/probability-sweep-input.schema.json +1 -1
  46. package/schemas/reference-context-input.schema.json +1 -1
  47. package/schemas/reference-read-input.schema.json +1 -1
  48. package/schemas/reference-search-input.schema.json +1 -1
  49. package/schemas/scenario-suite.schema.json +1 -1
  50. package/schemas/scenario-test-input.schema.json +1 -1
  51. package/schemas/shared-scenario.schema.json +1 -1
  52. package/schemas/source-lookup-input.schema.json +1 -1
  53. 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.';
@@ -386,35 +386,52 @@ export class ReferenceService {
386
386
  async context(workspace, input, signal) {
387
387
  const inventory = await this.inventory(workspace, signal);
388
388
  const wanted = surfaceNames[input.surface];
389
- const selected = [];
389
+ const ranked = input.question === undefined ? [] : this.rank(inventory.sections, input.question);
390
+ const required = [];
390
391
  const missing = [];
392
+ const includeRequired = (name, section) => {
393
+ if (section === undefined) {
394
+ missing.push(name);
395
+ return;
396
+ }
397
+ const matching = ranked.find((entry) => entry.section.path === section.path && entry.section.source === section.source);
398
+ required.push({ name, section: matching?.section ?? section });
399
+ };
391
400
  for (const name of [...new Set(wanted)]) {
392
401
  const section = inventory.sections.find((entry) => entry.source === 'wiki' && entry.title.toLowerCase() === name.toLowerCase());
393
- if (section === undefined)
394
- missing.push(name);
395
- else
396
- selected.push(section);
402
+ includeRequired(name, section);
397
403
  }
398
404
  for (const name of gameDocNames[input.surface]) {
399
405
  const section = inventory.sections.find((entry) => entry.source === 'game_doc' &&
400
406
  entry.path.replaceAll('\\', '/').toLowerCase().endsWith(`${name}.md`));
401
- if (section !== undefined)
402
- selected.push(section);
403
- else
404
- missing.push(name);
407
+ includeRequired(name, section);
405
408
  }
406
- const ranked = input.question === undefined ? [] : this.rank(inventory.sections, input.question).slice(0, 5);
409
+ const questionOrder = new Map(ranked.map(({ section }, index) => [section.id, index]));
410
+ const byQuestion = (left, right) => (questionOrder.get(left.section.id) ?? Number.MAX_SAFE_INTEGER) -
411
+ (questionOrder.get(right.section.id) ?? Number.MAX_SAFE_INTEGER);
412
+ const game = required.filter(({ section }) => section.source === 'game_doc').sort(byQuestion);
413
+ const wiki = required.filter(({ section }) => section.source === 'wiki').sort(byQuestion);
414
+ const seen = new Set();
407
415
  const prioritized = [
408
- ...ranked.map(({ section }) => section),
409
- ...selected.filter((section) => !ranked.some((entry) => entry.section.id === section.id)),
410
- ];
416
+ ...[...game.slice(0, 1), ...wiki.slice(0, 1), ...game.slice(1), ...wiki.slice(1)].map(({ section }) => section),
417
+ ...ranked.slice(0, 5).map(({ section }) => section),
418
+ ].filter((section) => {
419
+ if (seen.has(section.id))
420
+ return false;
421
+ seen.add(section.id);
422
+ return true;
423
+ });
424
+ const selected = prioritized.slice(0, input.limit);
411
425
  return {
412
426
  surface: input.surface,
413
- sections: prioritized.slice(0, input.limit).map((section) => ({
427
+ sections: selected.map((section) => ({
414
428
  ...this.publicSection(section),
415
429
  excerpt: ranked.find((entry) => entry.section.id === section.id)?.excerpt ?? section.excerpt,
416
430
  })),
417
431
  omitted: Math.max(0, prioritized.length - input.limit),
432
+ omittedSources: required
433
+ .filter(({ section }) => !selected.some((entry) => entry.path === section.path && entry.source === section.source))
434
+ .map(({ name }) => name),
418
435
  missing,
419
436
  coverage: inventory.coverage,
420
437
  skipped: inventory.skipped,
@@ -109,6 +109,7 @@ export const referenceContextDataSchema = z
109
109
  surface: referenceSurfaceSchema,
110
110
  sections: z.array(referenceSectionSchema).max(24),
111
111
  omitted: count,
112
+ omittedSources: z.array(z.string().max(256)).max(32),
112
113
  missing: z.array(z.string().max(256)).max(32),
113
114
  coverage: coverageBySource,
114
115
  skipped: count,
@@ -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.0';
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/reference.md CHANGED
@@ -14,7 +14,7 @@ Only Markdown headings and recognizable standalone names in generated `.log` fil
14
14
 
15
15
  ## Tools
16
16
 
17
- - `hoi4.reference_context`: request a surface such as `event`, `focus`, `technology`, `gui`, or `map`. It returns only the relevant offline wiki and installed documentation citations for that surface. Supply `question` to put the five best matching sections first. `missing` names a selected source absent from the configured roots. Citations are pointers and excerpts; read the pertinent sections before changing code.
17
+ - `hoi4.reference_context`: request a surface such as `event`, `focus`, `technology`, `gui`, or `map`. It reserves citations for available installed documentation and offline wiki sources before adding further question matches. Supply `question` to select the best matching section within each required source. At a limit of two or more, both source kinds are represented when available; a one-result limit prioritizes installed documentation. `omittedSources` names required sources excluded by the result limit, while `missing` names sources absent from the configured roots. Increase the limit or search an omitted source directly before treating the bundle as sufficient. Citations are pointers and excerpts; read the pertinent sections before changing code.
18
18
  - `hoi4.reference_search`: search page names, headings, exact script tokens, and matching body lines. The default limit is six results and the maximum is twelve. Filter `sources` to `game_doc`, `wiki`, or `script_doc` when appropriate.
19
19
  - `hoi4.reference_read`: provide a search/context result's `id` and `revision`. Read up to 80 lines and 8,000 UTF-8 bytes. Continue with the returned `nextLine`, `nextColumn` as `startColumn`, and the same revision. Long source lines continue without losing characters. A changed source returns `REFERENCE_REVISION_STALE`.
20
20
  - `hoi4.source_lookup`: find exact definitions and indexed usages in the installed game and configured mod source. The response includes load order, override status, up to three source blocks bounded to 2,000 bytes each, a scan revision, and explicit reference counts and truncation. Follow `nextLine` and `nextColumn` as `fromLine` and `fromColumn`, with `expectedRevision`, to read more of the active definition.
@@ -26,3 +26,17 @@ Revision-bound section IDs stayed stable across unrelated retrieval calls in the
26
26
  The retrieval benchmark used the local service directly. A separate real-workspace stdio run identified the built server as 3.5.0 and completed documentation context in 306 ms, a cited section read in 162 ms, and a first exact source lookup in 58,851 ms. That lookup builds the broader game/mod symbol inventory; it should not be confused with the much narrower documentation lookup. MCP transport fixtures cover both protocol adapters, workspace authorization, read-scope denial, and source usage discovery. Individual task adequacy and actual engine behavior remain separate checks.
27
27
 
28
28
  The four additional public tool definitions add approximately 6.5 KB to the one-time MCP tool discovery response. The complete list is about 72 KB in the discovery fixture and is tested against a 72 KiB ceiling. The context reduction above applies to reference retrieval after discovery.
29
+
30
+ ## Compact context budgets
31
+
32
+ A 2026-09-27 check of the installed 3.5.0 server found that eight GUI results could contain only wiki sections even though the installed GUI documentation was available. The 3.5.1 source qualifies limits of one, two, and eight citations across the same six work surfaces. All eighteen cases retained installed documentation; limits of two or more also retained wiki citations. Required sources excluded by the limit appeared in `omittedSources`, separately from files absent under `missing`. The nine search queries still found all expected answer-bearing sections.
33
+
34
+ For the GUI question “Where are scripted GUI click effects and triggers defined?”, the measured results were:
35
+
36
+ | Citation limit | Returned source kinds | Required sources omitted | JSON bytes | Milliseconds |
37
+ | -------------- | -------------------------------- | -----------------------: | ---------: | -----------: |
38
+ | 1 | Installed documentation | 5 | 877 | 348 |
39
+ | 2 | Installed documentation and wiki | 4 | 1,480 | 396 |
40
+ | 8 | Installed documentation and wiki | 0 | 5,926 | 339 |
41
+
42
+ The default six-surface context bundles used 3,545–5,556 bytes including omission metadata. A smaller response is not sufficient evidence when `omittedSources` is nonempty: increase the limit or search and read those sources directly. The evaluation script fails when a compact budget drops a required source kind or silently omits its required-source pointers.