hoi4-agent-tools 3.10.0 → 4.0.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 (150) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +115 -147
  3. package/dist/bin/http.js +10 -0
  4. package/dist/bin/setup.js +3 -2
  5. package/dist/bin/stdio.js +10 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
  7. package/dist/hoi4_agent_tools/core/canonical.js +18 -4
  8. package/dist/hoi4_agent_tools/core/configuration.js +7 -0
  9. package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
  10. package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
  11. package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
  12. package/dist/hoi4_agent_tools/core/engine.js +29 -2
  13. package/dist/hoi4_agent_tools/core/index.js +2 -2
  14. package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
  15. package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
  16. package/dist/hoi4_agent_tools/core/job-store.js +78 -5
  17. package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
  18. package/dist/hoi4_agent_tools/core/job-worker-host.js +215 -116
  19. package/dist/hoi4_agent_tools/core/job-worker.js +90 -36
  20. package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
  21. package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
  22. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
  23. package/dist/hoi4_agent_tools/core/server-registry.js +22 -5
  24. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
  25. package/dist/hoi4_agent_tools/core/source/cache.js +20 -10
  26. package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
  27. package/dist/hoi4_agent_tools/core/transactions.js +114 -23
  28. package/dist/hoi4_agent_tools/core/workspace.js +84 -8
  29. package/dist/hoi4_agent_tools/decision/ai.js +1 -1
  30. package/dist/hoi4_agent_tools/decision/service.js +28 -0
  31. package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
  32. package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
  33. package/dist/hoi4_agent_tools/event/operations.js +14 -1
  34. package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
  35. package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
  36. package/dist/hoi4_agent_tools/gui/validators.js +8 -6
  37. package/dist/hoi4_agent_tools/map/bmp.js +1 -1
  38. package/dist/hoi4_agent_tools/map/derived.js +748 -0
  39. package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
  40. package/dist/hoi4_agent_tools/map/model.js +103 -0
  41. package/dist/hoi4_agent_tools/map/operations.js +427 -58
  42. package/dist/hoi4_agent_tools/map/references.js +483 -0
  43. package/dist/hoi4_agent_tools/map/service.js +136 -61
  44. package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
  45. package/dist/hoi4_agent_tools/map/validation.js +3 -0
  46. package/dist/hoi4_agent_tools/map/world.js +1280 -0
  47. package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
  48. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
  49. package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
  50. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
  51. package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
  52. package/dist/hoi4_agent_tools/mcp/tools/analysis.js +25 -5
  53. package/dist/hoi4_agent_tools/mcp/tools/event.js +18 -29
  54. package/dist/hoi4_agent_tools/mcp/tools/focus.js +4 -12
  55. package/dist/hoi4_agent_tools/mcp/tools/gui.js +3 -3
  56. package/dist/hoi4_agent_tools/mcp/tools/job.js +2 -2
  57. package/dist/hoi4_agent_tools/mcp/tools/map.js +13 -16
  58. package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
  59. package/dist/hoi4_agent_tools/mcp/tools/reference.js +22 -5
  60. package/dist/hoi4_agent_tools/mcp/tools/technology.js +16 -29
  61. package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
  62. package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +31 -8
  63. package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
  64. package/dist/hoi4_agent_tools/probability/operations.js +175 -13
  65. package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
  66. package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
  67. package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
  68. package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
  69. package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
  70. package/dist/hoi4_agent_tools/reference/operations.js +88 -19
  71. package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
  72. package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
  73. package/dist/hoi4_agent_tools/reference/service.js +6 -0
  74. package/dist/hoi4_agent_tools/schemas/event.js +78 -19
  75. package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
  76. package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
  77. package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
  78. package/dist/hoi4_agent_tools/schemas/map.js +122 -3
  79. package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
  80. package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
  81. package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
  82. package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
  83. package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
  84. package/dist/hoi4_agent_tools/schemas/technology.js +79 -21
  85. package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
  86. package/dist/hoi4_agent_tools/technology/operations.js +14 -1
  87. package/dist/hoi4_agent_tools/version.js +1 -1
  88. package/docs/README.md +7 -4
  89. package/docs/analysis.md +13 -0
  90. package/docs/configuration.md +281 -0
  91. package/docs/development.md +24 -2
  92. package/docs/events.md +27 -3
  93. package/docs/examples.md +6 -6
  94. package/docs/focus.md +125 -4
  95. package/docs/gui.md +98 -17
  96. package/docs/helper-expansion.md +8 -8
  97. package/docs/http.md +3 -1
  98. package/docs/jobs.md +19 -13
  99. package/docs/map.md +197 -6
  100. package/docs/mechanics.md +4 -0
  101. package/docs/probability.md +42 -15
  102. package/docs/reference.md +71 -4
  103. package/docs/setup.md +44 -11
  104. package/docs/technology.md +26 -1
  105. package/docs/third-party-notices.md +11 -0
  106. package/docs/tools.md +151 -42
  107. package/docs/troubleshooting.md +61 -0
  108. package/examples/clients/codex-config.toml +5 -2
  109. package/examples/clients/generic.json +2 -2
  110. package/examples/config/config.json +7 -0
  111. package/npm-shrinkwrap.json +151 -156
  112. package/package.json +13 -8
  113. package/schemas/configuration.schema.json +144 -128
  114. package/schemas/continuous-focus-palette.schema.json +1 -1
  115. package/schemas/custom-weighted-pool.schema.json +1 -1
  116. package/schemas/decision-inspect-input.schema.json +1 -1
  117. package/schemas/error-log-input.schema.json +55 -0
  118. package/schemas/event-feature-manifest.schema.json +1 -1
  119. package/schemas/focus-plan.schema.json +1 -1
  120. package/schemas/focus-planning-sidecar.schema.json +1 -1
  121. package/schemas/gui-animation-source.schema.json +1 -1
  122. package/schemas/gui-helper.schema.json +1 -1
  123. package/schemas/gui-scenario.schema.json +1 -1
  124. package/schemas/helper-expansion-request.schema.json +1 -1
  125. package/schemas/helper-expansion-summary.schema.json +1 -1
  126. package/schemas/impact-inspect-input.schema.json +1 -1
  127. package/schemas/map-operation.schema.json +597 -126
  128. package/schemas/mechanic-test-input.schema.json +1 -1
  129. package/schemas/mod-index-input.schema.json +51 -0
  130. package/schemas/operation-result.schema.json +2 -2
  131. package/schemas/package-check-input.schema.json +1 -1
  132. package/schemas/package-manifest.schema.json +1 -1
  133. package/schemas/probability-analysis-result.schema.json +1 -1
  134. package/schemas/probability-compare-input.schema.json +1 -1
  135. package/schemas/probability-evaluate-input.schema.json +1 -1
  136. package/schemas/probability-inspect-input.schema.json +1 -1
  137. package/schemas/probability-render-input.schema.json +1 -1
  138. package/schemas/probability-scenario-set.schema.json +1 -1
  139. package/schemas/probability-sequence-input.schema.json +1 -1
  140. package/schemas/probability-simulate-input.schema.json +1 -1
  141. package/schemas/probability-sweep-input.schema.json +1 -1
  142. package/schemas/reference-context-input.schema.json +1 -1
  143. package/schemas/reference-read-input.schema.json +1 -1
  144. package/schemas/reference-search-input.schema.json +1 -1
  145. package/schemas/scenario-suite.schema.json +3 -5
  146. package/schemas/scenario-test-input.schema.json +3 -5
  147. package/schemas/script-validate-input.schema.json +1 -1
  148. package/schemas/shared-scenario.schema.json +1 -1
  149. package/schemas/source-lookup-input.schema.json +1 -1
  150. package/server.json +3 -3
@@ -5,7 +5,10 @@ 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';
7
7
  import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SELECTED_PIXEL_LIMIT, } from './limits.js';
8
- import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
8
+ import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
9
+ import { planReferenceRemap, ReferenceRemapError } from './references.js';
10
+ import { generateWorld } from './world.js';
11
+ import { planMapPositions, planSupplyRebuild } from './derived.js';
9
12
  function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
10
13
  if (iteration % stride !== 0)
11
14
  return;
@@ -132,6 +135,7 @@ function compileHistory(data, indent, newline) {
132
135
  if (data.stateBuildings.size > 0 || data.provinceBuildings.size > 0) {
133
136
  rows.push(`${indent}\tbuildings = ${renderBuildings(data, `${indent}\t`, newline)}`);
134
137
  }
138
+ rows.push(...(data.datedHistory ?? []).map((block) => `${indent}\t${block}`));
135
139
  return rows.length === 0 ? `{ }` : `{${newline}${rows.join(newline)}${newline}${indent}}`;
136
140
  }
137
141
  function compileState(data, newline = '\n') {
@@ -165,8 +169,8 @@ function removeAssignments(block, key, replacements) {
165
169
  });
166
170
  }
167
171
  }
168
- function patchState(state, data, fields) {
169
- const replacements = [];
172
+ function patchState(state, data, fields, extra = []) {
173
+ const replacements = [...extra];
170
174
  const stateInsertions = [];
171
175
  const newline = state.document.newline;
172
176
  const stateIndent = lineIndent(state.document.text, state.assignment.start);
@@ -720,10 +724,14 @@ function syntheticFile(relativePath, bytes, loadOrder) {
720
724
  bytes,
721
725
  };
722
726
  }
723
- export function indexWithProposedChanges(base, changes) {
727
+ export function indexWithProposedChanges(base, changes, replacedRoots = []) {
724
728
  const maximumLoadOrder = base.sourceFiles.reduce((maximum, { loadOrder }) => Math.max(maximum, loadOrder), 0) + 1;
725
729
  const byPath = new Map(changes.map((change) => [normalizeRelative(change.relativePath), change]));
726
- const files = base.sourceFiles.filter((file) => !(file.rootKind === 'mod' && byPath.has(normalizeRelative(file.relativePath))));
730
+ // A replace_path folder drops every lower-layer file directly inside it, as the game does.
731
+ const replaced = replacedRoots.map((root) => normalizeRelative(root).replace(/\/$/u, ''));
732
+ const files = base.sourceFiles.filter((file) => !(file.rootKind === 'mod' && byPath.has(normalizeRelative(file.relativePath))) &&
733
+ !(file.rootKind !== 'mod' &&
734
+ replaced.some((root) => path.posix.dirname(normalizeRelative(file.relativePath)) === root)));
727
735
  for (const change of changes) {
728
736
  if (change.content !== null)
729
737
  files.push(syntheticFile(change.relativePath, Buffer.from(change.content), maximumLoadOrder));
@@ -757,8 +765,12 @@ function proposedChanges(changes) {
757
765
  mediaType: change.mediaType,
758
766
  }));
759
767
  }
768
+ /** Source folders a new world replaces, per plan, so lower layers stop counting. */
769
+ /** Diagnostics an operation raises while applying, drained into the plan after it succeeds. */
770
+ const diagnosticsByPlan = new WeakMap();
771
+ const replacedRootsByPlan = new WeakMap();
760
772
  function currentIndex(base, changes) {
761
- return indexWithProposedChanges(base, proposedChanges(changes));
773
+ return indexWithProposedChanges(base, proposedChanges(changes), replacedRootsByPlan.get(changes) ?? []);
762
774
  }
763
775
  function samePolicy(actual, expected) {
764
776
  if (actual === null || typeof actual !== 'object')
@@ -766,22 +778,38 @@ function samePolicy(actual, expected) {
766
778
  const object = actual;
767
779
  return Object.entries(expected).every(([key, value]) => object[key] === value);
768
780
  }
781
+ const AUTOMATIC_MOVE_POLICY = {
782
+ stateValues: 'proportional-by-land-pixels',
783
+ ownership: 'retain-in-current-states',
784
+ provinceBuildings: 'follow-province',
785
+ victoryPoints: 'follow-province',
786
+ ports: 'follow-province',
787
+ supplyNodes: 'follow-province',
788
+ railways: 'follow-province',
789
+ positions: 'follow-province',
790
+ strategicRegion: 'require-same',
791
+ };
769
792
  function requireMovePolicy(operation) {
770
- const distribution = operation.distribution;
771
- if (!samePolicy(distribution, {
772
- stateValues: 'retain-in-current-states',
773
- ownership: 'retain-in-current-states',
774
- provinceBuildings: 'follow-province',
775
- victoryPoints: 'follow-province',
776
- ports: 'follow-province',
777
- supplyNodes: 'follow-province',
778
- railways: 'follow-province',
779
- positions: 'follow-province',
780
- }) ||
781
- (distribution?.strategicRegion !== 'require-same' &&
782
- distribution?.strategicRegion !== 'move-to-target-region')) {
783
- throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'Moving state provinces requires explicit state-value, ownership, building, victory-point, port, network, position, and strategic-region policies');
793
+ const requested = operation.distribution;
794
+ if (requested === undefined || requested === 'proportional')
795
+ return AUTOMATIC_MOVE_POLICY;
796
+ const distribution = requested;
797
+ if (!(samePolicy(distribution, { stateValues: 'retain-in-current-states' }) ||
798
+ samePolicy(distribution, { stateValues: 'proportional-by-land-pixels' })) ||
799
+ !samePolicy(distribution, {
800
+ ownership: 'retain-in-current-states',
801
+ provinceBuildings: 'follow-province',
802
+ victoryPoints: 'follow-province',
803
+ ports: 'follow-province',
804
+ supplyNodes: 'follow-province',
805
+ railways: 'follow-province',
806
+ positions: 'follow-province',
807
+ }) ||
808
+ (distribution.strategicRegion !== 'require-same' &&
809
+ distribution.strategicRegion !== 'move-to-target-region')) {
810
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_REQUIRED', 'A move needs either no distribution (values follow land share) or a complete one with state-value, ownership, building, victory-point, port, network, position, and strategic-region policies');
784
811
  }
812
+ return distribution;
785
813
  }
786
814
  function requireSplitPolicy(operation) {
787
815
  const requested = operation.distribution;
@@ -970,15 +998,37 @@ function splitScalar(total, ratio, policy) {
970
998
  }
971
999
  return [policy.source, policy.destination];
972
1000
  }
973
- function splitMap(total, ratio, policy) {
1001
+ /**
1002
+ * Whether a state building is a level (infrastructure, air base, anti-air, radar) rather than a
1003
+ * count of slot buildings. A level applies to the whole state, so a split keeps it in both parts
1004
+ * and a merge keeps the highest; slot buildings are divided and summed.
1005
+ */
1006
+ function isLevelStateBuilding(index, building) {
1007
+ const definition = index.buildingDefinitions.get(building);
1008
+ if (definition !== undefined)
1009
+ return !definition.sharesSlots && !definition.provincial;
1010
+ return LEVEL_STATE_BUILDINGS.has(building);
1011
+ }
1012
+ function splitMap(total, ratio, policy, isLevel = () => false) {
974
1013
  if (policy.method === 'retain-in-source')
975
1014
  return [new Map(total), new Map()];
976
1015
  if (policy.method === 'proportional-by-land-pixels') {
977
1016
  const source = new Map();
978
1017
  const destination = new Map();
979
1018
  for (const [key, value] of total) {
980
- const moved = Math.round(value * ratio * 1_000_000) / 1_000_000;
981
- source.set(key, value - moved);
1019
+ if (isLevel(key)) {
1020
+ source.set(key, value);
1021
+ destination.set(key, value);
1022
+ continue;
1023
+ }
1024
+ // Whole buildings and resource units stay whole; fractional source values keep two
1025
+ // decimals.
1026
+ const moved = Number.isInteger(value)
1027
+ ? Math.round(value * ratio)
1028
+ : Math.round(value * ratio * 100) / 100;
1029
+ const kept = Math.round((value - moved) * 100) / 100;
1030
+ if (kept !== 0)
1031
+ source.set(key, kept);
982
1032
  if (moved !== 0)
983
1033
  destination.set(key, moved);
984
1034
  }
@@ -1033,9 +1083,98 @@ function stateMembershipId(index, provinceId) {
1033
1083
  throw new ServiceError('MAP_STATE_MEMBERSHIP_AMBIGUOUS', `Province ${provinceId} belongs to multiple states`);
1034
1084
  return states[0]?.id;
1035
1085
  }
1036
- function commitState(changes, state, data, fields, operationId) {
1037
- assertDatedStateHistorySafe(state, fields, operationId);
1038
- addChange(changes, state.file.relativePath, patchState(state, data, fields), operationId, 'text/plain');
1086
+ function commitState(changes, state, data, fields, operationId, datedRewrite, datedRewriteFollowsProvinces = datedRewrite !== undefined) {
1087
+ // Distributed dated history already follows the new province membership.
1088
+ assertDatedStateHistorySafe(state, datedRewriteFollowsProvinces
1089
+ ? { ...fields, provinces: false, victoryPoints: false, buildings: false }
1090
+ : fields, operationId);
1091
+ addChange(changes, state.file.relativePath, patchState(state, data, fields, datedRewrite), operationId, 'text/plain');
1092
+ }
1093
+ /**
1094
+ * Split a state's dated history blocks between the part it keeps and a new state: ownership,
1095
+ * cores and other dated effects apply to both, victory points and province buildings follow
1096
+ * their provinces, level buildings keep their level, and slot buildings divide by the same land
1097
+ * share as the undated values.
1098
+ */
1099
+ function distributeDatedHistory(index, state, moved, ratio) {
1100
+ const result = {
1101
+ source: [],
1102
+ destination: [],
1103
+ };
1104
+ if (state.historyBlock === undefined)
1105
+ return result;
1106
+ const text = state.document.text;
1107
+ const newline = state.document.newline;
1108
+ for (const entry of assignments(state.historyBlock)) {
1109
+ if (entry.value.type !== 'block' || parseMapDate(entry.key.value) === undefined)
1110
+ continue;
1111
+ const block = entry.value;
1112
+ // A block without province-bound entries applies unchanged to both parts.
1113
+ if (!block.entries.some((child) => child.type === 'assignment' &&
1114
+ (child.key.value === 'victory_points' || child.key.value === 'buildings'))) {
1115
+ result.destination.push(text.slice(entry.start, entry.end));
1116
+ continue;
1117
+ }
1118
+ const part = (keep, destination, indent) => {
1119
+ const rows = [];
1120
+ for (const child of block.entries) {
1121
+ if (child.type === 'assignment' &&
1122
+ child.key.value === 'victory_points' &&
1123
+ child.value.type === 'block') {
1124
+ const first = child.value.entries[0];
1125
+ if (first?.type === 'scalar' && !keep(Number(first.value)))
1126
+ continue;
1127
+ rows.push(text.slice(child.start, child.end));
1128
+ continue;
1129
+ }
1130
+ if (child.type === 'assignment' &&
1131
+ child.key.value === 'buildings' &&
1132
+ child.value.type === 'block') {
1133
+ const inner = [];
1134
+ for (const building of child.value.entries) {
1135
+ if (building.type === 'assignment' && /^\d+$/u.test(building.key.value)) {
1136
+ if (keep(Number(building.key.value)))
1137
+ inner.push(text.slice(building.start, building.end));
1138
+ continue;
1139
+ }
1140
+ if (building.type === 'assignment' &&
1141
+ building.value.type === 'scalar' &&
1142
+ /^\d+(?:\.\d+)?$/u.test(building.value.value)) {
1143
+ const value = Number(building.value.value);
1144
+ if (isLevelStateBuilding(index, building.key.value)) {
1145
+ inner.push(`${building.key.value} = ${building.value.value}`);
1146
+ continue;
1147
+ }
1148
+ const share = Math.round(value * ratio);
1149
+ const kept = destination ? share : value - share;
1150
+ if (kept > 0)
1151
+ inner.push(`${building.key.value} = ${kept}`);
1152
+ continue;
1153
+ }
1154
+ inner.push(text.slice(building.start, building.end));
1155
+ }
1156
+ if (inner.length > 0)
1157
+ rows.push(`buildings = {${newline}${inner.map((row) => `${indent}\t\t${row}`).join(newline)}${newline}${indent}\t}`);
1158
+ continue;
1159
+ }
1160
+ rows.push(text.slice(child.start, child.end));
1161
+ }
1162
+ return rows.length === 0
1163
+ ? undefined
1164
+ : `${entry.key.value} = {${newline}${rows.map((row) => `${indent}\t${row}`).join(newline)}${newline}${indent}}`;
1165
+ };
1166
+ const sourceText = part((id) => !moved.has(id), false, lineIndent(text, entry.start));
1167
+ result.source.push({
1168
+ start: entry.start,
1169
+ end: entry.end,
1170
+ text: sourceText ?? '',
1171
+ description: 'Keep the dated history that follows the remaining provinces',
1172
+ });
1173
+ const destinationText = part((id) => moved.has(id), true, '\t\t');
1174
+ if (destinationText !== undefined)
1175
+ result.destination.push(destinationText);
1176
+ }
1177
+ return result;
1039
1178
  }
1040
1179
  function assertDatedStateHistorySafe(state, fields, operationId) {
1041
1180
  if (state.historyBlock === undefined)
@@ -1088,20 +1227,6 @@ function assertDatedStateHistorySafe(state, fields, operationId) {
1088
1227
  });
1089
1228
  }
1090
1229
  }
1091
- function assertNoDatedStateHistory(state, operationId) {
1092
- if (state.historyBlock === undefined)
1093
- return;
1094
- const dated = assignments(state.historyBlock).find((entry) => entry.value.type === 'block' && parseMapDate(entry.key.value) !== undefined);
1095
- if (dated === undefined)
1096
- return;
1097
- throw new ServiceError('MAP_DATED_STATE_DISTRIBUTION_UNSUPPORTED', `State ${state.id} has dated history that cannot be distributed to a new or merged state`, {
1098
- operationId,
1099
- stateId: state.id,
1100
- datedAt: dated.key.value,
1101
- sourcePath: state.file.displayPath,
1102
- sourceOffset: dated.start,
1103
- });
1104
- }
1105
1230
  function commitRegion(changes, region, provinces, operationId) {
1106
1231
  addChange(changes, region.file.relativePath, patchRegion(region, provinces), operationId, 'text/plain');
1107
1232
  }
@@ -1222,7 +1347,7 @@ function applyUpdateState(base, changes, operation) {
1222
1347
  commitState(changes, state, desired, fields, operation.id);
1223
1348
  }
1224
1349
  function applyMoveStateProvinces(base, changes, operation) {
1225
- requireMovePolicy(operation);
1350
+ const policy = requireMovePolicy(operation);
1226
1351
  const index = currentIndex(base, changes);
1227
1352
  const source = index.statesById.get(operation.sourceStateId);
1228
1353
  const target = index.statesById.get(operation.targetStateId);
@@ -1239,6 +1364,32 @@ function applyMoveStateProvinces(base, changes, operation) {
1239
1364
  const sourceData = cloneState(source);
1240
1365
  const targetData = cloneState(target);
1241
1366
  const movingSet = new Set(moving);
1367
+ const redistribute = policy.stateValues === 'proportional-by-land-pixels';
1368
+ if (redistribute) {
1369
+ const sourcePixels = provincePixelTotal(index, source.provinces);
1370
+ const movedPixels = provincePixelTotal(index, moving);
1371
+ if (sourcePixels <= 0 || movedPixels <= 0)
1372
+ throw new ServiceError('MAP_STATE_DISTRIBUTION_GEOMETRY_MISSING', 'Proportional state distribution requires province geometry');
1373
+ const ratio = movedPixels / sourcePixels;
1374
+ const proportional = { method: 'proportional-by-land-pixels' };
1375
+ // The share that leaves the source joins the target: people and resources and slot
1376
+ // buildings are carried over, and a level (infrastructure, air base) is the higher of both.
1377
+ const [keptManpower, movedManpower] = splitScalar(source.manpower, ratio, proportional);
1378
+ sourceData.manpower = keptManpower;
1379
+ targetData.manpower = target.manpower + movedManpower;
1380
+ const [keptResources, movedResources] = splitMap(source.resources, ratio, proportional);
1381
+ sourceData.resources = keptResources;
1382
+ targetData.resources = new Map(target.resources);
1383
+ addMaps(targetData.resources, movedResources);
1384
+ const isLevel = (building) => isLevelStateBuilding(index, building);
1385
+ const [keptBuildings, movedBuildings] = splitMap(source.stateBuildings, ratio, proportional, isLevel);
1386
+ sourceData.stateBuildings = keptBuildings;
1387
+ targetData.stateBuildings = new Map(target.stateBuildings);
1388
+ for (const [building, value] of movedBuildings)
1389
+ targetData.stateBuildings.set(building, isLevel(building)
1390
+ ? Math.max(value, targetData.stateBuildings.get(building) ?? 0)
1391
+ : (targetData.stateBuildings.get(building) ?? 0) + value);
1392
+ }
1242
1393
  sourceData.provinces = sourceData.provinces.filter((id) => !movingSet.has(id));
1243
1394
  targetData.provinces = uniqueSorted([...targetData.provinces, ...moving]);
1244
1395
  const movedVps = sourceData.victoryPoints.filter(({ provinceId }) => movingSet.has(provinceId));
@@ -1256,13 +1407,25 @@ function applyMoveStateProvinces(base, changes, operation) {
1256
1407
  if (targetRegionIds.size !== 1)
1257
1408
  throw new ServiceError('MAP_TARGET_REGION_AMBIGUOUS', 'Target state spans multiple strategic regions');
1258
1409
  const targetRegionId = required([...targetRegionIds][0], 'MAP_TARGET_REGION_AMBIGUOUS', 'Target state has no strategic region');
1259
- if (operation.distribution.strategicRegion === 'require-same' &&
1410
+ if (policy.strategicRegion === 'require-same' &&
1260
1411
  [...movingRegionIds].some((id) => id !== targetRegionId)) {
1261
1412
  throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Moved provinces are not in the target strategic region');
1262
1413
  }
1263
- commitState(changes, source, sourceData, { provinces: true, victoryPoints: true, buildings: true }, operation.id);
1264
- commitState(changes, target, targetData, { provinces: true, victoryPoints: true, buildings: true }, operation.id);
1265
- if (operation.distribution.strategicRegion === 'move-to-target-region') {
1414
+ commitState(changes, source, sourceData, {
1415
+ provinces: true,
1416
+ victoryPoints: true,
1417
+ buildings: true,
1418
+ manpower: redistribute,
1419
+ resources: redistribute,
1420
+ }, operation.id);
1421
+ commitState(changes, target, targetData, {
1422
+ provinces: true,
1423
+ victoryPoints: true,
1424
+ buildings: true,
1425
+ manpower: redistribute,
1426
+ resources: redistribute,
1427
+ }, operation.id);
1428
+ if (policy.strategicRegion === 'move-to-target-region') {
1266
1429
  for (const regionId of movingRegionIds) {
1267
1430
  if (regionId === targetRegionId)
1268
1431
  continue;
@@ -1285,7 +1448,6 @@ function* applySplitState(base, changes, allocations, operation, signal) {
1285
1448
  const source = index.statesById.get(sourceStateId);
1286
1449
  if (source === undefined)
1287
1450
  throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${sourceStateId} does not exist`);
1288
- assertNoDatedStateHistory(source, operation.id);
1289
1451
  const selectedSet = new Set(selected);
1290
1452
  if (selected.length === 0 || selected.some((id) => !source.provinces.includes(id))) {
1291
1453
  throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every new-state province must belong to the source state');
@@ -1329,7 +1491,7 @@ function* applySplitState(base, changes, allocations, operation, signal) {
1329
1491
  destinationData.provinces = selected;
1330
1492
  [sourceData.manpower, destinationData.manpower] = splitScalar(source.manpower, ratio, policy.manpower);
1331
1493
  [sourceData.resources, destinationData.resources] = splitMap(source.resources, ratio, policy.resources);
1332
- [sourceData.stateBuildings, destinationData.stateBuildings] = splitMap(source.stateBuildings, ratio, policy.stateBuildings);
1494
+ [sourceData.stateBuildings, destinationData.stateBuildings] = splitMap(source.stateBuildings, ratio, policy.stateBuildings, (building) => isLevelStateBuilding(index, building));
1333
1495
  [sourceData.owner, destinationData.owner] = splitTag(source.owner, policy.owner);
1334
1496
  [sourceData.controller, destinationData.controller] = splitTag(source.controller, policy.controller);
1335
1497
  [sourceData.cores, destinationData.cores] = splitTags(source.cores, policy.cores);
@@ -1338,17 +1500,20 @@ function* applySplitState(base, changes, allocations, operation, signal) {
1338
1500
  destinationData.victoryPoints = destinationData.victoryPoints.filter(({ provinceId }) => selectedSet.has(provinceId));
1339
1501
  sourceData.provinceBuildings = new Map([...sourceData.provinceBuildings].filter(([provinceId]) => !selectedSet.has(provinceId)));
1340
1502
  destinationData.provinceBuildings = new Map([...destinationData.provinceBuildings].filter(([provinceId]) => selectedSet.has(provinceId)));
1503
+ const dated = distributeDatedHistory(index, source, selectedSet, ratio);
1504
+ destinationData.datedHistory = dated.destination;
1341
1505
  commitState(changes, source, sourceData, {
1342
1506
  manpower: true,
1343
1507
  resources: true,
1344
1508
  provinces: true,
1345
- owner: true,
1346
- controller: true,
1347
- cores: true,
1348
- claims: true,
1509
+ // Copied ownership is unchanged in the source and needs no rewrite.
1510
+ owner: policy.owner.method !== 'copy-source',
1511
+ controller: policy.controller.method !== 'copy-source',
1512
+ cores: policy.cores.method !== 'copy-source',
1513
+ claims: policy.claims.method !== 'copy-source',
1349
1514
  victoryPoints: true,
1350
1515
  buildings: true,
1351
- }, operation.id);
1516
+ }, operation.id, dated.source);
1352
1517
  const requestedName = operation.fileName ?? `${allocation.id}-AGENT_STATE.txt`;
1353
1518
  const safeName = requestedName.replaceAll('\\', '/').split('/').at(-1) ?? requestedName;
1354
1519
  if (!/^[-A-Za-z0-9_.]+\.txt$/u.test(safeName))
@@ -1360,8 +1525,17 @@ function* applySplitState(base, changes, allocations, operation, signal) {
1360
1525
  }
1361
1526
  addChange(changes, relativePath, compileState(destinationData, source.document.newline), operation.id, 'text/plain');
1362
1527
  remapBuildingPositionStates(index, changes, selectedSet, allocation.id, operation.id);
1528
+ if (operation.remapReferences !== undefined)
1529
+ remapScriptReferences(base, changes, operation.id, {
1530
+ entity: 'state',
1531
+ mapping: [{ from: source.id, to: [source.id, allocation.id] }],
1532
+ readOnlySources: operation.remapReferences,
1533
+ });
1363
1534
  }
1364
1535
  function requireMergeStatePolicy(operation) {
1536
+ // Every merge policy field has one allowed value, so an omitted policy means that one.
1537
+ if (operation.distribution === undefined)
1538
+ return;
1365
1539
  if (!samePolicy(operation.distribution, {
1366
1540
  stateValues: 'sum-into-target',
1367
1541
  ownership: 'retain-target',
@@ -1403,8 +1577,26 @@ function applyMergeStates(base, changes, operation) {
1403
1577
  if (target === undefined || sourceIds.length === 0 || sources.length !== sourceIds.length) {
1404
1578
  throw new ServiceError('MAP_STATE_SELECTION_INVALID', 'Merge target and source states must exist');
1405
1579
  }
1406
- for (const source of sources)
1407
- assertNoDatedStateHistory(source, operation.id);
1580
+ // Dated ownership, cores and effects of a merged state carry over to the target; dated
1581
+ // building levels cannot be combined, because each part's later value replaces its own.
1582
+ const carriedHistory = [];
1583
+ for (const source of sources) {
1584
+ if (source.historyBlock === undefined)
1585
+ continue;
1586
+ for (const entry of assignments(source.historyBlock)) {
1587
+ if (entry.value.type !== 'block' || parseMapDate(entry.key.value) === undefined)
1588
+ continue;
1589
+ if (assignments(entry.value, 'buildings').length > 0)
1590
+ throw new ServiceError('MAP_DATED_STATE_DISTRIBUTION_UNSUPPORTED', `State ${source.id} sets building levels in dated history (${entry.key.value}), which a merge cannot combine; move its provinces with move_state_provinces or edit that block first`, {
1591
+ operationId: operation.id,
1592
+ stateId: source.id,
1593
+ datedAt: entry.key.value,
1594
+ sourcePath: source.file.displayPath,
1595
+ sourceOffset: entry.start,
1596
+ });
1597
+ carriedHistory.push(source.document.text.slice(entry.start, entry.end));
1598
+ }
1599
+ }
1408
1600
  const regionIds = new Set([...target.provinces, ...sources.flatMap((state) => state.provinces)].map((id) => regionMembershipId(index, id)));
1409
1601
  if (regionIds.size !== 1)
1410
1602
  throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Merged states must share one strategic region');
@@ -1412,7 +1604,10 @@ function applyMergeStates(base, changes, operation) {
1412
1604
  for (const source of sources) {
1413
1605
  data.manpower += source.manpower;
1414
1606
  addMaps(data.resources, source.resources);
1415
- addMaps(data.stateBuildings, source.stateBuildings);
1607
+ for (const [building, value] of source.stateBuildings)
1608
+ data.stateBuildings.set(building, isLevelStateBuilding(index, building)
1609
+ ? Math.max(data.stateBuildings.get(building) ?? 0, value)
1610
+ : (data.stateBuildings.get(building) ?? 0) + value);
1416
1611
  data.provinces.push(...source.provinces);
1417
1612
  data.cores.push(...source.cores);
1418
1613
  data.claims.push(...source.claims);
@@ -1423,6 +1618,20 @@ function applyMergeStates(base, changes, operation) {
1423
1618
  data.provinces = uniqueSorted(data.provinces);
1424
1619
  data.cores = uniqueStrings(data.cores);
1425
1620
  data.claims = uniqueStrings(data.claims);
1621
+ const historyClose = target.historyBlock?.close;
1622
+ const carried = [];
1623
+ if (carriedHistory.length > 0 && historyClose !== undefined) {
1624
+ const indent = lineIndent(target.document.text, target.historyBlock.start);
1625
+ const newline = target.document.newline;
1626
+ carried.push({
1627
+ start: historyClose.start,
1628
+ end: historyClose.start,
1629
+ text: carriedHistory.map((block) => `\t${block}${newline}${indent}`).join(''),
1630
+ description: 'Carry dated history from merged states',
1631
+ });
1632
+ }
1633
+ else
1634
+ data.datedHistory = carriedHistory;
1426
1635
  commitState(changes, target, data, {
1427
1636
  manpower: true,
1428
1637
  resources: true,
@@ -1431,11 +1640,17 @@ function applyMergeStates(base, changes, operation) {
1431
1640
  claims: true,
1432
1641
  victoryPoints: true,
1433
1642
  buildings: true,
1434
- }, operation.id);
1643
+ }, operation.id, carried.length === 0 ? undefined : carried, false);
1435
1644
  for (const source of sources) {
1436
1645
  addChange(changes, source.file.relativePath, removalContent(index, source.file, `State ${source.id} merged into ${target.id}`), operation.id, 'text/plain');
1437
1646
  }
1438
1647
  remapBuildingPositionStates(index, changes, new Set(sources.flatMap(({ provinces }) => provinces)), target.id, operation.id);
1648
+ if (operation.remapReferences !== undefined)
1649
+ remapScriptReferences(base, changes, operation.id, {
1650
+ entity: 'state',
1651
+ mapping: sources.map((source) => ({ from: source.id, to: target.id })),
1652
+ readOnlySources: operation.remapReferences,
1653
+ });
1439
1654
  }
1440
1655
  function pointInEvenOddPolygon(x, y, points) {
1441
1656
  let inside = false;
@@ -3026,6 +3241,149 @@ function applyRenumberMapEntity(base, changes, operation) {
3026
3241
  if (operation.renameLocalisation ?? true)
3027
3242
  renameLocalisationKeys(currentIndex(base, changes), changes, localisationKeys, operation.id);
3028
3243
  }
3244
+ /** Folders a new world replaces in the base game. */
3245
+ export const WORLD_REPLACED_ROOTS = [
3246
+ 'history/states',
3247
+ 'history/countries',
3248
+ 'history/units',
3249
+ 'map/strategicregions',
3250
+ 'map/supplyareas',
3251
+ 'common/bookmarks',
3252
+ ];
3253
+ /** Add replace_path lines to descriptor.mod, creating a minimal descriptor when none exists. */
3254
+ function descriptorWithReplacePaths(index, roots) {
3255
+ const existing = index.sourceFiles.find((file) => file.rootKind === 'mod' && normalizeRelative(file.relativePath) === 'descriptor.mod');
3256
+ const textValue = existing === undefined
3257
+ ? 'version="1.0"\nname="New World"\nsupported_version="1.*"\n'
3258
+ : existing.bytes.toString('utf8');
3259
+ const newline = textValue.includes('\r\n') ? '\r\n' : '\n';
3260
+ const present = new Set([...textValue.matchAll(/replace_path\s*=\s*"([^"]+)"/gu)].map((match) => normalizeRelative(match[1])));
3261
+ const missing = roots.filter((root) => !present.has(normalizeRelative(root)));
3262
+ const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
3263
+ return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
3264
+ }
3265
+ function applyCreateWorld(base, changes, operation) {
3266
+ const mapRoot = base.sourceRoots.map[0] ?? 'map';
3267
+ const world = generateWorld({
3268
+ width: operation.width ?? 1_024,
3269
+ height: operation.height ?? 512,
3270
+ seed: operation.seed ?? 1,
3271
+ landCoverage: operation.landCoverage ?? 0.35,
3272
+ continents: operation.continents ?? 3,
3273
+ ...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
3274
+ landProvincePixels: operation.landProvincePixels ?? 400,
3275
+ seaProvincePixels: operation.seaProvincePixels ?? 2_500,
3276
+ provincesPerState: operation.provincesPerState ?? 5,
3277
+ landProvincesPerRegion: operation.landProvincesPerRegion ?? 25,
3278
+ seaProvincesPerRegion: operation.seaProvincesPerRegion ?? 10,
3279
+ countries: operation.countries,
3280
+ ...(operation.continentNames === undefined
3281
+ ? {}
3282
+ : { continentNames: operation.continentNames }),
3283
+ startDate: operation.startDate ?? '1936.1.1',
3284
+ rivers: operation.rivers ?? 8,
3285
+ }, {
3286
+ palettes: {},
3287
+ mapRoot,
3288
+ statesRoot: base.sourceRoots.states[0] ?? 'history/states',
3289
+ localisationRoot: base.sourceRoots.localisation[0] ?? 'localisation',
3290
+ });
3291
+ const roots = operation.replaceBaseGame === false ? [] : [...WORLD_REPLACED_ROOTS];
3292
+ replacedRootsByPlan.set(changes, roots);
3293
+ for (const file of world.files)
3294
+ addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
3295
+ // Start from empty derived files so no base-game position or supply line survives.
3296
+ for (const name of [
3297
+ 'buildings.txt',
3298
+ 'unitstacks.txt',
3299
+ 'weatherpositions.txt',
3300
+ 'supply_nodes.txt',
3301
+ 'railways.txt',
3302
+ ])
3303
+ addChange(changes, `${mapRoot}/${name}`, Buffer.alloc(0), operation.id, 'text/plain');
3304
+ if (roots.length > 0)
3305
+ addChange(changes, 'descriptor.mod', descriptorWithReplacePaths(base, roots), operation.id, 'text/plain');
3306
+ const generated = currentIndex(base, changes);
3307
+ for (const file of planMapPositions(generated, { mode: 'replace' }))
3308
+ addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3309
+ const positioned = currentIndex(base, changes);
3310
+ for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
3311
+ addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3312
+ return [
3313
+ {
3314
+ code: 'MAP_WORLD_CREATED',
3315
+ severity: 'info',
3316
+ category: 'map',
3317
+ operationId: operation.id,
3318
+ message: `Created a ${world.summary.width}x${world.summary.height} world: ${world.summary.landProvinces} land, ${world.summary.seaProvinces} sea and ${world.summary.lakeProvinces} lake provinces, ${world.summary.states} states, ${world.summary.landRegions + world.summary.seaRegions} strategic regions, ${world.summary.countries} countries and ${world.summary.rivers} rivers`,
3319
+ details: world.summary,
3320
+ },
3321
+ ...(roots.length === 0
3322
+ ? []
3323
+ : [
3324
+ {
3325
+ code: 'MAP_WORLD_LAUNCHER_REPLACE_PATHS',
3326
+ severity: 'info',
3327
+ category: 'map',
3328
+ operationId: operation.id,
3329
+ message: `descriptor.mod replaces ${roots.join(', ')}; the launcher's .mod file beside the mod folder needs the same replace_path lines`,
3330
+ details: { replacePaths: roots },
3331
+ },
3332
+ ]),
3333
+ ];
3334
+ }
3335
+ function applyRegenerateMapPositions(base, changes, operation) {
3336
+ const index = currentIndex(base, changes);
3337
+ for (const file of planMapPositions(index, operation))
3338
+ addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3339
+ }
3340
+ function applyRebuildSupply(base, changes, operation) {
3341
+ const index = currentIndex(base, changes);
3342
+ for (const file of planSupplyRebuild(index, operation))
3343
+ addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
3344
+ }
3345
+ /**
3346
+ * Rewrite the script references of one entity kind in the proposed changes. Returns the number
3347
+ * of script files changed; an empty result is an error only when the caller asks for it.
3348
+ */
3349
+ function remapScriptReferences(base, changes, operationId, request) {
3350
+ const index = currentIndex(base, changes);
3351
+ let plan;
3352
+ try {
3353
+ plan = planReferenceRemap(index.sourceFiles, request, (document, replacements) => applyReplacements(document, replacements));
3354
+ }
3355
+ catch (error) {
3356
+ if (error instanceof ReferenceRemapError)
3357
+ throw new ServiceError(error.code, error.message, error.details);
3358
+ throw error;
3359
+ }
3360
+ for (const file of plan.files)
3361
+ addChange(changes, file.relativePath, file.content, operationId, 'text/plain');
3362
+ if (plan.unchanged.length > 0) {
3363
+ const raised = diagnosticsByPlan.get(changes) ?? [];
3364
+ for (const item of plan.unchanged.slice(0, 20))
3365
+ raised.push({
3366
+ code: 'MAP_REFERENCE_NEEDS_REVIEW',
3367
+ severity: 'info',
3368
+ category: 'map',
3369
+ message: `${item.relativePath}:${item.line} names ${request.entity} ${item.id} through ${item.key}, which takes one value; it was left as it is. Point it at the successor you want.`,
3370
+ operationId,
3371
+ details: { ...item, entity: request.entity },
3372
+ });
3373
+ diagnosticsByPlan.set(changes, raised);
3374
+ }
3375
+ return plan.files.length;
3376
+ }
3377
+ function applyRemapMapReferences(base, changes, operation) {
3378
+ const changed = remapScriptReferences(base, changes, operation.id, {
3379
+ entity: operation.entity,
3380
+ mapping: operation.mapping,
3381
+ readOnlySources: operation.readOnlySources ?? 'refuse',
3382
+ ...(operation.pathPrefixes === undefined ? {} : { pathPrefixes: operation.pathPrefixes }),
3383
+ });
3384
+ if (changed === 0)
3385
+ throw new ServiceError('MAP_REFERENCE_REMAP_EMPTY', `No script references the mapped ${operation.entity} IDs`, { entity: operation.entity, from: operation.mapping.map(({ from }) => from) });
3386
+ }
3029
3387
  function applyEntityLocator(base, changes, operation) {
3030
3388
  const index = currentIndex(base, changes);
3031
3389
  const matching = index.entityLocators.filter(({ entity, name }) => entity === operation.entity && name === operation.name);
@@ -3084,7 +3442,9 @@ function* planMapOperationSteps(base, operations, signal) {
3084
3442
  const allocationsCheckpoint = allocations.length;
3085
3443
  const boundsCheckpoint = expectedChangedBounds;
3086
3444
  try {
3087
- if (operation.kind === 'move_state_provinces')
3445
+ if (operation.kind === 'create_world')
3446
+ diagnostics.push(...applyCreateWorld(base, changes, operation));
3447
+ else if (operation.kind === 'move_state_provinces')
3088
3448
  applyMoveStateProvinces(base, changes, operation);
3089
3449
  else if (operation.kind === 'update_state')
3090
3450
  applyUpdateState(base, changes, operation);
@@ -3107,10 +3467,19 @@ function* planMapOperationSteps(base, operations, signal) {
3107
3467
  applyEntityLocator(base, changes, operation);
3108
3468
  else if (operation.kind === 'renumber_map_entity')
3109
3469
  applyRenumberMapEntity(base, changes, operation);
3470
+ else if (operation.kind === 'remap_map_references')
3471
+ applyRemapMapReferences(base, changes, operation);
3472
+ else if (operation.kind === 'regenerate_map_positions')
3473
+ applyRegenerateMapPositions(base, changes, operation);
3474
+ else if (operation.kind === 'rebuild_supply')
3475
+ applyRebuildSupply(base, changes, operation);
3110
3476
  else
3111
3477
  handleSimpleOperation(base, changes, operation);
3478
+ diagnostics.push(...(diagnosticsByPlan.get(changes) ?? []));
3479
+ diagnosticsByPlan.delete(changes);
3112
3480
  }
3113
3481
  catch (error) {
3482
+ diagnosticsByPlan.delete(changes);
3114
3483
  if (signal?.aborted)
3115
3484
  throw error;
3116
3485
  changes.clear();
@@ -3142,7 +3511,7 @@ function* planMapOperationSteps(base, operations, signal) {
3142
3511
  }
3143
3512
  const finalized = proposedChanges(changes);
3144
3513
  yield* cancellationCheckpoint(signal);
3145
- const finalIndex = indexWithProposedChanges(base, finalized);
3514
+ const finalIndex = indexWithProposedChanges(base, finalized, replacedRootsByPlan.get(changes) ?? []);
3146
3515
  return {
3147
3516
  changes: finalized,
3148
3517
  diagnostics,