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.
- package/CHANGELOG.md +16 -0
- package/dist/hoi4_agent_tools/map/border-quality.js +25 -7
- package/dist/hoi4_agent_tools/map/derived.js +16 -3
- package/dist/hoi4_agent_tools/map/gameplay/crossing-validation.js +268 -0
- package/dist/hoi4_agent_tools/map/gameplay/crossings.js +961 -0
- package/dist/hoi4_agent_tools/map/gameplay/development.js +168 -0
- package/dist/hoi4_agent_tools/map/gameplay/economy.js +75 -0
- package/dist/hoi4_agent_tools/map/gameplay/facts.js +264 -0
- package/dist/hoi4_agent_tools/map/gameplay/inland-sea.js +66 -0
- package/dist/hoi4_agent_tools/map/gameplay/metrics.js +132 -0
- package/dist/hoi4_agent_tools/map/gameplay/names.js +419 -0
- package/dist/hoi4_agent_tools/map/gameplay/ports.js +244 -0
- package/dist/hoi4_agent_tools/map/gameplay/prepare.js +27 -0
- package/dist/hoi4_agent_tools/map/gameplay/rail-cost.js +77 -0
- package/dist/hoi4_agent_tools/map/gameplay/sites.js +61 -0
- package/dist/hoi4_agent_tools/map/geography/resolve.js +15 -4
- package/dist/hoi4_agent_tools/map/operations.js +109 -32
- package/dist/hoi4_agent_tools/map/relief/hydrology.js +316 -192
- package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +43 -0
- package/dist/hoi4_agent_tools/map/service.js +78 -16
- package/dist/hoi4_agent_tools/map/state-restructure.js +378 -0
- package/dist/hoi4_agent_tools/map/territory-graph.js +238 -0
- package/dist/hoi4_agent_tools/map/territory-operations.js +944 -0
- package/dist/hoi4_agent_tools/map/territory-quality.js +189 -0
- package/dist/hoi4_agent_tools/map/territory.js +1108 -0
- package/dist/hoi4_agent_tools/map/tool-operations.js +78 -0
- package/dist/hoi4_agent_tools/map/validation.js +4 -0
- package/dist/hoi4_agent_tools/map/world.js +301 -107
- package/dist/hoi4_agent_tools/schemas/geography.js +12 -1
- package/dist/hoi4_agent_tools/schemas/map.js +206 -21
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/map.md +98 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/geography-model.schema.json +13 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1355 -736
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/reference-sheets.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- 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
|
-
|
|
1276
|
-
|
|
1277
|
-
const state = index.statesById.get(
|
|
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 ${
|
|
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,
|
|
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, {
|
|
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
|
}
|