hoi4-agent-tools 4.0.4 → 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 +20 -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/gui/layout.js +61 -25
- package/dist/hoi4_agent_tools/gui/native-state.js +40 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +9 -6
- package/dist/hoi4_agent_tools/gui/studio.js +4 -1
- 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/gui.md +6 -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,230 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { canonicalJson } from '../core/canonical.js';
|
|
3
|
+
import { RENDER_MAX_DECODED_PIXELS, RenderBudget } from '../core/render-budget.js';
|
|
4
|
+
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { previewGraphicalTerrain, previewPoliticalPalette } from './world-preview-style.js';
|
|
6
|
+
/** A rolled-back generation must never present the unchanged source map as a new world. */
|
|
7
|
+
export function generatedWorldOperationIds(plan) {
|
|
8
|
+
const requested = new Set(plan.operations.filter(({ kind }) => kind === 'create_world').map(({ id }) => id));
|
|
9
|
+
const blocked = new Set(plan.blockers.map(({ operationId }) => operationId));
|
|
10
|
+
return new Set(plan.diagnostics.flatMap(({ code, operationId }) => code === 'MAP_WORLD_CREATED' &&
|
|
11
|
+
operationId !== undefined &&
|
|
12
|
+
requested.has(operationId) &&
|
|
13
|
+
!blocked.has(operationId)
|
|
14
|
+
? [operationId]
|
|
15
|
+
: []));
|
|
16
|
+
}
|
|
17
|
+
const terrainColors = {
|
|
18
|
+
plains: [151, 177, 104],
|
|
19
|
+
hills: [128, 147, 98],
|
|
20
|
+
mountain: [139, 139, 130],
|
|
21
|
+
forest: [64, 115, 66],
|
|
22
|
+
jungle: [40, 93, 56],
|
|
23
|
+
marsh: [110, 154, 144],
|
|
24
|
+
desert: [219, 194, 128],
|
|
25
|
+
urban: [159, 144, 136],
|
|
26
|
+
ocean: [40, 80, 116],
|
|
27
|
+
lakes: [68, 127, 159],
|
|
28
|
+
lake: [68, 127, 159],
|
|
29
|
+
snow: [232, 235, 232],
|
|
30
|
+
};
|
|
31
|
+
// A fixed bitmap alphabet avoids machine-dependent installed fonts in preview labels.
|
|
32
|
+
const glyphs = {
|
|
33
|
+
A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'],
|
|
34
|
+
C: ['01111', '10000', '10000', '10000', '10000', '10000', '01111'],
|
|
35
|
+
D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'],
|
|
36
|
+
E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'],
|
|
37
|
+
F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'],
|
|
38
|
+
G: ['01111', '10000', '10000', '10111', '10001', '10001', '01111'],
|
|
39
|
+
I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'],
|
|
40
|
+
L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'],
|
|
41
|
+
N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'],
|
|
42
|
+
O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'],
|
|
43
|
+
P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'],
|
|
44
|
+
R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'],
|
|
45
|
+
T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'],
|
|
46
|
+
V: ['10001', '10001', '10001', '10001', '10001', '01010', '00100'],
|
|
47
|
+
W: ['10001', '10001', '10001', '10101', '10101', '11011', '10001'],
|
|
48
|
+
};
|
|
49
|
+
function labelSvg(width, height, labels) {
|
|
50
|
+
const rectangles = [];
|
|
51
|
+
for (const label of labels)
|
|
52
|
+
for (let letter = 0; letter < label.text.length; letter += 1)
|
|
53
|
+
for (const [y, row] of (glyphs[label.text[letter]] ?? []).entries())
|
|
54
|
+
for (let x = 0; x < row.length; x += 1)
|
|
55
|
+
if (row[x] === '1')
|
|
56
|
+
rectangles.push(`<rect x="${label.x + letter * 12 + x * 2}" y="${label.y + y * 2}" width="2" height="2"/>`);
|
|
57
|
+
return Buffer.from(`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}"><g fill="#e8edf2">${rectangles.join('')}</g></svg>`);
|
|
58
|
+
}
|
|
59
|
+
/** Bounded offline evidence, sampled directly from the generated map, never from the game. */
|
|
60
|
+
export async function buildWorldPreviewArtifacts(input) {
|
|
61
|
+
input.signal?.throwIfAborted();
|
|
62
|
+
const raster = input.index.raster;
|
|
63
|
+
if (raster === undefined)
|
|
64
|
+
return [];
|
|
65
|
+
const scale = Math.min(1, 768 / raster.width, 512 / raster.height);
|
|
66
|
+
const width = Math.max(1, Math.round(raster.width * scale));
|
|
67
|
+
const height = Math.max(1, Math.round(raster.height * scale));
|
|
68
|
+
const panelWidth = Math.max(width, 128);
|
|
69
|
+
const budget = input.budget ?? new RenderBudget();
|
|
70
|
+
budget.reserve(width, height, 'world terrain preview');
|
|
71
|
+
budget.reserve(width, height, 'world political preview');
|
|
72
|
+
const terrain = Buffer.alloc(width * height * 3);
|
|
73
|
+
const political = Buffer.alloc(width * height * 3);
|
|
74
|
+
const ownerByProvince = new Map();
|
|
75
|
+
for (const state of input.index.states)
|
|
76
|
+
for (const province of state.provinces)
|
|
77
|
+
ownerByProvince.set(province, state.owner ?? 'unowned');
|
|
78
|
+
const politicalPalette = previewPoliticalPalette(input.index, new Set(ownerByProvince.values()));
|
|
79
|
+
const politicalColors = politicalPalette.colors;
|
|
80
|
+
const graphical = previewGraphicalTerrain(input.index);
|
|
81
|
+
const styleEvidence = {
|
|
82
|
+
politicalColors: Object.fromEntries(politicalColors),
|
|
83
|
+
politicalColorSources: Object.fromEntries(politicalPalette.sources),
|
|
84
|
+
politicalColorFallback: { algorithm: 'stable-tag-sha256', tags: politicalPalette.fallbackTags },
|
|
85
|
+
graphicalTerrain: {
|
|
86
|
+
bitmapAvailable: graphical.terrain !== undefined,
|
|
87
|
+
cityFootprintsAvailable: graphical.cities !== undefined,
|
|
88
|
+
sourceFiles: graphical.sourceFiles,
|
|
89
|
+
permanentSnowIndices: [...graphical.snowIndices].sort((a, b) => a - b),
|
|
90
|
+
missingDefinitionsFallback: 'generated-world vanilla terrain slots, then province definitions',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
for (let y = 0; y < height; y += 1) {
|
|
94
|
+
input.signal?.throwIfAborted();
|
|
95
|
+
for (let x = 0; x < width; x += 1) {
|
|
96
|
+
const sourceX = Math.min(raster.width - 1, Math.floor(x / scale));
|
|
97
|
+
const sourceY = Math.min(raster.height - 1, Math.floor(y / scale));
|
|
98
|
+
const province = raster.provinceIds[sourceY * raster.width + sourceX];
|
|
99
|
+
const definition = input.index.definitionsById.get(province);
|
|
100
|
+
const land = definition?.type === 'land';
|
|
101
|
+
const graphicalIndex = graphical.terrain?.paletteIndexAt(sourceX, sourceY);
|
|
102
|
+
const graphicalType = graphicalIndex === undefined ? undefined : graphical.types.get(graphicalIndex);
|
|
103
|
+
const snow = graphicalIndex !== undefined && graphical.snowIndices.has(graphicalIndex);
|
|
104
|
+
const city = land && graphical.cities?.paletteIndexAt(sourceX, sourceY) === 15;
|
|
105
|
+
const terrainColor = terrainColors[city ? 'urban' : snow && land ? 'snow' : (graphicalType ?? definition?.terrain ?? 'ocean')] ?? terrainColors.plains;
|
|
106
|
+
const owner = ownerByProvince.get(province) ?? 'unowned';
|
|
107
|
+
const politicalColor = land
|
|
108
|
+
? (politicalColors.get(owner) ?? [120, 120, 120])
|
|
109
|
+
: terrainColors.ocean;
|
|
110
|
+
const shade = land
|
|
111
|
+
? Math.max(0.72, Math.min(1.2, 1 +
|
|
112
|
+
(input.index.heightAt(sourceX, sourceY) -
|
|
113
|
+
input.index.heightAt(Math.max(0, sourceX - 2), Math.max(0, sourceY - 2))) *
|
|
114
|
+
0.035))
|
|
115
|
+
: 1;
|
|
116
|
+
for (let channel = 0; channel < 3; channel += 1) {
|
|
117
|
+
terrain[(y * width + x) * 3 + channel] = Math.round(Math.min(255, terrainColor[channel] * shade));
|
|
118
|
+
political[(y * width + x) * 3 + channel] = politicalColor[channel];
|
|
119
|
+
}
|
|
120
|
+
const neighbor = sourceX > 0 ? raster.provinceIds[sourceY * raster.width + sourceX - 1] : province;
|
|
121
|
+
if (land && ownerByProvince.get(neighbor) !== owner)
|
|
122
|
+
political.fill(37, (y * width + x) * 3, (y * width + x) * 3 + 3);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const raw = { width, height, channels: 3 };
|
|
126
|
+
const [terrainPng, politicalPng] = await Promise.all([
|
|
127
|
+
sharp(terrain, { raw }).png().toBuffer(),
|
|
128
|
+
sharp(political, { raw }).png().toBuffer(),
|
|
129
|
+
]);
|
|
130
|
+
const writes = [];
|
|
131
|
+
const references = input.references.length > 0 ? input.references : [undefined];
|
|
132
|
+
for (const [referenceIndex, reference] of references.entries()) {
|
|
133
|
+
input.signal?.throwIfAborted();
|
|
134
|
+
budget.reserve(panelWidth, height, 'world reference preview');
|
|
135
|
+
let referencePng;
|
|
136
|
+
try {
|
|
137
|
+
referencePng =
|
|
138
|
+
reference === undefined
|
|
139
|
+
? await sharp({
|
|
140
|
+
create: { width: panelWidth, height, channels: 3, background: '#18212a' },
|
|
141
|
+
})
|
|
142
|
+
.png()
|
|
143
|
+
.toBuffer()
|
|
144
|
+
: await sharp(reference.bytes, {
|
|
145
|
+
limitInputPixels: RENDER_MAX_DECODED_PIXELS,
|
|
146
|
+
animated: false,
|
|
147
|
+
})
|
|
148
|
+
.resize(panelWidth, height, { fit: 'contain', background: '#18212a' })
|
|
149
|
+
.png()
|
|
150
|
+
.toBuffer();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new ServiceError('MAP_WORLD_PREVIEW_REFERENCE_INVALID', 'The reference image could not be safely rendered');
|
|
154
|
+
}
|
|
155
|
+
const gap = 12, header = 32, footer = 28;
|
|
156
|
+
const combinedWidth = panelWidth * 3 + gap * 4;
|
|
157
|
+
const combinedHeight = height + header + footer;
|
|
158
|
+
budget.reserve(combinedWidth, combinedHeight, 'world side-by-side preview');
|
|
159
|
+
const png = await sharp({
|
|
160
|
+
create: { width: combinedWidth, height: combinedHeight, channels: 3, background: '#101820' },
|
|
161
|
+
})
|
|
162
|
+
.composite([
|
|
163
|
+
{ input: referencePng, left: gap, top: header },
|
|
164
|
+
{
|
|
165
|
+
input: terrainPng,
|
|
166
|
+
left: panelWidth + gap * 2 + Math.floor((panelWidth - width) / 2),
|
|
167
|
+
top: header,
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
input: politicalPng,
|
|
171
|
+
left: panelWidth * 2 + gap * 3 + Math.floor((panelWidth - width) / 2),
|
|
172
|
+
top: header,
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
input: labelSvg(combinedWidth, combinedHeight, [
|
|
176
|
+
{ text: reference === undefined ? 'GENERATED' : 'REFERENCE', x: gap, y: 9 },
|
|
177
|
+
{ text: 'TERRAIN', x: panelWidth + gap * 2, y: 9 },
|
|
178
|
+
{ text: 'POLITICAL', x: panelWidth * 2 + gap * 3, y: 9 },
|
|
179
|
+
{ text: 'OFFLINE GENERATED PREVIEW', x: gap, y: header + height + 8 },
|
|
180
|
+
]),
|
|
181
|
+
left: 0,
|
|
182
|
+
top: 0,
|
|
183
|
+
},
|
|
184
|
+
])
|
|
185
|
+
.png()
|
|
186
|
+
.toBuffer();
|
|
187
|
+
writes.push({
|
|
188
|
+
name: referenceIndex === 0
|
|
189
|
+
? 'map-world-preview.png'
|
|
190
|
+
: `map-world-preview-${referenceIndex + 1}.png`,
|
|
191
|
+
mimeType: 'image/png',
|
|
192
|
+
content: png,
|
|
193
|
+
provenance: {
|
|
194
|
+
...input.provenance,
|
|
195
|
+
kind: 'map-world-preview',
|
|
196
|
+
schemaVersion: 'map-world-preview.v1',
|
|
197
|
+
renderProfile: {
|
|
198
|
+
offline: true,
|
|
199
|
+
panels: ['reference', 'terrain', 'political'],
|
|
200
|
+
width,
|
|
201
|
+
height,
|
|
202
|
+
},
|
|
203
|
+
metadata: {
|
|
204
|
+
...input.provenance.metadata,
|
|
205
|
+
...styleEvidence,
|
|
206
|
+
operationId: reference?.operationId ?? null,
|
|
207
|
+
binding: 'content-addressed offline world comparison evidence',
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
description: 'Offline side-by-side reference, generated terrain and political map; not an in-game screenshot',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
const provenance = {
|
|
214
|
+
...input.provenance,
|
|
215
|
+
kind: 'map-world-preview',
|
|
216
|
+
schemaVersion: 'map-world-preview.v1',
|
|
217
|
+
metadata: {
|
|
218
|
+
...input.provenance.metadata,
|
|
219
|
+
...styleEvidence,
|
|
220
|
+
binding: 'content-addressed offline world comparison evidence',
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
writes.push({ name: 'map-world-terrain.png', mimeType: 'image/png', content: terrainPng, provenance }, { name: 'map-world-political.png', mimeType: 'image/png', content: politicalPng, provenance }, {
|
|
224
|
+
name: 'map-world-preview.json',
|
|
225
|
+
mimeType: 'application/json',
|
|
226
|
+
content: `${canonicalJson({ schemaVersion: 1, offlineRepresentation: true, width: raster.width, height: raster.height, previewWidth: width, previewHeight: height, panelOrder: ['reference', 'terrain', 'political'], terrainLegend: terrainColors, ...styleEvidence, provinces: input.index.definitions.filter(({ id }) => id > 0).length, states: input.index.states.length, regions: input.index.regions.length, countries: [...new Set(input.index.states.flatMap(({ owner }) => (owner === undefined ? [] : [owner])))].sort(), referenceCount: input.references.length, warnings: input.diagnostics.filter(({ severity }) => severity === 'warning').map(({ code, message }) => ({ code, message })) })}\n`,
|
|
227
|
+
provenance,
|
|
228
|
+
});
|
|
229
|
+
return writes;
|
|
230
|
+
}
|