hoi4-agent-tools 4.3.0 → 4.5.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 +33 -1
- package/dist/hoi4_agent_tools/core/bounded-source.js +31 -9
- package/dist/hoi4_agent_tools/core/configuration.js +11 -0
- package/dist/hoi4_agent_tools/core/managed-outputs.js +26 -6
- package/dist/hoi4_agent_tools/core/operation-context.js +17 -1
- package/dist/hoi4_agent_tools/core/source/encoding.js +56 -2
- package/dist/hoi4_agent_tools/core/source/parser.js +16 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +7 -2
- package/dist/hoi4_agent_tools/core/transaction-limits.js +18 -4
- package/dist/hoi4_agent_tools/core/transactions.js +23 -4
- package/dist/hoi4_agent_tools/core/workspace-json.js +54 -0
- package/dist/hoi4_agent_tools/core/workspace.js +42 -6
- package/dist/hoi4_agent_tools/focus/drift.js +34 -0
- package/dist/hoi4_agent_tools/focus/service.js +9 -3
- package/dist/hoi4_agent_tools/focus/tool-operations.js +2 -0
- package/dist/hoi4_agent_tools/map/appearance-validation.js +1110 -0
- package/dist/hoi4_agent_tools/map/bmp.js +9 -1
- package/dist/hoi4_agent_tools/map/border-quality-input.js +209 -0
- package/dist/hoi4_agent_tools/map/border-quality.js +1180 -0
- package/dist/hoi4_agent_tools/map/colormap.js +128 -0
- package/dist/hoi4_agent_tools/map/geography/geometry.js +284 -0
- package/dist/hoi4_agent_tools/map/geography/index.js +6 -0
- package/dist/hoi4_agent_tools/map/geography/model.js +196 -0
- package/dist/hoi4_agent_tools/map/geography/preparation.js +120 -0
- package/dist/hoi4_agent_tools/map/geography/preview.js +647 -0
- package/dist/hoi4_agent_tools/map/geography/raster.js +253 -0
- package/dist/hoi4_agent_tools/map/geography/resolve.js +1467 -0
- package/dist/hoi4_agent_tools/map/model.js +17 -3
- package/dist/hoi4_agent_tools/map/operations.js +284 -14
- package/dist/hoi4_agent_tools/map/partition-fields.js +398 -0
- package/dist/hoi4_agent_tools/map/partition-geography.js +113 -0
- package/dist/hoi4_agent_tools/map/partition-graph.js +165 -0
- package/dist/hoi4_agent_tools/map/partition-groups.js +164 -0
- package/dist/hoi4_agent_tools/map/partition-provinces.js +727 -0
- package/dist/hoi4_agent_tools/map/partition-tuning.js +101 -0
- package/dist/hoi4_agent_tools/map/partition-watershed.js +252 -0
- package/dist/hoi4_agent_tools/map/partition.js +163 -0
- package/dist/hoi4_agent_tools/map/reference-import.js +333 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +142 -6
- package/dist/hoi4_agent_tools/map/reference-sheets.js +231 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +104 -13
- package/dist/hoi4_agent_tools/map/relief/checks.js +281 -0
- package/dist/hoi4_agent_tools/map/relief/fields.js +15 -0
- package/dist/hoi4_agent_tools/map/relief/full-resolution.js +133 -0
- package/dist/hoi4_agent_tools/map/relief/grid.js +190 -0
- package/dist/hoi4_agent_tools/map/relief/hydrology.js +1315 -0
- package/dist/hoi4_agent_tools/map/relief/index.js +27 -0
- package/dist/hoi4_agent_tools/map/relief/land.js +110 -0
- package/dist/hoi4_agent_tools/map/relief/macro-relief.js +535 -0
- package/dist/hoi4_agent_tools/map/relief/math.js +93 -0
- package/dist/hoi4_agent_tools/map/relief/river-raster.js +680 -0
- package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +304 -0
- package/dist/hoi4_agent_tools/map/render.js +36 -0
- package/dist/hoi4_agent_tools/map/service.js +206 -16
- package/dist/hoi4_agent_tools/map/tool-operations.js +249 -3
- package/dist/hoi4_agent_tools/map/validation.js +9 -0
- package/dist/hoi4_agent_tools/map/water.js +227 -0
- package/dist/hoi4_agent_tools/map/world-context.js +47 -1
- package/dist/hoi4_agent_tools/map/world-edge.js +335 -0
- package/dist/hoi4_agent_tools/map/world-noise.js +34 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +22 -10
- package/dist/hoi4_agent_tools/map/world-partition.js +119 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +131 -56
- package/dist/hoi4_agent_tools/map/world-realms.js +152 -0
- package/dist/hoi4_agent_tools/map/world.js +562 -163
- package/dist/hoi4_agent_tools/mcp/tools/map.js +69 -1
- package/dist/hoi4_agent_tools/reference/operations.js +3 -2
- package/dist/hoi4_agent_tools/schemas/geography.js +325 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +20 -2
- package/dist/hoi4_agent_tools/schemas/map.js +280 -15
- package/dist/hoi4_agent_tools/schemas/reference-sheets.js +92 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/configuration.md +17 -14
- package/docs/focus.md +6 -0
- package/docs/map.md +292 -15
- package/docs/tools.md +4 -4
- package/docs/troubleshooting.md +11 -11
- package/npm-shrinkwrap.json +109 -91
- package/package.json +3 -3
- package/schemas/configuration.schema.json +65 -58
- 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 +1148 -0
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +5 -11
- 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 +2325 -292
- 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 +25 -7
- 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 +267 -0
- 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
|
@@ -4,7 +4,7 @@ import { SymbolIndex } from '../core/index.js';
|
|
|
4
4
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
5
5
|
import { addMapDiagnostic, addMapDiagnostics } from './diagnostic-limit.js';
|
|
6
6
|
import { ServiceError } from '../core/result.js';
|
|
7
|
-
import { assignments, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
|
|
7
|
+
import { assignments, assertSourceEncodingPreserved, childBlocks, decodeSource, encodeSource, firstScalar, nodeLocation, parseClausewitz, parseLocalisation, } from '../core/source/index.js';
|
|
8
8
|
import { BmpImage, rgbKey } from './bmp.js';
|
|
9
9
|
const defaultMapIndexSourceRoots = {
|
|
10
10
|
map: ['map'],
|
|
@@ -87,7 +87,11 @@ export function selectActiveFiles(files) {
|
|
|
87
87
|
}
|
|
88
88
|
export function parseTextDocument(file) {
|
|
89
89
|
if (file.bytes.length > MAP_TEXT_MAX_BYTES) {
|
|
90
|
-
throw new ServiceError('MAP_TEXT_FILE_LIMIT',
|
|
90
|
+
throw new ServiceError('MAP_TEXT_FILE_LIMIT', `Map text table ${file.relativePath} exceeds the supported 16 MiB parsing limit (${file.bytes.length} bytes)`, {
|
|
91
|
+
relativePath: file.relativePath,
|
|
92
|
+
bytes: file.bytes.length,
|
|
93
|
+
maximumBytes: MAP_TEXT_MAX_BYTES,
|
|
94
|
+
});
|
|
91
95
|
}
|
|
92
96
|
const decoded = decodeSource(file.bytes);
|
|
93
97
|
const lines = [];
|
|
@@ -101,7 +105,11 @@ export function parseTextDocument(file) {
|
|
|
101
105
|
const text = full.replace(/(?:\r\n|\n|\r)$/u, '');
|
|
102
106
|
const start = match.index;
|
|
103
107
|
if (index >= MAP_TEXT_MAX_RECORDS) {
|
|
104
|
-
throw new ServiceError('MAP_TEXT_RECORD_LIMIT',
|
|
108
|
+
throw new ServiceError('MAP_TEXT_RECORD_LIMIT', `Map text table ${file.relativePath} exceeds the supported record limit`, {
|
|
109
|
+
relativePath: file.relativePath,
|
|
110
|
+
records: index + 1,
|
|
111
|
+
maximumRecords: MAP_TEXT_MAX_RECORDS,
|
|
112
|
+
});
|
|
105
113
|
}
|
|
106
114
|
lines.push({ index, start, end: start + text.length, fullEnd: start + full.length, text });
|
|
107
115
|
index += 1;
|
|
@@ -111,6 +119,7 @@ export function parseTextDocument(file) {
|
|
|
111
119
|
return { ...decoded, file, lines };
|
|
112
120
|
}
|
|
113
121
|
export function encodeTextDocument(document, text) {
|
|
122
|
+
assertSourceEncodingPreserved(document, document.file.displayPath);
|
|
114
123
|
return encodeSource(text, document.encoding);
|
|
115
124
|
}
|
|
116
125
|
function lineLocation(document, line) {
|
|
@@ -1114,6 +1123,9 @@ export class MapWorkspaceIndex {
|
|
|
1114
1123
|
railwayFile;
|
|
1115
1124
|
/** The active heightmap, when the scan included it; decoded on first use. */
|
|
1116
1125
|
heightmapFile;
|
|
1126
|
+
/** The active river and terrain bitmaps, when the scan included them (border quality). */
|
|
1127
|
+
riversFile;
|
|
1128
|
+
terrainBitmapFile;
|
|
1117
1129
|
#heightmap;
|
|
1118
1130
|
/**
|
|
1119
1131
|
* Map height (the Y coordinate) at a bitmap pixel: the heightmap value divided by ten, as the
|
|
@@ -1297,6 +1309,8 @@ export class MapWorkspaceIndex {
|
|
|
1297
1309
|
? undefined
|
|
1298
1310
|
: parseTerrainCategories(terrainFiles, this.diagnostics);
|
|
1299
1311
|
this.heightmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'heightmap', 'heightmap.bmp'), this.sourceRoots.map);
|
|
1312
|
+
this.riversFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'rivers', 'rivers.bmp'), this.sourceRoots.map);
|
|
1313
|
+
this.terrainBitmapFile = mapFile(this.activeFiles, defaultMapSelectorValue(this.defaultMapFile, 'terrain', 'terrain.bmp'), this.sourceRoots.map);
|
|
1300
1314
|
const buildingFiles = this.activeFiles.all.filter((file) => /^common\/buildings\/[^/]+\.txt$/iu.test(normalizedPath(file.relativePath)));
|
|
1301
1315
|
const buildings = parseBuildingDefinitions(buildingFiles, []);
|
|
1302
1316
|
this.buildingDefinitions = buildings.definitions;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
3
|
-
import { compareCodeUnits, deterministicId, sha256Bytes } from '../core/canonical.js';
|
|
3
|
+
import { canonicalJson, compareCodeUnits, deterministicId, sha256Bytes, } from '../core/canonical.js';
|
|
4
4
|
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';
|
|
@@ -8,9 +8,12 @@ import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES,
|
|
|
8
8
|
import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
|
|
9
9
|
import { planReferenceRemap, ReferenceRemapError } from './references.js';
|
|
10
10
|
import { generateWorld } from './world.js';
|
|
11
|
+
import { resolveEdge } from './world-edge.js';
|
|
11
12
|
import { planMapPositions, planSupplyRebuild } from './derived.js';
|
|
12
|
-
import { buildWorldContext } from './world-context.js';
|
|
13
|
-
import {
|
|
13
|
+
import { buildWorldContext, countryTagCatalog, WORLD_COUNTRY_TAG_FILE, } from './world-context.js';
|
|
14
|
+
import { planWorldOutputs } from './world-outputs.js';
|
|
15
|
+
import { resolvePartitionMode } from './partition.js';
|
|
16
|
+
import { unsupportedGeographyParts } from './relief/index.js';
|
|
14
17
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
15
18
|
if (iteration % stride !== 0)
|
|
16
19
|
return;
|
|
@@ -38,6 +41,8 @@ async function completeCooperatively(steps, signal) {
|
|
|
38
41
|
yieldDeadline = performance.now() + 8;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
44
|
+
/** Paths listed per kind in diagnostics and tool results; the artifact lists them all. */
|
|
45
|
+
export const WORLD_OUTPUT_PATH_SAMPLE = 200;
|
|
41
46
|
function normalizeRelative(value) {
|
|
42
47
|
return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
|
|
43
48
|
}
|
|
@@ -771,6 +776,7 @@ function proposedChanges(changes) {
|
|
|
771
776
|
/** Diagnostics an operation raises while applying, drained into the plan after it succeeds. */
|
|
772
777
|
const diagnosticsByPlan = new WeakMap();
|
|
773
778
|
const replacedRootsByPlan = new WeakMap();
|
|
779
|
+
const worldOutputsByPlan = new WeakMap();
|
|
774
780
|
function currentIndex(base, changes) {
|
|
775
781
|
return indexWithProposedChanges(base, proposedChanges(changes), replacedRootsByPlan.get(changes) ?? []);
|
|
776
782
|
}
|
|
@@ -3272,33 +3278,100 @@ function descriptorWithReplacePaths(index, roots) {
|
|
|
3272
3278
|
const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
|
|
3273
3279
|
return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
|
|
3274
3280
|
}
|
|
3275
|
-
function
|
|
3281
|
+
function colormapPaletteOverrides(palette) {
|
|
3282
|
+
return Object.fromEntries(Object.entries(palette).flatMap(([name, color]) => color === undefined ? [] : [[name, [color.r, color.g, color.b]]]));
|
|
3283
|
+
}
|
|
3284
|
+
function applyCreateWorld(base, changes, operation, context, signal) {
|
|
3285
|
+
const geography = context.geographyModels?.get(operation.id);
|
|
3286
|
+
const hasGeography = operation.geography !== undefined || operation.geographyPath !== undefined;
|
|
3287
|
+
if (hasGeography) {
|
|
3288
|
+
if (geography === undefined)
|
|
3289
|
+
throw new ServiceError('MAP_GEOGRAPHY_NOT_PREPARED', 'Geography models must be prepared by the authorized workspace service');
|
|
3290
|
+
if (!geography.valid)
|
|
3291
|
+
throw new ServiceError('MAP_GEOGRAPHY_INVALID', 'The geography model has errors; fix them (see the MAP_GEOGRAPHY_* diagnostics and map-geography-report.json) before generating a world', { modelHash: geography.geography.hash });
|
|
3292
|
+
const pending = unsupportedGeographyParts(geography.geography);
|
|
3293
|
+
if (pending.length > 0)
|
|
3294
|
+
throw new ServiceError('MAP_GEOGRAPHY_SYNTHESIS_UNAVAILABLE', `The geography model is valid and its relief, rivers and lakes can be synthesized, but this version cannot yet use ${pending.join(', ')} (the climate and biome stage); remove them from the model to generate relief and rivers only, or use preview:true to review the model overlay and report`, {
|
|
3295
|
+
modelHash: geography.geography.hash,
|
|
3296
|
+
available: ['relief', 'rivers', 'lakes'],
|
|
3297
|
+
unavailable: pending,
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
else {
|
|
3301
|
+
if (operation.relief !== undefined)
|
|
3302
|
+
throw new ServiceError('MAP_RELIEF_REQUIRES_GEOGRAPHY', 'relief shapes the terrain synthesised from a geography model; give geography or geographyPath, or remove relief');
|
|
3303
|
+
if (typeof operation.rivers === 'object')
|
|
3304
|
+
throw new ServiceError('MAP_RIVER_DENSITY_REQUIRES_GEOGRAPHY', 'rivers { density } sets the river network of a geography world; give geography or geographyPath, or use an integer count');
|
|
3305
|
+
}
|
|
3276
3306
|
const reference = context.referenceWorlds?.get(operation.id);
|
|
3277
3307
|
if (operation.reference !== undefined && reference === undefined)
|
|
3278
3308
|
throw new ServiceError('MAP_REFERENCE_NOT_PREPARED', 'Reference worlds must be prepared by the authorized workspace service');
|
|
3279
3309
|
const mapRoot = base.sourceRoots.map[0] ?? 'map';
|
|
3280
3310
|
const startingScenario = operation.startingScenario ?? reference !== undefined;
|
|
3311
|
+
const worldWidth = operation.width ?? reference?.surface.width ?? 1_024;
|
|
3312
|
+
const worldHeight = operation.height ?? reference?.surface.height ?? 512;
|
|
3313
|
+
const edge = resolveEdge(operation.edge, worldWidth, worldHeight);
|
|
3314
|
+
const look = operation.look;
|
|
3315
|
+
const colormapProfile = operation.colormap?.profile ?? (look === undefined ? 'legacy' : 'calibrated');
|
|
3316
|
+
if (look === 'custom' && operation.colormap?.palette === undefined)
|
|
3317
|
+
throw new ServiceError('MAP_LOOK_CUSTOM_PALETTE_REQUIRED', 'look "custom" needs colormap.palette with at least one class colour');
|
|
3318
|
+
if (colormapProfile === 'legacy' && look !== undefined)
|
|
3319
|
+
throw new ServiceError('MAP_LOOK_REQUIRES_CALIBRATED_COLORMAP', 'look selects a calibrated palette; remove colormap.profile "legacy" or look');
|
|
3320
|
+
const workspaceCountries = operation.countries !== undefined && !Array.isArray(operation.countries)
|
|
3321
|
+
? operation.countries
|
|
3322
|
+
: undefined;
|
|
3323
|
+
const tagCatalog = countryTagCatalog(base);
|
|
3324
|
+
if (workspaceCountries !== undefined) {
|
|
3325
|
+
const declared = new Set(tagCatalog.static);
|
|
3326
|
+
const dynamicTags = new Set(tagCatalog.dynamic);
|
|
3327
|
+
for (const realm of workspaceCountries.realms) {
|
|
3328
|
+
if (dynamicTags.has(realm.tag))
|
|
3329
|
+
throw new ServiceError('MAP_WORLD_REALM_TAG_DYNAMIC', `${realm.tag} is a dynamic tag; a world can only place countries with static tags`, { tag: realm.tag });
|
|
3330
|
+
if (!declared.has(realm.tag))
|
|
3331
|
+
throw new ServiceError('MAP_WORLD_REALM_TAG_UNKNOWN', `No active common/country_tags file other than the generated world's declares ${realm.tag}`, { tag: realm.tag });
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
const partitionMode = resolvePartitionMode(operation.partition, hasGeography);
|
|
3335
|
+
if (partitionMode === 'legacy' && operation.partitionTuning !== undefined)
|
|
3336
|
+
throw new ServiceError('MAP_PARTITION_TUNING_REQUIRES_FEATURE_AWARE', 'partitionTuning applies to the feature-aware partition; set partition to "feature-aware" or remove it');
|
|
3281
3337
|
const world = generateWorld({
|
|
3282
3338
|
startingScenario,
|
|
3283
3339
|
...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
|
|
3284
|
-
width:
|
|
3285
|
-
height:
|
|
3340
|
+
width: worldWidth,
|
|
3341
|
+
height: worldHeight,
|
|
3286
3342
|
seed: operation.seed ?? 1,
|
|
3287
3343
|
landCoverage: operation.landCoverage ?? 0.35,
|
|
3288
3344
|
continents: operation.continents ?? 3,
|
|
3289
3345
|
...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
|
|
3290
3346
|
landProvincePixels: operation.landProvincePixels ?? 400,
|
|
3291
3347
|
seaProvincePixels: operation.seaProvincePixels ?? 2_500,
|
|
3348
|
+
provinceSizesDefaulted: operation.landProvincePixels === undefined && operation.seaProvincePixels === undefined,
|
|
3292
3349
|
provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
|
|
3293
3350
|
landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
|
|
3294
3351
|
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
|
|
3295
|
-
countries:
|
|
3352
|
+
countries: workspaceCountries?.realms.map((realm) => ({
|
|
3353
|
+
tag: realm.tag,
|
|
3354
|
+
name: realm.tag,
|
|
3355
|
+
...(realm.weight === undefined ? {} : { share: realm.weight }),
|
|
3356
|
+
})) ??
|
|
3357
|
+
operation.countries ??
|
|
3296
3358
|
reference?.countries ?? [
|
|
3297
3359
|
{ tag: 'X01', name: 'Aster' },
|
|
3298
3360
|
{ tag: 'X02', name: 'Brum' },
|
|
3299
3361
|
{ tag: 'X03', name: 'Corvel' },
|
|
3300
3362
|
{ tag: 'X04', name: 'Doran' },
|
|
3301
3363
|
],
|
|
3364
|
+
...(workspaceCountries === undefined
|
|
3365
|
+
? {}
|
|
3366
|
+
: {
|
|
3367
|
+
realms: {
|
|
3368
|
+
list: workspaceCountries.realms,
|
|
3369
|
+
...(workspaceCountries.unowned === undefined
|
|
3370
|
+
? {}
|
|
3371
|
+
: { unowned: workspaceCountries.unowned }),
|
|
3372
|
+
islands: workspaceCountries.islands ?? 'nearest',
|
|
3373
|
+
},
|
|
3374
|
+
}),
|
|
3302
3375
|
...(reference === undefined
|
|
3303
3376
|
? {}
|
|
3304
3377
|
: {
|
|
@@ -3310,8 +3383,47 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3310
3383
|
? {}
|
|
3311
3384
|
: { continentNames: operation.continentNames }),
|
|
3312
3385
|
startDate: operation.startDate ?? '1936.1.1',
|
|
3313
|
-
rivers: operation.rivers
|
|
3314
|
-
|
|
3386
|
+
rivers: typeof operation.rivers === 'number' ? operation.rivers : reference === undefined ? 8 : 80,
|
|
3387
|
+
...(geography === undefined
|
|
3388
|
+
? {}
|
|
3389
|
+
: {
|
|
3390
|
+
geography: geography.geography,
|
|
3391
|
+
...(operation.relief === undefined
|
|
3392
|
+
? {}
|
|
3393
|
+
: {
|
|
3394
|
+
relief: {
|
|
3395
|
+
...(operation.relief.scale === undefined
|
|
3396
|
+
? {}
|
|
3397
|
+
: { scale: operation.relief.scale }),
|
|
3398
|
+
...(operation.relief.detail === undefined
|
|
3399
|
+
? {}
|
|
3400
|
+
: { detail: operation.relief.detail }),
|
|
3401
|
+
},
|
|
3402
|
+
}),
|
|
3403
|
+
...(typeof operation.rivers === 'object'
|
|
3404
|
+
? { riverDensity: operation.rivers.density }
|
|
3405
|
+
: typeof operation.rivers === 'number'
|
|
3406
|
+
? { riverCount: operation.rivers }
|
|
3407
|
+
: {}),
|
|
3408
|
+
}),
|
|
3409
|
+
...(edge.mode === 'none' ? {} : { edge }),
|
|
3410
|
+
...(operation.ambientFrame === true ? { ambientFrame: true } : {}),
|
|
3411
|
+
colormapProfile,
|
|
3412
|
+
...(partitionMode === 'legacy'
|
|
3413
|
+
? {}
|
|
3414
|
+
: {
|
|
3415
|
+
partition: {
|
|
3416
|
+
mode: partitionMode,
|
|
3417
|
+
...(operation.partitionTuning === undefined
|
|
3418
|
+
? {}
|
|
3419
|
+
: { tuning: operation.partitionTuning }),
|
|
3420
|
+
},
|
|
3421
|
+
}),
|
|
3422
|
+
...(look === undefined ? {} : { look }),
|
|
3423
|
+
...(operation.colormap?.palette === undefined
|
|
3424
|
+
? {}
|
|
3425
|
+
: { colormapPalette: colormapPaletteOverrides(operation.colormap.palette) }),
|
|
3426
|
+
}, buildWorldContext(base), { signal });
|
|
3315
3427
|
const roots = operation.replaceBaseGame === false
|
|
3316
3428
|
? []
|
|
3317
3429
|
: [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
|
|
@@ -3319,7 +3431,7 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3319
3431
|
for (const file of world.files)
|
|
3320
3432
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
|
|
3321
3433
|
// Remove only unchanged outputs recorded by a previous generation, before indexing new IDs.
|
|
3322
|
-
for (const file of
|
|
3434
|
+
for (const file of planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined }).changes)
|
|
3323
3435
|
if (file.content === null)
|
|
3324
3436
|
addChange(changes, file.relativePath, null, operation.id, 'text/plain');
|
|
3325
3437
|
// Start from empty derived files so no base-game position or supply line survives.
|
|
@@ -3339,8 +3451,16 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3339
3451
|
const positioned = currentIndex(base, changes);
|
|
3340
3452
|
for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
|
|
3341
3453
|
addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
|
|
3342
|
-
|
|
3454
|
+
const outputs = planWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation, { releaseCountryOutputs: workspaceCountries !== undefined });
|
|
3455
|
+
for (const file of outputs.changes)
|
|
3343
3456
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType ?? 'text/plain');
|
|
3457
|
+
worldOutputsByPlan.set(changes, {
|
|
3458
|
+
operationId: operation.id,
|
|
3459
|
+
written: outputs.written,
|
|
3460
|
+
retired: outputs.retired,
|
|
3461
|
+
released: outputs.released,
|
|
3462
|
+
...(world.realms === undefined ? {} : { realms: world.realms }),
|
|
3463
|
+
});
|
|
3344
3464
|
return [
|
|
3345
3465
|
...(reference?.diagnostics ?? []).map((message) => ({
|
|
3346
3466
|
code: 'MAP_REFERENCE_INTERPRETATION',
|
|
@@ -3349,6 +3469,14 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3349
3469
|
operationId: operation.id,
|
|
3350
3470
|
message,
|
|
3351
3471
|
})),
|
|
3472
|
+
...world.notes.map((note) => ({
|
|
3473
|
+
code: note.code,
|
|
3474
|
+
severity: note.severity,
|
|
3475
|
+
category: 'map',
|
|
3476
|
+
operationId: operation.id,
|
|
3477
|
+
message: note.message,
|
|
3478
|
+
...(note.details === undefined ? {} : { details: note.details }),
|
|
3479
|
+
})),
|
|
3352
3480
|
...(world.warnings.length === 0
|
|
3353
3481
|
? []
|
|
3354
3482
|
: [
|
|
@@ -3361,6 +3489,24 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3361
3489
|
details: { warningCount: world.warnings.length, samples: world.warnings.slice(0, 5) },
|
|
3362
3490
|
},
|
|
3363
3491
|
]),
|
|
3492
|
+
worldOutputsDiagnostic(operation.id, outputs),
|
|
3493
|
+
...worldTagDiagnostics(operation.id, tagCatalog, world, workspaceCountries !== undefined),
|
|
3494
|
+
...worldRealmDiagnostics(operation.id, world),
|
|
3495
|
+
...(outputs.released.length === 0
|
|
3496
|
+
? []
|
|
3497
|
+
: [
|
|
3498
|
+
{
|
|
3499
|
+
code: 'MAP_WORLD_OUTPUT_RELEASED',
|
|
3500
|
+
severity: 'info',
|
|
3501
|
+
category: 'map',
|
|
3502
|
+
operationId: operation.id,
|
|
3503
|
+
message: `${outputs.released.length} edited country file(s) of the previous world are left to the workspace and no longer recorded as generated`,
|
|
3504
|
+
details: {
|
|
3505
|
+
count: outputs.released.length,
|
|
3506
|
+
files: outputs.released.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
|
|
3507
|
+
},
|
|
3508
|
+
},
|
|
3509
|
+
]),
|
|
3364
3510
|
{
|
|
3365
3511
|
code: 'MAP_WORLD_CREATED',
|
|
3366
3512
|
severity: 'info',
|
|
@@ -3383,6 +3529,107 @@ function applyCreateWorld(base, changes, operation, context) {
|
|
|
3383
3529
|
]),
|
|
3384
3530
|
];
|
|
3385
3531
|
}
|
|
3532
|
+
/** Tag count observed to overflow the flag sheet of one 1.19 Windows build; never a block. */
|
|
3533
|
+
export const WORLD_TAG_COUNT_OBSERVED_LIMIT = 439;
|
|
3534
|
+
function worldTagDiagnostics(operationId, catalog, world, workspaceOwned) {
|
|
3535
|
+
const declared = new Set(catalog.static);
|
|
3536
|
+
const generated = workspaceOwned
|
|
3537
|
+
? 0
|
|
3538
|
+
: worldGeneratedTags(world).filter((tag) => !declared.has(tag)).length;
|
|
3539
|
+
const total = declared.size + generated + catalog.dynamic.length;
|
|
3540
|
+
const details = {
|
|
3541
|
+
static: declared.size + generated,
|
|
3542
|
+
generated,
|
|
3543
|
+
dynamic: catalog.dynamic.length,
|
|
3544
|
+
total,
|
|
3545
|
+
observedLimit: WORLD_TAG_COUNT_OBSERVED_LIMIT,
|
|
3546
|
+
};
|
|
3547
|
+
return [
|
|
3548
|
+
{
|
|
3549
|
+
code: 'MAP_WORLD_TAG_COUNT',
|
|
3550
|
+
severity: 'info',
|
|
3551
|
+
category: 'map',
|
|
3552
|
+
operationId,
|
|
3553
|
+
message: `After this write ${total} country tags are declared: ${details.static} static (${generated} written by the world) and ${details.dynamic} dynamic`,
|
|
3554
|
+
details,
|
|
3555
|
+
},
|
|
3556
|
+
...(total <= WORLD_TAG_COUNT_OBSERVED_LIMIT
|
|
3557
|
+
? []
|
|
3558
|
+
: [
|
|
3559
|
+
{
|
|
3560
|
+
code: 'MAP_WORLD_TAG_COUNT_HIGH',
|
|
3561
|
+
severity: 'warning',
|
|
3562
|
+
category: 'map',
|
|
3563
|
+
operationId,
|
|
3564
|
+
message: `${total} country tags exceed ${WORLD_TAG_COUNT_OBSERVED_LIMIT}, the count at which one measured game build overflowed its flag sheet`,
|
|
3565
|
+
details,
|
|
3566
|
+
},
|
|
3567
|
+
]),
|
|
3568
|
+
];
|
|
3569
|
+
}
|
|
3570
|
+
/** Tags of the country files a generated world writes. */
|
|
3571
|
+
function worldGeneratedTags(world) {
|
|
3572
|
+
const file = world.files.find(({ relativePath }) => relativePath === WORLD_COUNTRY_TAG_FILE);
|
|
3573
|
+
if (file === undefined)
|
|
3574
|
+
return [];
|
|
3575
|
+
return [
|
|
3576
|
+
...new Set([...file.content.toString('utf8').matchAll(/^([A-Z][A-Z0-9]{2}) =/gmu)].map((match) => match[1])),
|
|
3577
|
+
];
|
|
3578
|
+
}
|
|
3579
|
+
function worldRealmDiagnostics(operationId, world) {
|
|
3580
|
+
if (world.realms === undefined)
|
|
3581
|
+
return [];
|
|
3582
|
+
const { countries, unownedStates } = world.realms;
|
|
3583
|
+
return [
|
|
3584
|
+
{
|
|
3585
|
+
code: 'MAP_WORLD_REALMS_PLACED',
|
|
3586
|
+
severity: 'info',
|
|
3587
|
+
category: 'map',
|
|
3588
|
+
operationId,
|
|
3589
|
+
message: `Placed ${countries.length} workspace countries; their country files, histories, flags and names stay the workspace's`,
|
|
3590
|
+
details: {
|
|
3591
|
+
countries: countries
|
|
3592
|
+
.slice(0, 100)
|
|
3593
|
+
.map(({ tag, capitalState, states, landShare, targetShare }) => ({
|
|
3594
|
+
tag,
|
|
3595
|
+
capitalState,
|
|
3596
|
+
states: states.length,
|
|
3597
|
+
landShare,
|
|
3598
|
+
targetShare,
|
|
3599
|
+
})),
|
|
3600
|
+
truncated: countries.length > 100,
|
|
3601
|
+
},
|
|
3602
|
+
},
|
|
3603
|
+
...(unownedStates.length === 0
|
|
3604
|
+
? []
|
|
3605
|
+
: [
|
|
3606
|
+
{
|
|
3607
|
+
code: 'MAP_WORLD_STATE_UNOWNED',
|
|
3608
|
+
severity: 'info',
|
|
3609
|
+
category: 'map',
|
|
3610
|
+
operationId,
|
|
3611
|
+
message: `${unownedStates.length} state(s) have no owner, as the request asks; whether the game build accepts unowned land is for the mod to verify`,
|
|
3612
|
+
details: { count: unownedStates.length, states: unownedStates.slice(0, 200) },
|
|
3613
|
+
},
|
|
3614
|
+
]),
|
|
3615
|
+
];
|
|
3616
|
+
}
|
|
3617
|
+
function worldOutputsDiagnostic(operationId, outputs) {
|
|
3618
|
+
return {
|
|
3619
|
+
code: 'MAP_WORLD_OUTPUTS_RECONCILED',
|
|
3620
|
+
severity: 'info',
|
|
3621
|
+
category: 'map',
|
|
3622
|
+
operationId,
|
|
3623
|
+
message: `The world records ${outputs.written.length} generated file(s), retires ${outputs.retired.length} earlier output(s) it no longer writes and releases ${outputs.released.length} to the workspace`,
|
|
3624
|
+
details: {
|
|
3625
|
+
written: outputs.written.length,
|
|
3626
|
+
retired: outputs.retired.length,
|
|
3627
|
+
released: outputs.released.length,
|
|
3628
|
+
retiredFiles: outputs.retired.slice(0, WORLD_OUTPUT_PATH_SAMPLE),
|
|
3629
|
+
truncated: outputs.retired.length > WORLD_OUTPUT_PATH_SAMPLE,
|
|
3630
|
+
},
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3386
3633
|
function applyRegenerateMapPositions(base, changes, operation) {
|
|
3387
3634
|
const index = currentIndex(base, changes);
|
|
3388
3635
|
for (const file of planMapPositions(index, operation))
|
|
@@ -3456,6 +3703,22 @@ function applyEntityLocator(base, changes, operation) {
|
|
|
3456
3703
|
]);
|
|
3457
3704
|
addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
|
|
3458
3705
|
}
|
|
3706
|
+
/**
|
|
3707
|
+
* The operation as the transaction manifest records it. An inline geography model can hold
|
|
3708
|
+
* megabytes; the manifest keeps its SHA-256 and size instead, which still bind the plan hash to
|
|
3709
|
+
* the exact model, and a model given by path is bound by the file's read dependency.
|
|
3710
|
+
*/
|
|
3711
|
+
function manifestOperationData(operation) {
|
|
3712
|
+
const data = JSON.parse(JSON.stringify(operation));
|
|
3713
|
+
if (operation.kind === 'create_world' && operation.geography !== undefined) {
|
|
3714
|
+
const canonical = canonicalJson(operation.geography);
|
|
3715
|
+
data.geography = {
|
|
3716
|
+
sha256: sha256Bytes(canonical),
|
|
3717
|
+
bytes: Buffer.byteLength(canonical, 'utf8'),
|
|
3718
|
+
};
|
|
3719
|
+
}
|
|
3720
|
+
return data;
|
|
3721
|
+
}
|
|
3459
3722
|
function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
3460
3723
|
yield* cancellationCheckpoint(signal);
|
|
3461
3724
|
const changes = new Map();
|
|
@@ -3464,6 +3727,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3464
3727
|
const allocations = [];
|
|
3465
3728
|
let expectedChangedBounds;
|
|
3466
3729
|
const manifestOperations = [];
|
|
3730
|
+
const worldOutputs = [];
|
|
3467
3731
|
const ids = new Set();
|
|
3468
3732
|
for (const [operationIndex, operation] of operations.entries()) {
|
|
3469
3733
|
yield* cancellationCheckpoint(signal, operationIndex);
|
|
@@ -3480,7 +3744,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3480
3744
|
id: operation.id,
|
|
3481
3745
|
kind: operation.kind,
|
|
3482
3746
|
summary: operation.summary ?? operation.kind.replaceAll('_', ' '),
|
|
3483
|
-
data:
|
|
3747
|
+
data: manifestOperationData(operation),
|
|
3484
3748
|
});
|
|
3485
3749
|
const changesCheckpoint = new Map([...changes].map(([key, change]) => [
|
|
3486
3750
|
key,
|
|
@@ -3493,8 +3757,13 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3493
3757
|
const allocationsCheckpoint = allocations.length;
|
|
3494
3758
|
const boundsCheckpoint = expectedChangedBounds;
|
|
3495
3759
|
try {
|
|
3496
|
-
if (operation.kind === 'create_world')
|
|
3497
|
-
diagnostics.push(...applyCreateWorld(base, changes, operation, context));
|
|
3760
|
+
if (operation.kind === 'create_world') {
|
|
3761
|
+
diagnostics.push(...applyCreateWorld(base, changes, operation, context, signal));
|
|
3762
|
+
const outputs = worldOutputsByPlan.get(changes);
|
|
3763
|
+
worldOutputsByPlan.delete(changes);
|
|
3764
|
+
if (outputs !== undefined)
|
|
3765
|
+
worldOutputs.push(outputs);
|
|
3766
|
+
}
|
|
3498
3767
|
else if (operation.kind === 'move_state_provinces')
|
|
3499
3768
|
applyMoveStateProvinces(base, changes, operation);
|
|
3500
3769
|
else if (operation.kind === 'update_state')
|
|
@@ -3571,6 +3840,7 @@ function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
|
3571
3840
|
...(expectedChangedBounds === undefined ? {} : { expectedChangedBounds }),
|
|
3572
3841
|
finalIndex,
|
|
3573
3842
|
operations: manifestOperations,
|
|
3843
|
+
...(worldOutputs.length === 0 ? {} : { worldOutputs }),
|
|
3574
3844
|
};
|
|
3575
3845
|
}
|
|
3576
3846
|
export function planMapOperations(base, operations, signal, context = {}) {
|