hoi4-agent-tools 2.5.3 → 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 +6 -0
- package/README.md +5 -5
- 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/schemas/map.js +69 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/map.md +60 -23
- package/package.json +1 -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/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
|
@@ -2,6 +2,7 @@ import sharp from 'sharp';
|
|
|
2
2
|
import { compareCodeUnits, canonicalJson, sha256Bytes } from '../core/canonical.js';
|
|
3
3
|
import { assertRenderDimensions, RenderBudget, RENDER_MAX_PIXELS } from '../core/render-budget.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { buildMapCatalog } from './catalog.js';
|
|
5
6
|
import { validateMapAsync } from './validation.js';
|
|
6
7
|
function stableColor(key) {
|
|
7
8
|
const hash = sha256Bytes(key);
|
|
@@ -277,163 +278,55 @@ async function applyOverlays(index, bytes, overlays, signal) {
|
|
|
277
278
|
}
|
|
278
279
|
}
|
|
279
280
|
async function renderMetadata(index, options, signal) {
|
|
280
|
-
const
|
|
281
|
-
.flatMap((state) => state.victoryPoints.map(({ provinceId, value }) => ({
|
|
282
|
-
stateId: state.id,
|
|
283
|
-
provinceId,
|
|
284
|
-
value,
|
|
285
|
-
sourcePath: state.file.displayPath,
|
|
286
|
-
})))
|
|
287
|
-
.sort((left, right) => left.stateId - right.stateId ||
|
|
288
|
-
left.provinceId - right.provinceId ||
|
|
289
|
-
left.value - right.value);
|
|
290
|
-
const normalAdjacencies = [...(index.raster?.adjacency ?? [])]
|
|
291
|
-
.flatMap(([from, neighbors]) => [...neighbors].flatMap((to) => (from < to ? [{ from, to }] : [])))
|
|
292
|
-
.sort((left, right) => left.from - right.from || left.to - right.to);
|
|
281
|
+
const catalog = buildMapCatalog(index);
|
|
293
282
|
return {
|
|
294
|
-
|
|
295
|
-
renderer: 'hoi4-agent-tools-
|
|
283
|
+
...catalog,
|
|
284
|
+
renderer: 'hoi4-agent-tools-map-workbench',
|
|
296
285
|
layer: options.layer,
|
|
297
286
|
overlays: options.overlays,
|
|
298
287
|
scale: options.scale,
|
|
299
288
|
width: index.raster?.width ?? 0,
|
|
300
289
|
height: index.raster?.height ?? 0,
|
|
301
|
-
definitions:
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
type,
|
|
305
|
-
coastal,
|
|
306
|
-
terrain,
|
|
307
|
-
continent,
|
|
308
|
-
})),
|
|
309
|
-
states: [...index.states]
|
|
310
|
-
.sort((left, right) => left.id - right.id)
|
|
311
|
-
.map(({ id, name, capital, provinces, resources, stateBuildings, provinceBuildings, owner, controller, cores, claims, }) => ({
|
|
312
|
-
id,
|
|
313
|
-
name,
|
|
314
|
-
capital: capital ?? null,
|
|
315
|
-
manpower: index.statesById.get(id)?.manpower ?? 0,
|
|
316
|
-
category: index.statesById.get(id)?.category ?? '',
|
|
317
|
-
provinces: [...provinces].sort((a, b) => a - b),
|
|
318
|
-
resources: Object.fromEntries([...resources].sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
319
|
-
stateBuildings: Object.fromEntries([...stateBuildings].sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
320
|
-
provinceBuildings: Object.fromEntries([...provinceBuildings]
|
|
321
|
-
.sort(([left], [right]) => left - right)
|
|
322
|
-
.map(([provinceId, buildings]) => [
|
|
323
|
-
String(provinceId),
|
|
324
|
-
Object.fromEntries([...buildings].sort(([left], [right]) => compareCodeUnits(left, right))),
|
|
325
|
-
])),
|
|
326
|
-
owner: owner ?? null,
|
|
327
|
-
controller: controller ?? null,
|
|
328
|
-
cores: [...cores].sort(),
|
|
329
|
-
claims: [...claims].sort(),
|
|
330
|
-
})),
|
|
331
|
-
regions: [...index.regions]
|
|
332
|
-
.sort((left, right) => left.id - right.id)
|
|
333
|
-
.map(({ id, name, provinces }) => ({
|
|
334
|
-
id,
|
|
335
|
-
name,
|
|
336
|
-
provinces: [...provinces].sort((a, b) => a - b),
|
|
337
|
-
})),
|
|
338
|
-
victoryPoints,
|
|
339
|
-
ports: [...index.ports]
|
|
340
|
-
.sort((left, right) => left.stateId - right.stateId || left.provinceId - right.provinceId)
|
|
341
|
-
.map(({ stateId, provinceId, level, coastal, adjacentSeaProvinceIds, positions }) => ({
|
|
342
|
-
stateId,
|
|
343
|
-
provinceId,
|
|
344
|
-
level,
|
|
345
|
-
coastal,
|
|
346
|
-
adjacentSeaProvinceIds: [...adjacentSeaProvinceIds].sort((left, right) => left - right),
|
|
347
|
-
positions: positions.map(({ building, x, y, z, rotation, adjacentSeaProvince, document, line }) => ({
|
|
348
|
-
building,
|
|
349
|
-
x,
|
|
350
|
-
y,
|
|
351
|
-
z,
|
|
352
|
-
rotation,
|
|
353
|
-
adjacentSeaProvince,
|
|
354
|
-
sourcePath: document.file.displayPath,
|
|
355
|
-
line,
|
|
356
|
-
})),
|
|
357
|
-
})),
|
|
358
|
-
supplyNodes: index.supplyNodes.map(({ level, provinceId, document, line }) => ({
|
|
359
|
-
level,
|
|
360
|
-
provinceId,
|
|
361
|
-
sourcePath: document.file.displayPath,
|
|
362
|
-
line,
|
|
363
|
-
})),
|
|
364
|
-
railways: index.railways.map(({ level, declaredCount, provinces, document, line }, ordinal) => ({
|
|
365
|
-
ordinal,
|
|
366
|
-
level,
|
|
367
|
-
declaredCount,
|
|
368
|
-
provinces: [...provinces],
|
|
369
|
-
sourcePath: document.file.displayPath,
|
|
370
|
-
line,
|
|
371
|
-
})),
|
|
372
|
-
adjacencies: index.adjacencies.map(({ from, to, type, through, startX, startY, stopX, stopY, rule, comment, document, line }, ordinal) => ({
|
|
373
|
-
ordinal,
|
|
374
|
-
from,
|
|
375
|
-
to,
|
|
376
|
-
type,
|
|
377
|
-
through,
|
|
378
|
-
startX,
|
|
379
|
-
startY,
|
|
380
|
-
stopX,
|
|
381
|
-
stopY,
|
|
382
|
-
rule,
|
|
383
|
-
comment,
|
|
384
|
-
sourcePath: document.file.displayPath,
|
|
385
|
-
line,
|
|
386
|
-
})),
|
|
387
|
-
normalAdjacencies,
|
|
388
|
-
buildingPositions: index.buildingPositions.map(({ stateId, building, x, y, z, rotation, adjacentSeaProvince, document, line }) => ({
|
|
389
|
-
stateId,
|
|
390
|
-
building,
|
|
391
|
-
x,
|
|
392
|
-
y,
|
|
393
|
-
z,
|
|
394
|
-
rotation,
|
|
395
|
-
adjacentSeaProvince,
|
|
396
|
-
sourcePath: document.file.displayPath,
|
|
397
|
-
line,
|
|
398
|
-
})),
|
|
399
|
-
unitPositions: index.unitPositions.map(({ provinceId, type, x, y, z, rotation, offset, document, line }) => ({
|
|
400
|
-
provinceId,
|
|
401
|
-
type,
|
|
402
|
-
x,
|
|
403
|
-
y,
|
|
404
|
-
z,
|
|
405
|
-
rotation,
|
|
406
|
-
offset,
|
|
407
|
-
sourcePath: document.file.displayPath,
|
|
408
|
-
line,
|
|
409
|
-
})),
|
|
410
|
-
weatherPositions: index.weatherPositions.map(({ strategicRegionId, x, y, z, size, document, line }) => ({
|
|
411
|
-
strategicRegionId,
|
|
412
|
-
x,
|
|
413
|
-
y,
|
|
414
|
-
z,
|
|
415
|
-
size,
|
|
416
|
-
sourcePath: document.file.displayPath,
|
|
417
|
-
line,
|
|
418
|
-
})),
|
|
419
|
-
entityLocators: [...index.entityLocators]
|
|
420
|
-
.sort((left, right) => compareCodeUnits(left.entity, right.entity) || compareCodeUnits(left.name, right.name))
|
|
421
|
-
.map(({ entity, name, position, file }) => ({
|
|
422
|
-
entity,
|
|
423
|
-
name,
|
|
424
|
-
position: [...position],
|
|
425
|
-
sourcePath: file.displayPath,
|
|
426
|
-
})),
|
|
290
|
+
definitions: catalog.provinces.map(({ id, definition }) => ({ id, ...definition })),
|
|
291
|
+
regions: catalog.strategicRegions,
|
|
292
|
+
adjacencies: catalog.specialAdjacencies,
|
|
427
293
|
validation: await validateMapAsync(index, signal === undefined ? {} : { signal }),
|
|
428
294
|
};
|
|
429
295
|
}
|
|
430
|
-
function htmlDocument(title, png, json) {
|
|
296
|
+
function htmlDocument(title, png, provinceHitMap, json, renderScale) {
|
|
297
|
+
const escapedTitle = title
|
|
298
|
+
.replaceAll('&', '&')
|
|
299
|
+
.replaceAll('<', '<')
|
|
300
|
+
.replaceAll('>', '>');
|
|
301
|
+
const scriptJson = json.replaceAll('<', '\\u003c');
|
|
302
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:0;height:100vh;display:grid;grid-template-columns:minmax(0,1fr) 360px;overflow:hidden}.main{min-width:0;padding:14px;display:flex;flex-direction:column}.toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:10px}button,input,select{background:#202b38;color:#edf2f7;border:1px solid #526173;border-radius:4px;padding:6px 9px}input{min-width:260px;flex:1}.viewport{flex:1;min-height:0;overflow:hidden;border:1px solid #48515c;background:#080b0f;position:relative;touch-action:none}.viewport img{image-rendering:pixelated;transform-origin:0 0;position:absolute;max-width:none;cursor:grab;user-select:none}.viewport img.dragging{cursor:grabbing}.cursor{position:absolute;left:8px;bottom:8px;background:#10151bd9;padding:4px 7px;border-radius:3px;pointer-events:none}.side{border-left:1px solid #48515c;padding:14px;overflow:auto;background:#151d26}.results{display:grid;gap:4px;margin:8px 0 14px}.result{text-align:left;width:100%}.kind{color:#8fb7d9}.muted{color:#9ca9b5}pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px}h1{font-size:18px;margin:0 12px 0 0}h2{font-size:15px}</style></head><body><main class="main"><div class="toolbar"><h1>${escapedTitle}</h1><button id="out" type="button">-</button><button id="fit" type="button">Fit</button><button id="in" type="button">+</button><output id="zoom">100%</output><select id="kind"><option value="all">All IDs</option><option value="province">Provinces</option><option value="state">States</option><option value="strategic-region">Strategic regions</option></select><input id="search" type="search" placeholder="Find ID, localisation key, or name" autocomplete="off"></div><div class="viewport" id="viewport"><img id="map" draggable="false" alt="${escapedTitle}" src="data:image/png;base64,${png.toString('base64')}"><div class="cursor" id="cursor">Move over the map to inspect coordinates; click for IDs.</div></div></main><aside class="side"><h2>Search results</h2><div class="results" id="results"><span class="muted">Search by ID or name.</span></div><h2>Selection</h2><pre id="selection">Click a province or choose a search result.</pre><p class="muted">The complete source-linked catalog is embedded for navigation and is also available as the linked JSON resource.</p></aside><img id="hit" hidden src="data:image/png;base64,${provinceHitMap.toString('base64')}"><canvas id="hitCanvas" hidden></canvas><script>(()=>{const data=${scriptJson};const renderScale=${renderScale};const image=document.getElementById('map'),hit=document.getElementById('hit'),canvas=document.getElementById('hitCanvas'),viewport=document.getElementById('viewport'),zoomLabel=document.getElementById('zoom'),cursor=document.getElementById('cursor'),search=document.getElementById('search'),kind=document.getElementById('kind'),results=document.getElementById('results'),selection=document.getElementById('selection');const provinces=new Map(data.provinces.map(value=>[value.id,value])),states=new Map(data.states.map(value=>[value.id,value])),regions=new Map(data.strategicRegions.map(value=>[value.id,value]));let scale=1,x=0,y=0,drag=false,moved=false,lastX=0,lastY=0,hitContext=null;const draw=()=>{image.style.transform='translate('+x+'px,'+y+'px) scale('+scale+')';zoomLabel.value=Math.round(scale*100)+'%'};const fit=()=>{scale=Math.min(viewport.clientWidth/image.naturalWidth,viewport.clientHeight/image.naturalHeight);x=(viewport.clientWidth-image.naturalWidth*scale)/2;y=(viewport.clientHeight-image.naturalHeight*scale)/2;draw()};const zoom=(factor,cx=viewport.clientWidth/2,cy=viewport.clientHeight/2)=>{const next=Math.min(32,Math.max(.03,scale*factor));x=cx-(cx-x)*(next/scale);y=cy-(cy-y)*(next/scale);scale=next;draw()};const entity=(entityKind,id)=>entityKind==='province'?provinces.get(id):entityKind==='state'?states.get(id):regions.get(id);const linked=(province)=>({province,states:province.stateIds.map(id=>states.get(id)),strategicRegions:province.regionIds.map(id=>regions.get(id))});const show=(entityKind,id)=>{const value=entity(entityKind,id);selection.textContent=value?JSON.stringify(entityKind==='province'?linked(value):value,null,2):'No matching record.';if(value?.geometry){const g=value.geometry;x=viewport.clientWidth/2-g.centerX*renderScale*scale;y=viewport.clientHeight/2-g.centerY*renderScale*scale;draw()}};const searchable=[...data.provinces,...data.states,...data.strategicRegions].map(value=>({...value,search:[String(value.id),value.name.key,value.name.displayName,...value.name.values.map(item=>item.value)].join(' ').toLocaleLowerCase('en-US')}));const message=(text)=>Object.assign(document.createElement('span'),{className:'muted',textContent:text});const updateSearch=()=>{const query=search.value.trim().toLocaleLowerCase('en-US'),filter=kind.value;if(!query){results.replaceChildren(message('Search by ID or name.'));return}const matches=searchable.filter(value=>(filter==='all'||value.kind===filter)&&value.search.includes(query)).slice(0,100);results.replaceChildren(...matches.map(value=>{const button=document.createElement('button'),label=document.createElement('span');button.type='button';button.className='result';label.className='kind';label.textContent=value.kind;button.replaceChildren(label,document.createTextNode(' '+value.id+' — '+value.name.displayName));button.onclick=()=>show(value.kind,value.id);return button}),...(matches.length?[]:[message('No matches.')]))};const point=(event)=>{const bounds=viewport.getBoundingClientRect();const imageX=(event.clientX-bounds.left-x)/scale/renderScale,imageY=(event.clientY-bounds.top-y)/scale/renderScale;return{x:Math.floor(imageX),y:Math.floor(imageY)}};const provinceAt=(point)=>{if(!hitContext||point.x<0||point.y<0||point.x>=canvas.width||point.y>=canvas.height)return null;const pixel=hitContext.getImageData(point.x,point.y,1,1).data;const encoded=(pixel[0]<<16)|(pixel[1]<<8)|pixel[2];return encoded===0?null:encoded-1};hit.onload=()=>{canvas.width=hit.naturalWidth;canvas.height=hit.naturalHeight;hitContext=canvas.getContext('2d',{willReadFrequently:true});hitContext.drawImage(hit,0,0)};search.oninput=updateSearch;kind.onchange=updateSearch;document.getElementById('in').onclick=()=>zoom(1.25);document.getElementById('out').onclick=()=>zoom(.8);document.getElementById('fit').onclick=fit;viewport.addEventListener('wheel',event=>{event.preventDefault();const bounds=viewport.getBoundingClientRect();zoom(event.deltaY<0?1.15:.87,event.clientX-bounds.left,event.clientY-bounds.top)},{passive:false});image.addEventListener('pointerdown',event=>{drag=true;moved=false;lastX=event.clientX;lastY=event.clientY;image.setPointerCapture(event.pointerId);image.classList.add('dragging')});image.addEventListener('pointermove',event=>{const p=point(event),provinceId=provinceAt(p),province=provinceId===null?null:provinces.get(provinceId);cursor.textContent='pixel '+p.x+','+p.y+' | map '+p.x+','+(data.height-1-p.y)+(province?' | province '+province.id+' '+province.name.displayName+' | state '+province.stateIds.join(',')+' | region '+province.regionIds.join(','):'');if(!drag)return;const dx=event.clientX-lastX,dy=event.clientY-lastY;if(Math.abs(dx)+Math.abs(dy)>2)moved=true;x+=dx;y+=dy;lastX=event.clientX;lastY=event.clientY;draw()});image.addEventListener('pointerup',event=>{drag=false;image.classList.remove('dragging');if(!moved){const provinceId=provinceAt(point(event));if(provinceId!==null)show('province',provinceId)}});image.onload=fit;window.addEventListener('resize',fit);draw()})();</script></body></html>`;
|
|
303
|
+
}
|
|
304
|
+
function diffHtmlDocument(title, png, json) {
|
|
431
305
|
const escapedTitle = title
|
|
432
306
|
.replaceAll('&', '&')
|
|
433
307
|
.replaceAll('<', '<')
|
|
434
308
|
.replaceAll('>', '>');
|
|
435
309
|
const escapedJson = json.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
436
|
-
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.
|
|
310
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapedTitle}</title><style>html{background:#10151b;color:#edf2f7;font:14px system-ui}body{margin:20px}.viewport{height:70vh;overflow:auto;border:1px solid #48515c;background:#080b0f}.viewport img{image-rendering:pixelated;max-width:none}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><h1>${escapedTitle}</h1><div class="viewport"><img alt="${escapedTitle}" src="data:image/png;base64,${png.toString('base64')}"></div><details><summary>Diff metadata</summary><pre>${escapedJson}</pre></details></body></html>`;
|
|
311
|
+
}
|
|
312
|
+
async function encodeProvinceHitMap(index) {
|
|
313
|
+
const raster = index.raster;
|
|
314
|
+
if (raster === undefined)
|
|
315
|
+
throw new ServiceError('MAP_RENDER_RASTER_MISSING', 'Cannot build a province hit map without a valid province raster');
|
|
316
|
+
const raw = Buffer.alloc(raster.width * raster.height * 3);
|
|
317
|
+
for (let offset = 0; offset < raster.provinceIds.length; offset += 1) {
|
|
318
|
+
const encoded = (raster.provinceIds[offset] ?? -1) + 1;
|
|
319
|
+
const target = offset * 3;
|
|
320
|
+
raw[target] = (encoded >> 16) & 255;
|
|
321
|
+
raw[target + 1] = (encoded >> 8) & 255;
|
|
322
|
+
raw[target + 2] = encoded & 255;
|
|
323
|
+
}
|
|
324
|
+
return sharp(raw, {
|
|
325
|
+
raw: { width: raster.width, height: raster.height, channels: 3 },
|
|
326
|
+
limitInputPixels: RENDER_MAX_PIXELS,
|
|
327
|
+
})
|
|
328
|
+
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
329
|
+
.toBuffer();
|
|
437
330
|
}
|
|
438
331
|
async function encodePng(raw, width, height, scale) {
|
|
439
332
|
assertRenderDimensions(width, height, 'map Sharp source raster');
|
|
@@ -481,7 +374,8 @@ export async function renderMap(index, options = {}) {
|
|
|
481
374
|
const png = await encodePng(raw, raster.width, raster.height, resolved.scale);
|
|
482
375
|
options.signal?.throwIfAborted();
|
|
483
376
|
const json = `${canonicalJson(await renderMetadata(index, resolved, options.signal))}\n`;
|
|
484
|
-
const
|
|
377
|
+
const provinceHitMap = await encodeProvinceHitMap(index);
|
|
378
|
+
const html = htmlDocument(`HOI4 map - ${resolved.layer}`, png, provinceHitMap, json, resolved.scale);
|
|
485
379
|
return {
|
|
486
380
|
width: outputDimensions.width,
|
|
487
381
|
height: outputDimensions.height,
|
|
@@ -734,15 +628,14 @@ export async function renderMapDiff(before, after, options = {}) {
|
|
|
734
628
|
const semantic = semanticMapDiff(before, after);
|
|
735
629
|
const changedBounds = left.diffBounds(right);
|
|
736
630
|
const metadata = {
|
|
737
|
-
|
|
738
|
-
renderer: 'hoi4-agent-tools-agent-nudger-diff',
|
|
631
|
+
renderer: 'hoi4-agent-tools-map-diff',
|
|
739
632
|
changedBounds: changedBounds ?? null,
|
|
740
633
|
changedProvinceIds: [...changedProvinceIds].sort((a, b) => a - b),
|
|
741
634
|
semantic,
|
|
742
635
|
...(options.review === undefined ? {} : { review: options.review }),
|
|
743
636
|
};
|
|
744
637
|
const json = `${canonicalJson(metadata)}\n`;
|
|
745
|
-
const html =
|
|
638
|
+
const html = diffHtmlDocument('HOI4 map pixel and semantic diff', png, json);
|
|
746
639
|
return {
|
|
747
640
|
width: outputDimensions.width,
|
|
748
641
|
height: outputDimensions.height,
|
|
@@ -2,7 +2,7 @@ import { z } from 'zod/v4';
|
|
|
2
2
|
import { boundedSourceHashEvidence, publicArtifactLink, } from '../../core/artifacts.js';
|
|
3
3
|
import { canonicalJson, hashCanonical } from '../../core/canonical.js';
|
|
4
4
|
import { emptyServiceResult } from '../../core/result.js';
|
|
5
|
-
import { AgentNudger, allocateMapIdentifiersAsync, exportProvinceGeometryRowRuns, MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT, } from '../../map/index.js';
|
|
5
|
+
import { AgentNudger, allocateMapIdentifiersAsync, buildMapCatalog, exportProvinceGeometryRowRuns, lookupMapCoordinate, MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT, renderMap, searchMapCatalog, } from '../../map/index.js';
|
|
6
6
|
import { workspaceIdSchema } from '../../schemas/common.js';
|
|
7
7
|
import { mapAllocationRequestSchema, mapOperationSchema } from '../../schemas/map.js';
|
|
8
8
|
import { PACKAGE_VERSION } from '../../version.js';
|
|
@@ -78,6 +78,9 @@ const mapWorkspaceInspectOutputSchema = strictOperationResultSchema(z
|
|
|
78
78
|
inspectedStateCount: nonNegativeIntegerSchema,
|
|
79
79
|
inspectedRegionCount: nonNegativeIntegerSchema,
|
|
80
80
|
allocationCount: nonNegativeIntegerSchema,
|
|
81
|
+
queryMatchCount: nonNegativeIntegerSchema,
|
|
82
|
+
coordinateMatchCount: nonNegativeIntegerSchema,
|
|
83
|
+
overviewRendered: z.boolean(),
|
|
81
84
|
provinceGeometryCount: nonNegativeIntegerSchema,
|
|
82
85
|
provinceGeometryPixelCount: nonNegativeIntegerSchema,
|
|
83
86
|
provinceGeometryRowRunCount: nonNegativeIntegerSchema,
|
|
@@ -348,7 +351,7 @@ export function registerMapTools(server, engine, context) {
|
|
|
348
351
|
const nudger = new AgentNudger(engine);
|
|
349
352
|
server.registerTool('hoi4.map_inspect', {
|
|
350
353
|
title: 'Inspect HOI4 map',
|
|
351
|
-
description: '
|
|
354
|
+
description: 'Render, validate, search, and click the complete map.',
|
|
352
355
|
inputSchema: z
|
|
353
356
|
.object({
|
|
354
357
|
workspaceId: workspaceIdSchema,
|
|
@@ -356,21 +359,47 @@ export function registerMapTools(server, engine, context) {
|
|
|
356
359
|
.array(z.number().int().min(0))
|
|
357
360
|
.max(MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT)
|
|
358
361
|
.default([])
|
|
359
|
-
.describe(`
|
|
362
|
+
.describe(`Export row runs for up to ${MAP_PROVINCE_GEOMETRY_SELECTOR_LIMIT} province IDs`),
|
|
360
363
|
stateIds: z.array(z.number().int().positive()).max(1_000).default([]),
|
|
361
364
|
regionIds: z.array(z.number().int().positive()).max(1_000).default([]),
|
|
365
|
+
query: z.string().max(256).optional(),
|
|
366
|
+
queryLimit: z.number().int().min(1).max(1_000).default(100),
|
|
367
|
+
coordinates: z
|
|
368
|
+
.array(z.discriminatedUnion('kind', [
|
|
369
|
+
z
|
|
370
|
+
.object({
|
|
371
|
+
kind: z.literal('pixel'),
|
|
372
|
+
x: z.number().int().min(0),
|
|
373
|
+
y: z.number().int().min(0),
|
|
374
|
+
})
|
|
375
|
+
.strict(),
|
|
376
|
+
z
|
|
377
|
+
.object({
|
|
378
|
+
kind: z.literal('map'),
|
|
379
|
+
x: z.number(),
|
|
380
|
+
z: z.number(),
|
|
381
|
+
})
|
|
382
|
+
.strict(),
|
|
383
|
+
]))
|
|
384
|
+
.max(100)
|
|
385
|
+
.default([]),
|
|
386
|
+
includeOverview: z.boolean().default(true),
|
|
362
387
|
allocationRequests: z.array(mapAllocationRequestSchema).max(100).default([]),
|
|
363
388
|
})
|
|
364
389
|
.strict(),
|
|
365
390
|
outputSchema: mapWorkspaceInspectOutputSchema,
|
|
366
391
|
annotations: artifactProducing,
|
|
367
|
-
}, async ({ workspaceId: requestedWorkspaceId, provinceIds, stateIds, regionIds, allocationRequests }, extra) => {
|
|
392
|
+
}, async ({ workspaceId: requestedWorkspaceId, provinceIds, stateIds, regionIds, query, queryLimit, coordinates, includeOverview, allocationRequests, }, extra) => {
|
|
368
393
|
const workspaceId = await resolveServerWorkspaceId(engine, context, requestedWorkspaceId, extra.signal);
|
|
369
394
|
try {
|
|
370
395
|
const progress = progressReporter(extra);
|
|
371
396
|
await progress.report(0, 3, 'Inspecting and validating the map');
|
|
372
397
|
const { snapshot, validation } = await nudger.validate(workspaceId, context.principal, progress.signal);
|
|
373
398
|
const sharedRevision = snapshot.revision;
|
|
399
|
+
const catalog = buildMapCatalog(snapshot.index);
|
|
400
|
+
const queryMatches = query === undefined ? [] : searchMapCatalog(catalog, query, queryLimit);
|
|
401
|
+
const coordinateMatches = coordinates.map((coordinate) => lookupMapCoordinate(snapshot.index, coordinate));
|
|
402
|
+
const overviewRendered = includeOverview && snapshot.index.raster !== undefined;
|
|
374
403
|
const selected = selectedMapEntities(snapshot, provinceIds, stateIds, regionIds);
|
|
375
404
|
const provinceGeometry = provinceIds.length === 0
|
|
376
405
|
? undefined
|
|
@@ -387,7 +416,7 @@ export function registerMapTools(server, engine, context) {
|
|
|
387
416
|
const inspectionProvenance = {
|
|
388
417
|
kind: 'map-inspect',
|
|
389
418
|
toolVersion: PACKAGE_VERSION,
|
|
390
|
-
schemaVersion: 'map-inspect.
|
|
419
|
+
schemaVersion: 'map-inspect.v2',
|
|
391
420
|
sourceHashes: sourceEvidence.bounded.sourceHashes,
|
|
392
421
|
metadata: {
|
|
393
422
|
sharedRevision,
|
|
@@ -399,7 +428,7 @@ export function registerMapTools(server, engine, context) {
|
|
|
399
428
|
name: `map-inspect.${snapshot.revision.slice(0, 16)}.json`,
|
|
400
429
|
mimeType: 'application/json',
|
|
401
430
|
content: `${canonicalJson({
|
|
402
|
-
schemaVersion:
|
|
431
|
+
schemaVersion: 2,
|
|
403
432
|
revision: snapshot.revision,
|
|
404
433
|
sharedRevision,
|
|
405
434
|
dimensions: snapshot.index.raster === undefined
|
|
@@ -420,15 +449,59 @@ export function registerMapTools(server, engine, context) {
|
|
|
420
449
|
ports: snapshot.index.ports.length,
|
|
421
450
|
locators: snapshot.index.entityLocators.length,
|
|
422
451
|
},
|
|
452
|
+
catalog,
|
|
453
|
+
query: query ?? null,
|
|
454
|
+
queryMatches,
|
|
455
|
+
coordinateMatches,
|
|
423
456
|
selected,
|
|
424
457
|
allocationPreviews,
|
|
425
458
|
validation,
|
|
426
459
|
sourceHashes: sourceEvidence.complete,
|
|
427
460
|
})}\n`,
|
|
428
461
|
provenance: inspectionProvenance,
|
|
429
|
-
description: 'Complete map inspection, validation, selected records, and allocation previews',
|
|
462
|
+
description: 'Complete searchable map catalog, inspection, validation, selected records, and allocation previews',
|
|
430
463
|
},
|
|
431
464
|
];
|
|
465
|
+
if (overviewRendered) {
|
|
466
|
+
const overviewLayer = 'state';
|
|
467
|
+
const overviewOverlays = [
|
|
468
|
+
'coastlines',
|
|
469
|
+
'ports',
|
|
470
|
+
'victory-points',
|
|
471
|
+
'supply-nodes',
|
|
472
|
+
'railways',
|
|
473
|
+
];
|
|
474
|
+
const overview = await renderMap(snapshot.index, {
|
|
475
|
+
layer: overviewLayer,
|
|
476
|
+
overlays: overviewOverlays,
|
|
477
|
+
signal: progress.signal,
|
|
478
|
+
});
|
|
479
|
+
const overviewProvenance = {
|
|
480
|
+
kind: 'map-inspect-overview',
|
|
481
|
+
toolVersion: PACKAGE_VERSION,
|
|
482
|
+
schemaVersion: 'map-inspect-overview.v1',
|
|
483
|
+
sourceHashes: sourceEvidence.bounded.sourceHashes,
|
|
484
|
+
renderProfile: {
|
|
485
|
+
layer: overviewLayer,
|
|
486
|
+
overlays: [...new Set(overviewOverlays)].sort(),
|
|
487
|
+
scale: 1,
|
|
488
|
+
},
|
|
489
|
+
metadata: { sourceHashInventory: sourceEvidence.bounded.inventory },
|
|
490
|
+
};
|
|
491
|
+
artifactWrites.push({
|
|
492
|
+
name: `map-overview.${snapshot.revision.slice(0, 16)}.png`,
|
|
493
|
+
mimeType: 'image/png',
|
|
494
|
+
content: overview.png,
|
|
495
|
+
provenance: overviewProvenance,
|
|
496
|
+
description: 'Complete rendered map overview',
|
|
497
|
+
}, {
|
|
498
|
+
name: `map-overview.${snapshot.revision.slice(0, 16)}.html`,
|
|
499
|
+
mimeType: 'text/html',
|
|
500
|
+
content: overview.html,
|
|
501
|
+
provenance: overviewProvenance,
|
|
502
|
+
description: 'Searchable and clickable full-map navigator with exact IDs, names, coordinates, and linked records',
|
|
503
|
+
});
|
|
504
|
+
}
|
|
432
505
|
if (provinceGeometry !== undefined) {
|
|
433
506
|
const { width, height, requestedProvinceIds, unknownProvinceIds, missingGeometryProvinceIds, pixelCount, rowRunCount, provinces, } = provinceGeometry;
|
|
434
507
|
const selectorHash = hashCanonical(requestedProvinceIds).slice(0, 16);
|
|
@@ -480,6 +553,9 @@ export function registerMapTools(server, engine, context) {
|
|
|
480
553
|
inspectedStateCount: selected.states.length,
|
|
481
554
|
inspectedRegionCount: selected.regions.length,
|
|
482
555
|
allocationCount: allocationPreviews.length,
|
|
556
|
+
queryMatchCount: queryMatches.length,
|
|
557
|
+
coordinateMatchCount: coordinateMatches.length,
|
|
558
|
+
overviewRendered,
|
|
483
559
|
provinceGeometryCount: provinceGeometry?.provinces.length ?? 0,
|
|
484
560
|
provinceGeometryPixelCount: provinceGeometry?.pixelCount ?? 0,
|
|
485
561
|
provinceGeometryRowRunCount: provinceGeometry?.rowRunCount ?? 0,
|
|
@@ -500,7 +576,7 @@ export function registerMapTools(server, engine, context) {
|
|
|
500
576
|
});
|
|
501
577
|
server.registerTool('hoi4.map_render', {
|
|
502
578
|
title: 'Render map inspection artifacts',
|
|
503
|
-
description: 'Render
|
|
579
|
+
description: 'Render searchable full-map PNG, JSON, and HTML.',
|
|
504
580
|
inputSchema: z
|
|
505
581
|
.object({
|
|
506
582
|
workspaceId: workspaceIdSchema,
|
|
@@ -542,7 +618,7 @@ export function registerMapTools(server, engine, context) {
|
|
|
542
618
|
});
|
|
543
619
|
server.registerTool('hoi4.map_rewrite', {
|
|
544
620
|
title: 'Create or clean up map content',
|
|
545
|
-
description: 'Apply
|
|
621
|
+
description: 'Apply map creation, edits, and ID swaps with visual and semantic evidence.',
|
|
546
622
|
inputSchema: z
|
|
547
623
|
.object({
|
|
548
624
|
workspaceId: workspaceIdSchema,
|
|
@@ -133,6 +133,22 @@ const provinceGeometrySchema = z.discriminatedUnion('kind', [
|
|
|
133
133
|
.describe(`Exact coordinates to recolor; at most ${MAP_SELECTED_PIXEL_LIMIT} pixels per operation`),
|
|
134
134
|
})
|
|
135
135
|
.strict(),
|
|
136
|
+
z
|
|
137
|
+
.object({
|
|
138
|
+
kind: z.literal('rectangle'),
|
|
139
|
+
origin: pointSchema,
|
|
140
|
+
width: z.number().int().positive().max(RENDER_MAX_DIMENSION),
|
|
141
|
+
height: z.number().int().positive().max(RENDER_MAX_DIMENSION),
|
|
142
|
+
})
|
|
143
|
+
.strict()
|
|
144
|
+
.superRefine((value, context) => {
|
|
145
|
+
if (value.width * value.height > MAP_SELECTED_PIXEL_LIMIT)
|
|
146
|
+
context.addIssue({
|
|
147
|
+
code: 'custom',
|
|
148
|
+
path: ['width'],
|
|
149
|
+
message: `Rectangle selects more than ${MAP_SELECTED_PIXEL_LIMIT} pixels`,
|
|
150
|
+
});
|
|
151
|
+
}),
|
|
136
152
|
z
|
|
137
153
|
.object({
|
|
138
154
|
kind: z.literal('mask'),
|
|
@@ -262,6 +278,24 @@ const splitStateOperation = (kind) => z
|
|
|
262
278
|
distribution: splitStateDistributionSchema,
|
|
263
279
|
})
|
|
264
280
|
.strict();
|
|
281
|
+
const createStateOperation = z
|
|
282
|
+
.object({
|
|
283
|
+
...operationBase,
|
|
284
|
+
kind: z.literal('create_state'),
|
|
285
|
+
sourceStateId: stateId.optional(),
|
|
286
|
+
stateId: stateId.optional(),
|
|
287
|
+
provinceIds: z.array(provinceId).min(1).max(100_000),
|
|
288
|
+
name: z.string().min(1).max(1000).optional(),
|
|
289
|
+
displayName: z.string().min(1).max(100_000).optional(),
|
|
290
|
+
fileName: z.string().max(255).optional(),
|
|
291
|
+
localisation: stateLocalisationSchema.optional(),
|
|
292
|
+
distribution: z.union([z.literal('proportional'), splitStateDistributionSchema]).optional(),
|
|
293
|
+
})
|
|
294
|
+
.strict()
|
|
295
|
+
.refine((value) => value.name !== undefined || value.displayName !== undefined, {
|
|
296
|
+
message: 'create_state requires name or displayName',
|
|
297
|
+
path: ['displayName'],
|
|
298
|
+
});
|
|
265
299
|
const splitProvinceOperation = (kind) => z
|
|
266
300
|
.object({
|
|
267
301
|
...operationBase,
|
|
@@ -273,6 +307,19 @@ const splitProvinceOperation = (kind) => z
|
|
|
273
307
|
distribution: splitProvinceDistributionSchema,
|
|
274
308
|
})
|
|
275
309
|
.strict();
|
|
310
|
+
const createProvinceOperation = z
|
|
311
|
+
.object({
|
|
312
|
+
...operationBase,
|
|
313
|
+
kind: z.literal('create_province'),
|
|
314
|
+
sourceProvinceId: provinceId,
|
|
315
|
+
provinceId: provinceId.optional(),
|
|
316
|
+
geometry: provinceGeometrySchema,
|
|
317
|
+
definition: provinceDefinitionSchema.optional(),
|
|
318
|
+
distribution: z
|
|
319
|
+
.union([z.literal('inherit-source'), splitProvinceDistributionSchema])
|
|
320
|
+
.optional(),
|
|
321
|
+
})
|
|
322
|
+
.strict();
|
|
276
323
|
const occurrenceSchema = z.number().int().min(0).optional();
|
|
277
324
|
export const mapOperationSchema = z.discriminatedUnion('kind', [
|
|
278
325
|
z
|
|
@@ -354,7 +401,7 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
|
|
|
354
401
|
})
|
|
355
402
|
.strict(),
|
|
356
403
|
splitStateOperation('split_state'),
|
|
357
|
-
|
|
404
|
+
createStateOperation,
|
|
358
405
|
z
|
|
359
406
|
.object({
|
|
360
407
|
...operationBase,
|
|
@@ -365,7 +412,7 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
|
|
|
365
412
|
})
|
|
366
413
|
.strict(),
|
|
367
414
|
splitProvinceOperation('split_province'),
|
|
368
|
-
|
|
415
|
+
createProvinceOperation,
|
|
369
416
|
z
|
|
370
417
|
.object({
|
|
371
418
|
...operationBase,
|
|
@@ -405,6 +452,26 @@ export const mapOperationSchema = z.discriminatedUnion('kind', [
|
|
|
405
452
|
distribution: z.literal('move-membership'),
|
|
406
453
|
})
|
|
407
454
|
.strict(),
|
|
455
|
+
z
|
|
456
|
+
.object({
|
|
457
|
+
...operationBase,
|
|
458
|
+
kind: z.literal('renumber_map_entity'),
|
|
459
|
+
entity: z.enum(['province', 'state', 'strategic-region']),
|
|
460
|
+
fromId: z.number().int().min(0),
|
|
461
|
+
toId: z.number().int().min(0),
|
|
462
|
+
collision: z.enum(['swap', 'reject']).default('swap'),
|
|
463
|
+
renameLocalisation: z.boolean().default(true),
|
|
464
|
+
})
|
|
465
|
+
.strict()
|
|
466
|
+
.superRefine((value, context) => {
|
|
467
|
+
if (value.entity !== 'province' && (value.fromId === 0 || value.toId === 0)) {
|
|
468
|
+
context.addIssue({
|
|
469
|
+
code: 'custom',
|
|
470
|
+
path: ['fromId'],
|
|
471
|
+
message: 'State and strategic-region IDs must be positive',
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}),
|
|
408
475
|
z
|
|
409
476
|
.object({
|
|
410
477
|
...operationBase,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '2.
|
|
3
|
+
export const PACKAGE_VERSION = '2.6.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|