hoi4-agent-tools 3.10.0 → 4.0.1
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 +131 -0
- package/README.md +91 -147
- package/dist/bin/http.js +10 -0
- package/dist/bin/setup.js +3 -2
- package/dist/bin/stdio.js +10 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
- package/dist/hoi4_agent_tools/core/canonical.js +18 -4
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +7 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
- package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
- package/dist/hoi4_agent_tools/core/engine.js +29 -2
- package/dist/hoi4_agent_tools/core/index.js +2 -2
- package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
- package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
- package/dist/hoi4_agent_tools/core/job-store.js +78 -5
- package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
- package/dist/hoi4_agent_tools/core/job-worker-host.js +314 -127
- package/dist/hoi4_agent_tools/core/job-worker.js +96 -37
- package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
- package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
- package/dist/hoi4_agent_tools/core/server-registry.js +22 -5
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
- package/dist/hoi4_agent_tools/core/source/cache.js +20 -10
- package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
- package/dist/hoi4_agent_tools/core/temporary-directories.js +293 -0
- package/dist/hoi4_agent_tools/core/transactions.js +114 -23
- package/dist/hoi4_agent_tools/core/worker-exit.js +80 -0
- package/dist/hoi4_agent_tools/core/workspace.js +84 -8
- package/dist/hoi4_agent_tools/decision/ai.js +1 -1
- package/dist/hoi4_agent_tools/decision/service.js +28 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
- package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
- package/dist/hoi4_agent_tools/event/operations.js +14 -1
- package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
- package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
- package/dist/hoi4_agent_tools/gui/assets.js +3 -0
- package/dist/hoi4_agent_tools/gui/dds.js +8 -4
- package/dist/hoi4_agent_tools/gui/layout.js +45 -5
- package/dist/hoi4_agent_tools/gui/native-state.js +244 -0
- package/dist/hoi4_agent_tools/gui/renderer.js +12 -7
- package/dist/hoi4_agent_tools/gui/source-graph.js +17 -4
- package/dist/hoi4_agent_tools/gui/studio.js +33 -21
- package/dist/hoi4_agent_tools/gui/validators.js +8 -6
- package/dist/hoi4_agent_tools/map/bmp.js +1 -1
- package/dist/hoi4_agent_tools/map/derived.js +748 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
- package/dist/hoi4_agent_tools/map/model.js +103 -0
- package/dist/hoi4_agent_tools/map/operations.js +427 -58
- package/dist/hoi4_agent_tools/map/references.js +483 -0
- package/dist/hoi4_agent_tools/map/service.js +136 -61
- package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
- package/dist/hoi4_agent_tools/map/validation.js +3 -0
- package/dist/hoi4_agent_tools/map/world.js +1280 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +2 -2
- package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
- package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
- package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +28 -8
- package/dist/hoi4_agent_tools/mcp/tools/event.js +19 -30
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +5 -13
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -6
- package/dist/hoi4_agent_tools/mcp/tools/job.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -18
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +26 -9
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +17 -30
- package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
- package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +31 -8
- package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
- package/dist/hoi4_agent_tools/probability/operations.js +175 -13
- package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
- package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
- package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
- package/dist/hoi4_agent_tools/reference/operations.js +88 -19
- package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
- package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
- package/dist/hoi4_agent_tools/reference/service.js +6 -0
- package/dist/hoi4_agent_tools/runtime.js +14 -1
- package/dist/hoi4_agent_tools/schemas/event.js +78 -19
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
- package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
- package/dist/hoi4_agent_tools/schemas/map.js +122 -3
- package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
- package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
- package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
- package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
- package/dist/hoi4_agent_tools/schemas/technology.js +79 -21
- package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
- package/dist/hoi4_agent_tools/technology/operations.js +14 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +7 -4
- package/docs/analysis.md +15 -2
- package/docs/configuration.md +281 -0
- package/docs/development.md +26 -2
- package/docs/events.md +29 -6
- package/docs/examples.md +13 -28
- package/docs/focus.md +127 -5
- package/docs/gui.md +105 -21
- package/docs/helper-expansion.md +9 -9
- package/docs/http.md +3 -1
- package/docs/jobs.md +23 -14
- package/docs/map.md +199 -7
- package/docs/mechanics.md +6 -2
- package/docs/probability.md +46 -16
- package/docs/reference.md +73 -5
- package/docs/setup.md +45 -14
- package/docs/technology.md +28 -4
- package/docs/third-party-notices.md +11 -0
- package/docs/tools.md +150 -42
- package/docs/troubleshooting.md +61 -0
- package/examples/clients/codex-config.toml +5 -2
- package/examples/clients/generic.json +2 -2
- package/examples/config/config.json +7 -0
- package/npm-shrinkwrap.json +151 -156
- package/package.json +13 -8
- package/schemas/configuration.schema.json +144 -128
- 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 +55 -0
- 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/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 +597 -126
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +51 -0
- package/schemas/operation-result.schema.json +2 -2
- 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/scenario-suite.schema.json +3 -5
- package/schemas/scenario-test-input.schema.json +3 -5
- 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 +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
|
-
|
|
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
|
|
771
|
-
if (
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
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
|
-
|
|
1038
|
-
|
|
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 (
|
|
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, {
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1407
|
-
|
|
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
|
-
|
|
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 === '
|
|
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,
|