hoi4-agent-tools 2.5.2 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +5 -5
- package/dist/hoi4_agent_tools/core/index.js +22 -6
- package/dist/hoi4_agent_tools/focus/compact.js +103 -12
- package/dist/hoi4_agent_tools/focus/continuous-render.js +4 -10
- package/dist/hoi4_agent_tools/focus/layout.js +15 -7
- package/dist/hoi4_agent_tools/focus/render.js +2 -8
- package/dist/hoi4_agent_tools/gui/dds.js +5 -2
- package/dist/hoi4_agent_tools/gui/layout.js +1 -1
- package/dist/hoi4_agent_tools/gui/renderer.js +4 -16
- package/dist/hoi4_agent_tools/gui/source-graph.js +9 -8
- package/dist/hoi4_agent_tools/gui/studio.js +58 -4
- package/dist/hoi4_agent_tools/map/catalog.js +398 -0
- package/dist/hoi4_agent_tools/map/index.js +1 -0
- package/dist/hoi4_agent_tools/map/operations.js +337 -30
- package/dist/hoi4_agent_tools/map/render.js +40 -147
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +85 -9
- package/dist/hoi4_agent_tools/mcp/transports/http.js +4 -5
- package/dist/hoi4_agent_tools/mcp/transports/protocol-gate.js +7 -7
- package/dist/hoi4_agent_tools/schemas/map.js +69 -2
- package/dist/hoi4_agent_tools/technology/render.js +65 -19
- package/dist/hoi4_agent_tools/technology/service.js +67 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/focus.md +1 -1
- package/docs/gui.md +2 -0
- package/docs/map.md +60 -23
- package/docs/technology.md +1 -1
- package/package.json +3 -2
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/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/map-operation.schema.json +228 -74
- package/schemas/operation-result.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/server.json +2 -2
|
@@ -11,6 +11,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
11
11
|
import { WorkspaceScanner } from '../core/scanner.js';
|
|
12
12
|
import { applyReplacements, decodeSource, encodeSource, parseClausewitz, parseLocalisation, SOURCE_MAX_BYTES, sourcePartialLimitDiagnostics, } from '../core/source/index.js';
|
|
13
13
|
import { readDependenciesFromScannedFiles, } from '../core/transactions.js';
|
|
14
|
+
import { TRANSACTION_MAX_DIAGNOSTICS } from '../core/transaction-limits.js';
|
|
14
15
|
import { PACKAGE_VERSION } from '../version.js';
|
|
15
16
|
import { isSafeAnimationSourcePath } from './animation-manifest.js';
|
|
16
17
|
import { GuiAssetCatalog, parseBmFont } from './assets.js';
|
|
@@ -30,6 +31,43 @@ const staticGuiDefinitionPatterns = [
|
|
|
30
31
|
];
|
|
31
32
|
export const GUI_TEXT_PACKAGE_MAX_FILES = 32;
|
|
32
33
|
export const GUI_TEXT_PACKAGE_MAX_BYTES = 16_777_216;
|
|
34
|
+
const GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET = Math.floor(TRANSACTION_MAX_DIAGNOSTICS / 3);
|
|
35
|
+
function hardDiagnostic(diagnostic) {
|
|
36
|
+
return diagnostic.severity === 'error' || diagnostic.severity === 'blocker';
|
|
37
|
+
}
|
|
38
|
+
export function boundedGuiTransactionDiagnostics(diagnostics, maximumDiagnostics, evidenceArtifactName) {
|
|
39
|
+
if (diagnostics.length <= maximumDiagnostics)
|
|
40
|
+
return [...diagnostics];
|
|
41
|
+
if (maximumDiagnostics <= 0)
|
|
42
|
+
return [];
|
|
43
|
+
const retainedLimit = maximumDiagnostics - 1;
|
|
44
|
+
const hard = diagnostics.filter(hardDiagnostic);
|
|
45
|
+
const retainedHard = hard.slice(0, retainedLimit);
|
|
46
|
+
const retainedHardSet = new Set(retainedHard);
|
|
47
|
+
const retainedSoft = diagnostics
|
|
48
|
+
.filter((diagnostic) => !hardDiagnostic(diagnostic) && !retainedHardSet.has(diagnostic))
|
|
49
|
+
.slice(0, retainedLimit - retainedHard.length);
|
|
50
|
+
const omittedHard = hard.length - retainedHard.length;
|
|
51
|
+
return [
|
|
52
|
+
...retainedHard,
|
|
53
|
+
...retainedSoft,
|
|
54
|
+
{
|
|
55
|
+
code: 'GUI_REWRITE_DIAGNOSTICS_TRUNCATED',
|
|
56
|
+
severity: omittedHard > 0 ? 'blocker' : 'warning',
|
|
57
|
+
category: 'validation',
|
|
58
|
+
message: evidenceArtifactName === undefined
|
|
59
|
+
? `GUI rewrite validation produced ${diagnostics.length} diagnostics; transaction evidence retains the highest-priority ${maximumDiagnostics}`
|
|
60
|
+
: `GUI rewrite validation produced ${diagnostics.length} diagnostics; complete evidence is stored in ${evidenceArtifactName}`,
|
|
61
|
+
details: {
|
|
62
|
+
total: diagnostics.length,
|
|
63
|
+
returned: maximumDiagnostics,
|
|
64
|
+
omitted: diagnostics.length - retainedLimit,
|
|
65
|
+
omittedHard,
|
|
66
|
+
...(evidenceArtifactName === undefined ? {} : { evidenceArtifactName }),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
}
|
|
33
71
|
function normalizeConfiguredRoot(value) {
|
|
34
72
|
const normalized = value.replaceAll('\\', '/').replace(/^\.\//u, '').replace(/\/$/u, '');
|
|
35
73
|
if (!isSafeAnimationSourcePath(normalized)) {
|
|
@@ -439,7 +477,7 @@ export function guiArtifactProvenance(graph, kind, primaryScene, scenarioScenes
|
|
|
439
477
|
})),
|
|
440
478
|
},
|
|
441
479
|
metadata: {
|
|
442
|
-
rendererLabel: '
|
|
480
|
+
rendererLabel: 'HOI4 Agent Tools deterministic GUI render',
|
|
443
481
|
...metadata,
|
|
444
482
|
sourceHashInventory: sourceEvidence.inventory,
|
|
445
483
|
},
|
|
@@ -859,6 +897,7 @@ export class ScriptedGuiStudio {
|
|
|
859
897
|
const preflightDiagnostics = [];
|
|
860
898
|
const preflightChecks = [];
|
|
861
899
|
const preflightArtifacts = [];
|
|
900
|
+
let rewriteEvidenceArtifactName;
|
|
862
901
|
let dependencyFiles = baseline.files;
|
|
863
902
|
if (input.windowName !== undefined && input.scenario !== undefined) {
|
|
864
903
|
let proposedFiles = overlayGuiTextPackageFiles(baseline.files, prepared);
|
|
@@ -942,6 +981,7 @@ export class ScriptedGuiStudio {
|
|
|
942
981
|
}
|
|
943
982
|
const comparison = await compareGuiImages(baselinePng, proposedImage.png, renderBudget, input.signal);
|
|
944
983
|
const slug = safeSlug(input.windowName);
|
|
984
|
+
rewriteEvidenceArtifactName = `${slug}-rewrite-validation.json`;
|
|
945
985
|
const metadata = {
|
|
946
986
|
relativePath: input.relativePath,
|
|
947
987
|
packageFiles: prepared.map(({ relativePath }) => relativePath),
|
|
@@ -982,10 +1022,24 @@ export class ScriptedGuiStudio {
|
|
|
982
1022
|
})}\n`,
|
|
983
1023
|
provenance: guiArtifactProvenance(proposedGraph, 'gui-proposed-fidelity', proposedScene, [proposedScene], metadata),
|
|
984
1024
|
},
|
|
1025
|
+
{
|
|
1026
|
+
name: rewriteEvidenceArtifactName,
|
|
1027
|
+
mimeType: 'application/json',
|
|
1028
|
+
content: `${canonicalJson({
|
|
1029
|
+
sourceDiagnostics: proposedSourceDiagnostics,
|
|
1030
|
+
graphDiagnostics: proposedGraph.diagnostics,
|
|
1031
|
+
sceneDiagnostics: proposedScene.diagnostics,
|
|
1032
|
+
validation: proposedValidation,
|
|
1033
|
+
transactionDiagnostics: preflightDiagnostics,
|
|
1034
|
+
transactionChecks: preflightChecks,
|
|
1035
|
+
})}\n`,
|
|
1036
|
+
provenance: guiArtifactProvenance(proposedGraph, 'gui-rewrite-validation', proposedScene, [proposedScene], metadata),
|
|
1037
|
+
},
|
|
985
1038
|
];
|
|
986
1039
|
const stored = await this.artifacts.withAtomicWrites(workspace, writes, (artifacts) => Promise.resolve([...artifacts]), input.signal);
|
|
987
1040
|
preflightArtifacts.push(...stored.map(publicArtifactLink));
|
|
988
1041
|
}
|
|
1042
|
+
const transactionPreflightDiagnostics = boundedGuiTransactionDiagnostics(preflightDiagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName);
|
|
989
1043
|
const operationId = deterministicId('gui_source_change', {
|
|
990
1044
|
relativePath: input.relativePath,
|
|
991
1045
|
files: prepared.map(({ relativePath, bytes }) => ({
|
|
@@ -1020,7 +1074,7 @@ export class ScriptedGuiStudio {
|
|
|
1020
1074
|
})),
|
|
1021
1075
|
readDependencies: readDependenciesFromScannedFiles(dependencyFiles),
|
|
1022
1076
|
artifacts: preflightArtifacts,
|
|
1023
|
-
diagnostics:
|
|
1077
|
+
diagnostics: transactionPreflightDiagnostics,
|
|
1024
1078
|
validate: (proposed, signal) => {
|
|
1025
1079
|
const diagnostics = [];
|
|
1026
1080
|
let present = true;
|
|
@@ -1049,7 +1103,7 @@ export class ScriptedGuiStudio {
|
|
|
1049
1103
|
diagnostics.push(...blocking.map((diagnostic) => ({ ...diagnostic, operationId })));
|
|
1050
1104
|
}
|
|
1051
1105
|
return Promise.resolve({
|
|
1052
|
-
diagnostics,
|
|
1106
|
+
diagnostics: boundedGuiTransactionDiagnostics(diagnostics, GUI_TRANSACTION_PHASE_DIAGNOSTIC_BUDGET, rewriteEvidenceArtifactName),
|
|
1053
1107
|
checks: [
|
|
1054
1108
|
...preflightChecks,
|
|
1055
1109
|
{
|
|
@@ -1077,7 +1131,7 @@ export class ScriptedGuiStudio {
|
|
|
1077
1131
|
const scanned = await this.scan(workspaceId, principal, validationSignal);
|
|
1078
1132
|
const passed = !scanned.graph.diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
|
|
1079
1133
|
return {
|
|
1080
|
-
diagnostics: scanned.graph.diagnostics,
|
|
1134
|
+
diagnostics: boundedGuiTransactionDiagnostics(scanned.graph.diagnostics, Math.max(0, TRANSACTION_MAX_DIAGNOSTICS - _manifest.diagnostics.length), _manifest.artifacts.find(({ name }) => name.endsWith('-rewrite-validation.json'))?.name),
|
|
1081
1135
|
checks: [
|
|
1082
1136
|
{
|
|
1083
1137
|
id: 'post-write-gui-graph',
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
import { nodeLocation } from '../core/source/parser.js';
|
|
3
|
+
function localisedValues(entries) {
|
|
4
|
+
return entries
|
|
5
|
+
.map(({ file, entry }) => ({
|
|
6
|
+
language: entry.language,
|
|
7
|
+
value: entry.value,
|
|
8
|
+
sourcePath: file.displayPath,
|
|
9
|
+
line: entry.line,
|
|
10
|
+
loadOrder: file.loadOrder,
|
|
11
|
+
}))
|
|
12
|
+
.sort((left, right) => compareCodeUnits(left.language, right.language) ||
|
|
13
|
+
left.loadOrder - right.loadOrder ||
|
|
14
|
+
compareCodeUnits(left.sourcePath, right.sourcePath) ||
|
|
15
|
+
left.line - right.line)
|
|
16
|
+
.map(({ loadOrder: _loadOrder, ...value }) => value);
|
|
17
|
+
}
|
|
18
|
+
function resolvedName(entriesByKey, keys, fallback) {
|
|
19
|
+
for (const key of keys) {
|
|
20
|
+
const values = localisedValues(entriesByKey.get(key) ?? []);
|
|
21
|
+
if (values.length === 0)
|
|
22
|
+
continue;
|
|
23
|
+
const english = values.filter(({ language }) => language === 'l_english').at(-1);
|
|
24
|
+
return { key, displayName: english?.value ?? values.at(-1)?.value ?? fallback, values };
|
|
25
|
+
}
|
|
26
|
+
const key = keys[0] ?? fallback;
|
|
27
|
+
return { key, displayName: fallback, values: [] };
|
|
28
|
+
}
|
|
29
|
+
function aggregateGeometry(index, provinceIds) {
|
|
30
|
+
const geometries = provinceIds.flatMap((id) => {
|
|
31
|
+
const geometry = index.raster?.geometry.get(id);
|
|
32
|
+
return geometry === undefined ? [] : [geometry];
|
|
33
|
+
});
|
|
34
|
+
if (geometries.length === 0 || index.raster === undefined)
|
|
35
|
+
return null;
|
|
36
|
+
const pixelCount = geometries.reduce((total, { pixelCount: count }) => total + count, 0);
|
|
37
|
+
if (pixelCount <= 0)
|
|
38
|
+
return null;
|
|
39
|
+
const centerX = geometries.reduce((total, geometry) => total + geometry.centerX * geometry.pixelCount, 0) /
|
|
40
|
+
pixelCount;
|
|
41
|
+
const centerY = geometries.reduce((total, geometry) => total + geometry.centerY * geometry.pixelCount, 0) /
|
|
42
|
+
pixelCount;
|
|
43
|
+
return {
|
|
44
|
+
pixelCount,
|
|
45
|
+
minX: geometries.reduce((value, geometry) => Math.min(value, geometry.minX), Infinity),
|
|
46
|
+
minY: geometries.reduce((value, geometry) => Math.min(value, geometry.minY), Infinity),
|
|
47
|
+
maxX: geometries.reduce((value, geometry) => Math.max(value, geometry.maxX), -Infinity),
|
|
48
|
+
maxY: geometries.reduce((value, geometry) => Math.max(value, geometry.maxY), -Infinity),
|
|
49
|
+
centerX,
|
|
50
|
+
centerY,
|
|
51
|
+
mapX: centerX,
|
|
52
|
+
mapZ: index.raster.height - 1 - centerY,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function sortedObject(values) {
|
|
56
|
+
return Object.fromEntries([...values].sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
57
|
+
}
|
|
58
|
+
export function buildMapCatalog(index) {
|
|
59
|
+
const raster = index.raster;
|
|
60
|
+
const width = raster?.width ?? 0;
|
|
61
|
+
const height = raster?.height ?? 0;
|
|
62
|
+
const localisationEntriesByKey = new Map();
|
|
63
|
+
for (const entry of index.localisationEntries) {
|
|
64
|
+
const values = localisationEntriesByKey.get(entry.entry.key) ?? [];
|
|
65
|
+
values.push(entry);
|
|
66
|
+
localisationEntriesByKey.set(entry.entry.key, values);
|
|
67
|
+
}
|
|
68
|
+
const portsByProvince = new Map(index.ports.map((port) => [port.provinceId, port]));
|
|
69
|
+
const supplyNodeLevelsByProvince = new Map();
|
|
70
|
+
for (const node of index.supplyNodes) {
|
|
71
|
+
const levels = supplyNodeLevelsByProvince.get(node.provinceId) ?? [];
|
|
72
|
+
levels.push(node.level);
|
|
73
|
+
supplyNodeLevelsByProvince.set(node.provinceId, levels);
|
|
74
|
+
}
|
|
75
|
+
const buildingPositionsByProvince = new Map();
|
|
76
|
+
for (const [ordinal, position] of index.buildingPositions.entries()) {
|
|
77
|
+
const provinceId = index.provinceAtMapCoordinate(position.x, position.z);
|
|
78
|
+
if (provinceId === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
const ordinals = buildingPositionsByProvince.get(provinceId) ?? [];
|
|
81
|
+
ordinals.push(ordinal);
|
|
82
|
+
buildingPositionsByProvince.set(provinceId, ordinals);
|
|
83
|
+
}
|
|
84
|
+
const unitPositionsByProvince = new Map();
|
|
85
|
+
for (const [ordinal, position] of index.unitPositions.entries()) {
|
|
86
|
+
const ordinals = unitPositionsByProvince.get(position.provinceId) ?? [];
|
|
87
|
+
ordinals.push(ordinal);
|
|
88
|
+
unitPositionsByProvince.set(position.provinceId, ordinals);
|
|
89
|
+
}
|
|
90
|
+
const railwayOrdinalsByProvince = new Map();
|
|
91
|
+
for (const [ordinal, railway] of index.railways.entries()) {
|
|
92
|
+
for (const provinceId of new Set(railway.provinces)) {
|
|
93
|
+
const ordinals = railwayOrdinalsByProvince.get(provinceId) ?? [];
|
|
94
|
+
ordinals.push(ordinal);
|
|
95
|
+
railwayOrdinalsByProvince.set(provinceId, ordinals);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const specialAdjacencyOrdinalsByProvince = new Map();
|
|
99
|
+
for (const [ordinal, adjacency] of index.adjacencies.entries()) {
|
|
100
|
+
for (const provinceId of new Set([adjacency.from, adjacency.to, adjacency.through])) {
|
|
101
|
+
if (provinceId < 0)
|
|
102
|
+
continue;
|
|
103
|
+
const ordinals = specialAdjacencyOrdinalsByProvince.get(provinceId) ?? [];
|
|
104
|
+
ordinals.push(ordinal);
|
|
105
|
+
specialAdjacencyOrdinalsByProvince.set(provinceId, ordinals);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const provinces = [...index.definitions]
|
|
109
|
+
.sort((left, right) => left.id - right.id)
|
|
110
|
+
.map((definition) => {
|
|
111
|
+
const id = definition.id;
|
|
112
|
+
const port = portsByProvince.get(id);
|
|
113
|
+
return {
|
|
114
|
+
kind: 'province',
|
|
115
|
+
id,
|
|
116
|
+
name: resolvedName(localisationEntriesByKey, [`PROV${id}`, `VICTORY_POINTS_${id}`], `Province ${id}`),
|
|
117
|
+
definition: {
|
|
118
|
+
color: definition.color,
|
|
119
|
+
type: definition.type,
|
|
120
|
+
coastal: definition.coastal,
|
|
121
|
+
terrain: definition.terrain,
|
|
122
|
+
continent: definition.continent,
|
|
123
|
+
sourcePath: definition.document.file.displayPath,
|
|
124
|
+
line: definition.line,
|
|
125
|
+
},
|
|
126
|
+
geometry: aggregateGeometry(index, [id]),
|
|
127
|
+
stateIds: index.stateForProvince(id).map(({ id: stateId }) => stateId),
|
|
128
|
+
regionIds: index.regionForProvince(id).map(({ id: regionId }) => regionId),
|
|
129
|
+
neighborProvinceIds: [...(raster?.adjacency.get(id) ?? [])].sort((a, b) => a - b),
|
|
130
|
+
victoryPoints: index.victoryPointsByProvince
|
|
131
|
+
.get(id)
|
|
132
|
+
?.map(({ stateId, value }) => ({ stateId, value })) ?? [],
|
|
133
|
+
provinceBuildings: index.provinceBuildingsByProvince.get(id)?.map(({ stateId, buildings }) => ({
|
|
134
|
+
stateId,
|
|
135
|
+
buildings: sortedObject(buildings),
|
|
136
|
+
})) ?? [],
|
|
137
|
+
port: port === undefined
|
|
138
|
+
? null
|
|
139
|
+
: {
|
|
140
|
+
stateId: port.stateId,
|
|
141
|
+
level: port.level,
|
|
142
|
+
coastal: port.coastal,
|
|
143
|
+
adjacentSeaProvinceIds: [...port.adjacentSeaProvinceIds],
|
|
144
|
+
},
|
|
145
|
+
supplyNodeLevels: supplyNodeLevelsByProvince.get(id) ?? [],
|
|
146
|
+
railwayOrdinals: railwayOrdinalsByProvince.get(id) ?? [],
|
|
147
|
+
specialAdjacencyOrdinals: specialAdjacencyOrdinalsByProvince.get(id) ?? [],
|
|
148
|
+
buildingPositionOrdinals: buildingPositionsByProvince.get(id) ?? [],
|
|
149
|
+
unitPositionOrdinals: unitPositionsByProvince.get(id) ?? [],
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
const states = [...index.states]
|
|
153
|
+
.sort((left, right) => left.id - right.id)
|
|
154
|
+
.map((state) => ({
|
|
155
|
+
kind: 'state',
|
|
156
|
+
id: state.id,
|
|
157
|
+
name: resolvedName(localisationEntriesByKey, [state.name], state.name),
|
|
158
|
+
geometry: aggregateGeometry(index, state.provinces),
|
|
159
|
+
capital: state.capital ?? null,
|
|
160
|
+
manpower: state.manpower,
|
|
161
|
+
category: state.category,
|
|
162
|
+
provinceIds: [...state.provinces].sort((a, b) => a - b),
|
|
163
|
+
regionIds: [
|
|
164
|
+
...new Set(state.provinces.flatMap((provinceId) => index.regionForProvince(provinceId).map(({ id }) => id))),
|
|
165
|
+
].sort((a, b) => a - b),
|
|
166
|
+
resources: sortedObject(state.resources),
|
|
167
|
+
owner: state.owner ?? null,
|
|
168
|
+
controller: state.controller ?? null,
|
|
169
|
+
cores: [...state.cores].sort(compareCodeUnits),
|
|
170
|
+
claims: [...state.claims].sort(compareCodeUnits),
|
|
171
|
+
victoryPoints: state.victoryPoints
|
|
172
|
+
.map(({ provinceId, value }) => ({ provinceId, value }))
|
|
173
|
+
.sort((left, right) => left.provinceId - right.provinceId || left.value - right.value),
|
|
174
|
+
stateBuildings: sortedObject(state.stateBuildings),
|
|
175
|
+
provinceBuildings: Object.fromEntries([...state.provinceBuildings]
|
|
176
|
+
.sort(([left], [right]) => left - right)
|
|
177
|
+
.map(([provinceId, buildings]) => [String(provinceId), sortedObject(buildings)])),
|
|
178
|
+
sourcePath: state.file.displayPath,
|
|
179
|
+
sourceLine: nodeLocation(state.document, state.assignment).start.line,
|
|
180
|
+
}));
|
|
181
|
+
const strategicRegions = [...index.regions]
|
|
182
|
+
.sort((left, right) => left.id - right.id)
|
|
183
|
+
.map((region) => ({
|
|
184
|
+
kind: 'strategic-region',
|
|
185
|
+
id: region.id,
|
|
186
|
+
name: resolvedName(localisationEntriesByKey, [region.name], region.name),
|
|
187
|
+
geometry: aggregateGeometry(index, region.provinces),
|
|
188
|
+
provinceIds: [...region.provinces].sort((a, b) => a - b),
|
|
189
|
+
stateIds: [
|
|
190
|
+
...new Set(region.provinces.flatMap((provinceId) => index.stateForProvince(provinceId).map(({ id }) => id))),
|
|
191
|
+
].sort((a, b) => a - b),
|
|
192
|
+
navalTerrain: region.navalTerrain ?? null,
|
|
193
|
+
sourcePath: region.file.displayPath,
|
|
194
|
+
sourceLine: nodeLocation(region.document, region.assignment).start.line,
|
|
195
|
+
}));
|
|
196
|
+
const normalAdjacencies = [...(raster?.adjacency ?? [])]
|
|
197
|
+
.flatMap(([from, neighbors]) => [...neighbors].flatMap((to) => (from < to ? [{ from, to }] : [])))
|
|
198
|
+
.sort((left, right) => left.from - right.from || left.to - right.to);
|
|
199
|
+
return {
|
|
200
|
+
schemaVersion: 2,
|
|
201
|
+
dimensions: { width, height },
|
|
202
|
+
coordinateSystems: {
|
|
203
|
+
pixel: { origin: 'top-left', xDirection: 'right', yDirection: 'down' },
|
|
204
|
+
map: { origin: 'bottom-left', xDirection: 'right', zDirection: 'up' },
|
|
205
|
+
},
|
|
206
|
+
counts: {
|
|
207
|
+
provinces: provinces.length,
|
|
208
|
+
states: states.length,
|
|
209
|
+
strategicRegions: strategicRegions.length,
|
|
210
|
+
normalAdjacencies: normalAdjacencies.length,
|
|
211
|
+
specialAdjacencies: index.adjacencies.length,
|
|
212
|
+
supplyNodes: index.supplyNodes.length,
|
|
213
|
+
railways: index.railways.length,
|
|
214
|
+
ports: index.ports.length,
|
|
215
|
+
victoryPoints: [...index.victoryPointsByProvince.values()].reduce((count, entries) => count + entries.length, 0),
|
|
216
|
+
buildingPositions: index.buildingPositions.length,
|
|
217
|
+
unitPositions: index.unitPositions.length,
|
|
218
|
+
weatherPositions: index.weatherPositions.length,
|
|
219
|
+
entityLocators: index.entityLocators.length,
|
|
220
|
+
},
|
|
221
|
+
provinces,
|
|
222
|
+
states,
|
|
223
|
+
strategicRegions,
|
|
224
|
+
normalAdjacencies,
|
|
225
|
+
specialAdjacencies: index.adjacencies.map((adjacency, ordinal) => ({
|
|
226
|
+
ordinal,
|
|
227
|
+
from: adjacency.from,
|
|
228
|
+
to: adjacency.to,
|
|
229
|
+
type: adjacency.type,
|
|
230
|
+
through: adjacency.through,
|
|
231
|
+
startX: adjacency.startX,
|
|
232
|
+
startY: adjacency.startY,
|
|
233
|
+
stopX: adjacency.stopX,
|
|
234
|
+
stopY: adjacency.stopY,
|
|
235
|
+
rule: adjacency.rule,
|
|
236
|
+
comment: adjacency.comment,
|
|
237
|
+
sourcePath: adjacency.document.file.displayPath,
|
|
238
|
+
line: adjacency.line,
|
|
239
|
+
})),
|
|
240
|
+
supplyNodes: index.supplyNodes.map((node, ordinal) => ({
|
|
241
|
+
ordinal,
|
|
242
|
+
level: node.level,
|
|
243
|
+
provinceId: node.provinceId,
|
|
244
|
+
sourcePath: node.document.file.displayPath,
|
|
245
|
+
line: node.line,
|
|
246
|
+
})),
|
|
247
|
+
railways: index.railways.map((railway, ordinal) => ({
|
|
248
|
+
ordinal,
|
|
249
|
+
level: railway.level,
|
|
250
|
+
declaredCount: railway.declaredCount,
|
|
251
|
+
provinceIds: [...railway.provinces],
|
|
252
|
+
sourcePath: railway.document.file.displayPath,
|
|
253
|
+
line: railway.line,
|
|
254
|
+
})),
|
|
255
|
+
ports: index.ports.map(({ stateId, provinceId, level, coastal, adjacentSeaProvinceIds }) => ({
|
|
256
|
+
stateId,
|
|
257
|
+
provinceId,
|
|
258
|
+
level,
|
|
259
|
+
coastal,
|
|
260
|
+
adjacentSeaProvinceIds: [...adjacentSeaProvinceIds].sort((left, right) => left - right),
|
|
261
|
+
})),
|
|
262
|
+
victoryPoints: [...index.victoryPointsByProvince.values()]
|
|
263
|
+
.flatMap((entries) => entries.map(({ stateId, provinceId, value }) => ({ stateId, provinceId, value })))
|
|
264
|
+
.sort((left, right) => left.stateId - right.stateId ||
|
|
265
|
+
left.provinceId - right.provinceId ||
|
|
266
|
+
left.value - right.value),
|
|
267
|
+
buildingPositions: index.buildingPositions.map((position, ordinal) => ({
|
|
268
|
+
ordinal,
|
|
269
|
+
stateId: position.stateId,
|
|
270
|
+
building: position.building,
|
|
271
|
+
x: position.x,
|
|
272
|
+
y: position.y,
|
|
273
|
+
z: position.z,
|
|
274
|
+
rotation: position.rotation,
|
|
275
|
+
adjacentSeaProvince: position.adjacentSeaProvince,
|
|
276
|
+
sourcePath: position.document.file.displayPath,
|
|
277
|
+
line: position.line,
|
|
278
|
+
})),
|
|
279
|
+
unitPositions: index.unitPositions.map((position, ordinal) => ({
|
|
280
|
+
ordinal,
|
|
281
|
+
provinceId: position.provinceId,
|
|
282
|
+
type: position.type,
|
|
283
|
+
x: position.x,
|
|
284
|
+
y: position.y,
|
|
285
|
+
z: position.z,
|
|
286
|
+
rotation: position.rotation,
|
|
287
|
+
offset: position.offset,
|
|
288
|
+
sourcePath: position.document.file.displayPath,
|
|
289
|
+
line: position.line,
|
|
290
|
+
})),
|
|
291
|
+
weatherPositions: index.weatherPositions.map((position, ordinal) => ({
|
|
292
|
+
ordinal,
|
|
293
|
+
strategicRegionId: position.strategicRegionId,
|
|
294
|
+
x: position.x,
|
|
295
|
+
y: position.y,
|
|
296
|
+
z: position.z,
|
|
297
|
+
size: position.size,
|
|
298
|
+
sourcePath: position.document.file.displayPath,
|
|
299
|
+
line: position.line,
|
|
300
|
+
})),
|
|
301
|
+
entityLocators: [...index.entityLocators]
|
|
302
|
+
.sort((left, right) => compareCodeUnits(left.entity, right.entity) || compareCodeUnits(left.name, right.name))
|
|
303
|
+
.map(({ entity, name, position, file }) => ({
|
|
304
|
+
entity,
|
|
305
|
+
name,
|
|
306
|
+
position: [...position],
|
|
307
|
+
sourcePath: file.displayPath,
|
|
308
|
+
})),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function searchScore(query, id, key, displayName, alternatives) {
|
|
312
|
+
const idText = String(id);
|
|
313
|
+
const normalizedKey = key.toLocaleLowerCase('en-US');
|
|
314
|
+
const normalizedName = displayName.toLocaleLowerCase('en-US');
|
|
315
|
+
const normalizedAlternatives = alternatives.map((value) => value.toLocaleLowerCase('en-US'));
|
|
316
|
+
if (query === idText ||
|
|
317
|
+
query === normalizedKey ||
|
|
318
|
+
query === normalizedName ||
|
|
319
|
+
normalizedAlternatives.includes(query))
|
|
320
|
+
return 0;
|
|
321
|
+
if (idText.startsWith(query) ||
|
|
322
|
+
normalizedKey.startsWith(query) ||
|
|
323
|
+
normalizedName.startsWith(query) ||
|
|
324
|
+
normalizedAlternatives.some((value) => value.startsWith(query)))
|
|
325
|
+
return 1;
|
|
326
|
+
if (normalizedKey.includes(query) ||
|
|
327
|
+
normalizedName.includes(query) ||
|
|
328
|
+
normalizedAlternatives.some((value) => value.includes(query)))
|
|
329
|
+
return 2;
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
export function searchMapCatalog(catalog, query, limit = 100) {
|
|
333
|
+
const normalized = query.trim().toLocaleLowerCase('en-US');
|
|
334
|
+
if (normalized === '')
|
|
335
|
+
return [];
|
|
336
|
+
const entries = [
|
|
337
|
+
...catalog.provinces.map(({ id, name, geometry }) => ({
|
|
338
|
+
kind: 'province',
|
|
339
|
+
id,
|
|
340
|
+
key: name.key,
|
|
341
|
+
displayName: name.displayName,
|
|
342
|
+
alternatives: name.values.map(({ value }) => value),
|
|
343
|
+
geometry,
|
|
344
|
+
})),
|
|
345
|
+
...catalog.states.map(({ id, name, geometry }) => ({
|
|
346
|
+
kind: 'state',
|
|
347
|
+
id,
|
|
348
|
+
key: name.key,
|
|
349
|
+
displayName: name.displayName,
|
|
350
|
+
alternatives: name.values.map(({ value }) => value),
|
|
351
|
+
geometry,
|
|
352
|
+
})),
|
|
353
|
+
...catalog.strategicRegions.map(({ id, name, geometry }) => ({
|
|
354
|
+
kind: 'strategic-region',
|
|
355
|
+
id,
|
|
356
|
+
key: name.key,
|
|
357
|
+
displayName: name.displayName,
|
|
358
|
+
alternatives: name.values.map(({ value }) => value),
|
|
359
|
+
geometry,
|
|
360
|
+
})),
|
|
361
|
+
];
|
|
362
|
+
return entries
|
|
363
|
+
.flatMap((entry) => {
|
|
364
|
+
const score = searchScore(normalized, entry.id, entry.key, entry.displayName, entry.alternatives);
|
|
365
|
+
if (score === undefined)
|
|
366
|
+
return [];
|
|
367
|
+
const { alternatives: _alternatives, ...result } = entry;
|
|
368
|
+
return [{ ...result, score }];
|
|
369
|
+
})
|
|
370
|
+
.sort((left, right) => left.score - right.score || compareCodeUnits(left.kind, right.kind) || left.id - right.id)
|
|
371
|
+
.slice(0, Math.max(0, limit));
|
|
372
|
+
}
|
|
373
|
+
export function lookupMapCoordinate(index, coordinate) {
|
|
374
|
+
const raster = index.raster;
|
|
375
|
+
const pixel = coordinate.kind === 'pixel'
|
|
376
|
+
? { x: Math.floor(coordinate.x), y: Math.floor(coordinate.y) }
|
|
377
|
+
: raster === undefined
|
|
378
|
+
? null
|
|
379
|
+
: { x: Math.floor(coordinate.x), y: raster.height - 1 - Math.floor(coordinate.z) };
|
|
380
|
+
if (raster === undefined ||
|
|
381
|
+
pixel === null ||
|
|
382
|
+
pixel.x < 0 ||
|
|
383
|
+
pixel.y < 0 ||
|
|
384
|
+
pixel.x >= raster.width ||
|
|
385
|
+
pixel.y >= raster.height) {
|
|
386
|
+
return { coordinate, pixel: null, provinceId: null, stateIds: [], regionIds: [] };
|
|
387
|
+
}
|
|
388
|
+
const provinceId = raster.provinceIds[pixel.y * raster.width + pixel.x] ?? -1;
|
|
389
|
+
if (provinceId < 0)
|
|
390
|
+
return { coordinate, pixel, provinceId: null, stateIds: [], regionIds: [] };
|
|
391
|
+
return {
|
|
392
|
+
coordinate,
|
|
393
|
+
pixel,
|
|
394
|
+
provinceId,
|
|
395
|
+
stateIds: index.stateForProvince(provinceId).map(({ id }) => id),
|
|
396
|
+
regionIds: index.regionForProvince(provinceId).map(({ id }) => id),
|
|
397
|
+
};
|
|
398
|
+
}
|