hoi4-agent-tools 4.3.0 → 4.4.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 +25 -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/operation-context.js +17 -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/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 +15 -2
- package/dist/hoi4_agent_tools/map/operations.js +111 -8
- 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 +138 -5
- 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 +191 -16
- package/dist/hoi4_agent_tools/map/tool-operations.js +208 -2
- 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 +24 -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-partition.js +119 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +131 -56
- package/dist/hoi4_agent_tools/map/world.js +462 -124
- 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 +209 -2
- 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/map.md +258 -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 +2156 -259
- 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
|
@@ -16,10 +16,49 @@ import { MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SE
|
|
|
16
16
|
import { defaultMapSelectorValue, MapWorkspaceIndex, } from './model.js';
|
|
17
17
|
import { planMapOperationsAsync, } from './operations.js';
|
|
18
18
|
import { prepareReferenceWorld } from './reference-preparation.js';
|
|
19
|
+
import { buildImportArtifacts, buildImportReport, } from './reference-import.js';
|
|
20
|
+
import { buildGeographyArtifacts } from './geography/preview.js';
|
|
21
|
+
import { operationHasGeography, prepareGeography, } from './geography/preparation.js';
|
|
19
22
|
import { WORLD_OUTPUT_MANIFEST } from './world-outputs.js';
|
|
20
23
|
import { renderMap, renderMapDiff, } from './render.js';
|
|
21
24
|
import { validateMapAsync } from './validation.js';
|
|
22
25
|
import { buildWorldPreviewArtifacts, generatedWorldOperationIds } from './world-preview.js';
|
|
26
|
+
/**
|
|
27
|
+
* Evidence of an imported reference: registered sheets, an exact or frame-fitted picture, or a
|
|
28
|
+
* geography model beside the picture. Plain pictures keep their previous output.
|
|
29
|
+
*/
|
|
30
|
+
function importEvidence(operation, loaded, geography) {
|
|
31
|
+
const reference = operation.reference;
|
|
32
|
+
if (reference === undefined ||
|
|
33
|
+
(loaded.registration === undefined &&
|
|
34
|
+
geography === undefined &&
|
|
35
|
+
reference.fit !== 'frame' &&
|
|
36
|
+
reference.paletteMatch !== 'exact'))
|
|
37
|
+
return undefined;
|
|
38
|
+
const seed = operation.seed ?? 1;
|
|
39
|
+
const built = buildImportReport({
|
|
40
|
+
operationId: operation.id,
|
|
41
|
+
surface: loaded.prepared.surface,
|
|
42
|
+
seed,
|
|
43
|
+
...(loaded.registration === undefined ? {} : { registration: loaded.registration }),
|
|
44
|
+
...(geography === undefined ? {} : { geography: geography.geography }),
|
|
45
|
+
...(reference.agreement === undefined ? {} : { thresholds: reference.agreement }),
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
diagnostics: built.diagnostics,
|
|
49
|
+
prepared: {
|
|
50
|
+
report: built.report,
|
|
51
|
+
surface: loaded.prepared.surface,
|
|
52
|
+
...(loaded.previewBytes === undefined ? {} : { registered: loaded.previewBytes }),
|
|
53
|
+
...(geography === undefined ? {} : { geography: geography.geography }),
|
|
54
|
+
seed,
|
|
55
|
+
sourceHashes: Object.fromEntries([...loaded.images.map(({ source }) => source), ...loaded.documents].map((evidence) => [
|
|
56
|
+
evidence.displayPath,
|
|
57
|
+
evidence.sha256,
|
|
58
|
+
])),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
23
62
|
function compareNumbers(left, right) {
|
|
24
63
|
return left - right;
|
|
25
64
|
}
|
|
@@ -404,16 +443,30 @@ function sourceTextPatterns(roots) {
|
|
|
404
443
|
}),
|
|
405
444
|
];
|
|
406
445
|
}
|
|
407
|
-
|
|
446
|
+
const PROVINCE_BITMAP_SELECTORS = [
|
|
447
|
+
['provinces', 'provinces.bmp'],
|
|
448
|
+
['heightmap', 'heightmap.bmp'],
|
|
449
|
+
];
|
|
450
|
+
// Border quality (opt-in) also reads the river and terrain bitmaps in the main snapshot.
|
|
451
|
+
const BORDER_BITMAP_SELECTORS = [
|
|
452
|
+
['provinces', 'provinces.bmp'],
|
|
453
|
+
['heightmap', 'heightmap.bmp'],
|
|
454
|
+
['rivers', 'rivers.bmp'],
|
|
455
|
+
['terrain', 'terrain.bmp'],
|
|
456
|
+
];
|
|
457
|
+
const APPEARANCE_BITMAP_SELECTORS = [
|
|
458
|
+
['terrain', 'terrain.bmp'],
|
|
459
|
+
['rivers', 'rivers.bmp'],
|
|
460
|
+
['tree_definition', 'trees.bmp'],
|
|
461
|
+
];
|
|
462
|
+
function provinceBitmapPatterns(defaultMaps, mapRoots, selectors = PROVINCE_BITMAP_SELECTORS) {
|
|
408
463
|
const activeByPath = new Map(defaultMaps
|
|
409
464
|
.filter(({ shadowedBy }) => shadowedBy === undefined)
|
|
410
465
|
.map((file) => [file.relativePath.replaceAll('\\', '/').toLowerCase(), file]));
|
|
411
|
-
// The province bitmap, and the heightmap that gives generated positions their height.
|
|
466
|
+
// The province bitmap, and the heightmap that gives generated positions their height. Border
|
|
467
|
+
// naturalness (opt-in) also reads the river and terrain bitmaps.
|
|
412
468
|
const names = new Set();
|
|
413
|
-
for (const [selector, fallback] of
|
|
414
|
-
['provinces', 'provinces.bmp'],
|
|
415
|
-
['heightmap', 'heightmap.bmp'],
|
|
416
|
-
]) {
|
|
469
|
+
for (const [selector, fallback] of selectors) {
|
|
417
470
|
let activeName = fallback;
|
|
418
471
|
for (const root of mapRoots) {
|
|
419
472
|
const relativePath = `${normalizeSourceRoot(root)}/default.map`.toLowerCase();
|
|
@@ -572,7 +625,7 @@ export class AgentNudger {
|
|
|
572
625
|
this.scanner = this.engine.scanner;
|
|
573
626
|
this.transactions = this.engine.transactions;
|
|
574
627
|
}
|
|
575
|
-
async scan(workspaceId, principal, signal, includeExternalScriptReferences = false, includeWorldContext = false) {
|
|
628
|
+
async scan(workspaceId, principal, signal, includeExternalScriptReferences = false, includeWorldContext = false, includeAppearance = false) {
|
|
576
629
|
signal?.throwIfAborted();
|
|
577
630
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
578
631
|
const sourceRoots = {
|
|
@@ -581,7 +634,7 @@ export class AgentNudger {
|
|
|
581
634
|
localisation: workspace.registration.roots.localisation,
|
|
582
635
|
};
|
|
583
636
|
const selectorSnapshot = await this.engine.scan(workspaceId, { patterns: defaultMapPatterns(sourceRoots) }, principal, signal);
|
|
584
|
-
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map);
|
|
637
|
+
const bitmapPatterns = provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map, includeAppearance ? BORDER_BITMAP_SELECTORS : PROVINCE_BITMAP_SELECTORS);
|
|
585
638
|
const contentSnapshot = await this.engine.scan(workspaceId, { patterns: [...sourceTextPatterns(sourceRoots), ...bitmapPatterns] }, principal, signal);
|
|
586
639
|
const locatorSnapshot = await this.engine.scan(workspaceId, { patterns: ['**/*.asset'], rootKinds: ['mod', 'fixture'] }, principal, signal);
|
|
587
640
|
const externalScriptSnapshot = includeExternalScriptReferences
|
|
@@ -614,12 +667,30 @@ export class AgentNudger {
|
|
|
614
667
|
],
|
|
615
668
|
}, principal, signal)
|
|
616
669
|
: undefined;
|
|
670
|
+
// Appearance validation reads the bitmaps and colour maps the mod itself ships.
|
|
671
|
+
const appearanceSnapshot = includeAppearance
|
|
672
|
+
? await this.engine.scan(workspaceId, {
|
|
673
|
+
rootKinds: ['mod', 'fixture'],
|
|
674
|
+
patterns: [
|
|
675
|
+
...provinceBitmapPatterns(selectorSnapshot.files, sourceRoots.map, APPEARANCE_BITMAP_SELECTORS),
|
|
676
|
+
...sourceRoots.map.flatMap((root) => {
|
|
677
|
+
const normalized = normalizeSourceRoot(root);
|
|
678
|
+
return [
|
|
679
|
+
`${normalized}/cities.bmp`,
|
|
680
|
+
`${normalized}/world_normal.bmp`,
|
|
681
|
+
`${normalized}/terrain/colormap_*.dds`,
|
|
682
|
+
];
|
|
683
|
+
}),
|
|
684
|
+
],
|
|
685
|
+
}, principal, signal)
|
|
686
|
+
: undefined;
|
|
617
687
|
signal?.throwIfAborted();
|
|
618
688
|
const files = [
|
|
619
689
|
...contentSnapshot.files,
|
|
620
690
|
...locatorSnapshot.files,
|
|
621
691
|
...(externalScriptSnapshot?.files ?? []),
|
|
622
692
|
...(worldContextSnapshot?.files ?? []).filter((file) => !contentSnapshot.files.some((existing) => existing.displayPath === file.displayPath)),
|
|
693
|
+
...(appearanceSnapshot?.files ?? []).filter((file) => ![...contentSnapshot.files, ...(worldContextSnapshot?.files ?? [])].some((existing) => existing.displayPath === file.displayPath)),
|
|
623
694
|
...(generatedOutputsSnapshot?.files ?? []).filter((file) => ![...contentSnapshot.files, ...(worldContextSnapshot?.files ?? [])].some((existing) => existing.displayPath === file.displayPath)),
|
|
624
695
|
].sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
625
696
|
const revision = hashCanonical(files.map(({ displayPath, loadOrder, sha256 }) => ({ displayPath, loadOrder, sha256 })));
|
|
@@ -630,8 +701,8 @@ export class AgentNudger {
|
|
|
630
701
|
index: MapWorkspaceIndex.build(files, sourceRoots, SymbolIndex.build(files)),
|
|
631
702
|
};
|
|
632
703
|
}
|
|
633
|
-
async validate(workspaceId, principal, signal, scripts = false) {
|
|
634
|
-
const snapshot = await this.scan(workspaceId, principal, signal, scripts);
|
|
704
|
+
async validate(workspaceId, principal, signal, scripts = false, includeAppearance = false) {
|
|
705
|
+
const snapshot = await this.scan(workspaceId, principal, signal, scripts, false, includeAppearance);
|
|
635
706
|
const validation = await validateMapAsync(snapshot.index, signal === undefined ? {} : { signal });
|
|
636
707
|
if (!scripts)
|
|
637
708
|
return { snapshot, validation };
|
|
@@ -663,6 +734,7 @@ export class AgentNudger {
|
|
|
663
734
|
'remapReferences' in operation), operations.some((operation) => operation.kind === 'create_world'));
|
|
664
735
|
const referenceWorlds = new Map();
|
|
665
736
|
const referenceImages = [];
|
|
737
|
+
const referenceLoads = new Map();
|
|
666
738
|
for (const operation of operations) {
|
|
667
739
|
if (operation.kind !== 'create_world' || operation.reference === undefined)
|
|
668
740
|
continue;
|
|
@@ -670,18 +742,51 @@ export class AgentNudger {
|
|
|
670
742
|
referenceWorlds.set(operation.id, loaded.prepared);
|
|
671
743
|
referenceImages.push({
|
|
672
744
|
operationId: operation.id,
|
|
673
|
-
|
|
745
|
+
// A registered drawing is shown as it lies in the world frame, not as drawn.
|
|
746
|
+
bytes: loaded.previewBytes ?? loaded.images[0].bytes,
|
|
674
747
|
source: loaded.images[0].source,
|
|
675
748
|
});
|
|
676
|
-
|
|
749
|
+
referenceLoads.set(operation.id, loaded);
|
|
750
|
+
for (const evidence of [...loaded.images.map(({ source }) => source), ...loaded.documents]) {
|
|
751
|
+
snapshot.referenceSources ??= [];
|
|
752
|
+
const previous = snapshot.referenceSources.findIndex((file) => file.displayPath === evidence.displayPath);
|
|
753
|
+
if (previous < 0)
|
|
754
|
+
snapshot.referenceSources.push(evidence);
|
|
755
|
+
else
|
|
756
|
+
snapshot.referenceSources[previous] = evidence;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const geographyModels = new Map();
|
|
760
|
+
for (const operation of operations) {
|
|
761
|
+
if (operation.kind !== 'create_world' || !operationHasGeography(operation))
|
|
762
|
+
continue;
|
|
763
|
+
const geography = await prepareGeography(this.resolver, workspaceId, operation, principal, signal);
|
|
764
|
+
geographyModels.set(operation.id, geography);
|
|
765
|
+
if (geography.source.kind === 'file') {
|
|
766
|
+
const evidence = geography.source.evidence;
|
|
677
767
|
snapshot.referenceSources ??= [];
|
|
678
|
-
const previous = snapshot.referenceSources.findIndex((file) => file.displayPath ===
|
|
768
|
+
const previous = snapshot.referenceSources.findIndex((file) => file.displayPath === evidence.displayPath);
|
|
679
769
|
if (previous < 0)
|
|
680
|
-
snapshot.referenceSources.push(
|
|
770
|
+
snapshot.referenceSources.push(evidence);
|
|
681
771
|
else
|
|
682
|
-
snapshot.referenceSources[previous] =
|
|
772
|
+
snapshot.referenceSources[previous] = evidence;
|
|
683
773
|
}
|
|
684
774
|
}
|
|
775
|
+
// An imported reference reports what it supplied and how far the model agrees with it.
|
|
776
|
+
const imports = [];
|
|
777
|
+
const importDiagnostics = [];
|
|
778
|
+
for (const operation of operations) {
|
|
779
|
+
if (operation.kind !== 'create_world')
|
|
780
|
+
continue;
|
|
781
|
+
const loaded = referenceLoads.get(operation.id);
|
|
782
|
+
if (loaded === undefined)
|
|
783
|
+
continue;
|
|
784
|
+
const built = importEvidence(operation, loaded, geographyModels.get(operation.id));
|
|
785
|
+
if (built === undefined)
|
|
786
|
+
continue;
|
|
787
|
+
imports.push(built.prepared);
|
|
788
|
+
importDiagnostics.push(...built.diagnostics);
|
|
789
|
+
}
|
|
685
790
|
snapshot.files.sort((left, right) => left.loadOrder - right.loadOrder || compareCodeUnits(left.displayPath, right.displayPath));
|
|
686
791
|
snapshot.revision = hashCanonical(mapSourceEvidence(snapshot).map(({ displayPath, loadOrder, sha256 }) => ({
|
|
687
792
|
displayPath,
|
|
@@ -690,15 +795,24 @@ export class AgentNudger {
|
|
|
690
795
|
})));
|
|
691
796
|
const plan = await planMapOperationsAsync(snapshot.index, operations, signal, {
|
|
692
797
|
referenceWorlds,
|
|
798
|
+
geographyModels,
|
|
693
799
|
});
|
|
800
|
+
// Model diagnostics (including warnings of a model that is otherwise usable) travel with the plan.
|
|
801
|
+
for (const geography of geographyModels.values())
|
|
802
|
+
plan.diagnostics.push(...geography.diagnostics);
|
|
803
|
+
plan.diagnostics.push(...importDiagnostics);
|
|
694
804
|
const newWorld = operations.some(({ kind }) => kind === 'create_world');
|
|
695
805
|
const freshWorld = newWorld && snapshot.index.provinceBitmapFile?.rootKind !== 'mod';
|
|
696
806
|
const baselineValidation = freshWorld
|
|
697
807
|
? { passed: true, diagnostics: [], checks: [] }
|
|
698
808
|
: await validateMapAsync(snapshot.index, signal === undefined ? {} : { signal });
|
|
699
809
|
// A new world has nothing to compare its bitmap format and pixels with.
|
|
810
|
+
const worldOperation = operations.find((operation) => operation.kind === 'create_world');
|
|
700
811
|
const rawValidation = await validateMapAsync(plan.finalIndex, {
|
|
701
812
|
...(newWorld ? {} : { baseline: snapshot.index }),
|
|
813
|
+
...(worldOperation?.kind === 'create_world' && worldOperation.edge !== undefined
|
|
814
|
+
? { edgeMode: worldOperation.edge.mode ?? 'sea-band' }
|
|
815
|
+
: {}),
|
|
702
816
|
...(signal === undefined ? {} : { signal }),
|
|
703
817
|
...(plan.expectedChangedBounds === undefined
|
|
704
818
|
? {}
|
|
@@ -718,6 +832,8 @@ export class AgentNudger {
|
|
|
718
832
|
plan,
|
|
719
833
|
validation: validationWithPlan(attributedValidation, plan),
|
|
720
834
|
referenceImages,
|
|
835
|
+
geography: [...geographyModels.values()],
|
|
836
|
+
imports,
|
|
721
837
|
};
|
|
722
838
|
}
|
|
723
839
|
async plan(input) {
|
|
@@ -796,7 +912,7 @@ export class AgentNudger {
|
|
|
796
912
|
return { transaction, plan, validation };
|
|
797
913
|
}
|
|
798
914
|
async renderAndStore(workspaceId, options = {}) {
|
|
799
|
-
const snapshot = await this.scan(workspaceId, options.principal, options.signal);
|
|
915
|
+
const snapshot = await this.scan(workspaceId, options.principal, options.signal, false, false, options.overlays?.includes('border-quality') === true);
|
|
800
916
|
const rawBundle = await renderMap(snapshot.index, {
|
|
801
917
|
...options,
|
|
802
918
|
budget: options.budget ?? new RenderBudget(),
|
|
@@ -892,6 +1008,61 @@ export class AgentNudger {
|
|
|
892
1008
|
artifact.name.endsWith('.chunks.json'))))),
|
|
893
1009
|
};
|
|
894
1010
|
}
|
|
1011
|
+
async geographyArtifacts(prepared, signal, budget) {
|
|
1012
|
+
const writes = [];
|
|
1013
|
+
for (const [ordinal, geography] of prepared.geography.entries())
|
|
1014
|
+
writes.push(...(await buildGeographyArtifacts({
|
|
1015
|
+
prepared: geography,
|
|
1016
|
+
ordinal,
|
|
1017
|
+
sourceHashes: geography.source.kind === 'file'
|
|
1018
|
+
? { [geography.source.evidence.displayPath]: geography.source.sha256 }
|
|
1019
|
+
: { 'inline:geography': geography.source.sha256 },
|
|
1020
|
+
...(budget === undefined ? {} : { budget }),
|
|
1021
|
+
...(signal === undefined ? {} : { signal }),
|
|
1022
|
+
})));
|
|
1023
|
+
return writes;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Validate and draw the geography model of every `create_world` operation without scanning or
|
|
1027
|
+
* planning the map: the review step before any synthesis. Nothing in the mod changes.
|
|
1028
|
+
*/
|
|
1029
|
+
async previewGeographyModels(input) {
|
|
1030
|
+
input.signal?.throwIfAborted();
|
|
1031
|
+
const geography = [];
|
|
1032
|
+
const imports = [];
|
|
1033
|
+
const importDiagnostics = [];
|
|
1034
|
+
const referenceFiles = [];
|
|
1035
|
+
for (const operation of input.operations) {
|
|
1036
|
+
if (operation.kind !== 'create_world' || !operationHasGeography(operation))
|
|
1037
|
+
continue;
|
|
1038
|
+
const prepared = await prepareGeography(this.resolver, input.workspaceId, operation, input.principal, input.signal);
|
|
1039
|
+
geography.push(prepared);
|
|
1040
|
+
// The drawing beside the model is registered and compared too, without planning a map.
|
|
1041
|
+
if (operation.reference === undefined)
|
|
1042
|
+
continue;
|
|
1043
|
+
const loaded = await prepareReferenceWorld(this.resolver, input.workspaceId, operation, input.principal, input.signal);
|
|
1044
|
+
referenceFiles.push(...[...loaded.images.map(({ source }) => source), ...loaded.documents].map(({ displayPath }) => displayPath));
|
|
1045
|
+
const built = importEvidence(operation, loaded, prepared);
|
|
1046
|
+
if (built === undefined)
|
|
1047
|
+
continue;
|
|
1048
|
+
imports.push(built.prepared);
|
|
1049
|
+
importDiagnostics.push(...built.diagnostics);
|
|
1050
|
+
}
|
|
1051
|
+
const writes = await this.geographyArtifacts({ geography }, input.signal, new RenderBudget());
|
|
1052
|
+
for (const [ordinal, imported] of imports.entries())
|
|
1053
|
+
writes.push(...(await buildImportArtifacts(imported, ordinal)));
|
|
1054
|
+
const artifacts = await this.artifacts.withAtomicChunkedWrites(this.resolver.get(input.workspaceId, input.principal), writes, (stored) => Promise.resolve([...stored]), input.signal);
|
|
1055
|
+
return {
|
|
1056
|
+
geography,
|
|
1057
|
+
imports: imports.map(({ report }) => report),
|
|
1058
|
+
importDiagnostics,
|
|
1059
|
+
artifacts,
|
|
1060
|
+
filesScanned: [
|
|
1061
|
+
...geography.flatMap(({ source }) => source.kind === 'file' ? [source.evidence.displayPath] : []),
|
|
1062
|
+
...referenceFiles,
|
|
1063
|
+
],
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
895
1066
|
async renderPreparedDiffAndStore(prepared, operations, options) {
|
|
896
1067
|
const budget = new RenderBudget();
|
|
897
1068
|
const { snapshot, plan, validation } = prepared;
|
|
@@ -954,6 +1125,7 @@ export class AgentNudger {
|
|
|
954
1125
|
};
|
|
955
1126
|
const artifacts = await this.artifacts.withAtomicChunkedWrites(this.resolver.get(snapshot.workspaceId, options.principal), [
|
|
956
1127
|
report,
|
|
1128
|
+
...(await this.geographyArtifacts(prepared, options.signal, budget)),
|
|
957
1129
|
{
|
|
958
1130
|
name: 'map-generation-failure.json',
|
|
959
1131
|
mimeType: 'application/json',
|
|
@@ -1122,6 +1294,9 @@ export class AgentNudger {
|
|
|
1122
1294
|
content: beforeBundle.html,
|
|
1123
1295
|
provenance: comparisonProvenance('before'),
|
|
1124
1296
|
});
|
|
1297
|
+
writes.push(...(await this.geographyArtifacts(prepared, options.signal, budget)));
|
|
1298
|
+
for (const [ordinal, imported] of prepared.imports.entries())
|
|
1299
|
+
writes.push(...(await buildImportArtifacts(imported, ordinal)));
|
|
1125
1300
|
const generatedWorlds = generatedWorldOperationIds(plan);
|
|
1126
1301
|
if (generatedWorlds.size > 0)
|
|
1127
1302
|
writes.push(...(await buildWorldPreviewArtifacts({
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { boundedSourceHashEvidence, publicArtifactLink, } from '../core/artifacts.js';
|
|
2
2
|
import { canonicalJson, hashCanonical } from '../core/canonical.js';
|
|
3
3
|
import { diagnosticSeverityCounts } from '../core/diagnostic-rollup.js';
|
|
4
|
+
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
4
5
|
import { autonomousResultArtifacts, } from '../core/transaction-execution.js';
|
|
5
6
|
import { emptyServiceResult, ServiceError } from '../core/result.js';
|
|
6
7
|
import { setInlineFilesScanned } from '../core/operation-result.js';
|
|
7
8
|
import { PACKAGE_VERSION } from '../version.js';
|
|
8
9
|
import { allocateMapIdentifiersAsync, } from './operations.js';
|
|
10
|
+
import { borderQualityDiagnostics } from './border-quality.js';
|
|
11
|
+
import { measureIndexBorderNaturalness, summarizeBorderQuality, } from './border-quality-input.js';
|
|
9
12
|
import { buildMapCatalog, lookupMapCoordinate, searchMapCatalog } from './catalog.js';
|
|
13
|
+
import { GEOGRAPHY_FEATURE_KINDS } from './geography/model.js';
|
|
14
|
+
import { operationHasGeography } from './geography/preparation.js';
|
|
15
|
+
import { unsupportedGeographyParts } from './relief/index.js';
|
|
10
16
|
import { renderMap } from './render.js';
|
|
11
17
|
import { exportProvinceGeometryRowRuns } from './service.js';
|
|
12
18
|
import { generatedWorldOperationIds } from './world-preview.js';
|
|
@@ -241,6 +247,35 @@ export function normalizeMapRenderRequest(input) {
|
|
|
241
247
|
export function normalizeMapRewriteRequest(input) {
|
|
242
248
|
return input;
|
|
243
249
|
}
|
|
250
|
+
/** The figures of an import report an agent needs inline; the full report is an artifact. */
|
|
251
|
+
function importSummary(report) {
|
|
252
|
+
return {
|
|
253
|
+
operationId: report.operationId,
|
|
254
|
+
width: report.width,
|
|
255
|
+
height: report.height,
|
|
256
|
+
landPx: report.classes.landPx,
|
|
257
|
+
seaPx: report.classes.seaPx,
|
|
258
|
+
lakePx: report.classes.lakePx,
|
|
259
|
+
...(report.registration === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: {
|
|
262
|
+
sheets: report.registration.sheets.length,
|
|
263
|
+
hiddenSheets: report.registration.sheets
|
|
264
|
+
.filter(({ pixels }) => pixels === 0)
|
|
265
|
+
.map(({ id }) => id)
|
|
266
|
+
.slice(0, 16),
|
|
267
|
+
uncoveredPx: report.registration.uncoveredPx,
|
|
268
|
+
}),
|
|
269
|
+
...(report.agreement === undefined
|
|
270
|
+
? {}
|
|
271
|
+
: {
|
|
272
|
+
landIou: report.agreement.landIou,
|
|
273
|
+
drawingOnlyLandPx: report.agreement.drawingOnlyLandPx,
|
|
274
|
+
modelOnlyLandPx: report.agreement.modelOnlyLandPx,
|
|
275
|
+
misplacedFeatures: report.agreement.featureCount,
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
244
279
|
export async function prepareMapRewrite(nudger, input, context) {
|
|
245
280
|
await context.progress(0, 5, 'Planning map operations and visual diff');
|
|
246
281
|
const planned = await nudger.planRewriteWithDiff({
|
|
@@ -308,8 +343,131 @@ export function completeMapRewrite(workspaceId, execution, recipe) {
|
|
|
308
343
|
].slice(0, 100);
|
|
309
344
|
return result;
|
|
310
345
|
}
|
|
346
|
+
const EMPTY_SEMANTIC_DIFF = {
|
|
347
|
+
definitions: [],
|
|
348
|
+
stateMembership: [],
|
|
349
|
+
regionMembership: [],
|
|
350
|
+
states: [],
|
|
351
|
+
ports: [],
|
|
352
|
+
buildingPositions: [],
|
|
353
|
+
unitPositions: [],
|
|
354
|
+
weatherPositions: [],
|
|
355
|
+
entityLocators: [],
|
|
356
|
+
supplyNodes: [],
|
|
357
|
+
railways: [],
|
|
358
|
+
adjacencies: [],
|
|
359
|
+
normalAdjacencies: [],
|
|
360
|
+
supplyNodesChanged: false,
|
|
361
|
+
railwaysChanged: false,
|
|
362
|
+
adjacenciesChanged: false,
|
|
363
|
+
normalAdjacenciesChanged: false,
|
|
364
|
+
};
|
|
365
|
+
function geographySummary(prepared) {
|
|
366
|
+
const counts = Object.fromEntries(GEOGRAPHY_FEATURE_KINDS.map((kind) => [kind, prepared.geography.list(kind).length]));
|
|
367
|
+
const severities = diagnosticSeverityCounts(prepared.diagnostics);
|
|
368
|
+
const unsupported = unsupportedGeographyParts(prepared.geography);
|
|
369
|
+
return {
|
|
370
|
+
operationId: prepared.operationId,
|
|
371
|
+
modelHash: prepared.geography.hash,
|
|
372
|
+
valid: prepared.valid,
|
|
373
|
+
source: prepared.source.kind,
|
|
374
|
+
sourceSha256: prepared.source.sha256,
|
|
375
|
+
width: prepared.frame.width,
|
|
376
|
+
height: prepared.frame.height,
|
|
377
|
+
featureCount: prepared.geography.featureCount,
|
|
378
|
+
pointCount: prepared.geography.pointCount,
|
|
379
|
+
counts,
|
|
380
|
+
errorCount: severities.error,
|
|
381
|
+
warningCount: severities.warning,
|
|
382
|
+
synthesis: unsupported.length === 0 ? 'available' : 'unavailable',
|
|
383
|
+
unsupported,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Review the geography model of a `create_world` operation: validate it, resolve its ids and areas
|
|
388
|
+
* and return an overlay and a report. No map is planned or generated and no file changes.
|
|
389
|
+
*/
|
|
390
|
+
async function previewGeographyRewrite(nudger, input, context) {
|
|
391
|
+
await context.progress(0, 3, 'Validating the geography model and drawing its overlay');
|
|
392
|
+
const planned = await nudger.previewGeographyModels({
|
|
393
|
+
workspaceId: context.workspaceId,
|
|
394
|
+
operations: input.operations,
|
|
395
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
396
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
397
|
+
});
|
|
398
|
+
const compactSemantic = compactMapSemanticDiff(EMPTY_SEMANTIC_DIFF);
|
|
399
|
+
const diagnostics = [
|
|
400
|
+
...planned.geography.flatMap(({ diagnostics: found }) => found),
|
|
401
|
+
...planned.importDiagnostics,
|
|
402
|
+
];
|
|
403
|
+
const ignored = input.operations.filter((operation) => operation.kind !== 'create_world' || !operationHasGeography(operation)).length;
|
|
404
|
+
const notes = [
|
|
405
|
+
{
|
|
406
|
+
code: 'MAP_GEOGRAPHY_PREVIEW_MODEL_ONLY',
|
|
407
|
+
severity: 'info',
|
|
408
|
+
category: 'map',
|
|
409
|
+
message: 'This preview checks the geography model, and the registration and agreement of a reference picture beside it, only: no map was planned and nothing was synthesized. A request without preview builds relief, rivers and lakes from the model, and is blocked while the model declares biomes, climate, fertility or sea ice, which the climate stage does not synthesize yet.',
|
|
410
|
+
},
|
|
411
|
+
...(ignored === 0
|
|
412
|
+
? []
|
|
413
|
+
: [
|
|
414
|
+
{
|
|
415
|
+
code: 'MAP_GEOGRAPHY_PREVIEW_OTHER_OPERATIONS_SKIPPED',
|
|
416
|
+
severity: 'info',
|
|
417
|
+
category: 'map',
|
|
418
|
+
message: `${ignored} other operation(s) in the request were not evaluated by this model preview`,
|
|
419
|
+
},
|
|
420
|
+
]),
|
|
421
|
+
];
|
|
422
|
+
const all = sortDiagnostics([...diagnostics, ...notes]);
|
|
423
|
+
const valid = planned.geography.every(({ valid: ok }) => ok) &&
|
|
424
|
+
!planned.importDiagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
425
|
+
const result = emptyServiceResult(context.workspaceId, {
|
|
426
|
+
execution: 'preview',
|
|
427
|
+
allocations: [],
|
|
428
|
+
operationBlockers: [],
|
|
429
|
+
expectedChangedBounds: null,
|
|
430
|
+
changedProvinceIds: [],
|
|
431
|
+
changedProvinceCount: 0,
|
|
432
|
+
semanticDiff: compactSemantic.semantic,
|
|
433
|
+
semanticDiffCounts: compactSemantic.counts,
|
|
434
|
+
semanticDiffTruncated: compactSemantic.truncated,
|
|
435
|
+
fileCount: 0,
|
|
436
|
+
artifactCount: planned.artifacts.length,
|
|
437
|
+
geography: planned.geography.slice(0, 16).map(geographySummary),
|
|
438
|
+
...(planned.imports.length === 0
|
|
439
|
+
? {}
|
|
440
|
+
: { import: planned.imports.slice(0, 16).map(importSummary) }),
|
|
441
|
+
});
|
|
442
|
+
result.status = valid ? 'ok' : 'blocked';
|
|
443
|
+
result.code = valid ? 'MAP_GEOGRAPHY_PREVIEW_READY' : 'MAP_GEOGRAPHY_PREVIEW_BLOCKED';
|
|
444
|
+
setInlineFilesScanned(result, planned.filesScanned);
|
|
445
|
+
result.proposedFiles = [];
|
|
446
|
+
result.diagnostics = all.slice(0, 100);
|
|
447
|
+
result.artifacts = planned.artifacts.map(publicArtifactLink);
|
|
448
|
+
result.validation = {
|
|
449
|
+
passed: valid,
|
|
450
|
+
checks: [
|
|
451
|
+
{
|
|
452
|
+
id: 'geography-model',
|
|
453
|
+
passed: valid,
|
|
454
|
+
message: valid
|
|
455
|
+
? 'The geography model resolves without errors'
|
|
456
|
+
: 'The geography model has errors; see the MAP_GEOGRAPHY_* diagnostics and map-geography-report.json',
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
};
|
|
460
|
+
result.blockers = all
|
|
461
|
+
.filter(({ severity }) => severity === 'error' || severity === 'blocker')
|
|
462
|
+
.slice(0, 100)
|
|
463
|
+
.map(({ code, message }) => ({ code, message }));
|
|
464
|
+
await context.progress(3, 3, 'Geography model preview complete; workspace files were not changed');
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
311
467
|
/** Generate and validate the same proposed map without creating a write transaction. */
|
|
312
468
|
export async function previewMapRewrite(nudger, input, context) {
|
|
469
|
+
if (input.operations.some((operation) => operation.kind === 'create_world' && operationHasGeography(operation)))
|
|
470
|
+
return previewGeographyRewrite(nudger, input, context);
|
|
313
471
|
await context.progress(0, 5, 'Generating and validating an offline map preview');
|
|
314
472
|
const planned = await nudger.previewRewriteWithDiff({
|
|
315
473
|
workspaceId: context.workspaceId,
|
|
@@ -431,8 +589,32 @@ export async function inspectMap(engine, nudger, input, context) {
|
|
|
431
589
|
await context.progress(3, 3, 'Map lookup complete');
|
|
432
590
|
return result;
|
|
433
591
|
}
|
|
434
|
-
const { snapshot, validation, scriptReferences } = await nudger.validate(context.workspaceId, context.principal, context.signal, input.references);
|
|
592
|
+
const { snapshot, validation, scriptReferences } = await nudger.validate(context.workspaceId, context.principal, context.signal, input.references, input.quality);
|
|
435
593
|
const sharedRevision = snapshot.revision;
|
|
594
|
+
let borderQuality;
|
|
595
|
+
const qualityDiagnostics = [];
|
|
596
|
+
if (input.quality) {
|
|
597
|
+
await context.progress(1, 3, 'Measuring how natural the borders are');
|
|
598
|
+
try {
|
|
599
|
+
const measured = await measureIndexBorderNaturalness(snapshot.index, context.signal === undefined ? {} : { signal: context.signal });
|
|
600
|
+
borderQuality = {
|
|
601
|
+
report: measured.report,
|
|
602
|
+
summary: summarizeBorderQuality(measured.report, measured.skipped),
|
|
603
|
+
};
|
|
604
|
+
qualityDiagnostics.push(...borderQualityDiagnostics(measured.report));
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
if (!(error instanceof ServiceError) || !error.code.startsWith('MAP_BORDER_QUALITY_'))
|
|
608
|
+
throw error;
|
|
609
|
+
qualityDiagnostics.push({
|
|
610
|
+
code: 'MAP_BORDER_QUALITY_UNAVAILABLE',
|
|
611
|
+
severity: 'warning',
|
|
612
|
+
category: 'map',
|
|
613
|
+
message: `Border naturalness could not be measured: ${error.message}`,
|
|
614
|
+
details: { reason: error.code, ...error.details },
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
436
618
|
const catalog = buildMapCatalog(snapshot.index);
|
|
437
619
|
const queryMatches = input.query === undefined ? [] : searchMapCatalog(catalog, input.query, input.queryLimit);
|
|
438
620
|
const coordinateMatches = input.coordinates.map((coordinate) => lookupMapCoordinate(snapshot.index, coordinate));
|
|
@@ -493,12 +675,33 @@ export async function inspectMap(engine, nudger, input, context) {
|
|
|
493
675
|
selected,
|
|
494
676
|
allocationPreviews,
|
|
495
677
|
validation,
|
|
678
|
+
...(borderQuality === undefined ? {} : { borderQuality: borderQuality.report }),
|
|
496
679
|
sourceHashes: sourceEvidence.complete,
|
|
497
680
|
})}\n`,
|
|
498
681
|
provenance: inspectionProvenance,
|
|
499
682
|
description: 'Complete searchable map catalog, inspection, validation, selected records, and allocation previews',
|
|
500
683
|
},
|
|
501
684
|
];
|
|
685
|
+
if (borderQuality !== undefined)
|
|
686
|
+
artifactWrites.push({
|
|
687
|
+
name: `map-border-quality.${snapshot.revision.slice(0, 16)}.json`,
|
|
688
|
+
mimeType: 'application/json',
|
|
689
|
+
content: `${canonicalJson({
|
|
690
|
+
schemaVersion: 1,
|
|
691
|
+
revision: snapshot.revision,
|
|
692
|
+
report: borderQuality.report,
|
|
693
|
+
skippedInputs: borderQuality.summary.skippedInputs,
|
|
694
|
+
sourceHashes: sourceEvidence.complete,
|
|
695
|
+
})}\n`,
|
|
696
|
+
provenance: {
|
|
697
|
+
kind: 'map-border-quality',
|
|
698
|
+
toolVersion: PACKAGE_VERSION,
|
|
699
|
+
schemaVersion: 'map-border-quality.v1',
|
|
700
|
+
sourceHashes: sourceEvidence.bounded.sourceHashes,
|
|
701
|
+
metadata: { sharedRevision, sourceHashInventory: sourceEvidence.bounded.inventory },
|
|
702
|
+
},
|
|
703
|
+
description: 'Border naturalness: alignment with rivers, slopes, lake shores and terrain changes, enrichment over chance, straightness and the worst artificial runs for each border layer',
|
|
704
|
+
});
|
|
502
705
|
if (overviewRendered) {
|
|
503
706
|
const overviewLayer = 'state';
|
|
504
707
|
const overviewOverlays = [
|
|
@@ -592,10 +795,13 @@ export async function inspectMap(engine, nudger, input, context) {
|
|
|
592
795
|
unknownProvinceIds: provinceGeometry?.unknownProvinceIds ?? [],
|
|
593
796
|
missingGeometryProvinceIds: provinceGeometry?.missingGeometryProvinceIds ?? [],
|
|
594
797
|
...(scriptReferences === undefined ? {} : { scriptReferences }),
|
|
798
|
+
...(borderQuality === undefined ? {} : { quality: borderQuality.summary }),
|
|
595
799
|
});
|
|
596
800
|
result.code = 'MAP_INSPECTED';
|
|
597
801
|
setInlineFilesScanned(result, snapshot.files.map(({ displayPath }) => displayPath));
|
|
598
|
-
result.diagnostics =
|
|
802
|
+
result.diagnostics = (qualityDiagnostics.length === 0
|
|
803
|
+
? validation.diagnostics
|
|
804
|
+
: sortDiagnostics([...validation.diagnostics, ...qualityDiagnostics])).slice(0, 100);
|
|
599
805
|
result.artifacts = artifacts.map(publicArtifactLink);
|
|
600
806
|
result.validation = validationSummary(validation);
|
|
601
807
|
await context.progress(3, 3, 'Map inspection complete');
|
|
@@ -2,6 +2,7 @@ import { setImmediate as yieldToEventLoop } from 'node:timers/promises';
|
|
|
2
2
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
3
3
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
4
4
|
import { addActiveMapDiagnostic, addMapDiagnostic } from './diagnostic-limit.js';
|
|
5
|
+
import { validateAppearance } from './appearance-validation.js';
|
|
5
6
|
import { rgbKey } from './bmp.js';
|
|
6
7
|
import { MAP_PROVINCE_ENGINE_MAX_PIXELS } from './model.js';
|
|
7
8
|
const MAP_DIAGNOSTIC_DETAIL_SAMPLE_LIMIT = 100;
|
|
@@ -1207,6 +1208,8 @@ function* validateMapSteps(index, options = {}) {
|
|
|
1207
1208
|
yield* cancellationCheckpoint(options.signal);
|
|
1208
1209
|
yield* validatePositions(index, diagnostics, options);
|
|
1209
1210
|
yield* cancellationCheckpoint(options.signal);
|
|
1211
|
+
yield* validateAppearance(index, (diagnostic) => addDiagnostic(diagnostics, options, { ...diagnostic, category: 'map' }), { signal: options.signal, edgeMode: options.edgeMode });
|
|
1212
|
+
yield* cancellationCheckpoint(options.signal);
|
|
1210
1213
|
const sorted = sortDiagnostics(diagnostics);
|
|
1211
1214
|
const checks = [
|
|
1212
1215
|
{
|
|
@@ -1259,6 +1262,12 @@ function* validateMapSteps(index, options = {}) {
|
|
|
1259
1262
|
code.startsWith('MAP_ENTITY_LOCATOR'))),
|
|
1260
1263
|
message: 'Map positions, ports, and entity locators are valid',
|
|
1261
1264
|
},
|
|
1265
|
+
{
|
|
1266
|
+
id: 'map-appearance-files',
|
|
1267
|
+
passed: !sorted.some(({ code, severity }) => (severity === 'error' || severity === 'blocker') &&
|
|
1268
|
+
/^MAP_(RIVER|TERRAIN_BITMAP|HEIGHT|TREES|COLORMAP|WATER|NORMAL|CITIES|LAKE|EDGE|SEAM|POLAR)_/u.test(code)),
|
|
1269
|
+
message: 'Heightmap, terrain, river, tree, colour, normal and city bitmaps, lakes and the world edge are valid',
|
|
1270
|
+
},
|
|
1262
1271
|
];
|
|
1263
1272
|
const passed = checks.every((check) => check.passed) &&
|
|
1264
1273
|
!sorted.some(({ severity }) => severity === 'error' || severity === 'blocker');
|