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.
Files changed (110) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/dist/hoi4_agent_tools/core/bounded-source.js +31 -9
  3. package/dist/hoi4_agent_tools/core/configuration.js +11 -0
  4. package/dist/hoi4_agent_tools/core/operation-context.js +17 -1
  5. package/dist/hoi4_agent_tools/core/temporary-directories.js +7 -2
  6. package/dist/hoi4_agent_tools/core/transaction-limits.js +18 -4
  7. package/dist/hoi4_agent_tools/core/transactions.js +23 -4
  8. package/dist/hoi4_agent_tools/core/workspace-json.js +54 -0
  9. package/dist/hoi4_agent_tools/core/workspace.js +42 -6
  10. package/dist/hoi4_agent_tools/map/appearance-validation.js +1110 -0
  11. package/dist/hoi4_agent_tools/map/bmp.js +9 -1
  12. package/dist/hoi4_agent_tools/map/border-quality-input.js +209 -0
  13. package/dist/hoi4_agent_tools/map/border-quality.js +1180 -0
  14. package/dist/hoi4_agent_tools/map/colormap.js +128 -0
  15. package/dist/hoi4_agent_tools/map/geography/geometry.js +284 -0
  16. package/dist/hoi4_agent_tools/map/geography/index.js +6 -0
  17. package/dist/hoi4_agent_tools/map/geography/model.js +196 -0
  18. package/dist/hoi4_agent_tools/map/geography/preparation.js +120 -0
  19. package/dist/hoi4_agent_tools/map/geography/preview.js +647 -0
  20. package/dist/hoi4_agent_tools/map/geography/raster.js +253 -0
  21. package/dist/hoi4_agent_tools/map/geography/resolve.js +1467 -0
  22. package/dist/hoi4_agent_tools/map/model.js +15 -2
  23. package/dist/hoi4_agent_tools/map/operations.js +111 -8
  24. package/dist/hoi4_agent_tools/map/partition-fields.js +398 -0
  25. package/dist/hoi4_agent_tools/map/partition-geography.js +113 -0
  26. package/dist/hoi4_agent_tools/map/partition-graph.js +165 -0
  27. package/dist/hoi4_agent_tools/map/partition-groups.js +164 -0
  28. package/dist/hoi4_agent_tools/map/partition-provinces.js +727 -0
  29. package/dist/hoi4_agent_tools/map/partition-tuning.js +101 -0
  30. package/dist/hoi4_agent_tools/map/partition-watershed.js +252 -0
  31. package/dist/hoi4_agent_tools/map/partition.js +163 -0
  32. package/dist/hoi4_agent_tools/map/reference-import.js +333 -0
  33. package/dist/hoi4_agent_tools/map/reference-preparation.js +138 -5
  34. package/dist/hoi4_agent_tools/map/reference-sheets.js +231 -0
  35. package/dist/hoi4_agent_tools/map/reference-world.js +104 -13
  36. package/dist/hoi4_agent_tools/map/relief/checks.js +281 -0
  37. package/dist/hoi4_agent_tools/map/relief/fields.js +15 -0
  38. package/dist/hoi4_agent_tools/map/relief/full-resolution.js +133 -0
  39. package/dist/hoi4_agent_tools/map/relief/grid.js +190 -0
  40. package/dist/hoi4_agent_tools/map/relief/hydrology.js +1315 -0
  41. package/dist/hoi4_agent_tools/map/relief/index.js +27 -0
  42. package/dist/hoi4_agent_tools/map/relief/land.js +110 -0
  43. package/dist/hoi4_agent_tools/map/relief/macro-relief.js +535 -0
  44. package/dist/hoi4_agent_tools/map/relief/math.js +93 -0
  45. package/dist/hoi4_agent_tools/map/relief/river-raster.js +680 -0
  46. package/dist/hoi4_agent_tools/map/relief/terrain-synthesis.js +304 -0
  47. package/dist/hoi4_agent_tools/map/render.js +36 -0
  48. package/dist/hoi4_agent_tools/map/service.js +191 -16
  49. package/dist/hoi4_agent_tools/map/tool-operations.js +208 -2
  50. package/dist/hoi4_agent_tools/map/validation.js +9 -0
  51. package/dist/hoi4_agent_tools/map/water.js +227 -0
  52. package/dist/hoi4_agent_tools/map/world-context.js +24 -1
  53. package/dist/hoi4_agent_tools/map/world-edge.js +335 -0
  54. package/dist/hoi4_agent_tools/map/world-noise.js +34 -0
  55. package/dist/hoi4_agent_tools/map/world-partition.js +119 -0
  56. package/dist/hoi4_agent_tools/map/world-provinces.js +131 -56
  57. package/dist/hoi4_agent_tools/map/world.js +462 -124
  58. package/dist/hoi4_agent_tools/mcp/tools/map.js +69 -1
  59. package/dist/hoi4_agent_tools/reference/operations.js +3 -2
  60. package/dist/hoi4_agent_tools/schemas/geography.js +325 -0
  61. package/dist/hoi4_agent_tools/schemas/map-requests.js +20 -2
  62. package/dist/hoi4_agent_tools/schemas/map.js +209 -2
  63. package/dist/hoi4_agent_tools/schemas/reference-sheets.js +92 -0
  64. package/dist/hoi4_agent_tools/version.js +1 -1
  65. package/docs/configuration.md +17 -14
  66. package/docs/map.md +258 -15
  67. package/docs/tools.md +4 -4
  68. package/docs/troubleshooting.md +11 -11
  69. package/npm-shrinkwrap.json +109 -91
  70. package/package.json +3 -3
  71. package/schemas/configuration.schema.json +65 -58
  72. package/schemas/continuous-focus-palette.schema.json +1 -1
  73. package/schemas/custom-weighted-pool.schema.json +1 -1
  74. package/schemas/decision-inspect-input.schema.json +1 -1
  75. package/schemas/error-log-input.schema.json +1 -1
  76. package/schemas/event-feature-manifest.schema.json +1 -1
  77. package/schemas/focus-plan.schema.json +1 -1
  78. package/schemas/focus-planning-sidecar.schema.json +1 -1
  79. package/schemas/geography-model.schema.json +1148 -0
  80. package/schemas/gui-animation-source.schema.json +1 -1
  81. package/schemas/gui-helper.schema.json +5 -11
  82. package/schemas/gui-scenario.schema.json +1 -1
  83. package/schemas/helper-expansion-request.schema.json +1 -1
  84. package/schemas/helper-expansion-summary.schema.json +1 -1
  85. package/schemas/impact-inspect-input.schema.json +1 -1
  86. package/schemas/map-operation.schema.json +2156 -259
  87. package/schemas/mechanic-test-input.schema.json +1 -1
  88. package/schemas/mod-index-input.schema.json +1 -1
  89. package/schemas/operation-result.schema.json +1 -1
  90. package/schemas/package-check-input.schema.json +1 -1
  91. package/schemas/package-manifest.schema.json +1 -1
  92. package/schemas/probability-analysis-result.schema.json +25 -7
  93. package/schemas/probability-compare-input.schema.json +1 -1
  94. package/schemas/probability-evaluate-input.schema.json +1 -1
  95. package/schemas/probability-inspect-input.schema.json +1 -1
  96. package/schemas/probability-render-input.schema.json +1 -1
  97. package/schemas/probability-scenario-set.schema.json +1 -1
  98. package/schemas/probability-sequence-input.schema.json +1 -1
  99. package/schemas/probability-simulate-input.schema.json +1 -1
  100. package/schemas/probability-sweep-input.schema.json +1 -1
  101. package/schemas/reference-context-input.schema.json +1 -1
  102. package/schemas/reference-read-input.schema.json +1 -1
  103. package/schemas/reference-search-input.schema.json +1 -1
  104. package/schemas/reference-sheets.schema.json +267 -0
  105. package/schemas/scenario-suite.schema.json +1 -1
  106. package/schemas/scenario-test-input.schema.json +1 -1
  107. package/schemas/script-validate-input.schema.json +1 -1
  108. package/schemas/shared-scenario.schema.json +1 -1
  109. package/schemas/source-lookup-input.schema.json +1 -1
  110. package/server.json +2 -2
@@ -288,7 +288,9 @@ export function createBmp(options) {
288
288
  throw new ServiceError('MAP_BMP_PALETTE_INVALID', 'A 24-bit BMP cannot contain an indexed palette');
289
289
  }
290
290
  const pixelCount = options.width * options.height;
291
- if (options.bitsPerPixel === 24 && (options.rgbPixels?.length ?? 0) !== pixelCount) {
291
+ const packed = options.rgbBytes;
292
+ if (options.bitsPerPixel === 24 &&
293
+ (packed === undefined ? (options.rgbPixels?.length ?? 0) : packed.length / 3) !== pixelCount) {
292
294
  throw new ServiceError('MAP_BMP_PIXEL_COUNT_INVALID', 'RGB pixel count does not match BMP dimensions');
293
295
  }
294
296
  if (options.bitsPerPixel === 8 && (options.indexedPixels?.length ?? 0) !== pixelCount) {
@@ -340,6 +342,12 @@ export function createBmp(options) {
340
342
  }
341
343
  bytes[rowOffset + x] = index;
342
344
  }
345
+ else if (packed !== undefined) {
346
+ const offset = rowOffset + x * 3;
347
+ bytes[offset] = packed[logicalIndex * 3 + 2];
348
+ bytes[offset + 1] = packed[logicalIndex * 3 + 1];
349
+ bytes[offset + 2] = packed[logicalIndex * 3];
350
+ }
343
351
  else {
344
352
  const color = options.rgbPixels?.[logicalIndex];
345
353
  if (color === undefined) {
@@ -0,0 +1,209 @@
1
+ import { compareCodeUnits } from '../core/canonical.js';
2
+ import { ServiceError } from '../core/result.js';
3
+ import { assignments, childBlocks, firstScalar, parseClausewitz } from '../core/source/index.js';
4
+ import { BmpImage } from './bmp.js';
5
+ import { BORDER_PROVINCE_KIND, borderPlaneFromBmp, measureBorderNaturalness, } from './border-quality.js';
6
+ /** Terrain types whose boundaries count as natural lines when the terrain bitmap changes class. */
7
+ export const BORDER_OBSTACLE_TERRAIN_TYPES = ['mountain', 'forest', 'jungle', 'marsh'];
8
+ function normalized(value) {
9
+ return value.replaceAll('\\', '/').replace(/^\.\//u, '').toLowerCase();
10
+ }
11
+ function planeFromFile(index, name, file, skipped) {
12
+ if (file === undefined)
13
+ return undefined;
14
+ const raster = index.raster;
15
+ let image;
16
+ try {
17
+ image = BmpImage.decode(file.bytes);
18
+ }
19
+ catch {
20
+ skipped.push({ input: name, reason: 'The bitmap is malformed or unsupported' });
21
+ return undefined;
22
+ }
23
+ if (image.bitsPerPixel !== 8) {
24
+ skipped.push({ input: name, reason: 'The bitmap is not 8-bit indexed' });
25
+ return undefined;
26
+ }
27
+ if (image.width !== raster.width || image.height !== raster.height) {
28
+ skipped.push({
29
+ input: name,
30
+ reason: `The bitmap is ${image.width}x${image.height}, not the province map size`,
31
+ });
32
+ return undefined;
33
+ }
34
+ return borderPlaneFromBmp(image);
35
+ }
36
+ /**
37
+ * Class of each terrain bitmap index from the active `common/terrain` graphical entries: an
38
+ * obstacle terrain type gets its own class, water is ignored, everything else is open ground.
39
+ */
40
+ export function terrainClassTable(index) {
41
+ const classes = new Uint8Array(256);
42
+ const files = index.activeFiles.all
43
+ .filter((file) => /^common\/terrain\/[^/]+\.txt$/u.test(normalized(file.relativePath)))
44
+ .sort((a, b) => compareCodeUnits(normalized(a.relativePath), normalized(b.relativePath)));
45
+ const typeOfIndex = new Map();
46
+ for (const file of files) {
47
+ const document = parseClausewitz(file.bytes, file.displayPath);
48
+ for (const section of assignments(document.root, 'terrain')) {
49
+ if (section.value.type !== 'block')
50
+ continue;
51
+ for (const entry of assignments(section.value)) {
52
+ if (entry.value.type !== 'block')
53
+ continue;
54
+ const type = firstScalar(entry.value, 'type')?.value;
55
+ if (type === undefined)
56
+ continue;
57
+ for (const color of childBlocks(entry.value, 'color'))
58
+ for (const value of color.entries)
59
+ if (value.type === 'scalar' && /^\d+$/u.test(value.value)) {
60
+ const paletteIndex = Number(value.value);
61
+ if (paletteIndex < 256)
62
+ typeOfIndex.set(paletteIndex, type);
63
+ }
64
+ }
65
+ }
66
+ }
67
+ for (const [paletteIndex, type] of typeOfIndex) {
68
+ const obstacle = BORDER_OBSTACLE_TERRAIN_TYPES.indexOf(type);
69
+ classes[paletteIndex] =
70
+ obstacle >= 0 ? obstacle + 1 : index.terrainCategories?.get(type)?.water === true ? 255 : 0;
71
+ }
72
+ return classes;
73
+ }
74
+ function labelsFor(index, kinds, label) {
75
+ const labels = new Int32Array(kinds.length).fill(-1);
76
+ for (let id = 0; id < kinds.length; id += 1)
77
+ if (kinds[id] === BORDER_PROVINCE_KIND.land)
78
+ labels[id] = label(id);
79
+ void index;
80
+ return labels;
81
+ }
82
+ /**
83
+ * Build the border quality input from a scanned map: the province raster, province kinds, the
84
+ * province, state, strategic-region and country groupings the workspace defines, and the height,
85
+ * river and terrain bitmaps when they are present and the size of the province map.
86
+ */
87
+ export function borderQualityInputFromIndex(index) {
88
+ const raster = index.raster;
89
+ if (raster === undefined)
90
+ throw new ServiceError('MAP_BORDER_QUALITY_RASTER_MISSING', 'Border naturalness needs a valid province bitmap and definitions');
91
+ const skipped = [];
92
+ let highest = 0;
93
+ for (const definition of index.definitions)
94
+ highest = Math.max(highest, definition.id);
95
+ const kinds = new Uint8Array(highest + 1);
96
+ for (const definition of index.definitions) {
97
+ if (!Number.isInteger(definition.id) || definition.id < 0)
98
+ continue;
99
+ kinds[definition.id] =
100
+ definition.type === 'land'
101
+ ? BORDER_PROVINCE_KIND.land
102
+ : definition.type === 'sea'
103
+ ? BORDER_PROVINCE_KIND.sea
104
+ : definition.type === 'lake'
105
+ ? BORDER_PROVINCE_KIND.lake
106
+ : BORDER_PROVINCE_KIND.unknown;
107
+ }
108
+ kinds[0] = BORDER_PROVINCE_KIND.unknown;
109
+ const layers = [
110
+ { name: 'province', labels: labelsFor(index, kinds, (id) => id) },
111
+ ];
112
+ const stateLabels = labelsFor(index, kinds, (id) => {
113
+ const states = index.stateForProvince(id);
114
+ return states.length === 1 ? states[0].id : -1;
115
+ });
116
+ if (stateLabels.some((label) => label >= 0))
117
+ layers.push({ name: 'state', labels: stateLabels });
118
+ const regionLabels = labelsFor(index, kinds, (id) => {
119
+ const regions = index.regionForProvince(id);
120
+ return regions.length === 1 ? regions[0].id : -1;
121
+ });
122
+ if (regionLabels.some((label) => label >= 0))
123
+ layers.push({ name: 'region', labels: regionLabels });
124
+ const owners = [
125
+ ...new Set(index.states.flatMap(({ owner }) => (owner === undefined ? [] : [owner]))),
126
+ ].sort(compareCodeUnits);
127
+ const ownerIndex = new Map(owners.map((tag, position) => [tag, position]));
128
+ const countryLabels = labelsFor(index, kinds, (id) => {
129
+ const states = index.stateForProvince(id);
130
+ const owner = states.length === 1 ? states[0].owner : undefined;
131
+ return owner === undefined ? -1 : (ownerIndex.get(owner) ?? -1);
132
+ });
133
+ if (countryLabels.some((label) => label >= 0))
134
+ layers.push({
135
+ name: 'country',
136
+ labels: countryLabels,
137
+ describe: (label) => owners[label] ?? String(label),
138
+ });
139
+ const features = {};
140
+ const heights = planeFromFile(index, 'heightmap', index.heightmapFile, skipped);
141
+ if (heights !== undefined)
142
+ features.heights = heights;
143
+ else if (index.heightmapFile === undefined)
144
+ skipped.push({ input: 'heightmap', reason: 'No heightmap was scanned' });
145
+ const rivers = planeFromFile(index, 'rivers', index.riversFile, skipped);
146
+ if (rivers !== undefined)
147
+ features.rivers = rivers;
148
+ else if (index.riversFile === undefined)
149
+ skipped.push({ input: 'rivers', reason: 'No rivers bitmap was scanned' });
150
+ const terrainPlane = planeFromFile(index, 'terrain', index.terrainBitmapFile, skipped);
151
+ if (terrainPlane !== undefined) {
152
+ const terrain = {
153
+ plane: terrainPlane,
154
+ classes: terrainClassTable(index),
155
+ };
156
+ features.terrain = terrain;
157
+ }
158
+ else if (index.terrainBitmapFile === undefined)
159
+ skipped.push({ input: 'terrain', reason: 'No terrain bitmap was scanned' });
160
+ return {
161
+ input: {
162
+ width: raster.width,
163
+ height: raster.height,
164
+ provinceIds: raster.provinceIds,
165
+ provinceKinds: kinds,
166
+ layers,
167
+ features,
168
+ },
169
+ skipped,
170
+ };
171
+ }
172
+ /** Measure the borders of a scanned map workspace. */
173
+ export async function measureIndexBorderNaturalness(index, options = {}) {
174
+ const { input, skipped } = borderQualityInputFromIndex(index);
175
+ const result = await measureBorderNaturalness(input, options);
176
+ return { ...result, skipped };
177
+ }
178
+ export const BORDER_QUALITY_INLINE_RUNS = 3;
179
+ export function summarizeBorderQuality(report, skipped) {
180
+ return {
181
+ inputs: report.inputs,
182
+ skippedInputs: skipped,
183
+ layers: report.layers.map((layer) => ({
184
+ layer: layer.layer,
185
+ status: layer.status,
186
+ groups: layer.groups,
187
+ borderEdges: layer.borderEdges,
188
+ coastEdges: layer.coastEdges,
189
+ alignment: layer.barrier.alignment,
190
+ baseRate: layer.barrier.baseRate,
191
+ enrichment: layer.barrier.enrichment,
192
+ naturalShare: layer.natural.share,
193
+ naturalShareWithCoast: layer.natural.shareWithCoast,
194
+ straightFraction: layer.straightness.fraction,
195
+ longRunShare: layer.longRuns.share,
196
+ artificialRuns: layer.artificialRuns.count,
197
+ longestArtificialRunEdges: layer.artificialRuns.longestEdges,
198
+ worstRuns: layer.worstRuns
199
+ .slice(0, BORDER_QUALITY_INLINE_RUNS)
200
+ .map(({ sides, from, to, edges, chord }) => ({
201
+ sides: [sides[0].slice(0, 64), sides[1].slice(0, 64)],
202
+ from,
203
+ to,
204
+ edges,
205
+ chord,
206
+ })),
207
+ })),
208
+ };
209
+ }