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
@@ -4,7 +4,7 @@ import { SymbolIndex } from '../core/index.js';
4
4
  import { assertRenderDimensions } from '../core/render-budget.js';
5
5
  import { addMapDiagnostic, addMapDiagnostics } from './diagnostic-limit.js';
6
6
  import { ServiceError } from '../core/result.js';
7
- import { assignments, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
7
+ import { assignments, assertSourceEncodingPreserved, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
8
8
  import { BmpImage, rgbKey } from './bmp.js';
9
9
  const defaultMapIndexSourceRoots = {
10
10
  map: ['map'],
@@ -87,7 +87,11 @@ export function selectActiveFiles(files) {
87
87
  }
88
88
  export function parseTextDocument(file) {
89
89
  if (file.bytes.length > MAP_TEXT_MAX_BYTES) {
90
- throw new ServiceError('MAP_TEXT_FILE_LIMIT', 'Map text table exceeds the supported 16 MiB parsing limit', { bytes: file.bytes.length, maximumBytes: MAP_TEXT_MAX_BYTES });
90
+ throw new ServiceError('MAP_TEXT_FILE_LIMIT', `Map text table ${file.relativePath} exceeds the supported 16 MiB parsing limit (${file.bytes.length} bytes)`, {
91
+ relativePath: file.relativePath,
92
+ bytes: file.bytes.length,
93
+ maximumBytes: MAP_TEXT_MAX_BYTES,
94
+ });
91
95
  }
92
96
  const decoded = decodeSource(file.bytes);
93
97
  const lines = [];
@@ -101,7 +105,11 @@ export function parseTextDocument(file) {
101
105
  const text = full.replace(/(?:\r\n|\n|\r)$/u, '');
102
106
  const start = match.index;
103
107
  if (index >= MAP_TEXT_MAX_RECORDS) {
104
- throw new ServiceError('MAP_TEXT_RECORD_LIMIT', 'Map text table exceeds the supported record limit', { records: index + 1, maximumRecords: MAP_TEXT_MAX_RECORDS });
108
+ throw new ServiceError('MAP_TEXT_RECORD_LIMIT', `Map text table ${file.relativePath} exceeds the supported record limit`, {
109
+ relativePath: file.relativePath,
110
+ records: index + 1,
111
+ maximumRecords: MAP_TEXT_MAX_RECORDS,
112
+ });
105
113
  }
106
114
  lines.push({ index, start, end: start + text.length, fullEnd: start + full.length, text });
107
115
  index += 1;
@@ -111,6 +119,7 @@ export function parseTextDocument(file) {
111
119
  return { ...decoded, file, lines };
112
120
  }
113
121
  export function encodeTextDocument(document, text) {
122
+ assertSourceEncodingPreserved(document, document.file.displayPath);
114
123
  return encodeSource(text, document.encoding);
115
124
  }
116
125
  function lineLocation(document, line) {
@@ -1114,6 +1123,9 @@ export class MapWorkspaceIndex {
1114
1123
  railwayFile;
1115
1124
  /** The active heightmap, when the scan included it; decoded on first use. */
1116
1125
  heightmapFile;
1126
+ /** The active river and terrain bitmaps, when the scan included them (border quality). */
1127
+ riversFile;
1128
+ terrainBitmapFile;
1117
1129
  #heightmap;
1118
1130
  /**
1119
1131
  * Map height (the Y coordinate) at a bitmap pixel: the heightmap value divided by ten, as the
@@ -1297,6 +1309,8 @@ export class MapWorkspaceIndex {
1297
1309
  ? undefined
1298
1310
  : parseTerrainCategories(terrainFiles, this.diagnostics);
1299
1311
  this.heightmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'heightmap', 'heightmap.bmp'), this.sourceRoots.map);
1312
+ this.riversFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'rivers', 'rivers.bmp'), this.sourceRoots.map);
1313
+ this.terrainBitmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'terrain', 'terrain.bmp'), this.sourceRoots.map);
1300
1314
  const buildingFiles = this.activeFiles.all.filter((file) => /^common\/buildings\/[^/]+\.txt$/iu.test(normalizedPath(file.relativePath)));
1301
1315
  const buildings = parseBuildingDefinitions(buildingFiles, []);
1302
1316
  this.buildingDefinitions = buildings.definitions;
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
3
- import { compareCodeUnits, deterministicId, sha256Bytes } from '../core/canonical.js';
3
+ import { canonicalJson, compareCodeUnits, deterministicId, sha256Bytes, } from '../core/canonical.js';
4
4
  import { ServiceError } from '../core/result.js';
5
5
  import { applyReplacements, assignments, encodeSource, parseLocalisation, parseClausewitz, } from '../core/source/index.js';
6
6
  import { rgbKey } from './bmp.js';
@@ -8,9 +8,12 @@ import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES,
8
8
  import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
9
9
  import { planReferenceRemap, ReferenceRemapError } from './references.js';
10
10
  import { generateWorld } from './world.js';
11
+ import { resolveEdge } from './world-edge.js';
11
12
  import { planMapPositions, planSupplyRebuild } from './derived.js';
12
- import { buildWorldContext } from './world-context.js';
13
- import { reconcileWorldOutputs } from './world-outputs.js';
13
+ import { buildWorldContext, countryTagCatalog, WORLD_COUNTRY_TAG_FILE, } from './world-context.js';
14
+ import { planWorldOutputs } from './world-outputs.js';
15
+ import { resolvePartitionMode } from './partition.js';
16
+ import { unsupportedGeographyParts } from './relief/index.js';
14
17
  function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
15
18
  if (iteration % stride !== 0)
16
19
  return;
@@ -38,6 +41,8 @@ async function completeCooperatively(steps, signal) {
38
41
  yieldDeadline = performance.now() + 8;
39
42
  }
40
43
  }
44
+ /** Paths listed per kind in diagnostics and tool results; the artifact lists them all. */
45
+ export const WORLD_OUTPUT_PATH_SAMPLE = 200;
41
46
  function normalizeRelative(value) {
42
47
  return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
43
48
  }
@@ -771,6 +776,7 @@ function proposedChanges(changes) {
771
776
  /** Diagnostics an operation raises while applying, drained into the plan after it succeeds. */
772
777
  const diagnosticsByPlan = new WeakMap();
773
778
  const replacedRootsByPlan = new WeakMap();
779
+ const worldOutputsByPlan = new WeakMap();
774
780
  function currentIndex(base, changes) {
775
781
  return indexWithProposedChanges(base, proposedChanges(changes), replacedRootsByPlan.get(changes) ?? []);
776
782
  }
@@ -3272,33 +3278,100 @@ function descriptorWithReplacePaths(index, roots) {
3272
3278
  const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
3273
3279
  return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
3274
3280
  }
3275
- function applyCreateWorld(base, changes, operation, context) {
3281
+ function colormapPaletteOverrides(palette) {
3282
+ return Object.fromEntries(Object.entries(palette).flatMap(([name, color]) => color === undefined ? [] : [[name, [color.r, color.g, color.b]]]));
3283
+ }
3284
+ function applyCreateWorld(base, changes, operation, context, signal) {
3285
+ const geography = context.geographyModels?.get(operation.id);
3286
+ const hasGeography = operation.geography !== undefined || operation.geographyPath !== undefined;
3287
+ if (hasGeography) {
3288
+ if (geography === undefined)
3289
+ throw new ServiceError('MAP_GEOGRAPHY_NOT_PREPARED', 'Geography models must be prepared by the authorized workspace service');
3290
+ if (!geography.valid)
3291
+ throw new ServiceError('MAP_GEOGRAPHY_INVALID', 'The geography model has errors; fix them (see the MAP_GEOGRAPHY_* diagnostics and map-geography-report.json) before generating a world', { modelHash: geography.geography.hash });
3292
+ const pending = unsupportedGeographyParts(geography.geography);
3293
+ if (pending.length > 0)
3294
+ throw new ServiceError('MAP_GEOGRAPHY_SYNTHESIS_UNAVAILABLE', `The geography model is valid and its relief, rivers and lakes can be synthesized, but this version cannot yet use ${pending.join(', ')} (the climate and biome stage); remove them from the model to generate relief and rivers only, or use preview:true to review the model overlay and report`, {
3295
+ modelHash: geography.geography.hash,
3296
+ available: ['relief', 'rivers', 'lakes'],
3297
+ unavailable: pending,
3298
+ });
3299
+ }
3300
+ else {
3301
+ if (operation.relief !== undefined)
3302
+ throw new ServiceError('MAP_RELIEF_REQUIRES_GEOGRAPHY', 'relief shapes the terrain synthesised from a geography model; give geography or geographyPath, or remove relief');
3303
+ if (typeof operation.rivers === 'object')
3304
+ throw new ServiceError('MAP_RIVER_DENSITY_REQUIRES_GEOGRAPHY', 'rivers { density } sets the river network of a geography world; give geography or geographyPath, or use an integer count');
3305
+ }
3276
3306
  const reference = context.referenceWorlds?.get(operation.id);
3277
3307
  if (operation.reference !== undefined && reference === undefined)
3278
3308
  throw new ServiceError('MAP_REFERENCE_NOT_PREPARED', 'Reference worlds must be prepared by the authorized workspace service');
3279
3309
  const mapRoot = base.sourceRoots.map[0] ?? 'map';
3280
3310
  const startingScenario = operation.startingScenario ?? reference !== undefined;
3311
+ const worldWidth = operation.width ?? reference?.surface.width ?? 1_024;
3312
+ const worldHeight = operation.height ?? reference?.surface.height ?? 512;
3313
+ const edge = resolveEdge(operation.edge, worldWidth, worldHeight);
3314
+ const look = operation.look;
3315
+ const colormapProfile = operation.colormap?.profile ?? (look === undefined ? 'legacy' : 'calibrated');
3316
+ if (look === 'custom' && operation.colormap?.palette === undefined)
3317
+ throw new ServiceError('MAP_LOOK_CUSTOM_PALETTE_REQUIRED', 'look "custom" needs colormap.palette with at least one class colour');
3318
+ if (colormapProfile === 'legacy' && look !== undefined)
3319
+ throw new ServiceError('MAP_LOOK_REQUIRES_CALIBRATED_COLORMAP', 'look selects a calibrated palette; remove colormap.profile "legacy" or look');
3320
+ const workspaceCountries = operation.countries !== undefined && !Array.isArray(operation.countries)
3321
+ ? operation.countries
3322
+ : undefined;
3323
+ const tagCatalog = countryTagCatalog(base);
3324
+ if (workspaceCountries !== undefined) {
3325
+ const declared = new Set(tagCatalog.static);
3326
+ const dynamicTags = new Set(tagCatalog.dynamic);
3327
+ for (const realm of workspaceCountries.realms) {
3328
+ if (dynamicTags.has(realm.tag))
3329
+ throw new ServiceError('MAP_WORLD_REALM_TAG_DYNAMIC', `${realm.tag} is a dynamic tag; a world can only place countries with static tags`, { tag: realm.tag });
3330
+ if (!declared.has(realm.tag))
3331
+ throw new ServiceError('MAP_WORLD_REALM_TAG_UNKNOWN', `No active common/country_tags file other than the generated world's declares ${realm.tag}`, { tag: realm.tag });
3332
+ }
3333
+ }
3334
+ const partitionMode = resolvePartitionMode(operation.partition, hasGeography);
3335
+ if (partitionMode === 'legacy' && operation.partitionTuning !== undefined)
3336
+ throw new ServiceError('MAP_PARTITION_TUNING_REQUIRES_FEATURE_AWARE', 'partitionTuning applies to the feature-aware partition; set partition to "feature-aware" or remove it');
3281
3337
  const world = generateWorld({
3282
3338
  startingScenario,
3283
3339
  ...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
3284
- width: operation.width ?? reference?.surface.width ?? 1_024,
3285
- height: operation.height ?? reference?.surface.height ?? 512,
3340
+ width: worldWidth,
3341
+ height: worldHeight,
3286
3342
  seed: operation.seed ?? 1,
3287
3343
  landCoverage: operation.landCoverage ?? 0.35,
3288
3344
  continents: operation.continents ?? 3,
3289
3345
  ...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
3290
3346
  landProvincePixels: operation.landProvincePixels ?? 400,
3291
3347
  seaProvincePixels: operation.seaProvincePixels ?? 2_500,
3348
+ provinceSizesDefaulted: operation.landProvincePixels === undefined && operation.seaProvincePixels === undefined,
3292
3349
  provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
3293
3350
  landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
3294
3351
  seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
3295
- countries: operation.countries ??
3352
+ countries: workspaceCountries?.realms.map((realm) => ({
3353
+ tag: realm.tag,
3354
+ name: realm.tag,
3355
+ ...(realm.weight === undefined ? {} : { share: realm.weight }),
3356
+ })) ??
3357
+ operation.countries ??
3296
3358
  reference?.countries ?? [
3297
3359
  { tag: 'X01', name: 'Aster' },
3298
3360
  { tag: 'X02', name: 'Brum' },
3299
3361
  { tag: 'X03', name: 'Corvel' },
3300
3362
  { tag: 'X04', name: 'Doran' },
3301
3363
  ],
3364
+ ...(workspaceCountries === undefined
3365
+ ? {}
3366
+ : {
3367
+ realms: {
3368
+ list: workspaceCountries.realms,
3369
+ ...(workspaceCountries.unowned === undefined
3370
+ ? {}
3371
+ : { unowned: workspaceCountries.unowned }),
3372
+ islands: workspaceCountries.islands ?? 'nearest',
3373
+ },
3374
+ }),
3302
3375
  ...(reference === undefined
3303
3376
  ? {}
3304
3377
  : {
@@ -3310,8 +3383,47 @@ function applyCreateWorld(base, changes, operation, context) {
3310
3383
  ? {}
3311
3384
  : { continentNames: operation.continentNames }),
3312
3385
  startDate: operation.startDate ?? '1936.1.1',
3313
- rivers: operation.rivers ?? (reference === undefined ? 8 : 80),
3314
- }, buildWorldContext(base));
3386
+ rivers: typeof operation.rivers === 'number' ? operation.rivers : reference === undefined ? 8 : 80,
3387
+ ...(geography === undefined
3388
+ ? {}
3389
+ : {
3390
+ geography: geography.geography,
3391
+ ...(operation.relief === undefined
3392
+ ? {}
3393
+ : {
3394
+ relief: {
3395
+ ...(operation.relief.scale === undefined
3396
+ ? {}
3397
+ : { scale: operation.relief.scale }),
3398
+ ...(operation.relief.detail === undefined
3399
+ ? {}
3400
+ : { detail: operation.relief.detail }),
3401
+ },
3402
+ }),
3403
+ ...(typeof operation.rivers === 'object'
3404
+ ? { riverDensity: operation.rivers.density }
3405
+ : typeof operation.rivers === 'number'
3406
+ ? { riverCount: operation.rivers }
3407
+ : {}),
3408
+ }),
3409
+ ...(edge.mode === 'none' ? {} : { edge }),
3410
+ ...(operation.ambientFrame === true ? { ambientFrame: true } : {}),
3411
+ colormapProfile,
3412
+ ...(partitionMode === 'legacy'
3413
+ ? {}
3414
+ : {
3415
+ partition: {
3416
+ mode: partitionMode,
3417
+ ...(operation.partitionTuning === undefined
3418
+ ? {}
3419
+ : { tuning: operation.partitionTuning }),
3420
+ },
3421
+ }),
3422
+ ...(look === undefined ? {} : { look }),
3423
+ ...(operation.colormap?.palette === undefined
3424
+ ? {}
3425
+ : { colormapPalette: colormapPaletteOverrides(operation.colormap.palette) }),
3426
+ }, buildWorldContext(base), { signal });
3315
3427
  const roots = operation.replaceBaseGame === false
3316
3428
  ? []
3317
3429
  : [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
@@ -3319,7 +3431,7 @@ function applyCreateWorld(base, changes, operation, context) {
3319
3431
  for (const file of world.files)
3320
3432
  addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
3321
3433
  // Remove only unchanged outputs recorded by a previous generation, before indexing new IDs.
3322
- for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
3434
+ for (const file of planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined }).changes)
3323
3435
  if (file.content === null)
3324
3436
  addChange(changes, file.relativePath, null, operation.id, 'text/plain');
3325
3437
  // Start from empty derived files so no base-game position or supply line survives.
@@ -3339,8 +3451,16 @@ function applyCreateWorld(base, changes, operation, context) {
3339
3451
  const positioned = currentIndex(base, changes);
3340
3452
  for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
3341
3453
  addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3342
- for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
3454
+ const outputs = planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined });
3455
+ for (const file of outputs.changes)
3343
3456
  addChange(changes, file.relativePath, file.content, operation.id, file.mediaType ?? 'text/plain');
3457
+ worldOutputsByPlan.set(changes, {
3458
+ operationId: operation.id,
3459
+ written: outputs.written,
3460
+ retired: outputs.retired,
3461
+ released: outputs.released,
3462
+ ...(world.realms === undefined ? {} : { realms: world.realms }),
3463
+ });
3344
3464
  return [
3345
3465
  ...(reference?.diagnostics ?? []).map((message) => ({
3346
3466
  code: 'MAP_REFERENCE_INTERPRETATION',
@@ -3349,6 +3469,14 @@ function applyCreateWorld(base, changes, operation, context) {
3349
3469
  operationId: operation.id,
3350
3470
  message,
3351
3471
  })),
3472
+ ...world.notes.map((note) => ({
3473
+ code: note.code,
3474
+ severity: note.severity,
3475
+ category: 'map',
3476
+ operationId: operation.id,
3477
+ message: note.message,
3478
+ ...(note.details === undefined ? {} : { details: note.details }),
3479
+ })),
3352
3480
  ...(world.warnings.length === 0
3353
3481
  ? []
3354
3482
  : [
@@ -3361,6 +3489,24 @@ function applyCreateWorld(base, changes, operation, context) {
3361
3489
  details: { warningCount: world.warnings.length, samples: world.warnings.slice(0, 5) },
3362
3490
  },
3363
3491
  ]),
3492
+ worldOutputsDiagnostic(operation.id, outputs),
3493
+ ...worldTagDiagnostics(operation.id, tagCatalog, world, workspaceCountries !== undefined),
3494
+ ...worldRealmDiagnostics(operation.id, world),
3495
+ ...(outputs.released.length === 0
3496
+ ? []
3497
+ : [
3498
+ {
3499
+ code: 'MAP_WORLD_OUTPUT_RELEASED',
3500
+ severity: 'info',
3501
+ category: 'map',
3502
+ operationId: operation.id,
3503
+ message: `${outputs.released.length} edited country file(s) of the previous world are left to the workspace and no longer recorded as generated`,
3504
+ details: {
3505
+ count: outputs.released.length,
3506
+ files: outputs.released.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
3507
+ },
3508
+ },
3509
+ ]),
3364
3510
  {
3365
3511
  code: 'MAP_WORLD_CREATED',
3366
3512
  severity: 'info',
@@ -3383,6 +3529,107 @@ function applyCreateWorld(base, changes, operation, context) {
3383
3529
  ]),
3384
3530
  ];
3385
3531
  }
3532
+ /** Tag count observed to overflow the flag sheet of one 1.19 Windows build; never a block. */
3533
+ export const WORLD_TAG_COUNT_OBSERVED_LIMIT = 439;
3534
+ function worldTagDiagnostics(operationId, catalog, world, workspaceOwned) {
3535
+ const declared = new Set(catalog.static);
3536
+ const generated = workspaceOwned
3537
+ ? 0
3538
+ : worldGeneratedTags(world).filter((tag) => !declared.has(tag)).length;
3539
+ const total = declared.size + generated + catalog.dynamic.length;
3540
+ const details = {
3541
+ static: declared.size + generated,
3542
+ generated,
3543
+ dynamic: catalog.dynamic.length,
3544
+ total,
3545
+ observedLimit: WORLD_TAG_COUNT_OBSERVED_LIMIT,
3546
+ };
3547
+ return [
3548
+ {
3549
+ code: 'MAP_WORLD_TAG_COUNT',
3550
+ severity: 'info',
3551
+ category: 'map',
3552
+ operationId,
3553
+ message: `After this write ${total} country tags are declared: ${details.static} static (${generated} written by the world) and ${details.dynamic} dynamic`,
3554
+ details,
3555
+ },
3556
+ ...(total <= WORLD_TAG_COUNT_OBSERVED_LIMIT
3557
+ ? []
3558
+ : [
3559
+ {
3560
+ code: 'MAP_WORLD_TAG_COUNT_HIGH',
3561
+ severity: 'warning',
3562
+ category: 'map',
3563
+ operationId,
3564
+ message: `${total} country tags exceed ${WORLD_TAG_COUNT_OBSERVED_LIMIT}, the count at which one measured game build overflowed its flag sheet`,
3565
+ details,
3566
+ },
3567
+ ]),
3568
+ ];
3569
+ }
3570
+ /** Tags of the country files a generated world writes. */
3571
+ function worldGeneratedTags(world) {
3572
+ const file = world.files.find(({ relativePath }) => relativePath === WORLD_COUNTRY_TAG_FILE);
3573
+ if (file === undefined)
3574
+ return [];
3575
+ return [
3576
+ ...new Set([...file.content.toString('utf8').matchAll(/^([A-Z][A-Z0-9]{2}) =/gmu)].map((match) => match[1])),
3577
+ ];
3578
+ }
3579
+ function worldRealmDiagnostics(operationId, world) {
3580
+ if (world.realms === undefined)
3581
+ return [];
3582
+ const { countries, unownedStates } = world.realms;
3583
+ return [
3584
+ {
3585
+ code: 'MAP_WORLD_REALMS_PLACED',
3586
+ severity: 'info',
3587
+ category: 'map',
3588
+ operationId,
3589
+ message: `Placed ${countries.length} workspace countries; their country files, histories, flags and names stay the workspace's`,
3590
+ details: {
3591
+ countries: countries
3592
+ .slice(0, 100)
3593
+ .map(({ tag, capitalState, states, landShare, targetShare }) => ({
3594
+ tag,
3595
+ capitalState,
3596
+ states: states.length,
3597
+ landShare,
3598
+ targetShare,
3599
+ })),
3600
+ truncated: countries.length > 100,
3601
+ },
3602
+ },
3603
+ ...(unownedStates.length === 0
3604
+ ? []
3605
+ : [
3606
+ {
3607
+ code: 'MAP_WORLD_STATE_UNOWNED',
3608
+ severity: 'info',
3609
+ category: 'map',
3610
+ operationId,
3611
+ message: `${unownedStates.length} state(s) have no owner, as the request asks; whether the game build accepts unowned land is for the mod to verify`,
3612
+ details: { count: unownedStates.length, states: unownedStates.slice(0, 200) },
3613
+ },
3614
+ ]),
3615
+ ];
3616
+ }
3617
+ function worldOutputsDiagnostic(operationId, outputs) {
3618
+ return {
3619
+ code: 'MAP_WORLD_OUTPUTS_RECONCILED',
3620
+ severity: 'info',
3621
+ category: 'map',
3622
+ operationId,
3623
+ message: `The world records ${outputs.written.length} generated file(s), retires ${outputs.retired.length} earlier output(s) it no longer writes and releases ${outputs.released.length} to the workspace`,
3624
+ details: {
3625
+ written: outputs.written.length,
3626
+ retired: outputs.retired.length,
3627
+ released: outputs.released.length,
3628
+ retiredFiles: outputs.retired.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
3629
+ truncated: outputs.retired.length > WORLD_OUTPUT_PATH_SAMPLE,
3630
+ },
3631
+ };
3632
+ }
3386
3633
  function applyRegenerateMapPositions(base, changes, operation) {
3387
3634
  const index = currentIndex(base, changes);
3388
3635
  for (const file of planMapPositions(index, operation))
@@ -3456,6 +3703,22 @@ function applyEntityLocator(base, changes, operation) {
3456
3703
  ]);
3457
3704
  addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
3458
3705
  }
3706
+ /**
3707
+ * The operation as the transaction manifest records it. An inline geography model can hold
3708
+ * megabytes; the manifest keeps its SHA-256 and size instead, which still bind the plan hash to
3709
+ * the exact model, and a model given by path is bound by the file's read dependency.
3710
+ */
3711
+ function manifestOperationData(operation) {
3712
+ const data = JSON.parse(JSON.stringify(operation));
3713
+ if (operation.kind === 'create_world' && operation.geography !== undefined) {
3714
+ const canonical = canonicalJson(operation.geography);
3715
+ data.geography = {
3716
+ sha256: sha256Bytes(canonical),
3717
+ bytes: Buffer.byteLength(canonical, 'utf8'),
3718
+ };
3719
+ }
3720
+ return data;
3721
+ }
3459
3722
  function* planMapOperationSteps(base, operations, signal, context = {}) {
3460
3723
  yield* cancellationCheckpoint(signal);
3461
3724
  const changes = new Map();
@@ -3464,6 +3727,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3464
3727
  const allocations = [];
3465
3728
  let expectedChangedBounds;
3466
3729
  const manifestOperations = [];
3730
+ const worldOutputs = [];
3467
3731
  const ids = new Set();
3468
3732
  for (const [operationIndex, operation] of operations.entries()) {
3469
3733
  yield* cancellationCheckpoint(signal, operationIndex);
@@ -3480,7 +3744,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3480
3744
  id: operation.id,
3481
3745
  kind: operation.kind,
3482
3746
  summary: operation.summary ?? operation.kind.replaceAll('_', ' '),
3483
- data: JSON.parse(JSON.stringify(operation)),
3747
+ data: manifestOperationData(operation),
3484
3748
  });
3485
3749
  const changesCheckpoint = new Map([...changes].map(([key, change]) => [
3486
3750
  key,
@@ -3493,8 +3757,13 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3493
3757
  const allocationsCheckpoint = allocations.length;
3494
3758
  const boundsCheckpoint = expectedChangedBounds;
3495
3759
  try {
3496
- if (operation.kind === 'create_world')
3497
- diagnostics.push(...applyCreateWorld(base, changes, operation, context));
3760
+ if (operation.kind === 'create_world') {
3761
+ diagnostics.push(...applyCreateWorld(base, changes, operation, context, signal));
3762
+ const outputs = worldOutputsByPlan.get(changes);
3763
+ worldOutputsByPlan.delete(changes);
3764
+ if (outputs !== undefined)
3765
+ worldOutputs.push(outputs);
3766
+ }
3498
3767
  else if (operation.kind === 'move_state_provinces')
3499
3768
  applyMoveStateProvinces(base, changes, operation);
3500
3769
  else if (operation.kind === 'update_state')
@@ -3571,6 +3840,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3571
3840
  ...(expectedChangedBounds === undefined ? {} : { expectedChangedBounds }),
3572
3841
  finalIndex,
3573
3842
  operations: manifestOperations,
3843
+ ...(worldOutputs.length === 0 ? {} : { worldOutputs }),
3574
3844
  };
3575
3845
  }
3576
3846
  export function planMapOperations(base, operations, signal, context = {}) {