hoi4-agent-tools 4.4.0 → 4.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 (86) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
  3. package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
  4. package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
  5. package/dist/hoi4_agent_tools/focus/drift.js +34 -0
  6. package/dist/hoi4_agent_tools/focus/service.js +9 -3
  7. package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
  8. package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
  9. package/dist/hoi4_agent_tools/map/derived.js +16 -3
  10. package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
  11. package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
  12. package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
  13. package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
  14. package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
  15. package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
  16. package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
  17. package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
  18. package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
  19. package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
  20. package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
  21. package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
  22. package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
  23. package/dist/hoi4_agent_tools/map/model.js +2 -1
  24. package/dist/hoi4_agent_tools/map/operations.js +282 -38
  25. package/dist/hoi4_agent_tools/map/reference-preparation.js +4 -1
  26. package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
  27. package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +43 -0
  28. package/dist/hoi4_agent_tools/map/service.js +93 -16
  29. package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
  30. package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
  31. package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
  32. package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
  33. package/dist/hoi4_agent_tools/map/territory.js +1108 -0
  34. package/dist/hoi4_agent_tools/map/tool-operations.js +119 -1
  35. package/dist/hoi4_agent_tools/map/validation.js +4 -0
  36. package/dist/hoi4_agent_tools/map/world-context.js +23 -0
  37. package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
  38. package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
  39. package/dist/hoi4_agent_tools/map/world.js +400 -145
  40. package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
  41. package/dist/hoi4_agent_tools/schemas/map.js +277 -34
  42. package/dist/hoi4_agent_tools/version.js +1 -1
  43. package/docs/focus.md +6 -0
  44. package/docs/map.md +132 -4
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +1 -1
  47. package/schemas/configuration.schema.json +1 -1
  48. package/schemas/continuous-focus-palette.schema.json +1 -1
  49. package/schemas/custom-weighted-pool.schema.json +1 -1
  50. package/schemas/decision-inspect-input.schema.json +1 -1
  51. package/schemas/error-log-input.schema.json +1 -1
  52. package/schemas/event-feature-manifest.schema.json +1 -1
  53. package/schemas/focus-plan.schema.json +1 -1
  54. package/schemas/focus-planning-sidecar.schema.json +1 -1
  55. package/schemas/geography-model.schema.json +13 -1
  56. package/schemas/gui-animation-source.schema.json +1 -1
  57. package/schemas/gui-helper.schema.json +1 -1
  58. package/schemas/gui-scenario.schema.json +1 -1
  59. package/schemas/helper-expansion-request.schema.json +1 -1
  60. package/schemas/helper-expansion-summary.schema.json +1 -1
  61. package/schemas/impact-inspect-input.schema.json +1 -1
  62. package/schemas/map-operation.schema.json +1541 -786
  63. package/schemas/mechanic-test-input.schema.json +1 -1
  64. package/schemas/mod-index-input.schema.json +1 -1
  65. package/schemas/operation-result.schema.json +1 -1
  66. package/schemas/package-check-input.schema.json +1 -1
  67. package/schemas/package-manifest.schema.json +1 -1
  68. package/schemas/probability-analysis-result.schema.json +1 -1
  69. package/schemas/probability-compare-input.schema.json +1 -1
  70. package/schemas/probability-evaluate-input.schema.json +1 -1
  71. package/schemas/probability-inspect-input.schema.json +1 -1
  72. package/schemas/probability-render-input.schema.json +1 -1
  73. package/schemas/probability-scenario-set.schema.json +1 -1
  74. package/schemas/probability-sequence-input.schema.json +1 -1
  75. package/schemas/probability-simulate-input.schema.json +1 -1
  76. package/schemas/probability-sweep-input.schema.json +1 -1
  77. package/schemas/reference-context-input.schema.json +1 -1
  78. package/schemas/reference-read-input.schema.json +1 -1
  79. package/schemas/reference-search-input.schema.json +1 -1
  80. package/schemas/reference-sheets.schema.json +1 -1
  81. package/schemas/scenario-suite.schema.json +1 -1
  82. package/schemas/scenario-test-input.schema.json +1 -1
  83. package/schemas/script-validate-input.schema.json +1 -1
  84. package/schemas/shared-scenario.schema.json +1 -1
  85. package/schemas/source-lookup-input.schema.json +1 -1
  86. package/server.json +2 -2
@@ -79,6 +79,15 @@ export const geographySiteKindSchema = z.enum([
79
79
  'mine',
80
80
  'landmark',
81
81
  ]);
82
+ /** The strategic resources of the game that a mine site can yield. */
83
+ export const geographyResourceSchema = z.enum([
84
+ 'steel',
85
+ 'aluminium',
86
+ 'tungsten',
87
+ 'chromium',
88
+ 'oil',
89
+ 'rubber',
90
+ ]);
82
91
  export const geographyPlaceKindSchema = z.enum([
83
92
  'cape',
84
93
  'bay',
@@ -94,7 +103,7 @@ const seaSchema = z
94
103
  ...areaFields,
95
104
  depthClass: geographyDepthClassSchema.optional(),
96
105
  ice: unit.optional(),
97
- kind: z.enum(['ocean', 'sea', 'gulf', 'fjord']).optional(),
106
+ kind: z.enum(['ocean', 'sea', 'gulf', 'bay', 'fjord']).optional(),
98
107
  })
99
108
  .strict();
100
109
  const landSchema = z
@@ -266,6 +275,8 @@ const siteSchema = z
266
275
  onWater: z.boolean().optional(),
267
276
  /** A lake the site belongs to, for a port on a lake shore. */
268
277
  lake: geographyFeatureIdSchema.optional(),
278
+ /** What a `mine` site yields in the world's economy (steel when it names none). */
279
+ resource: geographyResourceSchema.optional(),
269
280
  })
270
281
  .strict();
271
282
  const roadSchema = z
@@ -24,6 +24,59 @@ const rgbSchema = z
24
24
  })
25
25
  .strict();
26
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
+ });
27
80
  const operationBase = {
28
81
  id: identifier,
29
82
  summary: z.string().max(1000).optional(),
@@ -225,6 +278,32 @@ export const worldColormapSchema = z
225
278
  export const worldPartitionSchema = z
226
279
  .enum(['legacy', 'feature-aware'])
227
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
+ /** Development model of a world built from a geography (ADR 0063). */
282
+ export const worldDevelopmentSchema = z
283
+ .object({
284
+ model: z
285
+ .enum(['geography', 'legacy'])
286
+ .optional()
287
+ .describe('geography (default for a geography world) plans ports, population, state categories, infrastructure, factories, resources, victory points, names and railway costs from the model: fertility, rivers, the coast, passes, ranges and sites. legacy keeps the random economy.'),
288
+ })
289
+ .strict();
290
+ /** Automatic detection of narrow sea channels between landmasses (ADR 0062). */
291
+ export const worldAutoStraitsSchema = z
292
+ .union([
293
+ z.boolean(),
294
+ z
295
+ .object({
296
+ maxGapPx: z
297
+ .number()
298
+ .int()
299
+ .min(4)
300
+ .max(96)
301
+ .optional()
302
+ .describe('Widest channel written as a crossing, in pixels; default 24 (about 170 km).'),
303
+ })
304
+ .strict(),
305
+ ])
306
+ .describe('Also write a sea crossing for every channel narrower than maxGapPx that one sea province fills between two landmasses and that the model does not declare as a strait. Needs a geography.');
228
307
  const partitionAreaPixels = z.number().int().min(20).max(50_000);
229
308
  export const worldPartitionTuningSchema = z
230
309
  .object({
@@ -581,6 +660,162 @@ const createProvinceOperation = z
581
660
  })
582
661
  .strict();
583
662
  const occurrenceSchema = z.number().int().min(0).optional();
663
+ const stateChangesSchema = z
664
+ .object({
665
+ capital: provinceId
666
+ .nullable()
667
+ .optional()
668
+ .describe('Assertion for the capital derived from the exact victoryPoints payload; never writes a state capital field'),
669
+ manpower: z.number().min(0).optional(),
670
+ category: z.string().min(1).optional(),
671
+ resources: nonnegativeMap.optional(),
672
+ stateBuildings: nonnegativeMap.optional(),
673
+ owner: z.string().min(1).nullable().optional(),
674
+ controller: z.string().min(1).nullable().optional(),
675
+ cores: z.array(z.string().min(1)).max(10_000).optional(),
676
+ claims: z.array(z.string().min(1)).max(10_000).optional(),
677
+ victoryPoints: z
678
+ .array(z.object({ provinceId, value: z.number().min(0) }).strict())
679
+ .max(100_000)
680
+ .optional(),
681
+ provinceBuildings: z.record(z.string().regex(/^\d+$/u), nonnegativeMap).optional(),
682
+ })
683
+ .strict();
684
+ const tagSchema = z
685
+ .string()
686
+ .regex(/^[A-Z0-9]{3}$/u, 'A country tag is three capital letters or digits');
687
+ const territoryRefSchema = z.union([
688
+ z.object({ state: stateId }).strict(),
689
+ z.object({ province: provinceId }).strict(),
690
+ z.object({ x: z.number().int().min(0), y: z.number().int().min(0) }).strict(),
691
+ ]);
692
+ const territorySelectionSchema = z
693
+ .object({
694
+ stateIds: z.array(stateId).max(100_000).optional(),
695
+ provinceIds: z.array(provinceId).max(100_000).optional(),
696
+ regionIds: z.array(z.number().int().positive()).max(10_000).optional(),
697
+ })
698
+ .strict();
699
+ const borderQualityModeSchema = z
700
+ .enum(['off', 'report', 'enforce'])
701
+ .describe('off skips the measurement; report adds border naturalness to the result; enforce blocks the write when the borders miss the targets.');
702
+ const borderTargetsSchema = z
703
+ .object({
704
+ minEnrichment: z.number().min(0).max(20).optional(),
705
+ maxStraightness: z.number().min(0).max(1).optional(),
706
+ minNaturalShare: z.number().min(0).max(1).optional(),
707
+ })
708
+ .strict()
709
+ .describe('Override the targets (enrichment 1.6; straightness 0.12 for country borders and 0.25 for state borders).');
710
+ const territoryCountrySchema = z
711
+ .object({
712
+ tag: tagSchema,
713
+ weight: z
714
+ .number()
715
+ .positive()
716
+ .max(1_000_000)
717
+ .optional()
718
+ .describe('Relative target size; default 1.'),
719
+ seeds: z
720
+ .array(territoryRefSchema)
721
+ .min(1)
722
+ .max(64)
723
+ .describe('{ state }, { province } or a pixel { x, y }. The first is the capital.'),
724
+ forced: territorySelectionSchema.optional(),
725
+ blocked: territorySelectionSchema.optional(),
726
+ regions: z
727
+ .array(z.number().int().positive())
728
+ .max(1_000)
729
+ .optional()
730
+ .describe('Strategic regions this country prefers to grow in.'),
731
+ overlord: tagSchema.optional().describe('Grow this country inside its overlord.'),
732
+ mergeInto: tagSchema
733
+ .optional()
734
+ .describe('Relabel the finished territory to another listed country (this tag stays a core).'),
735
+ cores: territorySelectionSchema.optional(),
736
+ claims: territorySelectionSchema.optional(),
737
+ })
738
+ .strict();
739
+ const territoryOperation = z
740
+ .object({
741
+ ...operationBase,
742
+ kind: z.literal('assign_territory'),
743
+ unit: z
744
+ .enum(['state', 'province'])
745
+ .default('state')
746
+ .describe('state assigns whole states. province assigns provinces and regroups the states a border cuts (conserving population, resources and buildings).'),
747
+ countries: z.array(territoryCountrySchema).min(1).max(512),
748
+ scope: z
749
+ .object({
750
+ stateIds: z.array(stateId).max(100_000).optional(),
751
+ regionIds: z.array(z.number().int().positive()).max(10_000).optional(),
752
+ owners: z.array(z.string().min(1).max(32)).max(1_000).optional(),
753
+ })
754
+ .strict()
755
+ .optional()
756
+ .describe('Only these states take part; the rest keep their owners.'),
757
+ unowned: territorySelectionSchema.optional(),
758
+ barriers: z
759
+ .enum(['natural', 'none'])
760
+ .default('natural')
761
+ .describe('none ignores rivers, ranges and shores (nearest seed).'),
762
+ rules: z
763
+ .object({
764
+ connectedMainland: z.boolean().default(true),
765
+ islandPolicy: z.enum(['one-owner', 'nearest', 'unowned']).default('one-owner'),
766
+ })
767
+ .strict()
768
+ .optional(),
769
+ smoothing: z
770
+ .object({
771
+ passes: z.number().int().min(0).max(20).default(4),
772
+ maxShareChange: z.number().min(0).max(1).default(0.15),
773
+ })
774
+ .strict()
775
+ .optional(),
776
+ shareTolerance: z.number().min(0.01).max(1).default(0.15),
777
+ seed: z.number().int().min(0).max(2_147_483_647).default(0),
778
+ ownership: z
779
+ .object({
780
+ controller: z.enum(['owner', 'keep']).default('owner'),
781
+ cores: z.enum(['keep', 'owner', 'add-owner']).default('keep'),
782
+ claims: z.enum(['keep', 'clear']).default('keep'),
783
+ unowned: z.enum(['clear', 'keep']).default('clear'),
784
+ })
785
+ .strict()
786
+ .optional(),
787
+ stateSize: z
788
+ .object({
789
+ min: z.number().int().min(1).max(1_000),
790
+ max: z.number().int().min(1).max(10_000).optional(),
791
+ })
792
+ .strict()
793
+ .optional()
794
+ .describe('Province unit: fragments below min provinces join a neighbour of the same owner.'),
795
+ remapReferences: z.enum(['refuse', 'override']).optional(),
796
+ borderQuality: borderQualityModeSchema.default('report'),
797
+ borderTargets: borderTargetsSchema.optional(),
798
+ })
799
+ .strict();
800
+ const repartitionOperation = z
801
+ .object({
802
+ ...operationBase,
803
+ kind: z.literal('repartition_states'),
804
+ countries: z.array(tagSchema).min(1).max(512).optional(),
805
+ stateIds: z.array(stateId).max(100_000).optional(),
806
+ provincesPerState: z
807
+ .object({
808
+ min: z.number().int().min(1).max(1_000).optional(),
809
+ target: z.number().int().min(1).max(1_000).optional(),
810
+ max: z.number().int().min(1).max(10_000).optional(),
811
+ })
812
+ .strict()
813
+ .optional(),
814
+ remapReferences: z.enum(['refuse', 'override']).optional(),
815
+ borderQuality: borderQualityModeSchema.default('report'),
816
+ borderTargets: borderTargetsSchema.optional(),
817
+ })
818
+ .strict();
584
819
  export const mapOperationSchema = z.discriminatedUnion('kind', [
585
820
  z
586
821
  .object({
@@ -637,29 +872,22 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
637
872
  ...operationBase,
638
873
  kind: z.literal('update_state'),
639
874
  stateId,
640
- changes: z
641
- .object({
642
- capital: provinceId
643
- .nullable()
644
- .optional()
645
- .describe('Assertion for the capital derived from the exact victoryPoints payload; never writes a state capital field'),
646
- manpower: z.number().min(0).optional(),
647
- category: z.string().min(1).optional(),
648
- resources: nonnegativeMap.optional(),
649
- stateBuildings: nonnegativeMap.optional(),
650
- owner: z.string().min(1).nullable().optional(),
651
- controller: z.string().min(1).nullable().optional(),
652
- cores: z.array(z.string().min(1)).max(10_000).optional(),
653
- claims: z.array(z.string().min(1)).max(10_000).optional(),
654
- victoryPoints: z
655
- .array(z.object({ provinceId, value: z.number().min(0) }).strict())
656
- .max(100_000)
657
- .optional(),
658
- provinceBuildings: z.record(z.string().regex(/^\d+$/u), nonnegativeMap).optional(),
659
- })
660
- .strict(),
875
+ changes: stateChangesSchema,
876
+ })
877
+ .strict(),
878
+ z
879
+ .object({
880
+ ...operationBase,
881
+ kind: z.literal('update_states'),
882
+ updates: z
883
+ .array(z.object({ stateId, changes: stateChangesSchema }).strict())
884
+ .min(1)
885
+ .max(5_000)
886
+ .describe('One entry per state, each with the changes update_state takes. The whole list is one validated rewrite; a state may appear once.'),
661
887
  })
662
888
  .strict(),
889
+ territoryOperation,
890
+ repartitionOperation,
663
891
  splitStateOperation('split_state'),
664
892
  createStateOperation,
665
893
  z
@@ -741,6 +969,10 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
741
969
  geography: geographyModelSchema
742
970
  .optional()
743
971
  .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.'),
972
+ synthesisCheck: z
973
+ .boolean()
974
+ .optional()
975
+ .describe('With preview:true and a geography: run only the relief and river synthesis and report every river that cannot reach its mouth, with the divide that stops it, without writing or planning the map. Refused without preview or without a geography.'),
744
976
  geographyPath: geographyPathSchema
745
977
  .optional()
746
978
  .describe('Workspace-relative .json file holding a geography model (at most 4 MiB). Alternative to the inline geography field; give one of them.'),
@@ -760,19 +992,24 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
760
992
  landProvincesPerRegion: z.number().int().min(2).max(400).optional(),
761
993
  seaProvincesPerRegion: z.number().int().min(1).max(200).optional(),
762
994
  countries: z
763
- .array(z
764
- .object({
765
- tag: z.string().regex(/^[A-Z][A-Z0-9]{2}$/u),
766
- name: worldName,
767
- adjective: worldName.optional(),
768
- color: rgbSchema.optional(),
769
- share: z.number().min(0.05).max(100).optional(),
770
- ideology: z.enum(['democratic', 'communism', 'fascism', 'neutrality']).optional(),
771
- })
772
- .strict())
773
- .min(1)
774
- .max(200)
775
- .optional(),
995
+ .union([
996
+ z
997
+ .array(z
998
+ .object({
999
+ tag: countryTagSchema,
1000
+ name: worldName,
1001
+ adjective: worldName.optional(),
1002
+ color: rgbSchema.optional(),
1003
+ share: z.number().min(0.05).max(100).optional(),
1004
+ ideology: z.enum(['democratic', 'communism', 'fascism', 'neutrality']).optional(),
1005
+ })
1006
+ .strict())
1007
+ .min(1)
1008
+ .max(200),
1009
+ workspaceCountriesSchema,
1010
+ ])
1011
+ .optional()
1012
+ .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.'),
776
1013
  continentNames: z.array(worldName).max(12).optional(),
777
1014
  startDate: z
778
1015
  .string()
@@ -793,9 +1030,15 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
793
1030
  look: worldLookSchema.optional(),
794
1031
  colormap: worldColormapSchema.optional(),
795
1032
  partition: worldPartitionSchema.optional(),
1033
+ development: worldDevelopmentSchema.optional(),
1034
+ autoStraits: worldAutoStraitsSchema.optional(),
796
1035
  partitionTuning: worldPartitionTuningSchema
797
1036
  .optional()
798
1037
  .describe('Size bands and strengths of the feature-aware partition; refused with partition legacy.'),
1038
+ borderQuality: borderQualityModeSchema
1039
+ .optional()
1040
+ .describe('Measure how well the new world’s province, state, region and country borders follow rivers, slopes and shores. enforce blocks the write below the targets and needs the feature-aware partition; omitted measures nothing.'),
1041
+ borderTargets: borderTargetsSchema.optional(),
799
1042
  replaceBaseGame: z
800
1043
  .boolean()
801
1044
  .default(true)
@@ -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.4.0';
3
+ export const PACKAGE_VERSION = '4.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 = 2;
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.
package/docs/map.md CHANGED
@@ -113,7 +113,7 @@ A model is one object with `"version": 1` and any of these arrays. Unknown keys
113
113
 
114
114
  | Array | Feature and main fields |
115
115
  | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
116
- | `seas` | `polygons` (outer rings), optional `holes`, `depthClass` (`shelf`, `open`, `abyss`), `ice` 0 to 1, `kind` (`ocean`, `sea`, `gulf`, `fjord`) |
116
+ | `seas` | `polygons` (outer rings), optional `holes`, `depthClass` (`shelf`, `open`, `abyss`), `ice` 0 to 1, `kind` (`ocean`, `sea`, `gulf`, `bay`, `fjord`) |
117
117
  | `lands` | `polygons`, `holes`, `kind` (`continent`, `island`, `archipelago`, `peninsula`), `region`, default `biome` and `fertility`, `elevationBase`, `coastDetail` |
118
118
  | `regions` | `polygons` (optional), `terrain` target shares, `density` (`provincesPerState` as `[smallest, usual, largest]`, `provinceAreaPx`) |
119
119
  | `ranges` | `path` (points, each optionally with `halfWidth` and `peak`), `closed`, `halfWidth`, `peak`, `profile` (`ridge`, `massif`, `escarpment`, `wall`), `asymmetry`, `snowline`, `spurs`, `barrier` (`none`, `impassable`, `fort-line`) |
@@ -161,7 +161,7 @@ The synthesis checks its own output and reports: `MAP_RELIEF_SYNTHESIZED` (info:
161
161
 
162
162
  Measured on the project's synthetic large fantasy model at 5,120 x 2,560 (13.1 million pixels): land median 110, 99th percentile 222, maximum 244, mean height step 1.02 per half-resolution pixel (base game: 107, 213, 239 and 0.97), 6.0 million land pixels with 129,987 river pixels (2.16%, 88 connected rivers, no 2 x 2 block, one source each, 1.6% of river pixels on straight stretches), and about six seconds and 450 MB for the synthesis.
163
163
 
164
- Not synthesised yet: biomes, climate, fertility, tree and colour-map classes from the model, sea ice, strait and pass adjacencies, and the render layers for relief and rivers. With a reference picture the picture still gives the coast and the terrain classes; its elevation and river hints are replaced. A reference picture's rivers are not routed.
164
+ Not synthesised yet: biomes, climate, fertility, tree and colour-map classes from the model, sea ice, and the render layers for relief and rivers. Straits, passes, walls, ports, development and names are described in [Crossings, ports, development and names from a model](#crossings-ports-development-and-names-from-a-model). With a reference picture the picture still gives the coast and the terrain classes; its elevation and river hints are replaced. A reference picture's rivers are not routed.
165
165
 
166
166
  ## Inspect and navigate the complete map
167
167
 
@@ -265,7 +265,8 @@ Every operation has a unique `id` (1 to 256 characters), an optional `summary`,
265
265
  The supported kinds are:
266
266
 
267
267
  - whole maps: `create_world`;
268
- - states: `create_state`, `split_state`, `merge_states`, `move_state_provinces`, `update_state`;
268
+ - states: `create_state`, `split_state`, `merge_states`, `move_state_provinces`, `update_state`, `update_states`;
269
+ - countries and whole maps: `assign_territory`, `repartition_states`;
269
270
  - provinces: `create_province`, `split_province`, `merge_provinces`, `remove_province`, `update_province_definition`;
270
271
  - strategic regions: `move_region_provinces`;
271
272
  - identifiers and script references: `renumber_map_entity`, `remap_map_references`;
@@ -358,6 +359,69 @@ Pass a full `distribution` with `stateValues: "retain-in-current-states"` to lea
358
359
 
359
360
  Nothing is lost or duplicated: for any split, merge or move, the sums of manpower, each resource and each slot building over all states are the same before and after.
360
361
 
362
+ ## Assign territory to countries
363
+
364
+ `assign_territory` gives the land of a whole map to up to 512 countries in one call, so that their borders follow rivers, ranges and shores. `repartition_states` regroups the provinces of existing states along the same natural lines, and `update_states` edits up to 5,000 states in one validated rewrite. [ADR 0060](adr/0060-territory-assignment-service.md) records the design.
365
+
366
+ ```json
367
+ {
368
+ "operations": [
369
+ {
370
+ "id": "politics",
371
+ "kind": "assign_territory",
372
+ "unit": "province",
373
+ "seed": 7,
374
+ "countries": [
375
+ { "tag": "AAA", "weight": 3, "seeds": [{ "state": 41 }, { "x": 1810, "y": 902 }] },
376
+ {
377
+ "tag": "BBB",
378
+ "weight": 2,
379
+ "seeds": [{ "province": 3318 }],
380
+ "blocked": { "regionIds": [12] }
381
+ },
382
+ {
383
+ "tag": "CCC",
384
+ "weight": 1,
385
+ "seeds": [{ "state": 108 }],
386
+ "overlord": "AAA",
387
+ "regions": [4]
388
+ }
389
+ ],
390
+ "stateSize": { "min": 3, "max": 12 },
391
+ "ownership": { "cores": "owner", "claims": "clear" },
392
+ "borderQuality": "report"
393
+ }
394
+ ]
395
+ }
396
+ ```
397
+
398
+ | Field of a country | Meaning |
399
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
400
+ | `tag`, `weight` | Three capital letters or digits, and the relative target size (default 1). |
401
+ | `seeds` | `{ state }`, `{ province }` or a pixel `{ x, y }` of the province map; the first is the capital. A seed in the sea is refused. |
402
+ | `forced`, `blocked` | `{ stateIds, provinceIds, regionIds }`: land the country always or never owns. `regionIds` are strategic regions. |
403
+ | `regions` | Strategic regions the country prefers to grow in (a step into another country's preferred region costs more). |
404
+ | `overlord`, `mergeInto` | A subject is grown inside its overlord's territory. `mergeInto` relabels the finished territory to another listed country and keeps its tag as a core. |
405
+ | `cores`, `claims` | Extra states the country holds a core on or claims. |
406
+
407
+ Top-level fields: `unit` (`state`, the default, assigns whole states; `province` assigns provinces and regroups the states a border cuts), `scope` (`stateIds`, `regionIds`, `owners`: only these states take part), `unowned` (land nobody owns), `barriers` (`natural` or `none`, which grows on distance alone), `rules.connectedMainland` (default true) and `rules.islandPolicy` (`one-owner`, `nearest` or `unowned` for landmasses without a seed), `smoothing` (`passes` 0 to 20, `maxShareChange`), `shareTolerance` (default 0.15), `seed` (the same seed gives the same borders), `ownership` (`controller`: `owner` or `keep`; `cores`: `keep`, `owner` or `add-owner`; `claims`: `keep` or `clear`; `unowned`: `clear` or `keep`), `stateSize` (province unit: fragments below `min` provinces join a neighbour of the same owner and strategic region), `remapReferences` (`refuse` or `override`, as for `merge_states`), `borderQuality` and `borderTargets`.
408
+
409
+ How it works: a multi-source shortest-path growth from the seeds over the province or state graph, where a step costs the distance between centres times `1 + 6 * barrier` (the barrier is the strength of the rivers, slopes, lake shores and terrain changes along the shared border, measured by the metric of [Border naturalness](#border-naturalness)); country multipliers are corrected until sizes are within the tolerance of their targets; a country cut apart on its capital's landmass is joined by a corridor; and a local search moves boundary atoms when that shortens the artificial border without disconnecting the donor or leaving the size tolerance. Targets follow the weights, corrected for what the land allows (a country alone on an island owns the island).
410
+
411
+ The province unit writes the new borders exactly: each state is split by owner, small fragments are merged, and states are rewritten in one pass. Population, resources and slot buildings follow the land by pixel share in whole units (every total is conserved), levels keep their value, victory points and province buildings follow their provinces, cores and claims are the union of the states that contribute, and new states are named `STATE_<id>` in `hoi4_agent_map_l_english.yml` after the state they were cut from, with a number. A state may hold provinces of one strategic region only; dated history that would be lost or rewritten, and provinces without geometry, refuse the operation.
412
+
413
+ `repartition_states` takes `countries` (owners), `stateIds`, `provincesPerState` (`min` 3, `target` 6 and `max` 10 by default) and `remapReferences`. States are clustered per owner and strategic region; a cluster equal to an existing state is left alone.
414
+
415
+ `update_states` takes `updates: [{ stateId, changes }]` with the changes of `update_state`. The index is built once; a state may appear once, and an unknown state refuses the whole list.
416
+
417
+ `borderQuality` is `report` by default: the result adds `MAP_BORDER_QUALITY_MEASURED` with the enrichment, natural share and straightness of the proposed country and state borders, and the artifact `map-territory-report.json` holds every number. `enforce` blocks the write when a layer misses the targets (enrichment 1.6; straightness 0.12 for countries and 0.25 for states; override with `borderTargets`) with `MAP_TERRITORY_BORDER_ARTIFICIAL`; `off` skips it. `data.territory` returns, per operation, the sizes against targets (worst eight countries), the border score, the same seeds grown on distance alone (`distanceOnly`), and the counts of states changed, created and removed. Previews work as for any rewrite and write nothing.
418
+
419
+ Unowned land: states in `unowned`, and landmasses without a seed under `islandPolicy: "unowned"`, are not grown into. With `ownership.unowned: "clear"` (the default) their owner is removed, and their controller too when `controller` is `owner`; `"keep"` leaves them as they were. States outside `scope` are never edited. Provinces that belong to no state cannot be owned and are left out.
420
+
421
+ Refusals name the cause and the state or province of the request: `MAP_TERRITORY_SEED_IN_WATER`, `_STATE_UNKNOWN`, `_PROVINCE_UNKNOWN`, `_REGION_UNKNOWN`, `_SCOPE_UNKNOWN`, `_POINT_OUTSIDE_MAP`, `_PROVINCE_WITHOUT_STATE`, `_SEED_MISSING` (a country with no seed), `_SEED_UNAVAILABLE` (a seed or forced land is unowned, outside the scope or outside its overlord), `_FORCED_CONFLICT` (two countries claim one state or province), `_FORCED_BLOCKED` (forced or a seed and blocked for the same country), `_FORCED_UNREACHABLE` (blocked or forced land cuts a seed off from its capital), `_TAG_INVALID`, `_TAG_DUPLICATE`, `_WEIGHT_INVALID`, `_TOO_MANY_COUNTRIES` (above 512), `_TOO_MANY_SEEDS` (above 64), `_OVERLORD_INVALID` (unknown, itself or a cycle), `_MERGE_INVALID`, `_OPTION_INVALID`, `_STATE_OVERLAP`, `_STATE_SIZE_INVALID`, `_STATE_SIZE_NEEDS_PROVINCE_UNIT`, `_NO_LAND`, `_MAP_MISSING`, `_NOTHING_TO_REPARTITION`, and `MAP_UPDATE_STATES_DUPLICATE` for a state listed twice in `update_states`. The schema refuses more than 512 countries, more than 64 seeds, an empty seed list, a tag that is not three capital letters or digits, a weight of zero or less, and more than 5,000 `update_states` entries before anything is planned. Warnings: `MAP_TERRITORY_SHARE_OFF` (a country is more than the tolerance from its target) and `MAP_TERRITORY_DISCONNECTED`; info: `MAP_TERRITORY_ASSIGNED`, `MAP_TERRITORY_REPARTITIONED` and `MAP_TERRITORY_NO_FEATURES` (no height, river or terrain bitmap, so borders fall back to distance), `MAP_TERRITORY_STATE_SIZE_UNMET` (a fragment below `stateSize.min` has no neighbour of the same owner and strategic region to join) and `MAP_TERRITORY_STATE_SKIPPED` (`repartition_states` leaves states that span several strategic regions or share provinces).
422
+
423
+ Measured on project-owned synthetic fixtures (ADR 0060): country-border enrichment 3.1 to 3.6 against 1.2 to 2.5 for the same seeds grown on distance alone on a partitioned world with rivers and ridges; on a generated total-conversion map of 13.1 million pixels with about 300 countries and 1,500 states, 15 seconds by state and 26 seconds by province.
424
+
361
425
  ## Other map changes
362
426
 
363
427
  `hoi4.map_rewrite` also moves and merges states, moves provinces between strategic regions, merges or removes provinces, changes province definitions and types, adds or removes normal and special adjacency, and updates supply, railway, building, unit, weather, and entity-locator records. Send one ordered operation list when several connected changes belong together.
@@ -397,6 +461,28 @@ Keys that take exactly one value (`capital`, `state`, `to_state`, `start_state`,
397
461
 
398
462
  `merge_states`, `create_state` and `split_state` can do this themselves: add `"remapReferences": "refuse"` (or `"override"`, which copies the game files that need it into the mod) and the merged-away states are renamed to the survivor, or the source state's references are extended to the new part, in the same plan.
399
463
 
464
+ ### Crossings, ports, development and names from a model
465
+
466
+ A world built from a geography also gets its gameplay facts from the model (ADR [0062](adr/0062-straits-passes-and-impassable-ranges.md) and [0063](adr/0063-geography-driven-ports-development-names-and-railways.md)). A world without a geography is unchanged.
467
+
468
+ | Model feature | What the world gets |
469
+ | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
470
+ | `strait` | A named `sea` row in `adjacencies.csv` (land to land through a sea province; sea to sea through land when the path crosses an isthmus) and a rule in `adjacency_rules.txt` whose access follows `rule`: `open`, `army-closed` or `navy-closed`. The closest shores give the crossing line. |
471
+ | `range` with `barrier: "impassable"` | `impassable` rows for every pair of land provinces whose border crosses the range's path, except at its passes. The partition cuts along the wall, so provinces do not straddle it. |
472
+ | `pass` | A gap in the wall: the province pairs whose border passes through it carry no row. `fort` above 0 puts `bunker = level` on both provinces of the gate (the same for `fort-line` ranges, which write no rows). |
473
+ | `lake` with `gameplay: "inland-sea"` | Sea provinces, so ships and naval bases can use it; a canal row joins a sealed one to the nearest ocean unless a declared strait does. Other lakes stay `lake` provinces and cannot hold a port. |
474
+ | `site` | Victory points and names for capital, city, port and fortress sites (importance 1 to 5 gives 3 to 20 points); a port site holds a port; a `mine` site gives its `resource` (steel by default); a `country` anchor chooses that country's capital. |
475
+ | fertility, rivers, coast, passes | Population, state category, infrastructure, factories and the harbour score of each coastal province, with no random rolls. |
476
+ | names | State, strategic region, victory-point and lake-province names from sites, lake shores, river valleys, ranges, regions, lands and seas by containment; shared names get a compass adjective. |
477
+
478
+ Options of `create_world`: `development: { model }` (`geography`, the default for a geography world, or `legacy` for the random economy) and `autoStraits` (`true` or `{ maxGapPx }`, default 24 pixels, about 170 km) to also write a rule-less crossing for every narrow channel the model did not declare. Both need a geography (`MAP_DEVELOPMENT_REQUIRES_GEOGRAPHY`, `MAP_AUTO_STRAITS_REQUIRES_GEOGRAPHY`).
479
+
480
+ Railways of a geography world follow a terrain-aware cost (ground, climb across the border, rivers, gates, model roads) and never cross a wall except at a gate; trunk lines of level 2 reach the cities of 8 or more victory points. The supply-node rule is unchanged.
481
+
482
+ Diagnostics raised when a world is created (they need the model): `MAP_CROSSINGS_PLANNED`, `MAP_DEVELOPMENT_PLANNED` and `MAP_INLAND_SEA_CREATED` (info), `MAP_STRAIT_UNRESOLVED`, `MAP_STRAIT_DUPLICATE`, `MAP_STRAIT_NOT_NARROW`, `MAP_PASS_BLOCKED`, `MAP_RANGE_NOT_CLOSED`, `MAP_INLAND_SEA_SEALED`, `MAP_PORT_LAKE_UNSUPPORTED`, `MAP_PORT_SITE_NOT_COASTAL`, `MAP_SITE_UNPLACED` and `MAP_NAME_PLACEHOLDER` (warnings; the last counts the states, strategic regions and victory points still called "District N", "Region N" or "Settlement N"), and `MAP_IMPASSABLE_ROW_LIMIT` (refusal above 2,000 wall rows). Raised on any map by `hoi4.map_inspect`: `MAP_ADJACENCY_RULE_MISSING`, `_DUPLICATE` and `_INVALID` (errors), `MAP_ADJACENCY_RULE_UNUSED`, `MAP_IMPASSABLE_NOT_ADJACENT` and `MAP_UNIT_POSITION_MISSING` (information, or warnings when they are a pattern), `MAP_IMPASSABLE_NOT_LAND`, `MAP_ADJACENCY_ROW_LIMIT`, `MAP_STRAIT_NOT_NARROW` and `MAP_RAILWAY_CROSSES_IMPASSABLE` (warnings). The installed game raises nothing above information.
483
+
484
+ Not done: whole provinces inside a massif are not made impassable states, the engine's handling of impassable rows by supply and the AI and of a naval base on an inland sea is not verified, and `positions.txt` stays empty as in the base game. Measured on the project's 5,120 x 2,560 synthetic model with about 6,000 provinces, the gameplay layer adds about 6 seconds to a 30 second `create_world` and the whole run peaks near 0.8 GiB.
485
+
400
486
  ## Positions and supply
401
487
 
402
488
  `regenerate_map_positions` places building, unit-stack and weather positions from province geometry. By default (`mode: missing`) it only adds what is absent: positions for each building model a state or province lacks (from `show_on_map` and the spawn points in `common/buildings`), a full unit-stack set for provinces without one, and a small and a big weather object for strategic regions without one. `mode: replace` rewrites the positions of the selected `stateIds`, `provinceIds` and `regionIds`. Interior points keep away from province borders, naval bases face the sea province they open onto, and floating harbours sit on the water and name their land province. `positions` limits it to `buildings`, `units` or `weather`; heights come from the heightmap.
@@ -429,8 +515,50 @@ Width and height are multiples of 256 with at most 13,238,272 pixels. Land comes
429
515
 
430
516
  The operation writes `provinces.bmp`, `definition.csv`, `default.map`, `continent.txt`, `heightmap.bmp`, `terrain.bmp`, `rivers.bmp` (rivers run downhill to the coast), `trees.bmp`, `cities.bmp`, `world_normal.bmp`, the land and water colour maps, `adjacencies.csv`, strategic regions (`landProvincesPerRegion`, `seaProvincesPerRegion`) with weather by latitude, states (`provincesPerState`) with population, category, buildings, resources, victory points and ports, country tags, country files, history and three-band flags, a bookmark, localisation, and every position, supply node and railway. Countries grow from spread-out capitals in proportion to `share`; each state lies within one country and one strategic region.
431
517
 
518
+ Each write records its generated files in `.hoi4-agent-world.json`. A later `create_world` deletes the recorded files it no longer writes (retires them) in the same transaction and refuses with `MANAGED_OUTPUT_EDITED` when a recorded file was edited since. `MAP_WORLD_OUTPUTS_RECONCILED` and `data.outputs` give the counts of written, retired and released files with up to 200 paths each; `map-world-outputs.json` lists them all.
519
+
520
+ ### Countries the mod already owns
521
+
522
+ A mod that writes its own country tags, country files, histories, orders of battle, flags and names gives `countries` as an object instead of an array (ADR 0069):
523
+
524
+ ```json
525
+ "countries": {
526
+ "source": "workspace",
527
+ "realms": [
528
+ { "tag": "AUR", "capital": { "x": 812, "y": 340 }, "weight": 2 },
529
+ {
530
+ "tag": "BOR",
531
+ "capital": { "x": 1210, "y": 300 },
532
+ "seeds": [{ "x": 1300, "y": 420 }],
533
+ "claim": [[{ "x": 1180, "y": 250 }, { "x": 1290, "y": 250 }, { "x": 1290, "y": 380 }]],
534
+ "exclude": [[{ "x": 1000, "y": 500 }, { "x": 1100, "y": 500 }, { "x": 1100, "y": 600 }]]
535
+ }
536
+ ],
537
+ "unowned": [[{ "x": 600, "y": 700 }, { "x": 680, "y": 700 }, { "x": 680, "y": 760 }]],
538
+ "islands": "nearest"
539
+ }
540
+ ```
541
+
542
+ Points and polygon vertices are world pixels with the origin at the top left. A capital or seed names the land province under it; a land province with more than half its pixels inside a claim belongs to that country, inside an exclusion never does, and inside `unowned` belongs to nobody. Countries grow from their capitals, seeds and claims by `weight` (default 1). Land that growth does not reach goes, on a landmass a country already holds, to the nearest country that may own it; a landmass no country starts on follows `islands`: `nearest` (each province to the nearest capital), `one-owner` (the whole landmass to the country nearest its centre) or `unowned`. At most 512 countries.
543
+
544
+ Every tag must be declared by an active `common/country_tags` file other than the world's own (`MAP_WORLD_REALM_TAG_UNKNOWN`) and not only as a dynamic tag (`MAP_WORLD_REALM_TAG_DYNAMIC`). A point outside the world (`MAP_WORLD_REALM_POINT_OUTSIDE`) or in water (`MAP_WORLD_REALM_POINT_WATER`), a point in unowned land, in the country's own exclusion or on another country's point (`MAP_WORLD_REALM_POINT_CONFLICT`), and claims that overlap each other or unowned land (`MAP_WORLD_REALM_CLAIM_OVERLAP`) are refused with the tag and position. A reference picture's political colours are not used with this form.
545
+
546
+ The world then writes no country tag file, country file, `colors.txt`, country history, order of battle, flag, country name or bookmark. Unowned land forms states with no `owner`, `controller` or `add_core_of` (`MAP_WORLD_STATE_UNOWNED`, and `MAP_STATE_OWNER_MISSING` for each); whether the game build accepts unowned land is the mod's to verify. `MAP_WORLD_REALMS_PLACED`, `data.realms` and `map-world-realms.json` give each country's capital state and province, states, provinces and land share against its target. The country histories stay the mod's: a `capital` in a state the country does not own is the existing warning `MAP_COUNTRY_CAPITAL_NOT_OWNED`.
547
+
548
+ A recorded country file of an earlier generated world that the mod has since edited is released: left in place, dropped from `.hoi4-agent-world.json` and listed as released (`MAP_WORLD_OUTPUT_RELEASED`). Unedited ones are retired. Other edited recorded files still refuse. To keep a file the mod changed, leave its recorded hash as the world wrote it.
549
+
550
+ Every `create_world` reports the country tags declared after the write (`MAP_WORLD_TAG_COUNT`: static, written by the world, dynamic, total) and warns above 439 (`MAP_WORLD_TAG_COUNT_HIGH`), the count at which one measured game build overflowed its flag sheet; this is an observation, not a documented limit, and never blocks.
551
+
432
552
  With `replaceBaseGame` (default `true`) it adds `replace_path` lines for `history/states`, `history/countries`, `history/units`, `map/strategicregions`, `map/supplyareas` and `common/bookmarks` to `descriptor.mod`, and its info diagnostic reminds you that the launcher's `.mod` file beside the mod folder needs the same lines. Base-game events, focuses and decisions still name base-game states; `map_inspect` with `references: true` lists them for replacement or remapping. The same seed always gives the same world. Refine it with the other operations.
433
553
 
554
+ ### Rivers, carved valleys and the river check
555
+
556
+ A river the model draws is authoritative over the synthesised relief (ADR 0071). Where its course crosses a divide more than 22 height units above its bed (the cone of a volcano, a foothill, the tip of a range), the bed is cut through it as a gorge, and each crossing is reported as the warning `MAP_RIVER_CARVED_CROSSING` with the river, the climb and the divide (world pixel, height, and the nearest range or the volcano whose cone it crosses). Move the river or the feature in the model if the gorge is unwanted.
557
+
558
+ Only a course that cannot exist is refused with `MAP_RIVER_UNREACHABLE_MOUTH`: a mouth on another landmass or not reachable over land, an unreachable source or via point, a lake without a free shore. The error lists every such river in `details.rivers` with its reason, or `blockedBy` another failed river.
559
+
560
+ To check all of this without a write, call `hoi4.map_rewrite` with `preview: true` and `synthesisCheck: true` on the `create_world` operation: it runs the relief and river synthesis only, writes nothing and returns `MAP_SYNTHESIS_CHECK_READY` or `MAP_SYNTHESIS_CHECK_BLOCKED` with `data.synthesisCheck` (unreachable rivers and carved crossings). It does not run the partition, the countries or the climate stage (ADR 0070). A world that fails to generate returns only its own blockers, never the findings of the map it would replace.
561
+
434
562
  ### Feature-aware provinces, states and strategic regions
435
563
 
436
564
  By default provinces grow evenly from spread seeds and states and regions grow by breadth-first clusters, which ignore rivers, ridges and shores. Set `partition: "feature-aware"` (default `legacy`; a world given a `geography` defaults to it) to cut along natural lines instead (ADR 0058):
@@ -456,7 +584,7 @@ By default provinces grow evenly from spread seeds and states and regions grow b
456
584
  }
457
585
  ```
458
586
 
459
- All the engine rules hold as before: provinces are connected, no box exceeds an eighth of the map, no four provinces meet at a corner, provinces are at least 20 pixels where a same-kind neighbour exists, and at most 65,000 exist. A plan that needs more than 21,000 provinces is refused with `MAP_PARTITION_PROVINCE_COUNT_HIGH` and the fields to change; both partitions warn with that code above 14,000 provinces. Contradictory bands (`MAP_PARTITION_AREA_BANDS_CONTRADICTORY`), reversed ranges, a map too small for `minProvincePixels` (`MAP_PARTITION_MAP_TOO_SMALL`) and `partitionTuning` with `legacy` are refused before anything is written. Info notes report the provinces cut along rivers or straits (`MAP_PARTITION_SPLITS`) and coast pixels adjusted because a 2 x 2 block touched its own kind only at a corner (`MAP_PARTITION_MASK_SMOOTHED`). Check the result with `hoi4.map_inspect` and `quality: true` (border naturalness).
587
+ All the engine rules hold as before: provinces are connected, no box exceeds an eighth of the map, no four provinces meet at a corner, provinces are at least 20 pixels where a same-kind neighbour exists, and at most 65,000 exist. A plan that needs more than 21,000 provinces is refused with `MAP_PARTITION_PROVINCE_COUNT_HIGH` and the fields to change; both partitions warn with that code above 14,000 provinces. Contradictory bands (`MAP_PARTITION_AREA_BANDS_CONTRADICTORY`), reversed ranges, a map too small for `minProvincePixels` (`MAP_PARTITION_MAP_TOO_SMALL`) and `partitionTuning` with `legacy` are refused before anything is written. Info notes report the provinces cut along rivers or straits (`MAP_PARTITION_SPLITS`) and coast pixels adjusted because a 2 x 2 block touched its own kind only at a corner (`MAP_PARTITION_MASK_SMOOTHED`). Check the result with `hoi4.map_inspect` and `quality: true` (border naturalness), or ask for it in the same call with `borderQuality`: `report` adds the measured province, state, region and country borders of the new world to the result, and `enforce` blocks the write below the targets (it needs the feature-aware partition and is refused with `legacy`, whose borders are at chance level). Countries of a feature-aware world without a reference picture are grown along natural lines by the territory service ([Assign territory to countries](#assign-territory-to-countries)).
460
588
 
461
589
  Code that needs the same cut calls `partitionWorld` (`src/hoi4_agent_tools/map/partition.ts`): it takes `PartitionFields` (distance to river, ridge strength, slope, terrain class, region id, fertility and a barrier mask; `partitionFieldsFromBitmaps` computes a basic version from `heightmap.bmp`, `terrain.bmp` and `rivers.bmp`, `partitionFieldsFromGeography` adds regions and walls) and returns the provinces, the barrier raster, the size distribution and a weighted province graph (`buildProvinceGraph`, `provinceEdgeCosts`) that the territory assignment service reuses.
462
590