hoi4-agent-tools 4.5.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 (74) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
  3. package/dist/hoi4_agent_tools/map/derived.js +16 -3
  4. package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
  5. package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
  6. package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
  7. package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
  8. package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
  9. package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
  10. package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
  11. package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
  12. package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
  13. package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
  14. package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
  15. package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
  16. package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
  17. package/dist/hoi4_agent_tools/map/operations.js +109 -32
  18. package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
  19. package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +43 -0
  20. package/dist/hoi4_agent_tools/map/service.js +78 -16
  21. package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
  22. package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
  23. package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
  24. package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
  25. package/dist/hoi4_agent_tools/map/territory.js +1108 -0
  26. package/dist/hoi4_agent_tools/map/tool-operations.js +78 -0
  27. package/dist/hoi4_agent_tools/map/validation.js +4 -0
  28. package/dist/hoi4_agent_tools/map/world.js +301 -107
  29. package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
  30. package/dist/hoi4_agent_tools/schemas/map.js +206 -21
  31. package/dist/hoi4_agent_tools/version.js +1 -1
  32. package/docs/map.md +98 -4
  33. package/npm-shrinkwrap.json +2 -2
  34. package/package.json +1 -1
  35. package/schemas/configuration.schema.json +1 -1
  36. package/schemas/continuous-focus-palette.schema.json +1 -1
  37. package/schemas/custom-weighted-pool.schema.json +1 -1
  38. package/schemas/decision-inspect-input.schema.json +1 -1
  39. package/schemas/error-log-input.schema.json +1 -1
  40. package/schemas/event-feature-manifest.schema.json +1 -1
  41. package/schemas/focus-plan.schema.json +1 -1
  42. package/schemas/focus-planning-sidecar.schema.json +1 -1
  43. package/schemas/geography-model.schema.json +13 -1
  44. package/schemas/gui-animation-source.schema.json +1 -1
  45. package/schemas/gui-helper.schema.json +1 -1
  46. package/schemas/gui-scenario.schema.json +1 -1
  47. package/schemas/helper-expansion-request.schema.json +1 -1
  48. package/schemas/helper-expansion-summary.schema.json +1 -1
  49. package/schemas/impact-inspect-input.schema.json +1 -1
  50. package/schemas/map-operation.schema.json +1355 -736
  51. package/schemas/mechanic-test-input.schema.json +1 -1
  52. package/schemas/mod-index-input.schema.json +1 -1
  53. package/schemas/operation-result.schema.json +1 -1
  54. package/schemas/package-check-input.schema.json +1 -1
  55. package/schemas/package-manifest.schema.json +1 -1
  56. package/schemas/probability-analysis-result.schema.json +1 -1
  57. package/schemas/probability-compare-input.schema.json +1 -1
  58. package/schemas/probability-evaluate-input.schema.json +1 -1
  59. package/schemas/probability-inspect-input.schema.json +1 -1
  60. package/schemas/probability-render-input.schema.json +1 -1
  61. package/schemas/probability-scenario-set.schema.json +1 -1
  62. package/schemas/probability-sequence-input.schema.json +1 -1
  63. package/schemas/probability-simulate-input.schema.json +1 -1
  64. package/schemas/probability-sweep-input.schema.json +1 -1
  65. package/schemas/reference-context-input.schema.json +1 -1
  66. package/schemas/reference-read-input.schema.json +1 -1
  67. package/schemas/reference-search-input.schema.json +1 -1
  68. package/schemas/reference-sheets.schema.json +1 -1
  69. package/schemas/scenario-suite.schema.json +1 -1
  70. package/schemas/scenario-test-input.schema.json +1 -1
  71. package/schemas/script-validate-input.schema.json +1 -1
  72. package/schemas/shared-scenario.schema.json +1 -1
  73. package/schemas/source-lookup-input.schema.json +1 -1
  74. package/server.json +2 -2
@@ -9,12 +9,14 @@ import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWork
9
9
  import { planReferenceRemap, ReferenceRemapError } from './references.js';
10
10
  import { generateWorld } from './world.js';
11
11
  import { resolveEdge } from './world-edge.js';
12
+ import { DEFAULT_AUTO_STRAIT_GAP_PX } from './gameplay/crossings.js';
12
13
  import { planMapPositions, planSupplyRebuild } from './derived.js';
13
14
  import { buildWorldContext, countryTagCatalog, WORLD_COUNTRY_TAG_FILE, } from './world-context.js';
14
15
  import { planWorldOutputs } from './world-outputs.js';
15
16
  import { resolvePartitionMode } from './partition.js';
17
+ import { applyAssignTerritory, applyRepartitionStates, } from './territory-operations.js';
16
18
  import { unsupportedGeographyParts } from './relief/index.js';
17
- function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
19
+ export function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
18
20
  if (iteration % stride !== 0)
19
21
  return;
20
22
  signal?.throwIfAborted();
@@ -43,7 +45,7 @@ async function completeCooperatively(steps, signal) {
43
45
  }
44
46
  /** Paths listed per kind in diagnostics and tool results; the artifact lists them all. */
45
47
  export const WORLD_OUTPUT_PATH_SAMPLE = 200;
46
- function normalizeRelative(value) {
48
+ export function normalizeRelative(value) {
47
49
  return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
48
50
  }
49
51
  function numberText(value) {
@@ -61,10 +63,10 @@ function required(value, code, message) {
61
63
  function quote(value) {
62
64
  return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
63
65
  }
64
- function uniqueSorted(values) {
66
+ export function uniqueSorted(values) {
65
67
  return [...new Set(values)].sort((left, right) => left - right);
66
68
  }
67
- function uniqueStrings(values) {
69
+ export function uniqueStrings(values) {
68
70
  return [...new Set(values)].sort((left, right) => compareCodeUnits(left, right));
69
71
  }
70
72
  function lineIndent(text, offset) {
@@ -110,7 +112,7 @@ function blockInsertion(block, documentText, content, newline) {
110
112
  description: 'Insert map source assignment',
111
113
  };
112
114
  }
113
- function cloneState(state) {
115
+ export function cloneState(state) {
114
116
  return {
115
117
  id: state.id,
116
118
  name: state.name,
@@ -145,7 +147,7 @@ function compileHistory(data, indent, newline) {
145
147
  rows.push(...(data.datedHistory ?? []).map((block) => `${indent}\t${block}`));
146
148
  return rows.length === 0 ? `{ }` : `{${newline}${rows.join(newline)}${newline}${indent}}`;
147
149
  }
148
- function compileState(data, newline = '\n') {
150
+ export function compileState(data, newline = '\n') {
149
151
  const resources = data.resources.size === 0
150
152
  ? ''
151
153
  : `\tresources = ${renderScalarMap(data.resources, '\t', newline)}${newline}`;
@@ -438,7 +440,7 @@ function patchDefinitionLine(source, updated) {
438
440
  replacements.set(7, String(updated.continent));
439
441
  return replaceDelimitedFields(line.text, replacements);
440
442
  }
441
- function applyTextRanges(document, replacements) {
443
+ export function applyTextRanges(document, replacements) {
442
444
  const ordered = [...replacements].sort((left, right) => left.start - right.start || left.end - right.end);
443
445
  let cursor = 0;
444
446
  const pieces = [];
@@ -495,7 +497,7 @@ function patchDefinitions(index, updates, removals, additions) {
495
497
  }
496
498
  return applyTextRanges(document, replacements);
497
499
  }
498
- function rootEvidence(records) {
500
+ export function rootEvidence(records) {
499
501
  const groups = new Map();
500
502
  for (const record of records) {
501
503
  const key = `${record.file.rootKind}:${record.file.loadOrder}`;
@@ -532,7 +534,7 @@ function contiguous(ids, start) {
532
534
  }
533
535
  return true;
534
536
  }
535
- function maximumObservedId(values, fallback) {
537
+ export function maximumObservedId(values, fallback) {
536
538
  let maximum = fallback;
537
539
  for (const value of values) {
538
540
  if (!Number.isSafeInteger(value)) {
@@ -745,7 +747,7 @@ export function indexWithProposedChanges(base, changes, replacedRoots = []) {
745
747
  }
746
748
  return MapWorkspaceIndex.build(files, base.sourceRoots, base.sharedIndex.rebuild(files));
747
749
  }
748
- function addChange(changes, relativePath, content, operationId, mediaType) {
750
+ export function addChange(changes, relativePath, content, operationId, mediaType) {
749
751
  const key = normalizeRelative(relativePath);
750
752
  const current = changes.get(key);
751
753
  if (current === undefined) {
@@ -774,10 +776,10 @@ function proposedChanges(changes) {
774
776
  }
775
777
  /** Source folders a new world replaces, per plan, so lower layers stop counting. */
776
778
  /** Diagnostics an operation raises while applying, drained into the plan after it succeeds. */
777
- const diagnosticsByPlan = new WeakMap();
779
+ export const diagnosticsByPlan = new WeakMap();
778
780
  const replacedRootsByPlan = new WeakMap();
779
781
  const worldOutputsByPlan = new WeakMap();
780
- function currentIndex(base, changes) {
782
+ export function currentIndex(base, changes) {
781
783
  return indexWithProposedChanges(base, proposedChanges(changes), replacedRootsByPlan.get(changes) ?? []);
782
784
  }
783
785
  function samePolicy(actual, expected) {
@@ -857,7 +859,7 @@ function requireSplitPolicy(operation) {
857
859
  }
858
860
  return policy;
859
861
  }
860
- function requireSafeLocalisationDocument(document, relativePath) {
862
+ export function requireSafeLocalisationDocument(document, relativePath) {
861
863
  if (document.encoding !== 'utf8-bom') {
862
864
  throw new ServiceError('MAP_STATE_LOCALISATION_ENCODING_INVALID', 'State localisation edits require an existing UTF-8 BOM source file', { file: relativePath, encoding: document.encoding });
863
865
  }
@@ -866,7 +868,7 @@ function requireSafeLocalisationDocument(document, relativePath) {
866
868
  throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation cannot target a malformed source file', { file: relativePath, diagnostics: errors.map(({ code }) => code) });
867
869
  }
868
870
  }
869
- function safeLocalisationRelativePath(index, requested) {
871
+ export function safeLocalisationRelativePath(index, requested) {
870
872
  const value = requested.replaceAll('\\', '/');
871
873
  const segments = value.split('/');
872
874
  if (value === '' ||
@@ -884,7 +886,7 @@ function safeLocalisationRelativePath(index, requested) {
884
886
  }
885
887
  return value;
886
888
  }
887
- function escapedLocalisationValue(value) {
889
+ export function escapedLocalisationValue(value) {
888
890
  return value
889
891
  .replaceAll('\\', '\\\\')
890
892
  .replaceAll('"', '\\"')
@@ -892,7 +894,7 @@ function escapedLocalisationValue(value) {
892
894
  .replaceAll('\r', '\\n')
893
895
  .replaceAll('\n', '\\n');
894
896
  }
895
- function localisationEntryLine(document, entry, value) {
897
+ export function localisationEntryLine(document, entry, value) {
896
898
  const line = document.text.slice(entry.start, entry.end);
897
899
  const opening = line.indexOf('"');
898
900
  if (opening < 0)
@@ -918,7 +920,7 @@ function localisationEntryLine(document, entry, value) {
918
920
  throw new ServiceError('MAP_STATE_LOCALISATION_SOURCE_INVALID', 'State localisation source entry has an unclosed quoted value', { file: document.path, line: entry.line });
919
921
  return `${line.slice(0, opening + 1)}${escapedLocalisationValue(value)}${line.slice(closing)}`;
920
922
  }
921
- function appendLocalisationEntry(document, key, value) {
923
+ export function appendLocalisationEntry(document, key, value) {
922
924
  let englishHeaders = 0;
923
925
  const matcher = /^\s*l_english:\s*(?:#.*)?$/gmu;
924
926
  while (matcher.exec(document.text) !== null && englishHeaders < 2)
@@ -1011,7 +1013,7 @@ function splitScalar(total, ratio, policy) {
1011
1013
  * count of slot buildings. A level applies to the whole state, so a split keeps it in both parts
1012
1014
  * and a merge keeps the highest; slot buildings are divided and summed.
1013
1015
  */
1014
- function isLevelStateBuilding(index, building) {
1016
+ export function isLevelStateBuilding(index, building) {
1015
1017
  const definition = index.buildingDefinitions.get(building);
1016
1018
  if (definition !== undefined)
1017
1019
  return !definition.sharesSlots && !definition.provincial;
@@ -1079,7 +1081,7 @@ function mergeBounds(left, right) {
1079
1081
  count: left.count + right.count,
1080
1082
  };
1081
1083
  }
1082
- function regionMembershipId(index, provinceId) {
1084
+ export function regionMembershipId(index, provinceId) {
1083
1085
  const regions = index.regionForProvince(provinceId);
1084
1086
  if (regions.length !== 1)
1085
1087
  throw new ServiceError('MAP_REGION_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} must have exactly one strategic region`);
@@ -1091,7 +1093,7 @@ function stateMembershipId(index, provinceId) {
1091
1093
  throw new ServiceError('MAP_STATE_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} belongs to multiple states`);
1092
1094
  return states[0]?.id;
1093
1095
  }
1094
- function commitState(changes, state, data, fields, operationId, datedRewrite, datedRewriteFollowsProvinces = datedRewrite !== undefined) {
1096
+ export function commitState(changes, state, data, fields, operationId, datedRewrite, datedRewriteFollowsProvinces = datedRewrite !== undefined) {
1095
1097
  // Distributed dated history already follows the new province membership.
1096
1098
  assertDatedStateHistorySafe(state, datedRewriteFollowsProvinces
1097
1099
  ? { ...fields, provinces: false, victoryPoints: false, buildings: false }
@@ -1184,7 +1186,7 @@ function distributeDatedHistory(index, state, moved, ratio) {
1184
1186
  }
1185
1187
  return result;
1186
1188
  }
1187
- function assertDatedStateHistorySafe(state, fields, operationId) {
1189
+ export function assertDatedStateHistorySafe(state, fields, operationId) {
1188
1190
  if (state.historyBlock === undefined)
1189
1191
  return;
1190
1192
  for (const entry of assignments(state.historyBlock)) {
@@ -1272,14 +1274,13 @@ function exactNumericMap(values, label) {
1272
1274
  }
1273
1275
  return result;
1274
1276
  }
1275
- function applyUpdateState(base, changes, operation) {
1276
- const index = currentIndex(base, changes);
1277
- const state = index.statesById.get(operation.stateId);
1277
+ /** Apply one state update against an index that already holds every earlier change. */
1278
+ export function updateStateAgainst(index, changes, stateId, values, operationId) {
1279
+ const state = index.statesById.get(stateId);
1278
1280
  if (state === undefined)
1279
- throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${operation.stateId} does not exist`);
1281
+ throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${stateId} does not exist`);
1280
1282
  const desired = cloneState(state);
1281
1283
  const fields = {};
1282
- const values = operation.changes;
1283
1284
  if (values.capital !== undefined) {
1284
1285
  if (values.victoryPoints === undefined)
1285
1286
  throw new ServiceError('MAP_STATE_CAPITAL_ASSERTION_REQUIRES_VICTORY_POINTS', 'A derived state-capital assertion requires an exact victoryPoints payload');
@@ -1352,7 +1353,25 @@ function applyUpdateState(base, changes, operation) {
1352
1353
  desired.provinceBuildings = provinceBuildings;
1353
1354
  fields.buildings = true;
1354
1355
  }
1355
- commitState(changes, state, desired, fields, operation.id);
1356
+ commitState(changes, state, desired, fields, operationId);
1357
+ }
1358
+ function applyUpdateState(base, changes, operation) {
1359
+ updateStateAgainst(currentIndex(base, changes), changes, operation.stateId, operation.changes, operation.id);
1360
+ }
1361
+ /**
1362
+ * Update many states in one operation: the index is built once and each state is patched in
1363
+ * place, so a politics pass over thousands of states costs one validation. All or nothing.
1364
+ */
1365
+ function applyUpdateStates(base, changes, operation) {
1366
+ const seen = new Set();
1367
+ for (const { stateId } of operation.updates) {
1368
+ if (seen.has(stateId))
1369
+ throw new ServiceError('MAP_UPDATE_STATES_DUPLICATE', `State ${stateId} appears twice in updates; merge its changes into one entry`, { stateId });
1370
+ seen.add(stateId);
1371
+ }
1372
+ const index = currentIndex(base, changes);
1373
+ for (const { stateId, changes: values } of operation.updates)
1374
+ updateStateAgainst(index, changes, stateId, values, operation.id);
1356
1375
  }
1357
1376
  function applyMoveStateProvinces(base, changes, operation) {
1358
1377
  const policy = requireMovePolicy(operation);
@@ -1561,7 +1580,7 @@ function requireMergeStatePolicy(operation) {
1561
1580
  throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'State merge requires a complete explicit value, ownership, port, network, position, and province-data distribution policy');
1562
1581
  }
1563
1582
  }
1564
- function removalContent(index, file, label) {
1583
+ export function removalContent(index, file, label) {
1565
1584
  const lowerExists = index.sourceFiles.some((candidate) => normalizeRelative(candidate.relativePath) === normalizeRelative(file.relativePath) &&
1566
1585
  candidate.displayPath !== file.displayPath &&
1567
1586
  candidate.loadOrder < file.loadOrder);
@@ -2787,7 +2806,7 @@ function handleSimpleOperation(base, changes, operation) {
2787
2806
  addChange(changes, file.relativePath, content, operation.id, 'text/plain');
2788
2807
  }
2789
2808
  }
2790
- function buildingPositionLine(value) {
2809
+ export function buildingPositionLine(value) {
2791
2810
  return [
2792
2811
  value.stateId,
2793
2812
  value.building,
@@ -3317,6 +3336,17 @@ function applyCreateWorld(base, changes, operation, context, signal) {
3317
3336
  throw new ServiceError('MAP_LOOK_CUSTOM_PALETTE_REQUIRED', 'look "custom" needs colormap.palette with at least one class colour');
3318
3337
  if (colormapProfile === 'legacy' && look !== undefined)
3319
3338
  throw new ServiceError('MAP_LOOK_REQUIRES_CALIBRATED_COLORMAP', 'look selects a calibrated palette; remove colormap.profile "legacy" or look');
3339
+ if (!hasGeography && operation.development?.model === 'geography')
3340
+ throw new ServiceError('MAP_DEVELOPMENT_REQUIRES_GEOGRAPHY', 'development.model "geography" plans the economy from a geography model; give geography or geographyPath, or use "legacy"');
3341
+ if (!hasGeography && operation.autoStraits !== undefined && operation.autoStraits !== false)
3342
+ throw new ServiceError('MAP_AUTO_STRAITS_REQUIRES_GEOGRAPHY', 'autoStraits detects channels between the landmasses of a geography world; give geography or geographyPath, or remove autoStraits');
3343
+ const autoStraits = operation.autoStraits === undefined || operation.autoStraits === false
3344
+ ? undefined
3345
+ : {
3346
+ maxGapPx: operation.autoStraits === true
3347
+ ? DEFAULT_AUTO_STRAIT_GAP_PX
3348
+ : (operation.autoStraits.maxGapPx ?? DEFAULT_AUTO_STRAIT_GAP_PX),
3349
+ };
3320
3350
  const workspaceCountries = operation.countries !== undefined && !Array.isArray(operation.countries)
3321
3351
  ? operation.countries
3322
3352
  : undefined;
@@ -3334,6 +3364,12 @@ function applyCreateWorld(base, changes, operation, context, signal) {
3334
3364
  const partitionMode = resolvePartitionMode(operation.partition, hasGeography);
3335
3365
  if (partitionMode === 'legacy' && operation.partitionTuning !== undefined)
3336
3366
  throw new ServiceError('MAP_PARTITION_TUNING_REQUIRES_FEATURE_AWARE', 'partitionTuning applies to the feature-aware partition; set partition to "feature-aware" or remove it');
3367
+ if (operation.synthesisCheck === true && context.synthesisCheck !== true)
3368
+ throw new ServiceError('MAP_SYNTHESIS_CHECK_REQUIRES_PREVIEW', "synthesisCheck checks the model's rivers without writing; call hoi4.map_rewrite with preview: true, or remove synthesisCheck to write the world");
3369
+ if (operation.synthesisCheck === true && !hasGeography)
3370
+ throw new ServiceError('MAP_SYNTHESIS_CHECK_REQUIRES_GEOGRAPHY', 'synthesisCheck runs the relief and river synthesis of a geography model; give geography or geographyPath');
3371
+ if (partitionMode === 'legacy' && operation.borderQuality === 'enforce')
3372
+ throw new ServiceError('MAP_BORDER_QUALITY_ENFORCE_NEEDS_FEATURE_AWARE', 'borderQuality "enforce" needs the feature-aware partition: the legacy partition cuts at chance level against rivers and slopes and would block every write. Set partition to "feature-aware", or use "report"');
3337
3373
  const world = generateWorld({
3338
3374
  startingScenario,
3339
3375
  ...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
@@ -3408,6 +3444,10 @@ function applyCreateWorld(base, changes, operation, context, signal) {
3408
3444
  }),
3409
3445
  ...(edge.mode === 'none' ? {} : { edge }),
3410
3446
  ...(operation.ambientFrame === true ? { ambientFrame: true } : {}),
3447
+ ...(autoStraits === undefined ? {} : { autoStraits }),
3448
+ ...(operation.development?.model === undefined
3449
+ ? {}
3450
+ : { developmentModel: operation.development.model }),
3411
3451
  colormapProfile,
3412
3452
  ...(partitionMode === 'legacy'
3413
3453
  ? {}
@@ -3423,7 +3463,25 @@ function applyCreateWorld(base, changes, operation, context, signal) {
3423
3463
  ...(operation.colormap?.palette === undefined
3424
3464
  ? {}
3425
3465
  : { colormapPalette: colormapPaletteOverrides(operation.colormap.palette) }),
3426
- }, buildWorldContext(base), { signal });
3466
+ }, buildWorldContext(base), { signal, ...(operation.synthesisCheck === true ? { riverCheck: true } : {}) });
3467
+ if (operation.synthesisCheck === true)
3468
+ return [
3469
+ ...world.notes.map((note) => ({
3470
+ code: note.code,
3471
+ severity: note.severity,
3472
+ category: 'map',
3473
+ operationId: operation.id,
3474
+ message: note.message,
3475
+ ...(note.details === undefined ? {} : { details: note.details }),
3476
+ })),
3477
+ {
3478
+ code: 'MAP_SYNTHESIS_CHECK_READY',
3479
+ severity: 'info',
3480
+ category: 'map',
3481
+ operationId: operation.id,
3482
+ message: `Every river of the model reaches its mouth over the synthesised relief of a ${world.summary.width}x${world.summary.height} map; nothing was written`,
3483
+ },
3484
+ ];
3427
3485
  const roots = operation.replaceBaseGame === false
3428
3486
  ? []
3429
3487
  : [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
@@ -3449,7 +3507,10 @@ function applyCreateWorld(base, changes, operation, context, signal) {
3449
3507
  for (const file of planMapPositions(generated, { mode: 'replace' }))
3450
3508
  addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3451
3509
  const positioned = currentIndex(base, changes);
3452
- for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
3510
+ for (const file of planSupplyRebuild(positioned, {
3511
+ mode: 'replace',
3512
+ ...(world.railCost === undefined ? {} : { railCost: world.railCost }),
3513
+ }))
3453
3514
  addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3454
3515
  const outputs = planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined });
3455
3516
  for (const file of outputs.changes)
@@ -3644,7 +3705,7 @@ function applyRebuildSupply(base, changes, operation) {
3644
3705
  * Rewrite the script references of one entity kind in the proposed changes. Returns the number
3645
3706
  * of script files changed; an empty result is an error only when the caller asks for it.
3646
3707
  */
3647
- function remapScriptReferences(base, changes, operationId, request) {
3708
+ export function remapScriptReferences(base, changes, operationId, request) {
3648
3709
  const index = currentIndex(base, changes);
3649
3710
  let plan;
3650
3711
  try {
@@ -3727,6 +3788,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3727
3788
  const allocations = [];
3728
3789
  let expectedChangedBounds;
3729
3790
  const manifestOperations = [];
3791
+ const territory = [];
3730
3792
  const worldOutputs = [];
3731
3793
  const ids = new Set();
3732
3794
  for (const [operationIndex, operation] of operations.entries()) {
@@ -3756,6 +3818,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3756
3818
  ]));
3757
3819
  const allocationsCheckpoint = allocations.length;
3758
3820
  const boundsCheckpoint = expectedChangedBounds;
3821
+ const territoryCheckpoint = territory.length;
3759
3822
  try {
3760
3823
  if (operation.kind === 'create_world') {
3761
3824
  diagnostics.push(...applyCreateWorld(base, changes, operation, context, signal));
@@ -3766,8 +3829,20 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3766
3829
  }
3767
3830
  else if (operation.kind === 'move_state_provinces')
3768
3831
  applyMoveStateProvinces(base, changes, operation);
3832
+ else if (operation.kind === 'assign_territory') {
3833
+ const outcome = yield* applyAssignTerritory(base, changes, allocations, operation, signal);
3834
+ diagnostics.push(...outcome.diagnostics);
3835
+ territory.push(outcome.report);
3836
+ }
3837
+ else if (operation.kind === 'repartition_states') {
3838
+ const outcome = yield* applyRepartitionStates(base, changes, allocations, operation, signal);
3839
+ diagnostics.push(...outcome.diagnostics);
3840
+ territory.push(outcome.report);
3841
+ }
3769
3842
  else if (operation.kind === 'update_state')
3770
3843
  applyUpdateState(base, changes, operation);
3844
+ else if (operation.kind === 'update_states')
3845
+ applyUpdateStates(base, changes, operation);
3771
3846
  else if (operation.kind === 'split_state' || operation.kind === 'create_state')
3772
3847
  yield* applySplitState(base, changes, allocations, operation, signal);
3773
3848
  else if (operation.kind === 'merge_states')
@@ -3807,6 +3882,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3807
3882
  changes.set(key, change);
3808
3883
  allocations.length = allocationsCheckpoint;
3809
3884
  expectedChangedBounds = boundsCheckpoint;
3885
+ territory.length = territoryCheckpoint;
3810
3886
  const serviceError = error instanceof ServiceError
3811
3887
  ? error
3812
3888
  : new ServiceError('MAP_OPERATION_FAILED', error instanceof Error ? error.message : String(error));
@@ -3840,6 +3916,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
3840
3916
  ...(expectedChangedBounds === undefined ? {} : { expectedChangedBounds }),
3841
3917
  finalIndex,
3842
3918
  operations: manifestOperations,
3919
+ ...(territory.length === 0 ? {} : { territory }),
3843
3920
  ...(worldOutputs.length === 0 ? {} : { worldOutputs }),
3844
3921
  };
3845
3922
  }