hoi4-agent-tools 2.5.2 → 2.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 (50) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +5 -5
  3. package/dist/hoi4_agent_tools/core/index.js +22 -6
  4. package/dist/hoi4_agent_tools/focus/compact.js +103 -12
  5. package/dist/hoi4_agent_tools/focus/continuous-render.js +4 -10
  6. package/dist/hoi4_agent_tools/focus/layout.js +15 -7
  7. package/dist/hoi4_agent_tools/focus/render.js +2 -8
  8. package/dist/hoi4_agent_tools/gui/dds.js +5 -2
  9. package/dist/hoi4_agent_tools/gui/layout.js +1 -1
  10. package/dist/hoi4_agent_tools/gui/renderer.js +4 -16
  11. package/dist/hoi4_agent_tools/gui/source-graph.js +9 -8
  12. package/dist/hoi4_agent_tools/gui/studio.js +58 -4
  13. package/dist/hoi4_agent_tools/map/catalog.js +398 -0
  14. package/dist/hoi4_agent_tools/map/index.js +1 -0
  15. package/dist/hoi4_agent_tools/map/operations.js +337 -30
  16. package/dist/hoi4_agent_tools/map/render.js +40 -147
  17. package/dist/hoi4_agent_tools/map/service.js +1 -0
  18. package/dist/hoi4_agent_tools/mcp/tools/map.js +85 -9
  19. package/dist/hoi4_agent_tools/mcp/transports/http.js +4 -5
  20. package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +7 -7
  21. package/dist/hoi4_agent_tools/schemas/map.js +69 -2
  22. package/dist/hoi4_agent_tools/technology/render.js +65 -19
  23. package/dist/hoi4_agent_tools/technology/service.js +67 -3
  24. package/dist/hoi4_agent_tools/version.js +1 -1
  25. package/docs/focus.md +1 -1
  26. package/docs/gui.md +2 -0
  27. package/docs/map.md +60 -23
  28. package/docs/technology.md +1 -1
  29. package/package.json +3 -2
  30. package/schemas/configuration.schema.json +1 -1
  31. package/schemas/continuous-focus-palette.schema.json +1 -1
  32. package/schemas/custom-weighted-pool.schema.json +1 -1
  33. package/schemas/event-feature-manifest.schema.json +1 -1
  34. package/schemas/focus-plan.schema.json +1 -1
  35. package/schemas/focus-planning-sidecar.schema.json +1 -1
  36. package/schemas/gui-animation-source.schema.json +1 -1
  37. package/schemas/gui-helper.schema.json +1 -1
  38. package/schemas/gui-scenario.schema.json +1 -1
  39. package/schemas/map-operation.schema.json +228 -74
  40. package/schemas/operation-result.schema.json +1 -1
  41. package/schemas/probability-analysis-result.schema.json +1 -1
  42. package/schemas/probability-compare-input.schema.json +1 -1
  43. package/schemas/probability-evaluate-input.schema.json +1 -1
  44. package/schemas/probability-inspect-input.schema.json +1 -1
  45. package/schemas/probability-render-input.schema.json +1 -1
  46. package/schemas/probability-scenario-set.schema.json +1 -1
  47. package/schemas/probability-sequence-input.schema.json +1 -1
  48. package/schemas/probability-simulate-input.schema.json +1 -1
  49. package/schemas/probability-sweep-input.schema.json +1 -1
  50. package/server.json +2 -2
@@ -170,6 +170,24 @@ function patchState(state, data, fields) {
170
170
  const stateInsertions = [];
171
171
  const newline = state.document.newline;
172
172
  const stateIndent = lineIndent(state.document.text, state.assignment.start);
173
+ const patchStateScalar = (key, value, description) => {
174
+ const assignment = assignmentFor(state.block, key);
175
+ if (assignment?.value.type === 'scalar') {
176
+ replacements.push({
177
+ start: assignment.value.start,
178
+ end: assignment.value.end,
179
+ text: value,
180
+ description,
181
+ });
182
+ }
183
+ else {
184
+ stateInsertions.push(`${key} = ${value}`);
185
+ }
186
+ };
187
+ if (fields.id === true)
188
+ patchStateScalar('id', numberText(data.id), 'Update state ID');
189
+ if (fields.name === true)
190
+ patchStateScalar('name', quote(data.name), 'Update state name key');
173
191
  if (fields.manpower === true) {
174
192
  const assignment = assignmentFor(state.block, 'manpower');
175
193
  if (assignment?.value.type === 'scalar') {
@@ -311,19 +329,42 @@ function patchState(state, data, fields) {
311
329
  }
312
330
  return applyReplacements(state.document, replacements);
313
331
  }
314
- function patchRegion(region, provinces) {
315
- const assignment = assignmentFor(region.block, 'provinces');
332
+ function patchRegion(region, provinces, identity, patchProvinces = true) {
333
+ const replacements = [];
316
334
  const indent = lineIndent(region.document.text, region.assignment.start);
317
- const rendered = `provinces = ${renderNumberList(provinces, `${indent}\t`, region.document.newline)}`;
318
- const replacement = assignment === undefined
319
- ? blockInsertion(region.block, region.document.text, rendered, region.document.newline)
320
- : {
321
- start: assignment.start,
322
- end: assignment.end,
323
- text: rendered,
324
- description: 'Update strategic region provinces',
325
- };
326
- return applyReplacements(region.document, [replacement]);
335
+ if (patchProvinces) {
336
+ const assignment = assignmentFor(region.block, 'provinces');
337
+ const rendered = `provinces = ${renderNumberList(provinces, `${indent}\t`, region.document.newline)}`;
338
+ replacements.push(assignment === undefined
339
+ ? blockInsertion(region.block, region.document.text, rendered, region.document.newline)
340
+ : {
341
+ start: assignment.start,
342
+ end: assignment.end,
343
+ text: rendered,
344
+ description: 'Update strategic region provinces',
345
+ });
346
+ }
347
+ for (const [key, value] of [
348
+ ['id', identity?.id],
349
+ ['name', identity?.name],
350
+ ]) {
351
+ if (value === undefined)
352
+ continue;
353
+ const renderedValue = key === 'name' ? quote(value) : numberText(value);
354
+ const scalar = assignmentFor(region.block, key);
355
+ if (scalar?.value.type === 'scalar') {
356
+ replacements.push({
357
+ start: scalar.value.start,
358
+ end: scalar.value.end,
359
+ text: renderedValue,
360
+ description: `Update strategic region ${key}`,
361
+ });
362
+ }
363
+ else {
364
+ replacements.push(blockInsertion(region.block, region.document.text, `${key} = ${renderedValue}`, region.document.newline));
365
+ }
366
+ }
367
+ return applyReplacements(region.document, replacements);
327
368
  }
328
369
  function definitionLine(definition) {
329
370
  return [
@@ -743,8 +784,26 @@ function requireMovePolicy(operation) {
743
784
  }
744
785
  }
745
786
  function requireSplitPolicy(operation) {
746
- const policy = operation
747
- .distribution;
787
+ const requested = operation.distribution;
788
+ if (operation.kind === 'create_state' &&
789
+ (requested === undefined || requested === 'proportional')) {
790
+ return {
791
+ manpower: { method: 'proportional-by-land-pixels' },
792
+ resources: { method: 'proportional-by-land-pixels' },
793
+ stateBuildings: { method: 'proportional-by-land-pixels' },
794
+ owner: { method: 'copy-source' },
795
+ controller: { method: 'copy-source' },
796
+ cores: { method: 'copy-source' },
797
+ claims: { method: 'copy-source' },
798
+ victoryPoints: 'follow-province',
799
+ provinceBuildings: 'follow-province',
800
+ ports: 'follow-province',
801
+ supplyNodes: 'follow-province',
802
+ railways: 'follow-province',
803
+ positions: 'follow-province',
804
+ };
805
+ }
806
+ const policy = requested;
748
807
  if (policy?.manpower === undefined ||
749
808
  policy.resources === undefined ||
750
809
  policy.stateBuildings === undefined ||
@@ -1152,10 +1211,14 @@ function applyMoveStateProvinces(base, changes, operation) {
1152
1211
  function* applySplitState(base, changes, allocations, operation, signal) {
1153
1212
  const policy = requireSplitPolicy(operation);
1154
1213
  const index = currentIndex(base, changes);
1155
- const source = index.statesById.get(operation.sourceStateId);
1156
- if (source === undefined)
1157
- throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${operation.sourceStateId} does not exist`);
1158
1214
  const selected = uniqueSorted(operation.provinceIds);
1215
+ const inferredSourceIds = uniqueSorted(selected.flatMap((provinceId) => index.stateForProvince(provinceId).map(({ id }) => id)));
1216
+ const sourceStateId = operation.sourceStateId ?? (inferredSourceIds.length === 1 ? inferredSourceIds[0] : undefined);
1217
+ if (sourceStateId === undefined)
1218
+ throw new ServiceError('MAP_STATE_SOURCE_AMBIGUOUS', 'Source state could not be inferred because the selected provinces do not all belong to one state', { provinceIds: selected, candidateStateIds: inferredSourceIds });
1219
+ const source = index.statesById.get(sourceStateId);
1220
+ if (source === undefined)
1221
+ throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${sourceStateId} does not exist`);
1159
1222
  const selectedSet = new Set(selected);
1160
1223
  if (selected.length === 0 || selected.some((id) => !source.provinces.includes(id))) {
1161
1224
  throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every new-state province must belong to the source state');
@@ -1172,10 +1235,29 @@ function* applySplitState(base, changes, allocations, operation, signal) {
1172
1235
  const sourceData = cloneState(source);
1173
1236
  const destinationData = cloneState(source);
1174
1237
  destinationData.id = allocation.id;
1175
- if (operation.name === undefined || operation.name.trim() === '')
1176
- throw new ServiceError('MAP_STATE_NAME_REQUIRED', 'State creation requires an explicit localisation key');
1177
- destinationData.name = operation.name;
1178
- applyStateLocalisation(index, changes, operation, destinationData.name);
1238
+ const requestedKey = operation.name?.trim();
1239
+ const displayName = operation.displayName?.trim();
1240
+ if ((requestedKey === undefined || requestedKey === '') &&
1241
+ (displayName === undefined || displayName === ''))
1242
+ throw new ServiceError('MAP_STATE_NAME_REQUIRED', 'State creation requires name or displayName');
1243
+ destinationData.name =
1244
+ requestedKey === undefined || requestedKey === '' ? `STATE_${allocation.id}` : requestedKey;
1245
+ const operationWithLocalisation = displayName !== undefined && displayName !== '' && operation.localisation === undefined
1246
+ ? {
1247
+ ...operation,
1248
+ localisation: {
1249
+ method: 'upsert',
1250
+ language: 'l_english',
1251
+ value: displayName,
1252
+ ...(index.localisationByKey.has(`l_english:${destinationData.name}`)
1253
+ ? {}
1254
+ : {
1255
+ file: `${index.sourceRoots.localisation[0] ?? 'localisation'}/english/hoi4_agent_map_l_english.yml`,
1256
+ }),
1257
+ },
1258
+ }
1259
+ : operation;
1260
+ applyStateLocalisation(index, changes, operationWithLocalisation, destinationData.name);
1179
1261
  sourceData.provinces = sourceData.provinces.filter((id) => !selectedSet.has(id));
1180
1262
  destinationData.provinces = selected;
1181
1263
  [sourceData.manpower, destinationData.manpower] = splitScalar(source.manpower, ratio, policy.manpower);
@@ -1408,6 +1490,33 @@ function* selectedPixels(index, provinceId, geometry, signal) {
1408
1490
  }
1409
1491
  return offsets.subarray(0, uniqueCount);
1410
1492
  }
1493
+ if (geometry.kind === 'rectangle') {
1494
+ const { origin, width, height } = geometry;
1495
+ const count = width * height;
1496
+ if (!Number.isSafeInteger(width) ||
1497
+ !Number.isSafeInteger(height) ||
1498
+ width <= 0 ||
1499
+ height <= 0 ||
1500
+ origin.x < 0 ||
1501
+ origin.y < 0 ||
1502
+ origin.x + width > raster.width ||
1503
+ origin.y + height > raster.height)
1504
+ throw new ServiceError('MAP_GEOMETRY_OUT_OF_BOUNDS', 'Province rectangle must be a positive integer rectangle inside the active raster', { origin, width, height, raster: { width: raster.width, height: raster.height } });
1505
+ assertSelectedPixelBudget(count, 'Rectangle geometry');
1506
+ const offsets = new Uint32Array(count);
1507
+ let selectedIndex = 0;
1508
+ for (let y = origin.y; y < origin.y + height; y += 1) {
1509
+ yield* cancellationCheckpoint(signal, y - origin.y, 16);
1510
+ for (let x = origin.x; x < origin.x + width; x += 1) {
1511
+ const offset = y * raster.width + x;
1512
+ if (raster.provinceIds[offset] !== provinceId)
1513
+ throw new ServiceError('MAP_GEOMETRY_OUTSIDE_SOURCE', 'Every rectangle pixel must belong to the source province', { x, y, provinceId });
1514
+ offsets[selectedIndex] = offset;
1515
+ selectedIndex += 1;
1516
+ }
1517
+ }
1518
+ return offsets;
1519
+ }
1411
1520
  if (geometry.kind === 'mask') {
1412
1521
  const offsets = yield* rasterMaskPixels(raster, geometry, signal);
1413
1522
  for (let indexOffset = 0; indexOffset < offsets.length; indexOffset += 1) {
@@ -1518,17 +1627,18 @@ function* provincePixelOffsets(raster, provinceIds, source, signal) {
1518
1627
  return offsets;
1519
1628
  }
1520
1629
  function definitionFromOperation(source, operation, id, allocatedColor) {
1521
- const values = operation.definition.method === 'inherit-source'
1630
+ const definitionPolicy = operation.definition ?? { method: 'inherit-source' };
1631
+ const values = definitionPolicy.method === 'inherit-source'
1522
1632
  ? {
1523
- type: operation.definition.overrides?.type ?? source.type,
1524
- coastal: operation.definition.overrides?.coastal ?? source.coastal,
1525
- terrain: operation.definition.overrides?.terrain ?? source.terrain,
1526
- continent: operation.definition.overrides?.continent ?? source.continent,
1527
- color: operation.definition.overrides?.color ?? allocatedColor,
1633
+ type: definitionPolicy.overrides?.type ?? source.type,
1634
+ coastal: definitionPolicy.overrides?.coastal ?? source.coastal,
1635
+ terrain: definitionPolicy.overrides?.terrain ?? source.terrain,
1636
+ continent: definitionPolicy.overrides?.continent ?? source.continent,
1637
+ color: definitionPolicy.overrides?.color ?? allocatedColor,
1528
1638
  }
1529
1639
  : {
1530
- ...operation.definition.value,
1531
- color: operation.definition.value.color ?? allocatedColor,
1640
+ ...definitionPolicy.value,
1641
+ color: definitionPolicy.value.color ?? allocatedColor,
1532
1642
  };
1533
1643
  return {
1534
1644
  id,
@@ -1542,7 +1652,22 @@ function definitionFromOperation(source, operation, id, allocatedColor) {
1542
1652
  };
1543
1653
  }
1544
1654
  function requireSplitProvincePolicy(operation, source) {
1545
- const policy = operation.distribution;
1655
+ const requested = operation.distribution;
1656
+ const policy = operation.kind === 'create_province' &&
1657
+ (requested === undefined || requested === 'inherit-source')
1658
+ ? {
1659
+ state: source.type === 'land' ? 'inherit-source' : 'none',
1660
+ strategicRegion: 'inherit-source',
1661
+ victoryPoints: 'retain-source',
1662
+ provinceBuildings: 'retain-source',
1663
+ ports: 'retain-source',
1664
+ supplyNodes: 'retain-source',
1665
+ railways: 'retain-source',
1666
+ adjacencies: 'retain-source',
1667
+ positions: 'retain-source',
1668
+ entityLocators: 'retain-source',
1669
+ }
1670
+ : requested;
1546
1671
  if (policy?.strategicRegion !== 'inherit-source' ||
1547
1672
  policy.victoryPoints !== 'retain-source' ||
1548
1673
  policy.provinceBuildings !== 'retain-source' ||
@@ -2579,6 +2704,186 @@ function remapReferenceFiles(index, changes, provinceIds, operationId) {
2579
2704
  addChange(changes, document.file.relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2580
2705
  }
2581
2706
  }
2707
+ function renumberMapping(existingIds, operation) {
2708
+ const ids = new Set(existingIds);
2709
+ if (!ids.has(operation.fromId))
2710
+ throw new ServiceError('MAP_ENTITY_ID_NOT_FOUND', `${operation.entity} ${operation.fromId} does not exist`);
2711
+ if (operation.fromId === operation.toId)
2712
+ throw new ServiceError('MAP_ENTITY_ID_UNCHANGED', `${operation.entity} already uses ID ${operation.toId}`);
2713
+ const targetOccupied = ids.has(operation.toId);
2714
+ if (targetOccupied && (operation.collision ?? 'swap') === 'reject')
2715
+ throw new ServiceError('MAP_ENTITY_ID_COLLISION', `${operation.entity} ID ${operation.toId} already exists`);
2716
+ return new Map([
2717
+ [operation.fromId, operation.toId],
2718
+ ...(targetOccupied ? [[operation.toId, operation.fromId]] : []),
2719
+ ]);
2720
+ }
2721
+ function renameLocalisationKeys(index, changes, keys, operationId) {
2722
+ if (keys.size === 0)
2723
+ return;
2724
+ for (const [from, to] of keys) {
2725
+ const destination = index.localisationEntries.filter(({ entry }) => entry.key === to);
2726
+ if (destination.length > 0 && !keys.has(to))
2727
+ throw new ServiceError('MAP_LOCALISATION_KEY_COLLISION', `Localisation key ${to} already exists`, { from, to });
2728
+ }
2729
+ const documents = new Map();
2730
+ for (const { file, document, entry } of index.localisationEntries) {
2731
+ if (file.rootKind !== 'mod')
2732
+ continue;
2733
+ const replacementKey = keys.get(entry.key);
2734
+ if (replacementKey === undefined)
2735
+ continue;
2736
+ requireSafeLocalisationDocument(document, file.relativePath);
2737
+ const group = documents.get(file.relativePath) ?? { document, replacements: [] };
2738
+ const line = document.text.slice(entry.start, entry.end);
2739
+ const keyOffset = line.indexOf(entry.key);
2740
+ if (keyOffset < 0)
2741
+ throw new ServiceError('MAP_LOCALISATION_KEY_SOURCE_INVALID', `Localisation source line does not contain key ${entry.key}`, { file: file.displayPath, line: entry.line });
2742
+ group.replacements.push({
2743
+ start: entry.start + keyOffset,
2744
+ end: entry.start + keyOffset + entry.key.length,
2745
+ text: replacementKey,
2746
+ description: 'Renumber map localisation key',
2747
+ });
2748
+ documents.set(file.relativePath, group);
2749
+ }
2750
+ for (const [relativePath, { document, replacements }] of documents) {
2751
+ const ordered = [...replacements].sort((left, right) => left.start - right.start);
2752
+ let cursor = 0;
2753
+ let text = '';
2754
+ for (const replacement of ordered) {
2755
+ if (replacement.start < cursor)
2756
+ throw new ServiceError('MAP_LOCALISATION_REPLACEMENT_OVERLAP', 'Map localisation key replacements overlap');
2757
+ text += document.text.slice(cursor, replacement.start) + replacement.text;
2758
+ cursor = replacement.end;
2759
+ }
2760
+ text += document.text.slice(cursor);
2761
+ addChange(changes, relativePath, encodeSource(text, 'utf8-bom'), operationId, 'text/yaml');
2762
+ }
2763
+ }
2764
+ function remapBuildingPositionStateIds(index, changes, stateIds, operationId) {
2765
+ const documents = new Map(index.buildingPositions.map((record) => [record.document.file.relativePath, record.document]));
2766
+ for (const [relativePath, document] of documents) {
2767
+ const replacements = index.buildingPositions
2768
+ .filter((record) => record.document === document)
2769
+ .flatMap((record) => {
2770
+ const stateId = stateIds.get(record.stateId);
2771
+ const line = document.lines[record.line - 1];
2772
+ return stateId === undefined || line === undefined
2773
+ ? []
2774
+ : [
2775
+ {
2776
+ start: line.start,
2777
+ end: line.end,
2778
+ text: replaceDelimitedFields(line.text, new Map([[0, numberText(stateId)]])),
2779
+ description: 'Renumber building-position state reference',
2780
+ },
2781
+ ];
2782
+ });
2783
+ if (replacements.length > 0)
2784
+ addChange(changes, relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2785
+ }
2786
+ }
2787
+ function remapWeatherPositionRegionIds(index, changes, regionIds, operationId) {
2788
+ const documents = new Map(index.weatherPositions.map((record) => [record.document.file.relativePath, record.document]));
2789
+ for (const [relativePath, document] of documents) {
2790
+ const replacements = index.weatherPositions
2791
+ .filter((record) => record.document === document)
2792
+ .flatMap((record) => {
2793
+ const strategicRegionId = regionIds.get(record.strategicRegionId);
2794
+ const line = document.lines[record.line - 1];
2795
+ return strategicRegionId === undefined || line === undefined
2796
+ ? []
2797
+ : [
2798
+ {
2799
+ start: line.start,
2800
+ end: line.end,
2801
+ text: replaceDelimitedFields(line.text, new Map([[0, numberText(strategicRegionId)]])),
2802
+ description: 'Renumber weather-position strategic-region reference',
2803
+ },
2804
+ ];
2805
+ });
2806
+ if (replacements.length > 0)
2807
+ addChange(changes, relativePath, applyTextRanges(document, replacements), operationId, 'text/plain');
2808
+ }
2809
+ }
2810
+ function applyRenumberMapEntity(base, changes, operation) {
2811
+ const index = currentIndex(base, changes);
2812
+ if (!Number.isSafeInteger(operation.fromId) || !Number.isSafeInteger(operation.toId))
2813
+ throw new ServiceError('MAP_ENTITY_ID_INVALID', 'Map IDs must be safe integers');
2814
+ if ((operation.entity === 'province' && (operation.fromId < 0 || operation.toId < 0)) ||
2815
+ (operation.entity !== 'province' && (operation.fromId <= 0 || operation.toId <= 0)))
2816
+ throw new ServiceError('MAP_ENTITY_ID_INVALID', 'Map entity ID is outside its valid range');
2817
+ if (operation.entity === 'province') {
2818
+ const mapping = renumberMapping(index.definitions.map(({ id }) => id), operation);
2819
+ const projectedIds = index.definitions.map(({ id }) => remapProvinceId(mapping, id));
2820
+ if (!contiguous(projectedIds, 0))
2821
+ throw new ServiceError('MAP_PROVINCE_ID_GAP', 'Province renumbering must preserve the contiguous zero-based ID table; use the default swap when the target is occupied', { projectedIds: uniqueSorted(projectedIds) });
2822
+ const definitionUpdates = new Map();
2823
+ for (const [fromId, toId] of mapping) {
2824
+ const definition = required(index.definitionsById.get(fromId), 'MAP_ENTITY_ID_NOT_FOUND', `Province ${fromId} does not exist`);
2825
+ definitionUpdates.set(fromId, { ...definition, id: toId });
2826
+ }
2827
+ addChange(changes, required(index.definitionFile, 'MAP_DEFINITION_FILE_MISSING', 'Definition file is missing')
2828
+ .relativePath, patchDefinitions(index, definitionUpdates, new Set(), []), operation.id, 'text/plain');
2829
+ for (const state of index.states) {
2830
+ const data = cloneState(state);
2831
+ const fields = remapStateProvinceData(data, mapping);
2832
+ if (Object.values(fields).some(Boolean))
2833
+ commitState(changes, state, data, fields, operation.id);
2834
+ }
2835
+ for (const region of index.regions) {
2836
+ if (!region.provinces.some((id) => mapping.has(id)))
2837
+ continue;
2838
+ commitRegion(changes, region, uniqueSorted(region.provinces.map((id) => remapProvinceId(mapping, id))), operation.id);
2839
+ }
2840
+ remapReferenceFiles(index, changes, mapping, operation.id);
2841
+ if (operation.renameLocalisation ?? true) {
2842
+ const keys = new Map();
2843
+ for (const [fromId, toId] of mapping) {
2844
+ keys.set(`PROV${fromId}`, `PROV${toId}`);
2845
+ keys.set(`VICTORY_POINTS_${fromId}`, `VICTORY_POINTS_${toId}`);
2846
+ }
2847
+ renameLocalisationKeys(currentIndex(base, changes), changes, keys, operation.id);
2848
+ }
2849
+ return;
2850
+ }
2851
+ if (operation.entity === 'state') {
2852
+ const mapping = renumberMapping(index.states.map(({ id }) => id), operation);
2853
+ const localisationKeys = new Map();
2854
+ for (const [fromId, toId] of mapping) {
2855
+ const state = required(index.statesById.get(fromId), 'MAP_ENTITY_ID_NOT_FOUND', `State ${fromId} does not exist`);
2856
+ const data = cloneState(state);
2857
+ data.id = toId;
2858
+ const standardName = state.name === `STATE_${fromId}`;
2859
+ if (standardName) {
2860
+ data.name = `STATE_${toId}`;
2861
+ localisationKeys.set(state.name, data.name);
2862
+ }
2863
+ commitState(changes, state, data, { id: true, ...(standardName ? { name: true } : {}) }, operation.id);
2864
+ }
2865
+ remapBuildingPositionStateIds(index, changes, mapping, operation.id);
2866
+ if (operation.renameLocalisation ?? true)
2867
+ renameLocalisationKeys(currentIndex(base, changes), changes, localisationKeys, operation.id);
2868
+ return;
2869
+ }
2870
+ const mapping = renumberMapping(index.regions.map(({ id }) => id), operation);
2871
+ const projectedIds = index.regions.map(({ id }) => mapping.get(id) ?? id);
2872
+ if (!contiguous(projectedIds, 1))
2873
+ throw new ServiceError('MAP_STRATEGIC_REGION_ID_GAP', 'Strategic-region renumbering must preserve the contiguous one-based ID table; use the default swap when the target is occupied', { projectedIds: uniqueSorted(projectedIds) });
2874
+ const localisationKeys = new Map();
2875
+ for (const [fromId, toId] of mapping) {
2876
+ const region = required(index.regionsById.get(fromId), 'MAP_ENTITY_ID_NOT_FOUND', `Strategic region ${fromId} does not exist`);
2877
+ const standardName = region.name === `STRATEGICREGION_${fromId}`;
2878
+ const name = standardName ? `STRATEGICREGION_${toId}` : region.name;
2879
+ if (standardName)
2880
+ localisationKeys.set(region.name, name);
2881
+ addChange(changes, region.file.relativePath, patchRegion(region, region.provinces, { id: toId, ...(standardName ? { name } : {}) }, false), operation.id, 'text/plain');
2882
+ }
2883
+ remapWeatherPositionRegionIds(index, changes, mapping, operation.id);
2884
+ if (operation.renameLocalisation ?? true)
2885
+ renameLocalisationKeys(currentIndex(base, changes), changes, localisationKeys, operation.id);
2886
+ }
2582
2887
  function applyEntityLocator(base, changes, operation) {
2583
2888
  const index = currentIndex(base, changes);
2584
2889
  const matching = index.entityLocators.filter(({ entity, name }) => entity === operation.entity && name === operation.name);
@@ -2658,6 +2963,8 @@ function* planMapOperationSteps(base, operations, signal) {
2658
2963
  expectedChangedBounds = mergeBounds(expectedChangedBounds, yield* applyNormalAdjacency(base, changes, operation, signal));
2659
2964
  else if (operation.kind === 'update_entity_locator')
2660
2965
  applyEntityLocator(base, changes, operation);
2966
+ else if (operation.kind === 'renumber_map_entity')
2967
+ applyRenumberMapEntity(base, changes, operation);
2661
2968
  else
2662
2969
  handleSimpleOperation(base, changes, operation);
2663
2970
  }