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
|
@@ -629,11 +629,12 @@ function explicitElementVisibility(definition, scenario, rowValues) {
|
|
|
629
629
|
visibilityValue(scripted));
|
|
630
630
|
}
|
|
631
631
|
/** Source visibility: `visible` and `show` mean shown when true, `hide` and `hidden` when false. */
|
|
632
|
-
function declaredVisibility(attributes) {
|
|
632
|
+
function declaredVisibility(attributes, fallback = true) {
|
|
633
633
|
const visible = scalarBoolean(property(attributes, 'visible', 'show'));
|
|
634
634
|
if (visible !== undefined)
|
|
635
635
|
return visible;
|
|
636
|
-
|
|
636
|
+
const hidden = scalarBoolean(property(attributes, 'hide', 'hidden'));
|
|
637
|
+
return hidden === undefined ? fallback : !hidden;
|
|
637
638
|
}
|
|
638
639
|
function clippedByChain(chain) {
|
|
639
640
|
let result;
|
|
@@ -694,7 +695,7 @@ function refreshSceneClips(context) {
|
|
|
694
695
|
}
|
|
695
696
|
}
|
|
696
697
|
function attachedScrollbars(definition, context) {
|
|
697
|
-
if (!/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType))
|
|
698
|
+
if (!/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType))
|
|
698
699
|
return [];
|
|
699
700
|
const result = [];
|
|
700
701
|
const add = (candidate, horizontal, reference) => {
|
|
@@ -718,8 +719,10 @@ function attachedScrollbars(definition, context) {
|
|
|
718
719
|
for (const horizontal of [false, true]) {
|
|
719
720
|
const name = scalarString(property(definition.attributes, horizontal ? 'horizontalScrollbar' : 'verticalScrollbar'));
|
|
720
721
|
if (name !== undefined) {
|
|
721
|
-
const candidate = context.
|
|
722
|
-
|
|
722
|
+
const candidate = context.elementsByNameCandidates
|
|
723
|
+
.get(name)
|
|
724
|
+
?.find((element) => element.parentId === undefined);
|
|
725
|
+
add(candidate, horizontal, name);
|
|
723
726
|
}
|
|
724
727
|
}
|
|
725
728
|
for (const id of definition.childIds) {
|
|
@@ -1218,10 +1221,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1218
1221
|
const clipChain = inheritedClipChain ?? (inheritedClip === undefined ? [] : [inheritedClip]);
|
|
1219
1222
|
const localScale = scalarNumber(property(definition.attributes, 'scale'), 1) ?? 1;
|
|
1220
1223
|
const scale = parentScale * localScale;
|
|
1221
|
-
const
|
|
1224
|
+
const expandedWindow = definition.elementType.toLowerCase() === 'expandedwindow';
|
|
1225
|
+
const shownPosition = depth === 0 || expandedWindow
|
|
1226
|
+
? objectProperty(property(definition.attributes, 'show_position'))
|
|
1227
|
+
: undefined;
|
|
1222
1228
|
const position = shownPosition ?? objectProperty(property(definition.attributes, 'position'));
|
|
1223
1229
|
if (shownPosition !== undefined) {
|
|
1224
|
-
addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested
|
|
1230
|
+
addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested window uses its declared settled show position.', definition);
|
|
1225
1231
|
}
|
|
1226
1232
|
const size = objectProperty(property(definition.attributes, 'size'));
|
|
1227
1233
|
const scriptedX = rowValues?.[`${definition.name}.x`] ??
|
|
@@ -1277,13 +1283,19 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1277
1283
|
width = Math.max(0, parentRect.width - localX + width);
|
|
1278
1284
|
if (height < 0)
|
|
1279
1285
|
height = Math.max(0, parentRect.height - localY + height);
|
|
1280
|
-
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues)
|
|
1286
|
+
const explicitlyVisible = explicitElementVisibility(definition, scenario, rowValues) ??
|
|
1287
|
+
context.nativeVisibility.get(definition.id);
|
|
1281
1288
|
const unresolvedVisibilityReason = context.unresolvedElementVisibility.get(definition.name);
|
|
1282
1289
|
const parentSceneElement = parentInstanceId === undefined ? undefined : context.instancesById.get(parentInstanceId);
|
|
1283
1290
|
const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
|
|
1284
1291
|
? parentSceneElement.visibilityStatusReason
|
|
1285
1292
|
: undefined;
|
|
1286
|
-
const
|
|
1293
|
+
const dropdown = definition.parentId === undefined ? undefined : context.elementsById.get(definition.parentId);
|
|
1294
|
+
const sourceVisible = declaredVisibility(definition.attributes, !expandedWindow ||
|
|
1295
|
+
depth === 0 ||
|
|
1296
|
+
visibilityValue(rowValues?.[`${dropdown?.name}.expanded`] ?? scenario.values[`${dropdown?.name}.expanded`]) === true);
|
|
1297
|
+
if (expandedWindow)
|
|
1298
|
+
addFidelity(fidelity, 'modelled', 'native_dropdown', 'Dropdown expansion is closed by default; bind <dropdown>.expanded or explicit panel visibility to show its settled position.', definition);
|
|
1287
1299
|
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
1288
1300
|
const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1289
1301
|
scenario.values[`${definition.name}.enabled`] ??
|
|
@@ -1619,12 +1631,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1619
1631
|
else if (origoAxes.vertical === 'bottom')
|
|
1620
1632
|
y -= height;
|
|
1621
1633
|
const unclippedRect = { x, y, width: Math.max(0, width), height: Math.max(0, height) };
|
|
1622
|
-
const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
|
|
1623
|
-
(backgrounds.length > 0 && /(?:containerwindow|windowtype)/iu.test(definition.elementType));
|
|
1624
1634
|
let scrollbars = attachedScrollbars(definition, context);
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1635
|
+
const ownClipping = scalarBoolean(property(definition.attributes, 'clipping')) ??
|
|
1636
|
+
((backgrounds.length > 0 || scrollbars.length > 0) &&
|
|
1637
|
+
/(?:containerwindow|windowtype|expandedwindow)/iu.test(definition.elementType));
|
|
1638
|
+
if (scrollbars.length > 0 && (!ownClipping || width <= 0 || height <= 0)) {
|
|
1639
|
+
diagnostics.push(diagnostic('GUI_SCROLLBAR_CONTAINER_BOUNDARY_MISSING', 'warning', `Container ${definition.name} needs a positive-sized clipping boundary for attached scrolling.`, definition));
|
|
1640
|
+
addFidelity(fidelity, 'unsupported', 'attached_scrollbar', 'The container has no active clipping boundary.', definition);
|
|
1628
1641
|
scrollbars = [];
|
|
1629
1642
|
}
|
|
1630
1643
|
const margin = scrollbars.length === 0 ? undefined : objectProperty(property(definition.attributes, 'margin'));
|
|
@@ -1835,10 +1848,12 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1835
1848
|
const childDefinitions = await scrollbarChildren(definition, definition.childIds
|
|
1836
1849
|
.map((id) => context.elementsById.get(id))
|
|
1837
1850
|
.filter((element) => element !== undefined)
|
|
1838
|
-
.filter((element) => !/^(?:containerwindowtype|windowtype)$/iu.test(definition.elementType) ||
|
|
1851
|
+
.filter((element) => !/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(definition.elementType) ||
|
|
1839
1852
|
element.elementType.toLowerCase() !== 'extendedscrollbartype')
|
|
1840
|
-
.sort((left, right) => (
|
|
1841
|
-
(
|
|
1853
|
+
.sort((left, right) => Number(context.nativeForeground.has(left.id)) -
|
|
1854
|
+
Number(context.nativeForeground.has(right.id)) ||
|
|
1855
|
+
(scalarNumber(property(left.attributes, 'priority'), 0) ?? 0) -
|
|
1856
|
+
(scalarNumber(property(right.attributes, 'priority'), 0) ?? 0) ||
|
|
1842
1857
|
left.definitionOrder - right.definitionOrder), unclippedRect, scale, context, rowValues);
|
|
1843
1858
|
const rowList = rowValues?.[`${definition.name}.list`];
|
|
1844
1859
|
let rows = (typeof rowList === 'string' ? scenario.lists[rowList] : undefined) ??
|
|
@@ -1908,7 +1923,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1908
1923
|
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1909
1924
|
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1910
1925
|
};
|
|
1911
|
-
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId,
|
|
1926
|
+
await layoutElement(child, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1912
1927
|
const rendered = context.instancesById.get(`${child.id}${rowSuffix(index)}`);
|
|
1913
1928
|
rowHeight = Math.max(rowHeight, rendered?.unclippedRect.height ?? 0);
|
|
1914
1929
|
}
|
|
@@ -1960,7 +1975,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1960
1975
|
...(slotWidth === undefined ? {} : { width: slotWidth }),
|
|
1961
1976
|
...(slotHeight === undefined ? {} : { height: slotHeight }),
|
|
1962
1977
|
};
|
|
1963
|
-
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId,
|
|
1978
|
+
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1964
1979
|
const rendered = context.instancesById.get(`${template.id}${rowSuffix(index)}`);
|
|
1965
1980
|
rowY += Math.max(slotHeight ?? 0, rendered?.unclippedRect.height ?? 0) + spacingY;
|
|
1966
1981
|
renderedRows += 1;
|
|
@@ -1970,7 +1985,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1970
1985
|
else {
|
|
1971
1986
|
for (const child of childDefinitions) {
|
|
1972
1987
|
context.work.spend('nested child expansion');
|
|
1973
|
-
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId,
|
|
1988
|
+
await layoutElement(child, childParentRect, childClip, scale, depth + 1, context, instanceSuffix, rowIndex, rowValues, instanceId, 'upper_left', resolveVisibleAppearance, childClipChain);
|
|
1974
1989
|
}
|
|
1975
1990
|
}
|
|
1976
1991
|
const childOutput = context.output.slice(firstChildOutput);
|
|
@@ -2195,10 +2210,14 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2195
2210
|
if (native !== undefined) {
|
|
2196
2211
|
for (const note of native.notes)
|
|
2197
2212
|
addFidelity(fidelity, 'modelled', 'native_window_state', note);
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2213
|
+
for (const note of native.approximations ?? [])
|
|
2214
|
+
addFidelity(fidelity, 'approximated', 'native_window_state', note);
|
|
2215
|
+
const sources = native.state?.sources;
|
|
2216
|
+
if (native.state !== undefined &&
|
|
2217
|
+
sources !== undefined &&
|
|
2218
|
+
(native.state.tag === undefined ||
|
|
2219
|
+
sources.faction === 'default' ||
|
|
2220
|
+
sources.balanceOfPower === 'default'))
|
|
2202
2221
|
addFidelity(fidelity, 'approximated', 'native_window_state', native.state.tag === undefined
|
|
2203
2222
|
? 'The scenario names no country, so the window shows the state of a country in no faction and without a balance of power; set country.tag or the country faction, balanceOfPower and ideology facts.'
|
|
2204
2223
|
: `No history for ${native.state.tag} was available, so its faction and balance of power default to none; set country.faction, country.balanceOfPower and country.ideology to override.`);
|
|
@@ -2211,7 +2230,6 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2211
2230
|
...scenario.values,
|
|
2212
2231
|
},
|
|
2213
2232
|
visibility: {
|
|
2214
|
-
...(native?.visibility ?? {}),
|
|
2215
2233
|
...scriptedWindowVisibility,
|
|
2216
2234
|
...constantElementVisibility,
|
|
2217
2235
|
...scenario.visibility,
|
|
@@ -2229,6 +2247,24 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2229
2247
|
elementsByNameCandidates.set(element.name, candidates);
|
|
2230
2248
|
}
|
|
2231
2249
|
const context = {
|
|
2250
|
+
nativeForeground: new Set(graph.elements
|
|
2251
|
+
.filter((element) => element.parentId === root.id && native?.foreground?.includes(element.name))
|
|
2252
|
+
.map(({ id }) => id)),
|
|
2253
|
+
nativeVisibility: new Map(graph.elements.flatMap((element) => {
|
|
2254
|
+
const visible = native?.visibility[element.name];
|
|
2255
|
+
if (visible === undefined)
|
|
2256
|
+
return [];
|
|
2257
|
+
// The designer reuses names (including division_title) inside selector panels.
|
|
2258
|
+
// Its branch decisions apply to root controls, not identically named descendants.
|
|
2259
|
+
if (windowName === 'countrydivisiondesignerview' && element.parentId !== root.id) {
|
|
2260
|
+
const parent = element.parentId === undefined ? undefined : elementsById.get(element.parentId);
|
|
2261
|
+
if (element.name !== 'hq_badge' ||
|
|
2262
|
+
parent?.name !== 'template_emblem_slot' ||
|
|
2263
|
+
parent.parentId !== root.id)
|
|
2264
|
+
return [];
|
|
2265
|
+
}
|
|
2266
|
+
return [[element.id, visible]];
|
|
2267
|
+
})),
|
|
2232
2268
|
graph,
|
|
2233
2269
|
scenario: layoutScenario,
|
|
2234
2270
|
catalog,
|
|
@@ -205,13 +205,52 @@ export function resolveNativeCountryState(scenario, files) {
|
|
|
205
205
|
}
|
|
206
206
|
/** Whether any native decision can apply to this window, so the country state is worth resolving. */
|
|
207
207
|
export function windowHasNativeState(windowName, elementNames) {
|
|
208
|
-
return (
|
|
208
|
+
return (windowName === 'countrydivisiondesignerview' ||
|
|
209
|
+
NATIVE_BRANCHES[windowName] !== undefined ||
|
|
209
210
|
Object.keys(IDEOLOGY_FRAME_ICONS).some((name) => elementNames.has(name)));
|
|
210
211
|
}
|
|
211
212
|
/** The engine-selected branches and frames for a scene, below any explicit scenario input. */
|
|
212
213
|
export function nativeWindowState(windowName, elementNames, scenario, files) {
|
|
213
214
|
if (!windowHasNativeState(windowName, elementNames))
|
|
214
215
|
return undefined;
|
|
216
|
+
if (windowName === 'countrydivisiondesignerview') {
|
|
217
|
+
const hq = scenario.values['countrydivisiondesignerview.hq'] === true;
|
|
218
|
+
const panel = scenario.values['countrydivisiondesignerview.panel'];
|
|
219
|
+
const visibility = {};
|
|
220
|
+
for (const name of ['hq_overlay_images', 'subunits_hq', 'hq_view', 'hq_title', 'hq_badge'])
|
|
221
|
+
if (elementNames.has(name))
|
|
222
|
+
visibility[name] = hq;
|
|
223
|
+
for (const name of [
|
|
224
|
+
'subunits',
|
|
225
|
+
'non_hq_view',
|
|
226
|
+
'division_title',
|
|
227
|
+
'division_composition_label',
|
|
228
|
+
'combat_width_view',
|
|
229
|
+
])
|
|
230
|
+
if (elementNames.has(name))
|
|
231
|
+
visibility[name] = !hq;
|
|
232
|
+
const selectors = [
|
|
233
|
+
'designerregimentswindow',
|
|
234
|
+
'division_equipment_view',
|
|
235
|
+
'div_templ_custom_icon_view',
|
|
236
|
+
'div_template_select_model',
|
|
237
|
+
'niche_icon_selector',
|
|
238
|
+
];
|
|
239
|
+
for (const name of selectors)
|
|
240
|
+
if (elementNames.has(name))
|
|
241
|
+
visibility[name] = panel === name;
|
|
242
|
+
return {
|
|
243
|
+
visibility,
|
|
244
|
+
values: {},
|
|
245
|
+
foreground: selectors,
|
|
246
|
+
notes: [
|
|
247
|
+
`Division designer uses the ${hq ? 'HQ' : 'division'} branch and only the explicitly selected native selector panel.`,
|
|
248
|
+
],
|
|
249
|
+
approximations: [
|
|
250
|
+
'Native division state is a preview convention: defaults to a division with selectors closed. Set values countrydivisiondesignerview.hq and countrydivisiondesignerview.panel, or explicit element visibility. Engine-populated rows and statistics require scenario bindings.',
|
|
251
|
+
],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
215
254
|
const state = resolveNativeCountryState(scenario, files);
|
|
216
255
|
const visibility = {};
|
|
217
256
|
const values = {};
|
|
@@ -119,7 +119,7 @@ export function guiElementAttributeFidelity(key, elementType) {
|
|
|
119
119
|
if (elementType?.toLowerCase() === 'extendedscrollbartype' &&
|
|
120
120
|
['horizontal', 'slider', 'track', 'decreaseButton', 'increaseButton'].some((candidate) => candidate.toLowerCase() === key.toLowerCase()))
|
|
121
121
|
return 'modelled';
|
|
122
|
-
if (/^(?:containerwindowtype|windowtype)$/iu.test(elementType ?? '') &&
|
|
122
|
+
if (/^(?:containerwindowtype|windowtype|expandedwindow)$/iu.test(elementType ?? '') &&
|
|
123
123
|
['verticalScrollbar', 'horizontalScrollbar', 'autohide_scrollbars', 'margin'].some((candidate) => candidate.toLowerCase() === key.toLowerCase()))
|
|
124
124
|
return 'structural';
|
|
125
125
|
if (elementType?.toLowerCase() === 'scrollbartype' &&
|
|
@@ -298,9 +298,12 @@ function nestedSizeFrom(block, key) {
|
|
|
298
298
|
function raw(document, node) {
|
|
299
299
|
return document.text.slice(node.start, node.end);
|
|
300
300
|
}
|
|
301
|
-
function isGuiElement(assignment) {
|
|
301
|
+
function isGuiElement(assignment, parentType) {
|
|
302
302
|
if (assignment.value.type !== 'block')
|
|
303
303
|
return false;
|
|
304
|
+
if (parentType?.toLowerCase() === 'dropdownboxtype' &&
|
|
305
|
+
/^(?:expandedwindow|expandbutton)$/iu.test(assignment.key.value))
|
|
306
|
+
return true;
|
|
304
307
|
if (explicitGuiElementTypes.has(assignment.key.value.toLowerCase()))
|
|
305
308
|
return true;
|
|
306
309
|
return (/type$/iu.test(assignment.key.value) && firstScalar(assignment.value, 'name') !== undefined);
|
|
@@ -363,11 +366,11 @@ function indexGuiElements(document, file, fileNodeId, nodes, edges, elements) {
|
|
|
363
366
|
const constants = constantsFor(document);
|
|
364
367
|
const firstFileElement = elements.length;
|
|
365
368
|
let order = 0;
|
|
366
|
-
const walk = (block, parentId) => {
|
|
369
|
+
const walk = (block, parentId, parentType) => {
|
|
367
370
|
for (const assignment of assignments(block)) {
|
|
368
371
|
if (assignment.value.type !== 'block')
|
|
369
372
|
continue;
|
|
370
|
-
if (isGuiElement(assignment)) {
|
|
373
|
+
if (isGuiElement(assignment, parentType)) {
|
|
371
374
|
const name = firstScalar(assignment.value, 'name')?.value;
|
|
372
375
|
if (name === undefined)
|
|
373
376
|
continue;
|
|
@@ -386,7 +389,7 @@ function indexGuiElements(document, file, fileNodeId, nodes, edges, elements) {
|
|
|
386
389
|
childIds: [],
|
|
387
390
|
attributes: attributes,
|
|
388
391
|
unsupportedAttributes: assignments(assignment.value)
|
|
389
|
-
.filter((child) => !isGuiElement(child))
|
|
392
|
+
.filter((child) => !isGuiElement(child, assignment.key.value))
|
|
390
393
|
.map(({ key }) => key.value)
|
|
391
394
|
.filter((key) => guiElementAttributeFidelity(key, assignment.key.value) === 'unsupported')
|
|
392
395
|
.sort((a, b) => compareCodeUnits(a, b)),
|
|
@@ -409,7 +412,7 @@ function indexGuiElements(document, file, fileNodeId, nodes, edges, elements) {
|
|
|
409
412
|
addEdge(edges, 'contains', parentId ?? fileNodeId, id, true, {}, element.location);
|
|
410
413
|
if (parentId !== undefined)
|
|
411
414
|
addEdge(edges, 'parent', id, parentId, true, {}, element.location);
|
|
412
|
-
walk(assignment.value, id);
|
|
415
|
+
walk(assignment.value, id, assignment.key.value);
|
|
413
416
|
}
|
|
414
417
|
else {
|
|
415
418
|
walk(assignment.value, parentId);
|
|
@@ -1048,7 +1048,10 @@ export class ScriptedGuiStudio {
|
|
|
1048
1048
|
const requiredNames = new Set([windowName, ...additionalTemplateNames]);
|
|
1049
1049
|
for (const element of selectedLayoutElements)
|
|
1050
1050
|
collectNamedAttributes(element.attributes, new Set(['verticalScrollbar', 'horizontalScrollbar', 'scrollbarType', 'scrollbartype']), requiredNames);
|
|
1051
|
-
|
|
1051
|
+
// Scrollbar and row references name public templates, not private controls with the same name.
|
|
1052
|
+
const availableNames = new Set(layoutGraph.elements.filter(({ parentId }) => parentId === undefined).map(({ name }) => name));
|
|
1053
|
+
if (layoutGraph.elements.some(({ name }) => name === windowName))
|
|
1054
|
+
availableNames.add(windowName);
|
|
1052
1055
|
if ([...requiredNames].some((name) => !availableNames.has(name))) {
|
|
1053
1056
|
const gameLayouts = await this.scanner.scan(workspace, {
|
|
1054
1057
|
patterns: layoutPatterns,
|
|
@@ -136,9 +136,9 @@ function coastPoint(raster, shape, isSea) {
|
|
|
136
136
|
[0, 1],
|
|
137
137
|
[0, -1],
|
|
138
138
|
]) {
|
|
139
|
-
const nx = x + dx;
|
|
139
|
+
const nx = (x + dx + raster.width) % raster.width;
|
|
140
140
|
const ny = y + dy;
|
|
141
|
-
if (
|
|
141
|
+
if (ny < 0 || ny >= raster.height)
|
|
142
142
|
continue;
|
|
143
143
|
const other = raster.provinceIds[ny * raster.width + nx];
|
|
144
144
|
if (other < 0 || !isSea(other))
|
|
@@ -154,6 +154,19 @@ function coastPoint(raster, shape, isSea) {
|
|
|
154
154
|
const middle = points[Math.floor(points.length / 2)];
|
|
155
155
|
return { ...middle, sea };
|
|
156
156
|
}
|
|
157
|
+
/** A sea pixel next to the landing province, staying inside narrow inlets and map edges. */
|
|
158
|
+
function floatingHarborPoint(raster, coast) {
|
|
159
|
+
for (const distance of [2, 1]) {
|
|
160
|
+
const x = (coast.x + coast.dx * distance + raster.width) % raster.width;
|
|
161
|
+
const y = coast.y + coast.dy * distance;
|
|
162
|
+
if (y < 0 || y >= raster.height)
|
|
163
|
+
continue;
|
|
164
|
+
// Keep the same sea association selected from the real coastal contact.
|
|
165
|
+
if (raster.provinceIds[y * raster.width + x] === coast.sea)
|
|
166
|
+
return { x, y };
|
|
167
|
+
}
|
|
168
|
+
throw new ServiceError('MAP_FLOATING_HARBOR_WATER_UNAVAILABLE', 'The selected coastal contact has no adjacent sea pixel for a floating harbor');
|
|
169
|
+
}
|
|
157
170
|
function rotationFor(dx, dy) {
|
|
158
171
|
// Bitmap y grows downward; map z grows upward.
|
|
159
172
|
return Math.atan2(-dy, dx);
|
|
@@ -315,9 +328,8 @@ function planBuildingPositions(index, request) {
|
|
|
315
328
|
continue;
|
|
316
329
|
for (let copy = 0; copy < perOwner && produced < wanted - have; copy += 1, produced += 1) {
|
|
317
330
|
const floating = slot.name === 'floating_harbor';
|
|
318
|
-
//
|
|
319
|
-
const x = floating ?
|
|
320
|
-
const y = floating ? coast.y + coast.dy * 2 : coast.y;
|
|
331
|
+
// Prefer two pixels offshore, with a verified adjacent-water fallback.
|
|
332
|
+
const { x, y } = floating ? floatingHarborPoint(raster, coast) : coast;
|
|
321
333
|
added.push([
|
|
322
334
|
state.id,
|
|
323
335
|
slot.name,
|
|
@@ -350,7 +362,10 @@ function planBuildingPositions(index, request) {
|
|
|
350
362
|
}
|
|
351
363
|
return {
|
|
352
364
|
relativePath: document?.file.relativePath ?? `${mapRoot(index)}/buildings.txt`,
|
|
353
|
-
|
|
365
|
+
// The native building-position reader treats a trailing newline as an empty CSV record.
|
|
366
|
+
content: Buffer.from(rewriteLines(document, drop, added)
|
|
367
|
+
.toString('utf8')
|
|
368
|
+
.replace(/\r?\n$/u, ''), 'utf8'),
|
|
354
369
|
};
|
|
355
370
|
}
|
|
356
371
|
function planUnitPositions(index, request) {
|
|
@@ -4,7 +4,7 @@ import { errorResult, toolResult } from '../core/operation-result.js';
|
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
5
|
import { MAP_VIEW_FIELDS, mapInspectRequestSchema, mapRewriteRequestSchema, } from '../schemas/map-requests.js';
|
|
6
6
|
import { AgentNudger } from './service.js';
|
|
7
|
-
import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, renderMapView, } from './tool-operations.js';
|
|
7
|
+
import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, previewMapRewrite, renderMapView, } from './tool-operations.js';
|
|
8
8
|
const wireResult = z.record(z.string(), z.json());
|
|
9
9
|
/** Registers read-only map operations without invoking transport handlers. */
|
|
10
10
|
export function registerMapJobs(operations, engine) {
|
|
@@ -54,12 +54,18 @@ export function registerMapJobs(operations, engine) {
|
|
|
54
54
|
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
55
55
|
const release = lifetime.begin();
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
const operationContext = {
|
|
58
58
|
workspaceId: context.workspaceId,
|
|
59
59
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
60
60
|
signal: context.signal,
|
|
61
61
|
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
62
|
-
}
|
|
62
|
+
};
|
|
63
|
+
const normalized = normalizeMapRewriteRequest(input);
|
|
64
|
+
if (normalized.preview)
|
|
65
|
+
return {
|
|
66
|
+
readOnlyPreview: wireResult.parse(JSON.parse(JSON.stringify(toolResult(await previewMapRewrite(nudger, normalized, operationContext))))),
|
|
67
|
+
};
|
|
68
|
+
return await prepareMapRewrite(nudger, normalized, operationContext);
|
|
63
69
|
}
|
|
64
70
|
finally {
|
|
65
71
|
release();
|
|
@@ -9,6 +9,8 @@ import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWork
|
|
|
9
9
|
import { planReferenceRemap, ReferenceRemapError } from './references.js';
|
|
10
10
|
import { generateWorld } from './world.js';
|
|
11
11
|
import { planMapPositions, planSupplyRebuild } from './derived.js';
|
|
12
|
+
import { buildWorldContext } from './world-context.js';
|
|
13
|
+
import { reconcileWorldOutputs } from './world-outputs.js';
|
|
12
14
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
13
15
|
if (iteration % stride !== 0)
|
|
14
16
|
return;
|
|
@@ -3250,6 +3252,14 @@ export const WORLD_REPLACED_ROOTS = [
|
|
|
3250
3252
|
'map/supplyareas',
|
|
3251
3253
|
'common/bookmarks',
|
|
3252
3254
|
];
|
|
3255
|
+
const STARTING_SCENARIO_REPLACED_ROOTS = [
|
|
3256
|
+
'common/on_actions',
|
|
3257
|
+
'common/ai_areas',
|
|
3258
|
+
'common/ai_strategy_plans',
|
|
3259
|
+
'common/ai_strategy',
|
|
3260
|
+
'common/decisions',
|
|
3261
|
+
'events',
|
|
3262
|
+
];
|
|
3253
3263
|
/** Add replace_path lines to descriptor.mod, creating a minimal descriptor when none exists. */
|
|
3254
3264
|
function descriptorWithReplacePaths(index, roots) {
|
|
3255
3265
|
const existing = index.sourceFiles.find((file) => file.rootKind === 'mod' && normalizeRelative(file.relativePath) === 'descriptor.mod');
|
|
@@ -3262,36 +3272,56 @@ function descriptorWithReplacePaths(index, roots) {
|
|
|
3262
3272
|
const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
|
|
3263
3273
|
return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
|
|
3264
3274
|
}
|
|
3265
|
-
function applyCreateWorld(base, changes, operation) {
|
|
3275
|
+
function applyCreateWorld(base, changes, operation, context) {
|
|
3276
|
+
const reference = context.referenceWorlds?.get(operation.id);
|
|
3277
|
+
if (operation.reference !== undefined && reference === undefined)
|
|
3278
|
+
throw new ServiceError('MAP_REFERENCE_NOT_PREPARED', 'Reference worlds must be prepared by the authorized workspace service');
|
|
3266
3279
|
const mapRoot = base.sourceRoots.map[0] ?? 'map';
|
|
3280
|
+
const startingScenario = operation.startingScenario ?? reference !== undefined;
|
|
3267
3281
|
const world = generateWorld({
|
|
3268
|
-
|
|
3269
|
-
|
|
3282
|
+
startingScenario,
|
|
3283
|
+
...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
|
|
3284
|
+
width: operation.width ?? reference?.surface.width ?? 1_024,
|
|
3285
|
+
height: operation.height ?? reference?.surface.height ?? 512,
|
|
3270
3286
|
seed: operation.seed ?? 1,
|
|
3271
3287
|
landCoverage: operation.landCoverage ?? 0.35,
|
|
3272
3288
|
continents: operation.continents ?? 3,
|
|
3273
3289
|
...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
|
|
3274
3290
|
landProvincePixels: operation.landProvincePixels ?? 400,
|
|
3275
3291
|
seaProvincePixels: operation.seaProvincePixels ?? 2_500,
|
|
3276
|
-
provincesPerState: operation.provincesPerState ?? 5,
|
|
3277
|
-
landProvincesPerRegion: operation.landProvincesPerRegion ?? 25,
|
|
3278
|
-
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? 10,
|
|
3279
|
-
countries: operation.countries
|
|
3292
|
+
provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
|
|
3293
|
+
landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
|
|
3294
|
+
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
|
|
3295
|
+
countries: operation.countries ??
|
|
3296
|
+
reference?.countries ?? [
|
|
3297
|
+
{ tag: 'X01', name: 'Aster' },
|
|
3298
|
+
{ tag: 'X02', name: 'Brum' },
|
|
3299
|
+
{ tag: 'X03', name: 'Corvel' },
|
|
3300
|
+
{ tag: 'X04', name: 'Doran' },
|
|
3301
|
+
],
|
|
3302
|
+
...(reference === undefined
|
|
3303
|
+
? {}
|
|
3304
|
+
: {
|
|
3305
|
+
referenceSurface: reference.surface,
|
|
3306
|
+
referenceCities: reference.cities,
|
|
3307
|
+
referenceLabels: reference.labels,
|
|
3308
|
+
}),
|
|
3280
3309
|
...(operation.continentNames === undefined
|
|
3281
3310
|
? {}
|
|
3282
3311
|
: { continentNames: operation.continentNames }),
|
|
3283
3312
|
startDate: operation.startDate ?? '1936.1.1',
|
|
3284
|
-
rivers: operation.rivers ?? 8,
|
|
3285
|
-
},
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
localisationRoot: base.sourceRoots.localisation[0] ?? 'localisation',
|
|
3290
|
-
});
|
|
3291
|
-
const roots = operation.replaceBaseGame === false ? [] : [...WORLD_REPLACED_ROOTS];
|
|
3313
|
+
rivers: operation.rivers ?? (reference === undefined ? 8 : 80),
|
|
3314
|
+
}, buildWorldContext(base));
|
|
3315
|
+
const roots = operation.replaceBaseGame === false
|
|
3316
|
+
? []
|
|
3317
|
+
: [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
|
|
3292
3318
|
replacedRootsByPlan.set(changes, roots);
|
|
3293
3319
|
for (const file of world.files)
|
|
3294
3320
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
|
|
3321
|
+
// Remove only unchanged outputs recorded by a previous generation, before indexing new IDs.
|
|
3322
|
+
for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
|
|
3323
|
+
if (file.content === null)
|
|
3324
|
+
addChange(changes, file.relativePath, null, operation.id, 'text/plain');
|
|
3295
3325
|
// Start from empty derived files so no base-game position or supply line survives.
|
|
3296
3326
|
for (const name of [
|
|
3297
3327
|
'buildings.txt',
|
|
@@ -3309,7 +3339,28 @@ function applyCreateWorld(base, changes, operation) {
|
|
|
3309
3339
|
const positioned = currentIndex(base, changes);
|
|
3310
3340
|
for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
|
|
3311
3341
|
addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
|
|
3342
|
+
for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
|
|
3343
|
+
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType ?? 'text/plain');
|
|
3312
3344
|
return [
|
|
3345
|
+
...(reference?.diagnostics ?? []).map((message) => ({
|
|
3346
|
+
code: 'MAP_REFERENCE_INTERPRETATION',
|
|
3347
|
+
severity: 'warning',
|
|
3348
|
+
category: 'map',
|
|
3349
|
+
operationId: operation.id,
|
|
3350
|
+
message,
|
|
3351
|
+
})),
|
|
3352
|
+
...(world.warnings.length === 0
|
|
3353
|
+
? []
|
|
3354
|
+
: [
|
|
3355
|
+
{
|
|
3356
|
+
code: 'MAP_WORLD_GENERATION_REVIEW',
|
|
3357
|
+
severity: 'warning',
|
|
3358
|
+
category: 'map',
|
|
3359
|
+
operationId: operation.id,
|
|
3360
|
+
message: `Drainage generation reported ${world.warnings.length} review notes`,
|
|
3361
|
+
details: { warningCount: world.warnings.length, samples: world.warnings.slice(0, 5) },
|
|
3362
|
+
},
|
|
3363
|
+
]),
|
|
3313
3364
|
{
|
|
3314
3365
|
code: 'MAP_WORLD_CREATED',
|
|
3315
3366
|
severity: 'info',
|
|
@@ -3405,7 +3456,7 @@ function applyEntityLocator(base, changes, operation) {
|
|
|
3405
3456
|
]);
|
|
3406
3457
|
addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
|
|
3407
3458
|
}
|
|
3408
|
-
function* planMapOperationSteps(base, operations, signal) {
|
|
3459
|
+
function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
3409
3460
|
yield* cancellationCheckpoint(signal);
|
|
3410
3461
|
const changes = new Map();
|
|
3411
3462
|
const diagnostics = [];
|
|
@@ -3443,7 +3494,7 @@ function* planMapOperationSteps(base, operations, signal) {
|
|
|
3443
3494
|
const boundsCheckpoint = expectedChangedBounds;
|
|
3444
3495
|
try {
|
|
3445
3496
|
if (operation.kind === 'create_world')
|
|
3446
|
-
diagnostics.push(...applyCreateWorld(base, changes, operation));
|
|
3497
|
+
diagnostics.push(...applyCreateWorld(base, changes, operation, context));
|
|
3447
3498
|
else if (operation.kind === 'move_state_provinces')
|
|
3448
3499
|
applyMoveStateProvinces(base, changes, operation);
|
|
3449
3500
|
else if (operation.kind === 'update_state')
|
|
@@ -3522,12 +3573,12 @@ function* planMapOperationSteps(base, operations, signal) {
|
|
|
3522
3573
|
operations: manifestOperations,
|
|
3523
3574
|
};
|
|
3524
3575
|
}
|
|
3525
|
-
export function planMapOperations(base, operations, signal) {
|
|
3526
|
-
return completeSynchronously(planMapOperationSteps(base, operations, signal));
|
|
3576
|
+
export function planMapOperations(base, operations, signal, context = {}) {
|
|
3577
|
+
return completeSynchronously(planMapOperationSteps(base, operations, signal, context));
|
|
3527
3578
|
}
|
|
3528
3579
|
/** Plans map mutations while periodically yielding so protocol cancellation can be delivered. */
|
|
3529
|
-
export async function planMapOperationsAsync(base, operations, signal) {
|
|
3530
|
-
return completeCooperatively(planMapOperationSteps(base, operations, signal), signal);
|
|
3580
|
+
export async function planMapOperationsAsync(base, operations, signal, context = {}) {
|
|
3581
|
+
return completeCooperatively(planMapOperationSteps(base, operations, signal, context), signal);
|
|
3531
3582
|
}
|
|
3532
3583
|
export function mapOperationId(operation) {
|
|
3533
3584
|
return deterministicId('map_operation', operation);
|