hoi4-agent-tools 3.3.0 → 3.4.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 (81) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/hoi4_agent_tools/core/condition-evaluator.js +13 -8
  3. package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
  4. package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
  5. package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
  6. package/dist/hoi4_agent_tools/event/operations.js +16 -0
  7. package/dist/hoi4_agent_tools/event/service.js +21 -2
  8. package/dist/hoi4_agent_tools/focus/compact.js +241 -27
  9. package/dist/hoi4_agent_tools/focus/layout.js +119 -8
  10. package/dist/hoi4_agent_tools/focus/render.js +62 -0
  11. package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
  12. package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
  13. package/dist/hoi4_agent_tools/gui/assets.js +54 -4
  14. package/dist/hoi4_agent_tools/gui/layout.js +935 -104
  15. package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
  16. package/dist/hoi4_agent_tools/gui/shader.js +515 -0
  17. package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
  18. package/dist/hoi4_agent_tools/gui/studio.js +245 -19
  19. package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
  20. package/dist/hoi4_agent_tools/gui/validators.js +38 -12
  21. package/dist/hoi4_agent_tools/map/operations.js +70 -1
  22. package/dist/hoi4_agent_tools/map/render.js +70 -1
  23. package/dist/hoi4_agent_tools/map/service.js +30 -6
  24. package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
  25. package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
  26. package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
  27. package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
  28. package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
  29. package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
  30. package/dist/hoi4_agent_tools/probability/evaluation.js +3 -0
  31. package/dist/hoi4_agent_tools/probability/operations.js +8 -0
  32. package/dist/hoi4_agent_tools/probability/service.js +33 -12
  33. package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
  34. package/dist/hoi4_agent_tools/schemas/event.js +8 -0
  35. package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
  36. package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
  37. package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
  38. package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
  39. package/dist/hoi4_agent_tools/technology/graph.js +25 -10
  40. package/dist/hoi4_agent_tools/technology/render.js +286 -31
  41. package/dist/hoi4_agent_tools/technology/service.js +55 -8
  42. package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
  43. package/dist/hoi4_agent_tools/version.js +1 -1
  44. package/docs/events.md +3 -1
  45. package/docs/focus.md +10 -0
  46. package/docs/gui.md +49 -3
  47. package/docs/map.md +8 -0
  48. package/docs/probability.md +3 -1
  49. package/docs/technology.md +7 -2
  50. package/package.json +1 -1
  51. package/schemas/configuration.schema.json +1 -1
  52. package/schemas/continuous-focus-palette.schema.json +1 -1
  53. package/schemas/custom-weighted-pool.schema.json +1 -1
  54. package/schemas/decision-inspect-input.schema.json +1 -1
  55. package/schemas/event-feature-manifest.schema.json +1 -1
  56. package/schemas/focus-plan.schema.json +1 -1
  57. package/schemas/focus-planning-sidecar.schema.json +1 -1
  58. package/schemas/gui-animation-source.schema.json +1 -1
  59. package/schemas/gui-helper.schema.json +1 -1
  60. package/schemas/gui-scenario.schema.json +1 -1
  61. package/schemas/helper-expansion-request.schema.json +1 -1
  62. package/schemas/helper-expansion-summary.schema.json +1 -1
  63. package/schemas/impact-inspect-input.schema.json +1 -1
  64. package/schemas/map-operation.schema.json +1 -1
  65. package/schemas/mechanic-test-input.schema.json +1 -1
  66. package/schemas/operation-result.schema.json +1 -1
  67. package/schemas/package-check-input.schema.json +1 -1
  68. package/schemas/package-manifest.schema.json +1 -1
  69. package/schemas/probability-analysis-result.schema.json +1 -1
  70. package/schemas/probability-compare-input.schema.json +1 -1
  71. package/schemas/probability-evaluate-input.schema.json +1 -1
  72. package/schemas/probability-inspect-input.schema.json +1 -1
  73. package/schemas/probability-render-input.schema.json +1 -1
  74. package/schemas/probability-scenario-set.schema.json +1 -1
  75. package/schemas/probability-sequence-input.schema.json +1 -1
  76. package/schemas/probability-simulate-input.schema.json +1 -1
  77. package/schemas/probability-sweep-input.schema.json +1 -1
  78. package/schemas/scenario-suite.schema.json +1 -1
  79. package/schemas/scenario-test-input.schema.json +1 -1
  80. package/schemas/shared-scenario.schema.json +1 -1
  81. package/server.json +2 -2
@@ -0,0 +1,99 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { ServiceError } from '../core/result.js';
3
+ function routeLockSatisfied(lock, completed) {
4
+ const predicates = [
5
+ ...lock.requiredFocusIds.map((id) => completed.has(id)),
6
+ ...lock.excludedFocusIds.map((id) => !completed.has(id)),
7
+ ...(lock.alwaysImpossible === true ? [false] : []),
8
+ ];
9
+ return lock.mode === 'any' ? predicates.some(Boolean) : predicates.every(Boolean);
10
+ }
11
+ /** Reports only structural evidence. Unsupported runtime triggers remain unresolved. */
12
+ export function inspectFocusScenario(plan, completedFocusIds) {
13
+ const known = new Set(plan.focuses.map(({ id }) => id));
14
+ const completed = new Set(completedFocusIds);
15
+ const excludedBy = new Map();
16
+ for (const focus of plan.focuses)
17
+ for (const excluded of focus.mutuallyExclusive) {
18
+ const owners = excludedBy.get(excluded) ?? [];
19
+ owners.push(focus.id);
20
+ excludedBy.set(excluded, owners);
21
+ }
22
+ for (const id of completed)
23
+ if (!known.has(id))
24
+ throw new ServiceError('FOCUS_SCENARIO_UNKNOWN_FOCUS', 'Scenario names an unknown focus', {
25
+ treeId: plan.id,
26
+ focusId: id,
27
+ });
28
+ const focuses = [...plan.focuses]
29
+ .sort((left, right) => compareCodeUnits(left.id, right.id))
30
+ .map((focus) => {
31
+ const missingPrerequisiteGroups = focus.prerequisites.groups
32
+ .filter(({ focusIds }) => !focusIds.some((id) => completed.has(id)))
33
+ .map(({ focusIds }) => [...focusIds].sort(compareCodeUnits));
34
+ const blockedRouteLockIds = focus.routeLocks
35
+ .filter((lock) => !routeLockSatisfied(lock, completed))
36
+ .map(({ id }) => id)
37
+ .sort(compareCodeUnits);
38
+ const exclusiveWithCompletedIds = [
39
+ ...new Set([...focus.mutuallyExclusive, ...(excludedBy.get(focus.id) ?? [])]),
40
+ ]
41
+ .filter((id) => completed.has(id))
42
+ .sort(compareCodeUnits);
43
+ const unresolvedFields = [
44
+ ...(focus.availability === undefined ? [] : ['available']),
45
+ ...(focus.allowBranch === undefined ? [] : ['allow_branch']),
46
+ ...(focus.bypass === undefined ? [] : ['bypass']),
47
+ ...(focus.visibility === 'normal' ||
48
+ (focus.reveal?.kind === 'allow_branch' &&
49
+ focus.routeLocks.some(({ field }) => field === 'allow_branch'))
50
+ ? []
51
+ : ['visibility']),
52
+ ];
53
+ const blocked = missingPrerequisiteGroups.length > 0 ||
54
+ blockedRouteLockIds.length > 0 ||
55
+ exclusiveWithCompletedIds.length > 0;
56
+ const status = completed.has(focus.id)
57
+ ? 'completed'
58
+ : blocked
59
+ ? 'blocked'
60
+ : unresolvedFields.length > 0
61
+ ? 'unresolved'
62
+ : 'structural_candidate';
63
+ return {
64
+ focusId: focus.id,
65
+ status,
66
+ missingPrerequisiteGroups,
67
+ blockedRouteLockIds,
68
+ exclusiveWithCompletedIds,
69
+ unresolvedFields,
70
+ };
71
+ });
72
+ const conflicts = new Set();
73
+ for (const focus of plan.focuses) {
74
+ if (!completed.has(focus.id))
75
+ continue;
76
+ for (const other of focus.mutuallyExclusive) {
77
+ if (!completed.has(other))
78
+ continue;
79
+ conflicts.add([focus.id, other].sort(compareCodeUnits).join('\0'));
80
+ }
81
+ }
82
+ const mutualExclusionConflicts = [...conflicts].sort(compareCodeUnits).map((pair) => {
83
+ const [leftFocusId, rightFocusId] = pair.split('\0');
84
+ return { leftFocusId: leftFocusId, rightFocusId: rightFocusId };
85
+ });
86
+ return {
87
+ treeId: plan.id,
88
+ completedFocusIds: [...completed].sort(compareCodeUnits),
89
+ focuses,
90
+ mutualExclusionConflicts,
91
+ counts: {
92
+ completed: focuses.filter(({ status }) => status === 'completed').length,
93
+ blocked: focuses.filter(({ status }) => status === 'blocked').length,
94
+ unresolved: focuses.filter(({ status }) => status === 'unresolved').length,
95
+ structuralCandidates: focuses.filter(({ status }) => status === 'structural_candidate')
96
+ .length,
97
+ },
98
+ };
99
+ }
@@ -11,6 +11,7 @@ import { setInlineFilesScanned } from '../core/operation-result.js';
11
11
  import { PACKAGE_VERSION } from '../version.js';
12
12
  import { focusPlanHash, focusPresentationEvidence, importContinuousFocusPalettes, assertCompactLayoutQuality, compactFocusTreePlanAsync, linkContinuousFocusPalettes, resolveFocusPresentation, } from './index.js';
13
13
  import { activeContinuousFocusFiles, activeFocusFiles, activeSidecar, continuousPalettes, importFocusFile, referenceCatalog, selectContinuousPlan, selectedContinuousFocusFile, selectedFocusFile, selectPlan, underConfiguredRoot, validationFromDiagnostics, } from './tool-support.js';
14
+ import { inspectFocusScenario } from './scenario.js';
14
15
  const LARGE_FOCUS_RENDER_NODE_THRESHOLD = 200;
15
16
  const LARGE_FOCUS_RENDER_SCALE = 0.5;
16
17
  function automaticFocusRenderScale(focusCount) {
@@ -196,6 +197,9 @@ export async function prepareFocusRewrite(engine, workbench, input, context) {
196
197
  compactSourcePlan = suppliedPlan;
197
198
  compactPlanning = await compactFocusTreePlanAsync(suppliedPlan, {
198
199
  ...(context.signal === undefined ? {} : { signal: context.signal }),
200
+ ...(input.compactFocusIds === undefined ? {} : { focusIds: input.compactFocusIds }),
201
+ ...(input.pinnedFocusIds === undefined ? {} : { pinnedFocusIds: input.pinnedFocusIds }),
202
+ ...(input.symmetryGroups === undefined ? {} : { symmetryGroups: input.symmetryGroups }),
199
203
  });
200
204
  plan = compactPlanning.plan;
201
205
  }
@@ -208,6 +212,9 @@ export async function prepareFocusRewrite(engine, workbench, input, context) {
208
212
  compactSourcePlan = selectPlan(imported.result, input.treeId);
209
213
  compactPlanning = await compactFocusTreePlanAsync(compactSourcePlan, {
210
214
  ...(context.signal === undefined ? {} : { signal: context.signal }),
215
+ ...(input.compactFocusIds === undefined ? {} : { focusIds: input.compactFocusIds }),
216
+ ...(input.pinnedFocusIds === undefined ? {} : { pinnedFocusIds: input.pinnedFocusIds }),
217
+ ...(input.symmetryGroups === undefined ? {} : { symmetryGroups: input.symmetryGroups }),
211
218
  });
212
219
  plan = compactPlanning.plan;
213
220
  }
@@ -267,7 +274,7 @@ export async function prepareFocusRewrite(engine, workbench, input, context) {
267
274
  ...(context.signal === undefined ? {} : { signal: context.signal }),
268
275
  }));
269
276
  if (input.layoutMode === 'compact')
270
- assertCompactLayoutQuality(currentLayout, proposedLayout);
277
+ assertCompactLayoutQuality(currentLayout, proposedLayout, input.compactFocusIds !== undefined);
271
278
  const proposed = await workbench.renderAndStore(context.workspaceId, reviewPlan, {
272
279
  ...(context.principal === undefined ? {} : { principal: context.principal }),
273
280
  index: snapshot.index,
@@ -481,7 +488,14 @@ export async function inspectFocus(engine, workbench, input, context) {
481
488
  layout,
482
489
  });
483
490
  diagnostics.push(...lintDiagnostics);
484
- inspectedPlans.push({ plan, layout, diagnosticCount: lintDiagnostics.length });
491
+ inspectedPlans.push({
492
+ plan,
493
+ layout,
494
+ diagnosticCount: lintDiagnostics.length,
495
+ ...(input.scenario === undefined
496
+ ? {}
497
+ : { scenario: inspectFocusScenario(plan, input.scenario.completedFocusIds) }),
498
+ });
485
499
  }
486
500
  const presentation = await resolveFocusPresentation({
487
501
  plans,
@@ -504,10 +518,11 @@ export async function inspectFocus(engine, workbench, input, context) {
504
518
  schemaVersion: 1,
505
519
  revision: snapshot.revision,
506
520
  plans,
507
- layouts: inspectedPlans.map(({ plan, layout, diagnosticCount }) => ({
521
+ layouts: inspectedPlans.map(({ plan, layout, diagnosticCount, scenario }) => ({
508
522
  treeId: plan.id,
509
523
  layout,
510
524
  diagnosticCount,
525
+ ...(scenario === undefined ? {} : { scenario }),
511
526
  })),
512
527
  continuousFocusPalettes: paletteImport.palettes,
513
528
  presentation: focusPresentationEvidence(presentation),
@@ -525,7 +540,7 @@ export async function inspectFocus(engine, workbench, input, context) {
525
540
  revision: snapshot.revision,
526
541
  treeCount: plans.length,
527
542
  paletteCount: paletteImport.palettes.length,
528
- trees: inspectedPlans.slice(0, 100).map(({ plan, layout, diagnosticCount }) => ({
543
+ trees: inspectedPlans.slice(0, 100).map(({ plan, layout, diagnosticCount, scenario }) => ({
529
544
  id: plan.id,
530
545
  sourcePath: plan.provenance.sourcePath,
531
546
  focusCount: plan.focuses.length,
@@ -539,6 +554,14 @@ export async function inspectFocus(engine, workbench, input, context) {
539
554
  layoutDecisionCount: layout.decisions.length,
540
555
  layoutMetrics: layout.metrics,
541
556
  diagnosticCount,
557
+ ...(scenario === undefined
558
+ ? {}
559
+ : {
560
+ scenarioSummary: {
561
+ ...scenario.counts,
562
+ choiceConflictCount: scenario.mutualExclusionConflicts.length,
563
+ },
564
+ }),
542
565
  })),
543
566
  palettes: [],
544
567
  });
@@ -559,6 +582,8 @@ export async function inspectFocus(engine, workbench, input, context) {
559
582
  }
560
583
  export async function renderFocus(engine, workbench, input, context, rasterize) {
561
584
  const outputName = rasterize ? 'raster' : 'render';
585
+ if (!rasterize && (input.cropFocusIds?.length ?? 0) > 0)
586
+ throw new ServiceError('FOCUS_CROP_REQUIRES_RASTER', 'Focus problem crops require focus_raster');
562
587
  await context.progress(0, 4, 'Importing and indexing focus source');
563
588
  const workspace = engine.resolver.get(context.workspaceId, context.principal);
564
589
  const snapshot = await engine.scan(context.workspaceId, { patterns: focusDomainScanPatterns(workspace) }, context.principal, context.signal);
@@ -638,6 +663,7 @@ export async function renderFocus(engine, workbench, input, context, rasterize)
638
663
  ...(input.padding === undefined ? {} : { padding: input.padding }),
639
664
  outputScale: effectiveReviewScale,
640
665
  rasterize,
666
+ ...(input.cropFocusIds === undefined ? {} : { cropFocusIds: input.cropFocusIds }),
641
667
  renderProfile: {
642
668
  sourceRevision: snapshot.revision,
643
669
  reviewScale: effectiveReviewScale,
@@ -8,6 +8,7 @@ import { DETERMINISTIC_TOOL_FONT_HASH, shapeFontkitOutline } from '../core/svg-t
8
8
  import { decodeDds } from './dds.js';
9
9
  import { GUI_BMFONT_MAX_CHARACTERS, GUI_BINARY_FONT_MAX_BYTES, GUI_BMFONT_MAX_BYTES, GUI_BMFONT_MAX_FIELDS_PER_RECORD, GUI_BMFONT_MAX_KERNING_PAIRS, GUI_BMFONT_MAX_PAGES, GUI_BMFONT_MAX_RECORDS, GUI_TEXT_MAX_CHARACTERS, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES, } from './limits.js';
10
10
  import { decodeTga } from './tga.js';
11
+ import { compileGuiProgressEffect, compileGuiShaderEffect, GUI_SHADER_MAX_BYTES, } from './shader.js';
11
12
  function normalizeAssetPath(value) {
12
13
  return value
13
14
  .replaceAll('\\', '/')
@@ -157,6 +158,8 @@ export class GuiAssetCatalog {
157
158
  rasters = new Map();
158
159
  frames = new Map();
159
160
  countryFlags = new Map();
161
+ shaders = new Map();
162
+ progressShaders = new Map();
160
163
  glyphRasters = new Map();
161
164
  metrics = new Map();
162
165
  fontDefinitions = new Map();
@@ -294,11 +297,58 @@ export class GuiAssetCatalog {
294
297
  loadSecondarySpriteFrame(sprite, requestedFrame) {
295
298
  return this.loadSpriteTextureFrame(sprite, sprite.texturePath2, requestedFrame, 'secondary');
296
299
  }
300
+ shaderSource(sprite) {
301
+ if ((sprite.shaderFeatures?.length ?? 0) > 0)
302
+ return {
303
+ supported: false,
304
+ reason: `Shader features require additional composition: ${sprite.shaderFeatures.join(', ')}.`,
305
+ };
306
+ const reference = sprite.effectFile;
307
+ if (reference === undefined)
308
+ return { supported: false, reason: 'Sprite has no effect source.' };
309
+ const file = this.resolveFile(reference, sprite.sourcePath) ??
310
+ (/\.lua$/iu.test(reference)
311
+ ? this.resolveFile(reference.replace(/\.lua$/iu, '.shader'), sprite.sourcePath)
312
+ : undefined);
313
+ if (file === undefined)
314
+ return { supported: false, reason: `Shader source not found: ${reference}.` };
315
+ if (file.bytes.length > GUI_SHADER_MAX_BYTES)
316
+ return { supported: false, reason: 'Shader source exceeds its byte budget.' };
317
+ return { supported: true, file };
318
+ }
319
+ progressShader(sprite, effect) {
320
+ const source = this.shaderSource(sprite);
321
+ if (!source.supported)
322
+ return source;
323
+ const { file } = source;
324
+ const key = `${file.displayPath}:${file.sha256}:${effect}`;
325
+ let result = this.progressShaders.get(key);
326
+ if (result === undefined) {
327
+ result = compileGuiProgressEffect(file.bytes.toString('utf8'), { path: file.displayPath, sha256: file.sha256 }, effect);
328
+ if (this.progressShaders.size >= 1_024)
329
+ this.progressShaders.delete(this.progressShaders.keys().next().value);
330
+ this.progressShaders.set(key, result);
331
+ }
332
+ return result;
333
+ }
334
+ spriteShader(sprite, effect, uniforms) {
335
+ const source = this.shaderSource(sprite);
336
+ if (!source.supported)
337
+ return source;
338
+ const { file } = source;
339
+ const key = `${file.displayPath}:${file.sha256}:${effect}:${hashCanonical(uniforms)}`;
340
+ let result = this.shaders.get(key);
341
+ if (result === undefined) {
342
+ result = compileGuiShaderEffect(file.bytes.toString('utf8'), { path: file.displayPath, sha256: file.sha256 }, effect, uniforms);
343
+ // Per-render caching is bounded even with a different clock or tint for every row.
344
+ if (this.shaders.size >= 1_024)
345
+ this.shaders.delete(this.shaders.keys().next().value);
346
+ this.shaders.set(key, result);
347
+ }
348
+ return result;
349
+ }
297
350
  loadCountryFlag(tag, sprite, ideology, slotName) {
298
- const normalizedTag = tag
299
- .trim()
300
- .replace(/[^A-Za-z0-9_]/gu, '')
301
- .toUpperCase();
351
+ const normalizedTag = tag.trim().toUpperCase();
302
352
  const normalizedIdeology = ideology
303
353
  ?.trim()
304
354
  .replace(/[^A-Za-z0-9_]/gu, '')