hoi4-agent-tools 4.0.5 → 4.1.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 +12 -0
- package/README.md +2 -0
- package/dist/hoi4_agent_tools/core/bounded-source.js +210 -0
- package/dist/hoi4_agent_tools/core/diagnostic-rollup.js +91 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +12 -0
- package/dist/hoi4_agent_tools/core/engine.js +2 -1
- package/dist/hoi4_agent_tools/core/job-executor.js +9 -0
- package/dist/hoi4_agent_tools/core/managed-outputs.js +72 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +28 -2
- package/dist/hoi4_agent_tools/core/scanner.js +17 -0
- package/dist/hoi4_agent_tools/core/server-state.js +9 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +5 -1
- package/dist/hoi4_agent_tools/core/transaction-canonical.js +43 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +7 -2
- package/dist/hoi4_agent_tools/core/transactions.js +355 -111
- package/dist/hoi4_agent_tools/map/derived.js +21 -6
- package/dist/hoi4_agent_tools/map/job-operations.js +9 -3
- package/dist/hoi4_agent_tools/map/operations.js +72 -21
- package/dist/hoi4_agent_tools/map/reference-labels.js +238 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +247 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +1097 -0
- package/dist/hoi4_agent_tools/map/render.js +9 -1
- package/dist/hoi4_agent_tools/map/service.js +222 -18
- package/dist/hoi4_agent_tools/map/tool-operations.js +71 -2
- package/dist/hoi4_agent_tools/map/world-context.js +148 -0
- package/dist/hoi4_agent_tools/map/world-drainage.js +273 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +55 -0
- package/dist/hoi4_agent_tools/map/world-preview-style.js +157 -0
- package/dist/hoi4_agent_tools/map/world-preview.js +230 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +524 -0
- package/dist/hoi4_agent_tools/map/world.js +374 -253
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -2
- package/dist/hoi4_agent_tools/schemas/map-requests.js +4 -0
- package/dist/hoi4_agent_tools/schemas/map.js +103 -10
- package/dist/hoi4_agent_tools/schemas/transaction.js +20 -3
- package/dist/hoi4_agent_tools/version.js +2 -2
- package/docs/images/reference-maps/political.png +0 -0
- package/docs/images/reference-maps/sketch.png +0 -0
- package/docs/map.md +45 -0
- package/docs/third-party-notices.md +6 -0
- package/npm-shrinkwrap.json +124 -2
- package/package.json +4 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +373 -70
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { readReferenceImageSource } from '../core/bounded-source.js';
|
|
2
|
+
import { extractReferenceLabels } from './reference-labels.js';
|
|
3
|
+
import { extractReferenceWorld } from './reference-world.js';
|
|
4
|
+
import { ServiceError } from '../core/result.js';
|
|
5
|
+
/** Analyze only bounded, authorized sources; the transaction retains the original source hashes. */
|
|
6
|
+
export async function prepareReferenceWorld(resolver, workspaceId, operation, principal, signal) {
|
|
7
|
+
const reference = operation.reference;
|
|
8
|
+
if (reference === undefined)
|
|
9
|
+
throw new ServiceError('MAP_REFERENCE_REQUIRED', 'Reference preparation requires imagePath');
|
|
10
|
+
const images = [];
|
|
11
|
+
const load = async (relativePath) => {
|
|
12
|
+
const image = await readReferenceImageSource(resolver, workspaceId, relativePath, principal, signal);
|
|
13
|
+
images.push(image);
|
|
14
|
+
return image;
|
|
15
|
+
};
|
|
16
|
+
const main = await load(reference.imagePath);
|
|
17
|
+
if (reference.crop !== undefined &&
|
|
18
|
+
(reference.crop.x + reference.crop.width > main.originalWidth ||
|
|
19
|
+
reference.crop.y + reference.crop.height > main.originalHeight))
|
|
20
|
+
throw new ServiceError('MAP_REFERENCE_CROP_BLOCKED', 'Reference crop exceeds the original image bounds');
|
|
21
|
+
const political = reference.politicalImagePath === undefined
|
|
22
|
+
? undefined
|
|
23
|
+
: await load(reference.politicalImagePath);
|
|
24
|
+
const terrain = reference.terrainImagePath === undefined ? undefined : await load(reference.terrainImagePath);
|
|
25
|
+
const height = reference.heightImagePath === undefined ? undefined : await load(reference.heightImagePath);
|
|
26
|
+
const crop = reference.crop === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: {
|
|
29
|
+
x: Math.floor((reference.crop.x * main.workingWidth) / main.originalWidth),
|
|
30
|
+
y: Math.floor((reference.crop.y * main.workingHeight) / main.originalHeight),
|
|
31
|
+
width: Math.max(1, Math.round((reference.crop.width * main.workingWidth) / main.originalWidth)),
|
|
32
|
+
height: Math.max(1, Math.round((reference.crop.height * main.workingHeight) / main.originalHeight)),
|
|
33
|
+
};
|
|
34
|
+
const text = await extractReferenceLabels(main.bytes);
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
const textMasks = text.labels
|
|
37
|
+
.slice(0, Math.max(0, 256 - (reference.ignoreMasks?.length ?? 0)))
|
|
38
|
+
.map((label) => {
|
|
39
|
+
const left = Math.max(0, label.x - label.width / 2 - 3 / main.workingWidth), right = Math.min(1, label.x + label.width / 2 + 3 / main.workingWidth);
|
|
40
|
+
const top = Math.max(0, label.y - label.height / 2 - 3 / main.workingHeight), bottom = Math.min(1, label.y + label.height / 2 + 3 / main.workingHeight);
|
|
41
|
+
return [
|
|
42
|
+
{ x: left, y: top },
|
|
43
|
+
{ x: right, y: top },
|
|
44
|
+
{ x: right, y: bottom },
|
|
45
|
+
{ x: left, y: bottom },
|
|
46
|
+
];
|
|
47
|
+
});
|
|
48
|
+
const surface = await extractReferenceWorld(main.bytes, {
|
|
49
|
+
width: operation.width ?? 5_632,
|
|
50
|
+
height: operation.height ?? 2_048,
|
|
51
|
+
seed: operation.seed ?? 1,
|
|
52
|
+
...(reference.mode === undefined ? {} : { mode: reference.mode }),
|
|
53
|
+
...(crop === undefined ? {} : { crop }),
|
|
54
|
+
ignoreMasks: [...(reference.ignoreMasks ?? []), ...textMasks],
|
|
55
|
+
...(reference.annotations === undefined ? {} : { annotations: reference.annotations }),
|
|
56
|
+
...(reference.riverAnnotations === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { riverAnnotations: reference.riverAnnotations }),
|
|
59
|
+
...(reference.palette === undefined ? {} : { palette: reference.palette }),
|
|
60
|
+
...(reference.coastDetail === undefined ? {} : { coastDetail: reference.coastDetail }),
|
|
61
|
+
...(political === undefined ? {} : { politicalImageBytes: political.bytes }),
|
|
62
|
+
...(terrain === undefined ? {} : { terrainImageBytes: terrain.bytes }),
|
|
63
|
+
...(height === undefined ? {} : { heightImageBytes: height.bytes }),
|
|
64
|
+
});
|
|
65
|
+
signal?.throwIfAborted();
|
|
66
|
+
const warnings = [
|
|
67
|
+
...surface.diagnostics,
|
|
68
|
+
...text.warnings,
|
|
69
|
+
'Reference interpretation uses visual heuristics. Terrain, borders, labels and generated demographic values are approximations; review the offline previews.',
|
|
70
|
+
...(textMasks.length === 0
|
|
71
|
+
? []
|
|
72
|
+
: [
|
|
73
|
+
'Recognized text bounds are excluded from terrain classification; nearby coastlines may need review.',
|
|
74
|
+
]),
|
|
75
|
+
];
|
|
76
|
+
const toTarget = (x, y) => ({
|
|
77
|
+
x: ((x * main.workingWidth - (crop?.x ?? 0)) / (crop?.width ?? main.workingWidth)) *
|
|
78
|
+
surface.referenceBounds.width +
|
|
79
|
+
surface.referenceBounds.x,
|
|
80
|
+
y: ((y * main.workingHeight - (crop?.y ?? 0)) / (crop?.height ?? main.workingHeight)) *
|
|
81
|
+
surface.referenceBounds.height +
|
|
82
|
+
surface.referenceBounds.y,
|
|
83
|
+
});
|
|
84
|
+
const nearestLand = (x, y) => {
|
|
85
|
+
const cx = Math.round(x), cy = Math.round(y);
|
|
86
|
+
const radius = Math.max(2, Math.round(Math.min(surface.width, surface.height) * 0.02));
|
|
87
|
+
let best;
|
|
88
|
+
for (let dy = -radius; dy <= radius; dy++)
|
|
89
|
+
for (let dx = -radius; dx <= radius; dx++) {
|
|
90
|
+
const px = cx + dx, py = cy + dy, distance = dx * dx + dy * dy;
|
|
91
|
+
if (px < 0 ||
|
|
92
|
+
py < 0 ||
|
|
93
|
+
px >= surface.width ||
|
|
94
|
+
py >= surface.height ||
|
|
95
|
+
surface.kind[py * surface.width + px] !== 1)
|
|
96
|
+
continue;
|
|
97
|
+
if (best === undefined || distance < best.distance)
|
|
98
|
+
best = { x: px, y: py, distance };
|
|
99
|
+
}
|
|
100
|
+
return best;
|
|
101
|
+
};
|
|
102
|
+
const reviewedLabels = (reference.labels ?? []).map((label) => ({ ...label, ...toTarget(label.x, label.y) }));
|
|
103
|
+
const nearReviewed = (point) => reviewedLabels.some((reviewed) => Math.hypot(reviewed.x - point.x, reviewed.y - point.y) <
|
|
104
|
+
Math.min(surface.width, surface.height) * 0.025);
|
|
105
|
+
// An explicit review is authoritative, including an empty list. OCR still helps mask ink,
|
|
106
|
+
// but must not reintroduce discarded or uncertain names outside the supplied label set.
|
|
107
|
+
const automaticText = reference.labels === undefined ? text.labels : [];
|
|
108
|
+
const labels = automaticText
|
|
109
|
+
.filter(({ text: label }) => !/\b(?:map|world|sea|ocean|bay|strait|channel|flow|belt|signature|north|south|east|west|compass)\b/iu.test(label))
|
|
110
|
+
.slice(0, 200)
|
|
111
|
+
.flatMap((label) => {
|
|
112
|
+
const point = toTarget(label.x, label.y);
|
|
113
|
+
const land = nearestLand(point.x, point.y);
|
|
114
|
+
return land === undefined ? [] : [{ name: label.text, x: land.x, y: land.y }];
|
|
115
|
+
});
|
|
116
|
+
const reviewedCountries = reviewedLabels.filter((label) => label.kind === 'country');
|
|
117
|
+
const countryNames = [...reviewedCountries, ...labels.filter((label) => !nearReviewed(label))];
|
|
118
|
+
const defaults = ['Aster', 'Brum', 'Corvel', 'Doran', 'Evin', 'Faron', 'Galen', 'Haven'];
|
|
119
|
+
const politicalRegionAt = (point) => {
|
|
120
|
+
const x = Math.floor(point.x), y = Math.floor(point.y);
|
|
121
|
+
if (x < 0 || y < 0 || x >= surface.width || y >= surface.height)
|
|
122
|
+
return undefined;
|
|
123
|
+
const pixel = y * surface.width + x;
|
|
124
|
+
if (surface.kind[pixel] !== 1)
|
|
125
|
+
return undefined;
|
|
126
|
+
const id = surface.country[pixel];
|
|
127
|
+
return id === 0 ? undefined : id;
|
|
128
|
+
};
|
|
129
|
+
let countries;
|
|
130
|
+
if (operation.countries !== undefined)
|
|
131
|
+
countries = operation.countries;
|
|
132
|
+
else if (surface.inferredCountries.length > 1) {
|
|
133
|
+
countries = surface.inferredCountries.slice(0, 200).map((country, index) => ({
|
|
134
|
+
tag: `X${(index + 1).toString(36).toUpperCase().padStart(2, '0')}`,
|
|
135
|
+
name: country.name,
|
|
136
|
+
color: country.color,
|
|
137
|
+
}));
|
|
138
|
+
const centres = new Map();
|
|
139
|
+
for (let pixel = 0; pixel < surface.country.length; pixel++) {
|
|
140
|
+
const id = surface.country[pixel];
|
|
141
|
+
if (id === 0)
|
|
142
|
+
continue;
|
|
143
|
+
const centre = centres.get(id) ?? { x: 0, y: 0, n: 0 };
|
|
144
|
+
centre.x += pixel % surface.width;
|
|
145
|
+
centre.y += Math.floor(pixel / surface.width);
|
|
146
|
+
centre.n++;
|
|
147
|
+
centres.set(id, centre);
|
|
148
|
+
}
|
|
149
|
+
const used = new Set();
|
|
150
|
+
for (const [index, country] of countries.entries()) {
|
|
151
|
+
const regionId = surface.inferredCountries[index].id;
|
|
152
|
+
const centre = centres.get(regionId);
|
|
153
|
+
if (centre === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
const label = countryNames
|
|
156
|
+
.filter((label) => !used.has(label.name) && politicalRegionAt(label) === regionId)
|
|
157
|
+
.sort((a, b) => Math.hypot(a.x - centre.x / centre.n, a.y - centre.y / centre.n) -
|
|
158
|
+
Math.hypot(b.x - centre.x / centre.n, b.y - centre.y / centre.n))[0];
|
|
159
|
+
if (label !== undefined) {
|
|
160
|
+
country.name = label.name;
|
|
161
|
+
used.add(label.name);
|
|
162
|
+
}
|
|
163
|
+
else
|
|
164
|
+
warnings.push(`Political region ${regionId} had no reliable name label within its borders; ${country.name} was retained.`);
|
|
165
|
+
}
|
|
166
|
+
warnings.push('Flat political colors define inferred countries; text within each region is an uncertain country-name candidate.');
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const count = reviewedCountries.length || Math.max(4, Math.min(8, countryNames.length || 4));
|
|
170
|
+
countries = Array.from({ length: count }, (_, index) => ({
|
|
171
|
+
tag: `X${(index + 1).toString(36).toUpperCase().padStart(2, '0')}`,
|
|
172
|
+
name: countryNames[index]?.name ?? defaults[index],
|
|
173
|
+
}));
|
|
174
|
+
warnings.push('Political borders were not reliably encoded; generated countries and ownership use deterministic defaults.');
|
|
175
|
+
}
|
|
176
|
+
const automaticLabels = automaticText
|
|
177
|
+
.filter(({ text: label }) => !/\b(?:map|world|signature|compass|north|south|east|west|flow|belt)\b/iu.test(label))
|
|
178
|
+
.map((label) => ({
|
|
179
|
+
...toTarget(label.x, label.y),
|
|
180
|
+
name: label.text,
|
|
181
|
+
kind: /\b(?:sea|ocean|bay|strait|channel)\b/iu.test(label.text)
|
|
182
|
+
? 'sea'
|
|
183
|
+
: /\b(?:mountain|mountains|range)\b/iu.test(label.text)
|
|
184
|
+
? 'mountain'
|
|
185
|
+
: 'city',
|
|
186
|
+
}));
|
|
187
|
+
const geographicLabels = [
|
|
188
|
+
...reviewedLabels,
|
|
189
|
+
...automaticLabels.filter((label) => !reviewedLabels.some((reviewed) => Math.hypot(reviewed.x - label.x, reviewed.y - label.y) <
|
|
190
|
+
Math.min(surface.width, surface.height) * 0.025)),
|
|
191
|
+
].slice(0, 2048);
|
|
192
|
+
const cityInputs = reference.cities === undefined
|
|
193
|
+
? geographicLabels
|
|
194
|
+
.filter((label) => label.kind === 'city')
|
|
195
|
+
.map(({ name, x, y }) => ({ name, x, y }))
|
|
196
|
+
: reference.cities.map((city) => ({ ...city, ...toTarget(city.x, city.y) }));
|
|
197
|
+
const cities = cityInputs.flatMap((city) => {
|
|
198
|
+
if ('countryTag' in city &&
|
|
199
|
+
city.countryTag !== undefined &&
|
|
200
|
+
!countries.some((country) => country.tag === city.countryTag))
|
|
201
|
+
throw new ServiceError('MAP_REFERENCE_CITY_COUNTRY_INVALID', 'City countryTag must name a generated country');
|
|
202
|
+
const land = nearestLand(city.x, city.y);
|
|
203
|
+
if (land === undefined) {
|
|
204
|
+
warnings.push(`City ${city.name} is outside nearby land and was omitted.`);
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
return [{ ...city, x: land.x, y: land.y }];
|
|
208
|
+
});
|
|
209
|
+
for (const [index, country] of countries.entries()) {
|
|
210
|
+
let candidates = cities;
|
|
211
|
+
if (surface.inferredCountries.length > 1) {
|
|
212
|
+
const color = country.color;
|
|
213
|
+
const matches = color === undefined
|
|
214
|
+
? []
|
|
215
|
+
: surface.inferredCountries
|
|
216
|
+
.map((region) => ({
|
|
217
|
+
id: region.id,
|
|
218
|
+
distance: (region.color.r - color.r) ** 2 +
|
|
219
|
+
(region.color.g - color.g) ** 2 +
|
|
220
|
+
(region.color.b - color.b) ** 2,
|
|
221
|
+
}))
|
|
222
|
+
.sort((a, b) => a.distance - b.distance || a.id - b.id);
|
|
223
|
+
const regionId = matches[0] !== undefined &&
|
|
224
|
+
matches[0].distance <= 3 * 24 ** 2 &&
|
|
225
|
+
(matches[1] === undefined || matches[0].distance < matches[1].distance)
|
|
226
|
+
? matches[0].id
|
|
227
|
+
: undefined;
|
|
228
|
+
if (regionId === undefined)
|
|
229
|
+
continue;
|
|
230
|
+
candidates = cities.filter((city) => city.countryTag === undefined && politicalRegionAt(city) === regionId);
|
|
231
|
+
}
|
|
232
|
+
const city = candidates.find(({ name }) => name === country.name) ??
|
|
233
|
+
(surface.inferredCountries.length > 1 ? candidates[0] : candidates[index]);
|
|
234
|
+
if (city !== undefined && city.countryTag === undefined)
|
|
235
|
+
city.countryTag = country.tag;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
prepared: {
|
|
239
|
+
surface,
|
|
240
|
+
countries,
|
|
241
|
+
cities,
|
|
242
|
+
labels: geographicLabels,
|
|
243
|
+
diagnostics: [...new Set(warnings)],
|
|
244
|
+
},
|
|
245
|
+
images,
|
|
246
|
+
};
|
|
247
|
+
}
|