hoi4-agent-tools 4.3.0 → 4.5.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 (119) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/dist/hoi4_agent_tools/core/bounded-source.js +31 -9
  3. package/dist/hoi4_agent_tools/core/configuration.js +11 -0
  4. package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
  5. package/dist/hoi4_agent_tools/core/operation-context.js +17 -1
  6. package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
  7. package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
  8. package/dist/hoi4_agent_tools/core/temporary-directories.js +7 -2
  9. package/dist/hoi4_agent_tools/core/transaction-limits.js +18 -4
  10. package/dist/hoi4_agent_tools/core/transactions.js +23 -4
  11. package/dist/hoi4_agent_tools/core/workspace-json.js +54 -0
  12. package/dist/hoi4_agent_tools/core/workspace.js +42 -6
  13. package/dist/hoi4_agent_tools/focus/drift.js +34 -0
  14. package/dist/hoi4_agent_tools/focus/service.js +9 -3
  15. package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
  16. package/dist/hoi4_agent_tools/map/appearance-validation.js +1110 -0
  17. package/dist/hoi4_agent_tools/map/bmp.js +9 -1
  18. package/dist/hoi4_agent_tools/map/border-quality-input.js +209 -0
  19. package/dist/hoi4_agent_tools/map/border-quality.js +1180 -0
  20. package/dist/hoi4_agent_tools/map/colormap.js +128 -0
  21. package/dist/hoi4_agent_tools/map/geography/geometry.js +284 -0
  22. package/dist/hoi4_agent_tools/map/geography/index.js +6 -0
  23. package/dist/hoi4_agent_tools/map/geography/model.js +196 -0
  24. package/dist/hoi4_agent_tools/map/geography/preparation.js +120 -0
  25. package/dist/hoi4_agent_tools/map/geography/preview.js +647 -0
  26. package/dist/hoi4_agent_tools/map/geography/raster.js +253 -0
  27. package/dist/hoi4_agent_tools/map/geography/resolve.js +1467 -0
  28. package/dist/hoi4_agent_tools/map/model.js +17 -3
  29. package/dist/hoi4_agent_tools/map/operations.js +284 -14
  30. package/dist/hoi4_agent_tools/map/partition-fields.js +398 -0
  31. package/dist/hoi4_agent_tools/map/partition-geography.js +113 -0
  32. package/dist/hoi4_agent_tools/map/partition-graph.js +165 -0
  33. package/dist/hoi4_agent_tools/map/partition-groups.js +164 -0
  34. package/dist/hoi4_agent_tools/map/partition-provinces.js +727 -0
  35. package/dist/hoi4_agent_tools/map/partition-tuning.js +101 -0
  36. package/dist/hoi4_agent_tools/map/partition-watershed.js +252 -0
  37. package/dist/hoi4_agent_tools/map/partition.js +163 -0
  38. package/dist/hoi4_agent_tools/map/reference-import.js +333 -0
  39. package/dist/hoi4_agent_tools/map/reference-preparation.js +142 -6
  40. package/dist/hoi4_agent_tools/map/reference-sheets.js +231 -0
  41. package/dist/hoi4_agent_tools/map/reference-world.js +104 -13
  42. package/dist/hoi4_agent_tools/map/relief/checks.js +281 -0
  43. package/dist/hoi4_agent_tools/map/relief/fields.js +15 -0
  44. package/dist/hoi4_agent_tools/map/relief/full-resolution.js +133 -0
  45. package/dist/hoi4_agent_tools/map/relief/grid.js +190 -0
  46. package/dist/hoi4_agent_tools/map/relief/hydrology.js +1315 -0
  47. package/dist/hoi4_agent_tools/map/relief/index.js +27 -0
  48. package/dist/hoi4_agent_tools/map/relief/land.js +110 -0
  49. package/dist/hoi4_agent_tools/map/relief/macro-relief.js +535 -0
  50. package/dist/hoi4_agent_tools/map/relief/math.js +93 -0
  51. package/dist/hoi4_agent_tools/map/relief/river-raster.js +680 -0
  52. package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +304 -0
  53. package/dist/hoi4_agent_tools/map/render.js +36 -0
  54. package/dist/hoi4_agent_tools/map/service.js +206 -16
  55. package/dist/hoi4_agent_tools/map/tool-operations.js +249 -3
  56. package/dist/hoi4_agent_tools/map/validation.js +9 -0
  57. package/dist/hoi4_agent_tools/map/water.js +227 -0
  58. package/dist/hoi4_agent_tools/map/world-context.js +47 -1
  59. package/dist/hoi4_agent_tools/map/world-edge.js +335 -0
  60. package/dist/hoi4_agent_tools/map/world-noise.js +34 -0
  61. package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
  62. package/dist/hoi4_agent_tools/map/world-partition.js +119 -0
  63. package/dist/hoi4_agent_tools/map/world-provinces.js +131 -56
  64. package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
  65. package/dist/hoi4_agent_tools/map/world.js +562 -163
  66. package/dist/hoi4_agent_tools/mcp/tools/map.js +69 -1
  67. package/dist/hoi4_agent_tools/reference/operations.js +3 -2
  68. package/dist/hoi4_agent_tools/schemas/geography.js +325 -0
  69. package/dist/hoi4_agent_tools/schemas/map-requests.js +20 -2
  70. package/dist/hoi4_agent_tools/schemas/map.js +280 -15
  71. package/dist/hoi4_agent_tools/schemas/reference-sheets.js +92 -0
  72. package/dist/hoi4_agent_tools/version.js +1 -1
  73. package/docs/configuration.md +17 -14
  74. package/docs/focus.md +6 -0
  75. package/docs/map.md +292 -15
  76. package/docs/tools.md +4 -4
  77. package/docs/troubleshooting.md +11 -11
  78. package/npm-shrinkwrap.json +109 -91
  79. package/package.json +3 -3
  80. package/schemas/configuration.schema.json +65 -58
  81. package/schemas/continuous-focus-palette.schema.json +1 -1
  82. package/schemas/custom-weighted-pool.schema.json +1 -1
  83. package/schemas/decision-inspect-input.schema.json +1 -1
  84. package/schemas/error-log-input.schema.json +1 -1
  85. package/schemas/event-feature-manifest.schema.json +1 -1
  86. package/schemas/focus-plan.schema.json +1 -1
  87. package/schemas/focus-planning-sidecar.schema.json +1 -1
  88. package/schemas/geography-model.schema.json +1148 -0
  89. package/schemas/gui-animation-source.schema.json +1 -1
  90. package/schemas/gui-helper.schema.json +5 -11
  91. package/schemas/gui-scenario.schema.json +1 -1
  92. package/schemas/helper-expansion-request.schema.json +1 -1
  93. package/schemas/helper-expansion-summary.schema.json +1 -1
  94. package/schemas/impact-inspect-input.schema.json +1 -1
  95. package/schemas/map-operation.schema.json +2325 -292
  96. package/schemas/mechanic-test-input.schema.json +1 -1
  97. package/schemas/mod-index-input.schema.json +1 -1
  98. package/schemas/operation-result.schema.json +1 -1
  99. package/schemas/package-check-input.schema.json +1 -1
  100. package/schemas/package-manifest.schema.json +1 -1
  101. package/schemas/probability-analysis-result.schema.json +25 -7
  102. package/schemas/probability-compare-input.schema.json +1 -1
  103. package/schemas/probability-evaluate-input.schema.json +1 -1
  104. package/schemas/probability-inspect-input.schema.json +1 -1
  105. package/schemas/probability-render-input.schema.json +1 -1
  106. package/schemas/probability-scenario-set.schema.json +1 -1
  107. package/schemas/probability-sequence-input.schema.json +1 -1
  108. package/schemas/probability-simulate-input.schema.json +1 -1
  109. package/schemas/probability-sweep-input.schema.json +1 -1
  110. package/schemas/reference-context-input.schema.json +1 -1
  111. package/schemas/reference-read-input.schema.json +1 -1
  112. package/schemas/reference-search-input.schema.json +1 -1
  113. package/schemas/reference-sheets.schema.json +267 -0
  114. package/schemas/scenario-suite.schema.json +1 -1
  115. package/schemas/scenario-test-input.schema.json +1 -1
  116. package/schemas/script-validate-input.schema.json +1 -1
  117. package/schemas/shared-scenario.schema.json +1 -1
  118. package/schemas/source-lookup-input.schema.json +1 -1
  119. package/server.json +2 -2
@@ -2,6 +2,8 @@ import { z } from 'zod/v4';
2
2
  import { renderDimensionViolation, RENDER_MAX_DIMENSION } from '../core/render-budget.js';
3
3
  import { MAP_MASK_CELL_LIMIT, MAP_SELECTED_PIXEL_LIMIT } from '../map/limits.js';
4
4
  import { workspaceRelativePathSchema } from './common.js';
5
+ import { geographyModelSchema, geographyPathSchema } from './geography.js';
6
+ import { referenceSheetsDocumentSchema, referenceSheetsPathSchema } from './reference-sheets.js';
5
7
  const identifier = z.string().min(1).max(256);
6
8
  // eslint-disable-next-line no-control-regex -- portable JSON Schema pattern rejects grammar-breaking controls.
7
9
  const worldNamePattern = /^[^\u0000-\u001f\u007f]+$/u;
@@ -22,6 +24,59 @@ const rgbSchema = z
22
24
  })
23
25
  .strict();
24
26
  const pointSchema = z.object({ x: z.number().int().min(0), y: z.number().int().min(0) }).strict();
27
+ const countryTagSchema = z.string().regex(/^[A-Z][A-Z0-9]{2}$/u);
28
+ const realmPolygonSchema = z.array(pointSchema).min(3).max(10_000);
29
+ const realmPolygonsSchema = z.array(realmPolygonSchema).min(1).max(64);
30
+ export const workspaceRealmSchema = z
31
+ .object({
32
+ tag: countryTagSchema,
33
+ capital: pointSchema.describe('World pixel, top-left origin, on land; the capital province.'),
34
+ seeds: z
35
+ .array(pointSchema)
36
+ .max(256)
37
+ .optional()
38
+ .describe('Further world pixels whose provinces the country must own.'),
39
+ claim: realmPolygonsSchema
40
+ .optional()
41
+ .describe('Polygons in world pixels; land provinces more than half inside belong to the country.'),
42
+ exclude: realmPolygonsSchema
43
+ .optional()
44
+ .describe('Polygons in world pixels; land provinces more than half inside never belong to it.'),
45
+ weight: z
46
+ .number()
47
+ .min(0.05)
48
+ .max(100)
49
+ .optional()
50
+ .describe('Relative share of the land claims leave free; default 1.'),
51
+ })
52
+ .strict();
53
+ export const workspaceCountriesSchema = z
54
+ .object({
55
+ source: z.literal('workspace'),
56
+ realms: z.array(workspaceRealmSchema).min(1).max(512),
57
+ unowned: z
58
+ .array(realmPolygonSchema)
59
+ .max(256)
60
+ .optional()
61
+ .describe('Polygons in world pixels whose land provinces no country owns.'),
62
+ islands: z
63
+ .enum(['nearest', 'one-owner', 'unowned'])
64
+ .default('nearest')
65
+ .describe('Landmasses no country reaches: every province to the nearest capital, the whole landmass to one country, or unowned.'),
66
+ })
67
+ .strict()
68
+ .superRefine((value, context) => {
69
+ const seen = new Set();
70
+ for (const [index, realm] of value.realms.entries()) {
71
+ if (seen.has(realm.tag))
72
+ context.addIssue({
73
+ code: 'custom',
74
+ path: ['realms', index, 'tag'],
75
+ message: `Country ${realm.tag} is listed twice`,
76
+ });
77
+ seen.add(realm.tag);
78
+ }
79
+ });
25
80
  const operationBase = {
26
81
  id: identifier,
27
82
  summary: z.string().max(1000).optional(),
@@ -75,6 +130,28 @@ export const worldReferenceSchema = z
75
130
  .max(64)
76
131
  .optional(),
77
132
  coastDetail: z.number().min(0).max(1).optional(),
133
+ paletteMatch: z
134
+ .enum(['nearest', 'exact'])
135
+ .optional()
136
+ .describe('nearest (default) snaps every pixel to the closest palette colour. exact requires every pixel to equal a palette colour, trusts the classes as painted (no text masking, cleanup, smoothing or political inference) and fails with MAP_REFERENCE_PALETTE_UNKNOWN_COLOR naming any other colour, its count and source pixels.'),
137
+ sheets: referenceSheetsDocumentSchema
138
+ .optional()
139
+ .describe('Registration of a picture made of drawn sheets onto the world frame (inline; use sheetsPath for a file). Each sheet maps its part of the picture with anchor, target and scale (or affine); the registered picture is read at the resolution the sheets need and every layer derives from it. Implies fit frame; width and height are required. Annotations, masks, labels and cities then use world coordinates.'),
140
+ agreement: z
141
+ .object({
142
+ minLandIou: z.number().min(0).max(1).optional(),
143
+ warnLandIou: z.number().min(0).max(1).optional(),
144
+ })
145
+ .strict()
146
+ .optional()
147
+ .describe('With a geography model: land intersection over union between the model and this reference below minLandIou (default 0.85) blocks the write with MAP_IMPORT_SOURCES_DISAGREE; below warnLandIou (default 0.95) it is a warning. The measured value is always in map-import-report.json.'),
148
+ sheetsPath: referenceSheetsPathSchema
149
+ .optional()
150
+ .describe('Workspace-relative .json file holding the sheet registration (reference-sheets version 1).'),
151
+ fit: z
152
+ .enum(['contain', 'frame'])
153
+ .optional()
154
+ .describe('contain (default) centres the picture with sea padding at a working size of at most 3,072 pixels per side. frame maps the picture (or its crop) onto exactly width x height, which must be given and have the same shape within 1%, and reads it at the frame resolution.'),
78
155
  labels: z
79
156
  .array(z
80
157
  .object({
@@ -102,7 +179,156 @@ export const worldReferenceSchema = z
102
179
  .optional(),
103
180
  })
104
181
  .strict()
182
+ .refine((value) => value.paletteMatch !== 'exact' || (value.palette?.length ?? 0) > 0, {
183
+ path: ['palette'],
184
+ message: 'paletteMatch "exact" needs a palette that lists every colour of the picture',
185
+ })
186
+ .refine((value) => value.sheets === undefined || value.sheetsPath === undefined, {
187
+ path: ['sheetsPath'],
188
+ message: 'Give the sheets inline (sheets) or by file (sheetsPath), not both',
189
+ })
190
+ .refine((value) => (value.sheets === undefined && value.sheetsPath === undefined) || value.fit !== 'contain', { path: ['fit'], message: 'Registered sheets always fill the frame; omit fit or use "frame"' })
105
191
  .describe('Generate from one local reference image. Optional controls use normalized 0..1 source coordinates; crop uses source pixels. Labels and political semantics may need refinement.');
192
+ /** World edge and seam policy (ADR 0061). Omit `edge` to keep land and sea as drawn. */
193
+ export const worldEdgeSchema = z
194
+ .object({
195
+ mode: z
196
+ .enum(['sea-band', 'rim', 'none'])
197
+ .optional()
198
+ .describe('sea-band (default when edge is given) clears land from the east and west edges and the poles to open sea; rim ends land at the seam in an impassable snow range with impassable adjacencies across the seam; none keeps the picture and warns about land cut by the wrapping seam.'),
199
+ bandPx: z
200
+ .number()
201
+ .int()
202
+ .min(8)
203
+ .max(2_048)
204
+ .optional()
205
+ .describe('East and west sea band in pixels; default max(192, 4% of the width), at most a quarter of the width.'),
206
+ polarPx: z
207
+ .number()
208
+ .int()
209
+ .min(0)
210
+ .max(1_024)
211
+ .optional()
212
+ .describe('North and south sea band in pixels; default 64, at most a quarter of the height.'),
213
+ rimPx: z
214
+ .number()
215
+ .int()
216
+ .min(16)
217
+ .max(2_048)
218
+ .optional()
219
+ .describe('Rim range width in pixels for mode rim; default 160, at most a quarter of the width.'),
220
+ })
221
+ .strict();
222
+ /** Relief synthesis from a geography model (ADR 0056). */
223
+ export const worldReliefSchema = z
224
+ .object({
225
+ scale: z
226
+ .enum(['vanilla', 'low', 'dramatic'])
227
+ .optional()
228
+ .describe('Height of everything above the lowland: vanilla (the default) follows the base game, low is 25% lower, dramatic 15% higher.'),
229
+ detail: z
230
+ .number()
231
+ .min(0)
232
+ .max(1)
233
+ .optional()
234
+ .describe('Full-resolution roughness of the terrain, 0 to 1; default 0.5.'),
235
+ })
236
+ .strict();
237
+ /** River network target for a geography world: a share of the land pixels. */
238
+ export const worldRiverDensitySchema = z
239
+ .object({
240
+ density: z
241
+ .number()
242
+ .min(0)
243
+ .max(0.04)
244
+ .describe('Share of land pixels the unnamed river network covers, 0 to 0.04; the named rivers come on top. Default 0.02 (the base game has 0.023).'),
245
+ })
246
+ .strict();
247
+ export const worldLookSchema = z
248
+ .enum(['vanilla-temperate', 'fertile', 'arid', 'custom'])
249
+ .describe('Colour palette set for the calibrated land colour map: the base game, a greener fertile palette, a drier arid palette, or custom with colormap.palette. Setting it selects the calibrated colour map.');
250
+ const colourClassNames = [
251
+ 'plains',
252
+ 'farmland',
253
+ 'forest',
254
+ 'hills',
255
+ 'desert',
256
+ 'mountain',
257
+ 'marsh',
258
+ 'jungle',
259
+ 'snow',
260
+ 'urban',
261
+ 'lakes',
262
+ 'ocean',
263
+ ];
264
+ export const worldColormapSchema = z
265
+ .object({
266
+ profile: z
267
+ .enum(['calibrated', 'legacy'])
268
+ .optional()
269
+ .describe('calibrated writes a dark, varied colour map calibrated to the base game; legacy keeps the original flat tints. Defaults to calibrated when look is set, otherwise legacy.'),
270
+ palette: z
271
+ .object(Object.fromEntries(colourClassNames.map((name) => [name, rgbSchema.optional()])))
272
+ .strict()
273
+ .optional()
274
+ .describe('Class colours for look custom: the intended colour map value of each class.'),
275
+ })
276
+ .strict();
277
+ /** Feature-aware provinces, states and strategic regions (ADR 0058). */
278
+ export const worldPartitionSchema = z
279
+ .enum(['legacy', 'feature-aware'])
280
+ .describe('legacy (default) keeps the warped Voronoi provinces and breadth-first clusters, so a seed gives the same world as before; feature-aware cuts provinces along rivers, ridges, steep slopes and shores, sizes them by terrain, fertility and coast, and clusters states and regions along natural lines. A world given a geography defaults to feature-aware.');
281
+ const partitionAreaPixels = z.number().int().min(20).max(50_000);
282
+ export const worldPartitionTuningSchema = z
283
+ .object({
284
+ landArea: z
285
+ .object({
286
+ fertile: partitionAreaPixels.optional(),
287
+ hills: partitionAreaPixels.optional(),
288
+ mountain: partitionAreaPixels.optional(),
289
+ waste: partitionAreaPixels.optional(),
290
+ })
291
+ .strict()
292
+ .optional()
293
+ .describe('Typical land province area in pixels by ground. Defaults follow landProvincePixels (0.35, 1, 2.2 and 3.4 times, scaled so the mean stays landProvincePixels). fertile must not exceed hills, nor hills mountain.'),
294
+ seaArea: z
295
+ .object({
296
+ coastal: z.number().int().min(20).max(200_000).optional(),
297
+ open: z.number().int().min(20).max(200_000).optional(),
298
+ })
299
+ .strict()
300
+ .optional()
301
+ .describe('Sea province area in pixels near a coast and on the open sea. Defaults follow seaProvincePixels (0.5 and 2.4 times, scaled so the mean stays seaProvincePixels).'),
302
+ provincesPerState: z
303
+ .tuple([z.number().int().min(1).max(64), z.number().int().min(1).max(64)])
304
+ .optional()
305
+ .describe('[smallest, largest] land provinces per state; the preferred size is the geometric mean.'),
306
+ regionSize: z
307
+ .tuple([z.number().int().min(1).max(400), z.number().int().min(1).max(400)])
308
+ .optional()
309
+ .describe('[smallest, largest] land provinces per strategic region.'),
310
+ barrierStrength: z
311
+ .number()
312
+ .min(0)
313
+ .max(60)
314
+ .optional()
315
+ .describe('How strongly borders are pulled onto natural lines, 0 to 60; default 20.'),
316
+ minProvincePixels: z
317
+ .number()
318
+ .int()
319
+ .min(20)
320
+ .max(2_000)
321
+ .optional()
322
+ .describe('No province is planned smaller than this; default 40. The engine floor is 8.'),
323
+ narrowStraitPx: z
324
+ .number()
325
+ .int()
326
+ .min(4)
327
+ .max(64)
328
+ .optional()
329
+ .describe('A sea channel at most this wide gets two sea provinces across it; default 24.'),
330
+ })
331
+ .strict();
106
332
  const moveStateDistributionSchema = z
107
333
  .object({
108
334
  stateValues: z.enum(['retain-in-current-states', 'proportional-by-land-pixels']),
@@ -565,6 +791,12 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
565
791
  .describe('Include basic starting units and stockpiles and replace old-world startup/AI geography. Defaults on for reference-image worlds.'),
566
792
  id: identifier.default('world'),
567
793
  reference: worldReferenceSchema.optional(),
794
+ geography: geographyModelSchema
795
+ .optional()
796
+ .describe('Geography model version 1, inline: seas, lands, ranges, passes, volcanoes, plateaus, rivers, lakes, biomes, straits, sites and climate in normalized 0..1 world coordinates. Use geographyPath for models over a few hundred vertices. With preview:true it returns a model overlay and map-geography-report.json. Without preview the world is built from it: relief (ranges with passes, volcanoes, plateaus), rivers and lakes are synthesised; biomes, climate and fertility are not yet, and a model that declares them is blocked with MAP_GEOGRAPHY_SYNTHESIS_UNAVAILABLE.'),
797
+ geographyPath: geographyPathSchema
798
+ .optional()
799
+ .describe('Workspace-relative .json file holding a geography model (at most 4 MiB). Alternative to the inline geography field; give one of them.'),
568
800
  width: z.number().int().min(256).max(8_192).multipleOf(256).optional(),
569
801
  height: z.number().int().min(256).max(4_096).multipleOf(256).optional(),
570
802
  seed: z.number().int().min(0).max(2_147_483_647).default(1),
@@ -581,31 +813,64 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
581
813
  landProvincesPerRegion: z.number().int().min(2).max(400).optional(),
582
814
  seaProvincesPerRegion: z.number().int().min(1).max(200).optional(),
583
815
  countries: z
584
- .array(z
585
- .object({
586
- tag: z.string().regex(/^[A-Z][A-Z0-9]{2}$/u),
587
- name: worldName,
588
- adjective: worldName.optional(),
589
- color: rgbSchema.optional(),
590
- share: z.number().min(0.05).max(100).optional(),
591
- ideology: z.enum(['democratic', 'communism', 'fascism', 'neutrality']).optional(),
592
- })
593
- .strict())
594
- .min(1)
595
- .max(200)
596
- .optional(),
816
+ .union([
817
+ z
818
+ .array(z
819
+ .object({
820
+ tag: countryTagSchema,
821
+ name: worldName,
822
+ adjective: worldName.optional(),
823
+ color: rgbSchema.optional(),
824
+ share: z.number().min(0.05).max(100).optional(),
825
+ ideology: z.enum(['democratic', 'communism', 'fascism', 'neutrality']).optional(),
826
+ })
827
+ .strict())
828
+ .min(1)
829
+ .max(200),
830
+ workspaceCountriesSchema,
831
+ ])
832
+ .optional()
833
+ .describe('Countries the world generates and writes (an array), or { source: "workspace", realms } for countries the workspace already declares: the world then places them from their capital, seeds, claims and exclusions and writes no country, flag, unit, country history, country name or bookmark file.'),
597
834
  continentNames: z.array(worldName).max(12).optional(),
598
835
  startDate: z
599
836
  .string()
600
837
  .regex(/^\d{3,4}\.\d{1,2}\.\d{1,2}$/u)
601
838
  .default('1936.1.1'),
602
- rivers: z.number().int().min(0).max(500).optional(),
839
+ rivers: z
840
+ .union([z.number().int().min(0).max(500), worldRiverDensitySchema])
841
+ .optional()
842
+ .describe('Without a geography: the number of generated rivers. With a geography: an integer limits the number of unnamed river systems, an object { density } sets the share of land the network covers; the rivers the model names are always routed.'),
843
+ relief: worldReliefSchema
844
+ .optional()
845
+ .describe('Relief of a world built from a geography model: height scale and detail. Needs geography.'),
846
+ edge: worldEdgeSchema.optional(),
847
+ ambientFrame: z
848
+ .boolean()
849
+ .optional()
850
+ .describe("Write the base map's north and south frame borders and logo frame into ambient_object.txt, placed for the world size. Off by default."),
851
+ look: worldLookSchema.optional(),
852
+ colormap: worldColormapSchema.optional(),
853
+ partition: worldPartitionSchema.optional(),
854
+ partitionTuning: worldPartitionTuningSchema
855
+ .optional()
856
+ .describe('Size bands and strengths of the feature-aware partition; refused with partition legacy.'),
603
857
  replaceBaseGame: z
604
858
  .boolean()
605
859
  .default(true)
606
860
  .describe('Add replace_path for the base game states, countries, units, strategic regions, supply areas and bookmarks to descriptor.mod.'),
607
861
  })
608
- .strict(),
862
+ .strict()
863
+ .refine((operation) => operation.geography === undefined || operation.geographyPath === undefined, {
864
+ path: ['geographyPath'],
865
+ message: 'Give the geography inline (geography) or by file (geographyPath), not both',
866
+ })
867
+ .refine((operation) => (operation.reference?.fit !== 'frame' &&
868
+ operation.reference?.sheets === undefined &&
869
+ operation.reference?.sheetsPath === undefined) ||
870
+ (operation.width !== undefined && operation.height !== undefined), {
871
+ path: ['reference', 'fit'],
872
+ message: 'reference.fit "frame" and registered sheets need explicit width and height',
873
+ }),
609
874
  z
610
875
  .object({
611
876
  ...operationBase,
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod/v4';
2
+ import { workspaceRelativePathSchema } from './common.js';
3
+ /** Bounds of a sheet document (ADR 0067). */
4
+ export const REFERENCE_SHEETS_SOURCE_MAX_BYTES = 1_048_576;
5
+ export const REFERENCE_SHEETS_MAX = 64;
6
+ export const REFERENCE_SHEET_CLIP_MAX_VERTICES = 512;
7
+ const unit = z.number().min(0).max(1);
8
+ const finite = z.number().refine(Number.isFinite, 'Must be a finite number');
9
+ const pointSchema = z.tuple([unit, unit]);
10
+ /**
11
+ * One drawn sheet: the part of the picture it occupies (`box`, optionally cut by `clip`), and the
12
+ * map from picture to world, `world = target + scale * (picture - anchor)` per axis, or a general
13
+ * affine `[a, b, c, d, e, f]` with `x' = a x + b y + c`, `y' = d x + e y + f`. Picture coordinates
14
+ * are normalised to the picture, world coordinates to the output frame; a negative scale mirrors.
15
+ */
16
+ export const referenceSheetSchema = z
17
+ .object({
18
+ id: z
19
+ .string()
20
+ .regex(/^[A-Za-z][A-Za-z0-9_-]{0,63}$/u)
21
+ .describe('Unique sheet id, named in diagnostics and artifacts.'),
22
+ box: z
23
+ .tuple([unit, unit, unit, unit])
24
+ .refine(([left, top, right, bottom]) => right > left && bottom > top, {
25
+ message: 'box is [left, top, right, bottom] with right > left and bottom > top',
26
+ })
27
+ .describe('[left, top, right, bottom] of the sheet in normalised picture coordinates.'),
28
+ clip: z
29
+ .array(pointSchema)
30
+ .min(3)
31
+ .max(REFERENCE_SHEET_CLIP_MAX_VERTICES)
32
+ .optional()
33
+ .describe('Polygon in picture coordinates that cuts the sheet out of its box, so a neighbouring sheet inside the box is not pulled in.'),
34
+ anchor: z.tuple([unit, unit]).optional().describe('Picture point that maps to target.'),
35
+ target: z.tuple([finite, finite]).optional().describe('World point of the anchor.'),
36
+ scale: z
37
+ .tuple([finite, finite])
38
+ .refine(([x, y]) => x !== 0 && y !== 0, 'scale must not be zero')
39
+ .optional()
40
+ .describe('World units per picture unit along x and y; a negative value mirrors.'),
41
+ affine: z
42
+ .tuple([finite, finite, finite, finite, finite, finite])
43
+ .optional()
44
+ .describe('General map [a, b, c, d, e, f] from picture to world, instead of anchor, target and scale.'),
45
+ order: z
46
+ .number()
47
+ .int()
48
+ .min(0)
49
+ .max(1_000)
50
+ .default(0)
51
+ .describe('Where targets overlap the higher order wins; equal orders that overlap are an error.'),
52
+ })
53
+ .strict()
54
+ .refine((sheet) => (sheet.affine === undefined) ===
55
+ (sheet.anchor !== undefined && sheet.target !== undefined && sheet.scale !== undefined) &&
56
+ (sheet.affine !== undefined || sheet.anchor !== undefined), { message: 'Give either anchor, target and scale, or affine' });
57
+ export const referenceSheetsDocumentSchema = z
58
+ .object({
59
+ version: z.literal(1),
60
+ source: z
61
+ .object({
62
+ width: z.number().int().positive(),
63
+ height: z.number().int().positive(),
64
+ sha256: z
65
+ .string()
66
+ .regex(/^[a-f0-9]{64}$/u)
67
+ .optional(),
68
+ })
69
+ .strict()
70
+ .optional()
71
+ .describe('The picture these registrations were made for; a picture of another size or hash is refused.'),
72
+ fill: z
73
+ .enum(['sea', 'error'])
74
+ .default('sea')
75
+ .describe('What world pixels outside every sheet become: open sea, or an error.'),
76
+ sheets: z.array(referenceSheetSchema).min(1).max(REFERENCE_SHEETS_MAX),
77
+ })
78
+ .strict()
79
+ .superRefine((document, context) => {
80
+ const seen = new Set();
81
+ for (const [index, sheet] of document.sheets.entries()) {
82
+ if (seen.has(sheet.id))
83
+ context.addIssue({
84
+ code: 'custom',
85
+ path: ['sheets', index, 'id'],
86
+ message: `Duplicate sheet id ${sheet.id}`,
87
+ });
88
+ seen.add(sheet.id);
89
+ }
90
+ })
91
+ .describe('Registration of a drawn picture made of sheets onto the world frame (ADR 0067). Coordinates are normalised: picture coordinates to the picture, world coordinates to width x height.');
92
+ export const referenceSheetsPathSchema = workspaceRelativePathSchema.refine((value) => !value.includes(':') && /\.json$/iu.test(value), 'Sheets must be a workspace-relative .json file');
@@ -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 = '4.3.0';
3
+ export const PACKAGE_VERSION = '4.5.0';
4
4
  export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
5
5
  export const CONFIG_VERSION = 1;
6
6
  export const TRANSACTION_VERSION = 2;
@@ -66,16 +66,18 @@ Every tool takes a `workspaceId` argument, which defaults to `"current"`.
66
66
  For `"current"`, the server asks the MCP client for its workspace roots when the client supports them:
67
67
 
68
68
  - It selects the one mod workspace that contains a client root or lies inside one.
69
- - No match fails with `WORKSPACE_CONTEXT_NOT_FOUND`, and more than one match fails with `WORKSPACE_CONTEXT_AMBIGUOUS`.
69
+ - More than one match fails with `WORKSPACE_CONTEXT_AMBIGUOUS`.
70
+ - With no match, for example a session started in another repository, it selects the `defaultWorkspace` when it is set, or the only mod workspace when there is one.
71
+ - Otherwise the call fails with `WORKSPACE_CONTEXT_NOT_FOUND`, and the error lists up to 32 workspace IDs and names (`workspaceIds`, `workspaceNames`, `workspaceCount`) with the three fixes.
70
72
 
71
73
  When the client does not supply roots, the server uses its own working directory:
72
74
 
73
75
  - It selects the one mod workspace that contains the working directory.
74
- - If the working directory is in no mod and exactly one mod workspace exists, it selects that mod.
75
- - If the working directory is in no mod and several mod workspaces exist, the call fails with `WORKSPACE_CONTEXT_REQUIRED`, and the error lists the available workspace IDs.
76
+ - If the working directory is in no mod, it selects the `defaultWorkspace` when it is set, or the only mod workspace when there is one.
77
+ - Otherwise the call fails with `WORKSPACE_CONTEXT_REQUIRED`, and the error lists the available workspace IDs and names.
76
78
  - With no mod workspace at all, the call fails with `WORKSPACE_CONTEXT_NOT_FOUND`.
77
79
 
78
- When a tool reads a named source file with `"current"` and the selected mod lacks that file, the server uses the one other mod that has it, and fails with `WORKSPACE_SOURCE_AMBIGUOUS` if several do.
80
+ When a tool reads a named source file with `"current"` (the `relativePath` of the focus tools, the `path` of `hoi4.script_validate`) and the selected mod lacks that file, or no mod could be selected, the server uses the one mod that has it, and fails with `WORKSPACE_SOURCE_AMBIGUOUS` if several do.
79
81
 
80
82
  To choose a mod explicitly, pass its workspace ID.
81
83
  A configured workspace keeps the `id` you gave it.
@@ -87,16 +89,17 @@ Over HTTP, a principal sees only the workspaces granted to it, and any other ID
87
89
 
88
90
  ## Paths
89
91
 
90
- | Key | Type | Range | Default | Purpose |
91
- | ---------------------- | ---------------- | ----------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
92
- | `version` | number | `1` | none | Configuration format version; required. |
93
- | `serverStateRoot` | absolute path | | unset | Private folder for indexes, jobs and server records. Required whenever `modRoots` or a `mod` workspace is configured. |
94
- | `modRoots` | array of paths | up to 16 | `[]` | Parent folders whose direct subfolders each become a writable mod workspace. Hidden and linked subfolders are skipped, and a subfolder already registered under `workspaces` keeps that registration. |
95
- | `gameRoot` | path | | unset | The installed game, used by every `mod` workspace that sets no `gameRoot` of its own. |
96
- | `gameUserRoot` | path | | unset | The game's user-data folder holding `logs/error.log`, read by `hoi4.error_log`. When unset, the server tries the folder containing a mod's parent `mod/` folder, then `Documents/Paradox Interactive/Hearts of Iron IV` (also under `%OneDrive%` on Windows), or `~/.local/share/Paradox Interactive/Hearts of Iron IV` on Linux. |
97
- | `workspaceStorageRoot` | path | | unset | Base folder for generated files: each workspace stores `artifacts/` and `cache/` in `<workspaceStorageRoot>/<workspace ID>/`. When unset, mods use `.hoi4-agent/` inside the mod. |
98
- | `storageRoots` | array of paths | up to 16 | `[]` | Additional folders in which an explicit `artifactRoot` or `cacheRoot` may be placed. |
99
- | `workspaces` | array of objects | up to 1,000 | `[]` | Explicit workspace registrations, described in the next section. |
92
+ | Key | Type | Range | Default | Purpose |
93
+ | ---------------------- | ---------------- | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `version` | number | `1` | none | Configuration format version; required. |
95
+ | `serverStateRoot` | absolute path | | unset | Private folder for indexes, jobs and server records. Required whenever `modRoots` or a `mod` workspace is configured. |
96
+ | `modRoots` | array of paths | up to 16 | `[]` | Parent folders whose direct subfolders each become a writable mod workspace. Hidden and linked subfolders are skipped, and a subfolder already registered under `workspaces` keeps that registration. |
97
+ | `gameRoot` | path | | unset | The installed game, used by every `mod` workspace that sets no `gameRoot` of its own. |
98
+ | `gameUserRoot` | path | | unset | The game's user-data folder holding `logs/error.log`, read by `hoi4.error_log`. When unset, the server tries the folder containing a mod's parent `mod/` folder, then `Documents/Paradox Interactive/Hearts of Iron IV` (also under `%OneDrive%` on Windows), or `~/.local/share/Paradox Interactive/Hearts of Iron IV` on Linux. |
99
+ | `workspaceStorageRoot` | path | | unset | Base folder for generated files: each workspace stores `artifacts/` and `cache/` in `<workspaceStorageRoot>/<workspace ID>/`. When unset, mods use `.hoi4-agent/` inside the mod. |
100
+ | `storageRoots` | array of paths | up to 16 | `[]` | Additional folders in which an explicit `artifactRoot` or `cacheRoot` may be placed. |
101
+ | `workspaces` | array of objects | up to 1,000 | `[]` | Explicit workspace registrations, described in the next section. |
102
+ | `defaultWorkspace` | string | workspace ID | unset | The mod workspace, by ID or short form, used for `"current"` when neither the client roots nor the working directory identify one. It must name a registered mod workspace, and over HTTP it applies only to principals granted that workspace. |
100
103
 
101
104
  ## Workspaces
102
105
 
package/docs/focus.md CHANGED
@@ -168,6 +168,12 @@ Branch names, lane assignments, working labels, and other planning-only data may
168
168
 
169
169
  If malformed or unsupported script makes a requested change ambiguous, the rewrite stops and explains the blocker instead of guessing.
170
170
 
171
+ ### Stale source and source encoding
172
+
173
+ A rewrite starts from a plan returned by `hoi4.focus_inspect`. If the source file changed in meaning after that inspection, `hoi4.focus_rewrite` refuses with `FOCUS_SOURCE_CHANGED_SINCE_PLAN` and writes nothing, because applying the plan would overwrite the other change with the plan's older values. Call `hoi4.focus_inspect` again, reapply the intended edit to the new plan, and retry. There is no override. A change that only touches comments or formatting does not trigger the refusal, and neither does a source that already equals the plan.
174
+
175
+ Source files are read as UTF-8 (with or without a byte order mark) and, when they are not valid UTF-8, as Windows-1252. Windows-1252 leaves the bytes 0x81, 0x8D, 0x8F, 0x90 and 0x9D undefined, so a file containing any of them cannot be edited without changing those bytes. Inspection still works and reports the warning `SOURCE_ENCODING_NOT_ROUND_TRIPPABLE`; every rewrite of that file is refused with the same code. Convert the file to UTF-8, or replace those bytes, and retry.
176
+
171
177
  ## What inspection checks
172
178
 
173
179
  Inspection and rendering can report missing references, invalid prerequisite structure, duplicate or overlapping positions, insufficient spacing, long or crossing connectors, branch asymmetry, route conflicts, missing localisation or sprites, weak terminal branches, repeated rewards, and missing AI metadata. National-tree inspection also returns the exact `continuous_focus_position` coordinates and linked palette IDs in its compact result, so an agent can identify and move the continuous-focus area without opening the full artifact. Renders are offline review artifacts, not game screenshots.