hoi4-agent-tools 3.4.0 → 3.5.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 +27 -0
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +48 -2
- package/dist/hoi4_agent_tools/core/condition-schema.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +10 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +9 -1
- package/dist/hoi4_agent_tools/core/impact-semantic.js +11 -4
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +19 -1
- package/dist/hoi4_agent_tools/core/operation-result.js +4 -1
- package/dist/hoi4_agent_tools/core/scanner.js +1 -1
- package/dist/hoi4_agent_tools/core/scenario-model.js +6 -0
- package/dist/hoi4_agent_tools/core/scenario-state.js +3 -0
- package/dist/hoi4_agent_tools/core/workspace.js +13 -1
- package/dist/hoi4_agent_tools/event/graph.js +8 -1
- package/dist/hoi4_agent_tools/event/operations.js +16 -10
- package/dist/hoi4_agent_tools/event/service.js +1 -0
- package/dist/hoi4_agent_tools/event/source-analysis.js +1 -1
- package/dist/hoi4_agent_tools/focus/render.js +10 -2
- package/dist/hoi4_agent_tools/gui/layout.js +58 -11
- package/dist/hoi4_agent_tools/gui/scenario.js +3 -1
- package/dist/hoi4_agent_tools/gui/source-graph.js +4 -1
- package/dist/hoi4_agent_tools/gui/studio.js +32 -10
- package/dist/hoi4_agent_tools/gui/validators.js +3 -0
- package/dist/hoi4_agent_tools/map/model.js +121 -12
- package/dist/hoi4_agent_tools/map/operations.js +80 -7
- package/dist/hoi4_agent_tools/map/service.js +1 -0
- package/dist/hoi4_agent_tools/map/validation.js +38 -1
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +18 -1
- package/dist/hoi4_agent_tools/mcp/server/create.js +2 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +60 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +91 -30
- package/dist/hoi4_agent_tools/mechanic/source.js +19 -4
- package/dist/hoi4_agent_tools/probability/service.js +2 -2
- package/dist/hoi4_agent_tools/probability/source-analysis.js +16 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +2 -1
- package/dist/hoi4_agent_tools/reference/lines.js +52 -0
- package/dist/hoi4_agent_tools/reference/operations.js +41 -0
- package/dist/hoi4_agent_tools/reference/service.js +427 -0
- package/dist/hoi4_agent_tools/reference/source-lookup.js +90 -0
- package/dist/hoi4_agent_tools/schemas/analysis.js +2 -0
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +1 -1
- package/dist/hoi4_agent_tools/schemas/probability.js +26 -2
- package/dist/hoi4_agent_tools/schemas/reference.js +160 -0
- package/dist/hoi4_agent_tools/technology/operations.js +29 -1
- package/dist/hoi4_agent_tools/technology/render.js +11 -5
- package/dist/hoi4_agent_tools/technology/service.js +1 -0
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/events.md +1 -1
- package/docs/gui.md +7 -0
- package/docs/jobs.md +1 -0
- package/docs/map.md +8 -0
- package/docs/mechanics.md +2 -1
- package/docs/probability.md +7 -0
- package/docs/reference.md +32 -0
- package/docs/research/reference-benchmark.md +28 -0
- package/docs/setup.md +6 -0
- package/docs/technology.md +5 -1
- package/examples/clients/codex-config.toml +2 -0
- package/package.json +4 -2
- package/schemas/configuration.schema.json +72 -58
- 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 +43 -26
- 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 +102 -75
- 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 +1 -1
- package/schemas/mechanic-test-input.schema.json +49 -32
- 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 +160 -131
- package/schemas/probability-evaluate-input.schema.json +161 -132
- package/schemas/probability-inspect-input.schema.json +20 -11
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +110 -90
- package/schemas/probability-sequence-input.schema.json +154 -134
- package/schemas/probability-simulate-input.schema.json +161 -132
- package/schemas/probability-sweep-input.schema.json +163 -134
- package/schemas/reference-context-input.schema.json +42 -0
- package/schemas/reference-read-input.schema.json +41 -0
- package/schemas/reference-search-input.schema.json +45 -0
- package/schemas/scenario-suite.schema.json +92 -75
- package/schemas/scenario-test-input.schema.json +103 -86
- package/schemas/shared-scenario.schema.json +39 -22
- package/schemas/source-lookup-input.schema.json +61 -0
- package/server.json +2 -2
- package/docs/images/readme/chaos-meter-ingame.png +0 -0
- package/docs/images/readme/chaos-meter-mcp.png +0 -0
- package/docs/images/readme/communist-insurgency-ingame.png +0 -0
- package/docs/images/readme/communist-insurgency-mcp.png +0 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/focus-tree-mcp.png +0 -0
- package/docs/images/readme/map-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/settings-advanced-mcp.png +0 -0
- package/docs/images/readme/settings-events-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
|
@@ -1172,7 +1172,11 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1172
1172
|
const clipChain = inheritedClipChain ?? (inheritedClip === undefined ? [] : [inheritedClip]);
|
|
1173
1173
|
const localScale = scalarNumber(property(definition.attributes, 'scale'), 1) ?? 1;
|
|
1174
1174
|
const scale = parentScale * localScale;
|
|
1175
|
-
const
|
|
1175
|
+
const shownPosition = depth === 0 ? objectProperty(property(definition.attributes, 'show_position')) : undefined;
|
|
1176
|
+
const position = shownPosition ?? objectProperty(property(definition.attributes, 'position'));
|
|
1177
|
+
if (shownPosition !== undefined) {
|
|
1178
|
+
addFidelity(fidelity, 'modelled', 'window_show_position', 'The requested root window uses its declared settled show position.', definition);
|
|
1179
|
+
}
|
|
1176
1180
|
const size = objectProperty(property(definition.attributes, 'size'));
|
|
1177
1181
|
const scriptedX = rowValues?.[`${definition.name}.x`] ??
|
|
1178
1182
|
scenario.values[`${definition.name}.x`] ??
|
|
@@ -1196,8 +1200,22 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1196
1200
|
const orientationAxes = anchorAxes(orientation);
|
|
1197
1201
|
const anchorX = anchoredCoordinate(parentRect.x, parentRect.width, localX, orientationAxes.horizontal);
|
|
1198
1202
|
const anchorY = anchoredCoordinate(parentRect.y, parentRect.height, localY, orientationAxes.vertical);
|
|
1199
|
-
const
|
|
1200
|
-
|
|
1203
|
+
const scriptedWidth = rowValues?.[`${definition.name}.width`] ??
|
|
1204
|
+
scenario.values[`${definition.name}.width`] ??
|
|
1205
|
+
scenario.scriptedGui[`${definition.name}.width`];
|
|
1206
|
+
const scriptedHeight = rowValues?.[`${definition.name}.height`] ??
|
|
1207
|
+
scenario.values[`${definition.name}.height`] ??
|
|
1208
|
+
scenario.scriptedGui[`${definition.name}.height`];
|
|
1209
|
+
const widthValue = typeof scriptedWidth === 'number' && Number.isFinite(scriptedWidth)
|
|
1210
|
+
? scriptedWidth
|
|
1211
|
+
: size === undefined
|
|
1212
|
+
? undefined
|
|
1213
|
+
: property(size, 'width', 'x');
|
|
1214
|
+
const heightValue = typeof scriptedHeight === 'number' && Number.isFinite(scriptedHeight)
|
|
1215
|
+
? scriptedHeight
|
|
1216
|
+
: size === undefined
|
|
1217
|
+
? undefined
|
|
1218
|
+
: property(size, 'height', 'y');
|
|
1201
1219
|
let width = scaledNumber(widthValue, parentRect.width, scale) ?? 0;
|
|
1202
1220
|
let height = scaledNumber(heightValue, parentRect.height, scale) ?? 0;
|
|
1203
1221
|
// A doubled percent names a far-edge coordinate in the parent, whereas a single
|
|
@@ -1455,10 +1473,17 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1455
1473
|
width = declaredMaxWidth;
|
|
1456
1474
|
if (width === 0 && inferWidth && /textbox/iu.test(definition.elementType))
|
|
1457
1475
|
width = nativeTextBoxWidth * scale;
|
|
1458
|
-
|
|
1476
|
+
const inferredTextHeight = height === 0 && inferHeight;
|
|
1477
|
+
if (inferredTextHeight && declaredMaxHeight !== undefined)
|
|
1459
1478
|
height = declaredMaxHeight;
|
|
1460
1479
|
const maxWidth = declaredMaxWidth ?? width;
|
|
1461
|
-
const wrapped = wrapText(catalog, fontName, displayText, visibleText.colours, fontSize * scale, maxWidth, context.work, resolvedInlineIcons);
|
|
1480
|
+
const wrapped = wrapText(catalog, fontName, displayText, visibleText.colours, fontSize * scale, scalarBoolean(property(definition.attributes, 'multiline')) === false ? 0 : maxWidth, context.work, resolvedInlineIcons);
|
|
1481
|
+
// Native maxHeight text boxes retain at least one font line. Explicit size
|
|
1482
|
+
// rectangles and inherited parent clipping still constrain their pixels.
|
|
1483
|
+
if (inferredTextHeight && declaredMaxHeight !== undefined && height < wrapped.lineHeight) {
|
|
1484
|
+
height = wrapped.lineHeight;
|
|
1485
|
+
addFidelity(fidelity, 'approximated', 'text_minimum_line_height', 'An inferred maxHeight smaller than one font line retains the first line; this native layout convention has no engine capture in this scenario.', definition);
|
|
1486
|
+
}
|
|
1462
1487
|
const glyphLines = [];
|
|
1463
1488
|
const inlineIcons = [];
|
|
1464
1489
|
for (const [lineIndex, line] of wrapped.lines.entries()) {
|
|
@@ -1747,7 +1772,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1747
1772
|
if (state === 'full-list' && rows === undefined)
|
|
1748
1773
|
rows = Array.from({ length: 12 }, (_unused, index) => ({ index }));
|
|
1749
1774
|
const dynamicList = context.dynamicListsByName.get(definition.name);
|
|
1750
|
-
const listElement = /(?:grid|listbox|overlappingelements)/iu.test(definition.elementType);
|
|
1775
|
+
const listElement = /(?:grid|listbox|overlappingelements|positiontype)/iu.test(definition.elementType);
|
|
1751
1776
|
const slotSize = objectProperty(property(definition.attributes, 'slotsize'));
|
|
1752
1777
|
const slotHeight = scaledNumber(slotSize === undefined ? undefined : property(slotSize, 'height', 'y'), height, scale);
|
|
1753
1778
|
const slotWidth = scaledNumber(slotSize === undefined ? undefined : property(slotSize, 'width', 'x'), width, scale);
|
|
@@ -1761,8 +1786,13 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1761
1786
|
const maxHorizontal = scalarNumber(property(definition.attributes, 'max_slots_horizontal'), 1);
|
|
1762
1787
|
const maxVertical = scalarNumber(property(definition.attributes, 'max_slots_vertical'), 1);
|
|
1763
1788
|
const grid = /grid/iu.test(definition.elementType);
|
|
1789
|
+
const singleColumnFlow = grid &&
|
|
1790
|
+
maxHorizontal === 1 &&
|
|
1791
|
+
slotHeight !== undefined &&
|
|
1792
|
+
slotHeight <= scale &&
|
|
1793
|
+
(maxVertical === undefined || maxVertical <= 0);
|
|
1764
1794
|
const rowPosition = (index, fallbackY) => {
|
|
1765
|
-
if (!grid || slotWidth === undefined || slotHeight === undefined)
|
|
1795
|
+
if (!grid || singleColumnFlow || slotWidth === undefined || slotHeight === undefined)
|
|
1766
1796
|
return { x: 0, y: fallbackY };
|
|
1767
1797
|
if (maxVertical !== undefined && maxVertical > 0) {
|
|
1768
1798
|
const rowsPerColumn = Math.max(1, Math.trunc(maxVertical));
|
|
@@ -1822,11 +1852,22 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1822
1852
|
(row.countryScope === true
|
|
1823
1853
|
? (dynamicList?.countryScopeEntryContainer ?? dynamicList?.entryContainer)
|
|
1824
1854
|
: (dynamicList?.entryContainer ?? dynamicList?.countryScopeEntryContainer));
|
|
1825
|
-
const
|
|
1855
|
+
const candidates = templateName === undefined
|
|
1856
|
+
? []
|
|
1857
|
+
: (context.elementsByNameCandidates.get(templateName) ?? []);
|
|
1858
|
+
const localCandidates = candidates.filter(({ sourcePath }) => sourcePath === definition.sourcePath);
|
|
1859
|
+
const localRoots = localCandidates.filter(({ parentId }) => parentId === undefined);
|
|
1860
|
+
const template = localRoots.length === 1
|
|
1861
|
+
? localRoots[0]
|
|
1862
|
+
: localCandidates.length === 1
|
|
1863
|
+
? localCandidates[0]
|
|
1864
|
+
: candidates.length === 1
|
|
1865
|
+
? candidates[0]
|
|
1866
|
+
: undefined;
|
|
1826
1867
|
if (template === undefined) {
|
|
1827
1868
|
diagnostics.push(diagnostic('GUI_DYNAMIC_LIST_TEMPLATE_MISSING', 'error', templateName === undefined
|
|
1828
1869
|
? `Dynamic list ${definition.name} has no entry container for scenario row ${index}.`
|
|
1829
|
-
: `Dynamic list ${definition.name} cannot resolve entry container ${templateName}.`, definition));
|
|
1870
|
+
: `Dynamic list ${definition.name} cannot unambiguously resolve entry container ${templateName}.`, definition));
|
|
1830
1871
|
continue;
|
|
1831
1872
|
}
|
|
1832
1873
|
const templatePosition = objectProperty(property(template.attributes, 'position'));
|
|
@@ -1842,7 +1883,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1842
1883
|
};
|
|
1843
1884
|
await layoutElement(template, shiftedParent, childClip, scale, depth + 1, context, rowSuffix(index), index, row, instanceId, origo, resolveVisibleAppearance, childClipChain);
|
|
1844
1885
|
const rendered = context.instancesById.get(`${template.id}${rowSuffix(index)}`);
|
|
1845
|
-
rowY += (slotHeight ?? rendered?.unclippedRect.height ?? 0) + spacingY;
|
|
1886
|
+
rowY += Math.max(slotHeight ?? 0, rendered?.unclippedRect.height ?? 0) + spacingY;
|
|
1846
1887
|
renderedRows += 1;
|
|
1847
1888
|
}
|
|
1848
1889
|
addFidelity(fidelity, renderedRows === rows.length ? 'modelled' : 'missing', dynamicList === undefined ? 'native_dynamic_list' : 'scripted_gui_dynamic_list', `Expanded ${renderedRows}/${rows.length} scenario rows for ${definition.name} through ${dynamicList === undefined ? 'explicit native' : 'scripted-GUI'} entry containers.`, definition);
|
|
@@ -2055,10 +2096,15 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2055
2096
|
};
|
|
2056
2097
|
const output = [];
|
|
2057
2098
|
const elementsByName = new Map();
|
|
2099
|
+
const elementsByNameCandidates = new Map();
|
|
2058
2100
|
for (const element of [...graph.elements].sort((left, right) => compareCodeUnits(right.sourcePath, left.sourcePath) ||
|
|
2059
|
-
right.definitionOrder - left.definitionOrder))
|
|
2101
|
+
right.definitionOrder - left.definitionOrder)) {
|
|
2060
2102
|
if (!elementsByName.has(element.name))
|
|
2061
2103
|
elementsByName.set(element.name, element);
|
|
2104
|
+
const candidates = elementsByNameCandidates.get(element.name) ?? [];
|
|
2105
|
+
candidates.push(element);
|
|
2106
|
+
elementsByNameCandidates.set(element.name, candidates);
|
|
2107
|
+
}
|
|
2062
2108
|
const context = {
|
|
2063
2109
|
graph,
|
|
2064
2110
|
scenario: layoutScenario,
|
|
@@ -2067,6 +2113,7 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2067
2113
|
diagnostics,
|
|
2068
2114
|
elementsById,
|
|
2069
2115
|
elementsByName,
|
|
2116
|
+
elementsByNameCandidates,
|
|
2070
2117
|
dynamicListsByName,
|
|
2071
2118
|
countryFlagProperties,
|
|
2072
2119
|
constantElementEnabled,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ConditionScopeBindingSchema, ConditionResultSchema } from '../core/condition-schema.js';
|
|
2
|
+
import { ConditionScopeBindingSchema, ConditionResultSchema, ConditionControlMapSchema, GuiScenarioDateSchema, } from '../core/condition-schema.js';
|
|
3
3
|
import { renderDimensionViolation, RENDER_MAX_DIMENSION } from '../core/render-budget.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
5
|
import { GUI_SCENARIO_MAX_KEYS, GUI_SCENARIO_MAX_LIST_KEYS, GUI_SCENARIO_MAX_ROWS, GUI_SCENARIO_MAX_STRING_CHARACTERS, } from './limits.js';
|
|
@@ -104,6 +104,8 @@ const GuiPreviewScenarioBodySchema = z
|
|
|
104
104
|
.max(128)
|
|
105
105
|
.regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
106
106
|
description: z.string().max(500).optional(),
|
|
107
|
+
date: GuiScenarioDateSchema.optional(),
|
|
108
|
+
controls: ConditionControlMapSchema.optional(),
|
|
107
109
|
resolution: GuiPreviewResolutionSchema.default({ width: 1920, height: 1080 }),
|
|
108
110
|
uiScale: z
|
|
109
111
|
.number()
|
|
@@ -56,6 +56,7 @@ const modelledElementAttributes = new Set([
|
|
|
56
56
|
'maxWidth',
|
|
57
57
|
'maxHeight',
|
|
58
58
|
'fixedsize',
|
|
59
|
+
'multiline',
|
|
59
60
|
'format',
|
|
60
61
|
'buttonText',
|
|
61
62
|
'buttonFont',
|
|
@@ -1292,7 +1293,7 @@ function referenceLabel(edge) {
|
|
|
1292
1293
|
return String(value);
|
|
1293
1294
|
return value === undefined ? edge.to : JSON.stringify(value);
|
|
1294
1295
|
}
|
|
1295
|
-
export function buildGuiSourceGraph(files, sharedIndex) {
|
|
1296
|
+
export function buildGuiSourceGraph(files, sharedIndex, additionalLocalisationKeys = []) {
|
|
1296
1297
|
const activeFiles = files.filter(({ shadowedBy }) => shadowedBy === undefined);
|
|
1297
1298
|
const nodes = [];
|
|
1298
1299
|
const edges = [];
|
|
@@ -1435,6 +1436,8 @@ export function buildGuiSourceGraph(files, sharedIndex) {
|
|
|
1435
1436
|
indexDecisionEntries(document, file, nodes, pendingDecisionEdges);
|
|
1436
1437
|
}
|
|
1437
1438
|
const referencedLocalisationKeys = referencedGuiLocalisationKeys(elements, scriptedLocalisation);
|
|
1439
|
+
for (const key of additionalLocalisationKeys)
|
|
1440
|
+
referencedLocalisationKeys.add(key);
|
|
1438
1441
|
for (const { file, fileNodeId } of localisationFiles.sort((left, right) => right.file.loadOrder - left.file.loadOrder ||
|
|
1439
1442
|
compareCodeUnits(left.file.displayPath, right.file.displayPath))) {
|
|
1440
1443
|
const document = parseLocalisation(file.bytes, file.displayPath);
|
|
@@ -498,6 +498,24 @@ function scenarioTemplateNames(scenarios) {
|
|
|
498
498
|
...new Set(scenarios.flatMap(({ lists }) => Object.values(lists).flatMap((rows) => rows.flatMap(({ entryContainer }) => typeof entryContainer === 'string' && entryContainer.length > 0 ? [entryContainer] : [])))),
|
|
499
499
|
].sort(compareCodeUnits);
|
|
500
500
|
}
|
|
501
|
+
function scenarioLocalisationKeys(scenarios) {
|
|
502
|
+
const keys = new Set();
|
|
503
|
+
const pending = [...scenarios];
|
|
504
|
+
while (pending.length > 0) {
|
|
505
|
+
const value = pending.pop();
|
|
506
|
+
if (typeof value === 'string') {
|
|
507
|
+
if (/^[A-Za-z0-9_.-]{1,512}$/u.test(value))
|
|
508
|
+
keys.add(value);
|
|
509
|
+
for (const match of value.matchAll(/\$([A-Za-z0-9_.-]+)\$/gu))
|
|
510
|
+
keys.add(match[1]);
|
|
511
|
+
}
|
|
512
|
+
else if (Array.isArray(value))
|
|
513
|
+
pending.push(...value);
|
|
514
|
+
else if (value !== null && typeof value === 'object')
|
|
515
|
+
pending.push(...Object.values(value));
|
|
516
|
+
}
|
|
517
|
+
return [...keys].sort(compareCodeUnits);
|
|
518
|
+
}
|
|
501
519
|
/** Native row bindings admit only bounded country tokens, never caller-supplied paths or globs. */
|
|
502
520
|
export function scenarioCountryFlagPatterns(scenarios) {
|
|
503
521
|
const tags = new Set();
|
|
@@ -861,8 +879,8 @@ export class ScriptedGuiStudio {
|
|
|
861
879
|
this.scanner = this.engine.scanner;
|
|
862
880
|
this.artifacts = this.engine.artifacts;
|
|
863
881
|
}
|
|
864
|
-
graphForFiles(files, scope) {
|
|
865
|
-
const key = `${scope}:${hashCanonical(files.map(({ displayPath, sha256, shadowedBy }) => ({
|
|
882
|
+
graphForFiles(files, scope, additionalLocalisationKeys = []) {
|
|
883
|
+
const key = `${scope}:${hashCanonical(additionalLocalisationKeys)}:${hashCanonical(files.map(({ displayPath, sha256, shadowedBy }) => ({
|
|
866
884
|
displayPath,
|
|
867
885
|
sha256,
|
|
868
886
|
...(shadowedBy === undefined ? {} : { shadowedBy }),
|
|
@@ -876,7 +894,7 @@ export class ScriptedGuiStudio {
|
|
|
876
894
|
const retainedFiles = [...files];
|
|
877
895
|
const scanned = {
|
|
878
896
|
files: retainedFiles,
|
|
879
|
-
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles)),
|
|
897
|
+
graph: buildGuiSourceGraph(retainedFiles, this.engine.indexFiles(retainedFiles), additionalLocalisationKeys),
|
|
880
898
|
};
|
|
881
899
|
this.#graphCache.set(key, scanned);
|
|
882
900
|
while (this.#graphCache.size > 1) {
|
|
@@ -906,7 +924,7 @@ export class ScriptedGuiStudio {
|
|
|
906
924
|
const files = mergeScannedFiles(snapshot.files, localisationFiles);
|
|
907
925
|
return this.graphForFiles(files, `workspace:${workspaceId}`);
|
|
908
926
|
}
|
|
909
|
-
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = []) {
|
|
927
|
+
async scanWindow(workspaceId, windowName, principal, signal, languages = ['l_english'], additionalSpriteNames = [], additionalTemplateNames = [], additionalAssetPatterns = [], additionalLocalisationKeys = []) {
|
|
910
928
|
const workspace = this.resolver.get(workspaceId, principal);
|
|
911
929
|
const layoutPatterns = guiLayoutPatterns(workspace);
|
|
912
930
|
const overlayLayouts = await this.engine.scan(workspaceId, {
|
|
@@ -934,7 +952,7 @@ export class ScriptedGuiStudio {
|
|
|
934
952
|
this.engine.scan(workspaceId, { patterns: guiLocalisationPatterns(workspace, languages) }, principal, signal),
|
|
935
953
|
]);
|
|
936
954
|
const definitionFiles = mergeScannedFiles(layoutFiles, linkedDefinitions.files, localisation.files);
|
|
937
|
-
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}
|
|
955
|
+
const definitions = this.graphForFiles(definitionFiles, `definitions:${workspaceId}`, additionalLocalisationKeys);
|
|
938
956
|
assertTargetWindowAvailable(definitions.graph, windowName);
|
|
939
957
|
const referencedPatterns = [
|
|
940
958
|
...referencedAssetPatternsForWindow(definitions.graph, windowName, additionalSpriteNames, languages, additionalTemplateNames),
|
|
@@ -973,7 +991,7 @@ export class ScriptedGuiStudio {
|
|
|
973
991
|
const fontPages = mergeScannedFiles(exactFontPages, fallbackFontPages);
|
|
974
992
|
signal?.throwIfAborted();
|
|
975
993
|
const files = mergeScannedFiles(definitions.files, referenced, fontPages);
|
|
976
|
-
return this.graphForFiles(files, `window:${workspaceId}:${windowName}
|
|
994
|
+
return this.graphForFiles(files, `window:${workspaceId}:${windowName}`, additionalLocalisationKeys);
|
|
977
995
|
}
|
|
978
996
|
async lint(input) {
|
|
979
997
|
input.signal?.throwIfAborted();
|
|
@@ -982,7 +1000,7 @@ export class ScriptedGuiStudio {
|
|
|
982
1000
|
const generatedOptions = input.generatedScenarios === undefined
|
|
983
1001
|
? undefined
|
|
984
1002
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
985
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]));
|
|
1003
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [placeholderScenario.language, ...explicitRelatedScenarios.map(({ language }) => language)], scenarioSpriteNames([placeholderScenario, ...explicitRelatedScenarios], generatedOptions?.textSamples), scenarioTemplateNames([placeholderScenario, ...explicitRelatedScenarios]), scenarioCountryFlagPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]));
|
|
986
1004
|
const generatedScenarios = generatedOptions === undefined
|
|
987
1005
|
? []
|
|
988
1006
|
: generateGuiPreviewScenarios(scanned.graph, input.windowName, placeholderScenario, generatedOptions, ClausewitzEvaluationDefinitions.build(scanned));
|
|
@@ -1012,7 +1030,7 @@ export class ScriptedGuiStudio {
|
|
|
1012
1030
|
const budget = new RenderBudget();
|
|
1013
1031
|
const beforeScenario = parsePreviewScenario(input.before);
|
|
1014
1032
|
const afterScenario = parsePreviewScenario(input.after);
|
|
1015
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]));
|
|
1033
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryFlagPatterns([beforeScenario, afterScenario]), scenarioLocalisationKeys([beforeScenario, afterScenario]));
|
|
1016
1034
|
const beforeCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, beforeScenario.language);
|
|
1017
1035
|
const afterCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, afterScenario.language);
|
|
1018
1036
|
const before = await buildGuiScene(scanned.graph, scanned.files, input.windowName, beforeScenario, beforeCatalog);
|
|
@@ -1069,6 +1087,10 @@ export class ScriptedGuiStudio {
|
|
|
1069
1087
|
placeholderScenario,
|
|
1070
1088
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1071
1089
|
...explicitRelatedScenarios,
|
|
1090
|
+
]), scenarioLocalisationKeys([
|
|
1091
|
+
placeholderScenario,
|
|
1092
|
+
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1093
|
+
...explicitRelatedScenarios,
|
|
1072
1094
|
]));
|
|
1073
1095
|
const generatedScenarios = generatedOptions === undefined
|
|
1074
1096
|
? []
|
|
@@ -1177,7 +1199,7 @@ export class ScriptedGuiStudio {
|
|
|
1177
1199
|
loadOrder: original.loadOrder,
|
|
1178
1200
|
},
|
|
1179
1201
|
]);
|
|
1180
|
-
const baselineGraph = this.graphForFiles(baselineFiles, `source-baseline:${input.workspaceId}
|
|
1202
|
+
const baselineGraph = this.graphForFiles(baselineFiles, `source-baseline:${input.workspaceId}`, scenarioLocalisationKeys([scenario])).graph;
|
|
1181
1203
|
if (!baselineGraph.elements.some(({ name }) => name === input.windowName))
|
|
1182
1204
|
throw new ServiceError('GUI_SOURCE_BASELINE_WINDOW_MISSING', 'Source baseline does not define the requested GUI window', { relativePath, windowName: input.windowName });
|
|
1183
1205
|
sourceBaselineScene = await buildGuiScene(baselineGraph, baselineFiles, input.windowName, scenario, new GuiAssetCatalog(baselineGraph, baselineFiles, budget, scenario.language));
|
|
@@ -1416,7 +1438,7 @@ export class ScriptedGuiStudio {
|
|
|
1416
1438
|
const previewScenario = input.scenario === undefined ? undefined : parsePreviewScenario(input.scenario);
|
|
1417
1439
|
const initiallyScanned = input.windowName === undefined
|
|
1418
1440
|
? await this.scan(input.workspaceId, input.principal, input.signal)
|
|
1419
|
-
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]));
|
|
1441
|
+
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryFlagPatterns([previewScenario]), scenarioLocalisationKeys([previewScenario]));
|
|
1420
1442
|
const exactTargets = await this.scanner.scan(workspace, {
|
|
1421
1443
|
patterns: prepared.map(({ relativePath }) => relativePath),
|
|
1422
1444
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
@@ -655,6 +655,9 @@ function validateReferencesAndScript(graph, scene, diagnostics) {
|
|
|
655
655
|
const elementsById = new Map(graph.elements.map((element) => [element.id, element]));
|
|
656
656
|
for (const button of scene.elements.filter(({ elementType }) => /button/iu.test(elementType))) {
|
|
657
657
|
const source = elementsById.get(button.sourceId);
|
|
658
|
+
const referencedByScript = effects.has(button.name) || triggers.has(button.name);
|
|
659
|
+
if ((source === undefined || !/button/iu.test(source.elementType)) && !referencedByScript)
|
|
660
|
+
continue;
|
|
658
661
|
const tooltipOnly = !effects.has(button.name) &&
|
|
659
662
|
!triggers.has(button.name) &&
|
|
660
663
|
(typeof source?.attributes.pdx_tooltip === 'string' ||
|
|
@@ -169,6 +169,39 @@ function parseNumber(value) {
|
|
|
169
169
|
const parsed = Number(value);
|
|
170
170
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
171
171
|
}
|
|
172
|
+
export function parseMapDate(value) {
|
|
173
|
+
const match = /^(\d{1,4})\.(\d{1,2})\.(\d{1,2})(?:\.(\d{1,2}))?$/u.exec(value);
|
|
174
|
+
if (match === null)
|
|
175
|
+
return undefined;
|
|
176
|
+
const year = Number(match[1]);
|
|
177
|
+
const month = Number(match[2]);
|
|
178
|
+
const day = Number(match[3]);
|
|
179
|
+
const hour = match[4] === undefined ? 0 : Number(match[4]);
|
|
180
|
+
if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31 || hour < 0 || hour > 23)
|
|
181
|
+
return undefined;
|
|
182
|
+
return year * 1_000_000 + month * 10_000 + day * 100 + hour;
|
|
183
|
+
}
|
|
184
|
+
function earliestBookmarkDate(files) {
|
|
185
|
+
let earliest;
|
|
186
|
+
for (const file of files) {
|
|
187
|
+
if (!/^common\/bookmarks\/.*\.txt$/iu.test(normalizedPath(file.relativePath)))
|
|
188
|
+
continue;
|
|
189
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
190
|
+
for (const root of childBlocks(document.root, 'bookmarks')) {
|
|
191
|
+
for (const bookmark of childBlocks(root, 'bookmark')) {
|
|
192
|
+
const value = firstScalar(bookmark, 'date')?.value;
|
|
193
|
+
if (value === undefined)
|
|
194
|
+
continue;
|
|
195
|
+
const ordinal = parseMapDate(value);
|
|
196
|
+
if (ordinal === undefined)
|
|
197
|
+
continue;
|
|
198
|
+
if (earliest === undefined || ordinal < earliest.ordinal)
|
|
199
|
+
earliest = { value, ordinal, sourcePath: file.displayPath };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return earliest;
|
|
204
|
+
}
|
|
172
205
|
function requiredNumber(values, index, label) {
|
|
173
206
|
const value = values[index];
|
|
174
207
|
if (value === undefined) {
|
|
@@ -260,7 +293,7 @@ export function parseDefinitions(file, diagnostics) {
|
|
|
260
293
|
}
|
|
261
294
|
return definitions;
|
|
262
295
|
}
|
|
263
|
-
export function parseState(file, diagnostics) {
|
|
296
|
+
export function parseState(file, diagnostics, earliestBookmark) {
|
|
264
297
|
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
265
298
|
addMapDiagnostics(diagnostics, document.diagnostics);
|
|
266
299
|
const assignment = firstTopLevelAssignment(document, 'state');
|
|
@@ -318,8 +351,85 @@ export function parseState(file, diagnostics) {
|
|
|
318
351
|
}
|
|
319
352
|
}
|
|
320
353
|
}
|
|
321
|
-
|
|
322
|
-
|
|
354
|
+
let owner = firstScalar(historyBlock ?? block, 'owner')?.value;
|
|
355
|
+
let controller = firstScalar(historyBlock ?? block, 'controller')?.value;
|
|
356
|
+
let cores = historyBlock === undefined
|
|
357
|
+
? []
|
|
358
|
+
: assignments(historyBlock, 'add_core_of').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []);
|
|
359
|
+
let claims = historyBlock === undefined
|
|
360
|
+
? []
|
|
361
|
+
: assignments(historyBlock, 'add_claim_by').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []);
|
|
362
|
+
if (historyBlock !== undefined) {
|
|
363
|
+
const dated = assignments(historyBlock)
|
|
364
|
+
.flatMap((entry) => {
|
|
365
|
+
if (entry.value.type !== 'block')
|
|
366
|
+
return [];
|
|
367
|
+
const date = parseMapDate(entry.key.value);
|
|
368
|
+
return date === undefined ? [] : [{ entry, date }];
|
|
369
|
+
})
|
|
370
|
+
.sort((left, right) => left.date - right.date || left.entry.start - right.entry.start);
|
|
371
|
+
if (dated.length > 0 && earliestBookmark === undefined) {
|
|
372
|
+
addMapDiagnostic(diagnostics, {
|
|
373
|
+
code: 'MAP_STATE_DATED_HISTORY_UNRESOLVED',
|
|
374
|
+
severity: 'warning',
|
|
375
|
+
category: 'map',
|
|
376
|
+
message: `State ${id} has dated history but no active bookmark date was found`,
|
|
377
|
+
location: nodeLocation(document, dated[0].entry),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
for (const { entry, date } of dated) {
|
|
381
|
+
if (earliestBookmark === undefined || date >= earliestBookmark.ordinal)
|
|
382
|
+
continue;
|
|
383
|
+
if (entry.value.type !== 'block')
|
|
384
|
+
continue;
|
|
385
|
+
for (const command of assignments(entry.value)) {
|
|
386
|
+
const value = command.value;
|
|
387
|
+
if (value.type === 'scalar') {
|
|
388
|
+
if (command.key.value === 'owner')
|
|
389
|
+
owner = value.value;
|
|
390
|
+
else if (command.key.value === 'controller')
|
|
391
|
+
controller = value.value;
|
|
392
|
+
else if (command.key.value === 'add_core_of' && !cores.includes(value.value))
|
|
393
|
+
cores.push(value.value);
|
|
394
|
+
else if (command.key.value === 'remove_core_of')
|
|
395
|
+
cores = cores.filter((tag) => tag !== value.value);
|
|
396
|
+
else if (command.key.value === 'add_claim_by' && !claims.includes(value.value))
|
|
397
|
+
claims.push(value.value);
|
|
398
|
+
else if (command.key.value === 'remove_claim_by')
|
|
399
|
+
claims = claims.filter((tag) => tag !== value.value);
|
|
400
|
+
}
|
|
401
|
+
else if (command.key.value === 'victory_points') {
|
|
402
|
+
const values = scalarList(value);
|
|
403
|
+
const provinceId = parseInteger(values[0]);
|
|
404
|
+
const amount = parseNumber(values[1]);
|
|
405
|
+
if (provinceId === undefined || amount === undefined)
|
|
406
|
+
continue;
|
|
407
|
+
const existing = victoryPoints.findIndex((point) => point.provinceId === provinceId);
|
|
408
|
+
if (existing >= 0)
|
|
409
|
+
victoryPoints.splice(existing, 1);
|
|
410
|
+
victoryPoints.push({ provinceId, value: amount, assignment: command });
|
|
411
|
+
}
|
|
412
|
+
else if (command.key.value === 'buildings') {
|
|
413
|
+
for (const building of assignments(value)) {
|
|
414
|
+
if (building.value.type === 'block') {
|
|
415
|
+
const provinceId = parseInteger(building.key.value);
|
|
416
|
+
if (provinceId === undefined)
|
|
417
|
+
continue;
|
|
418
|
+
const merged = new Map(provinceBuildings.get(provinceId) ?? []);
|
|
419
|
+
for (const [key, level] of numericMap(building.value))
|
|
420
|
+
merged.set(key, level);
|
|
421
|
+
provinceBuildings.set(provinceId, merged);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
const level = parseNumber(building.value.value);
|
|
425
|
+
if (level !== undefined)
|
|
426
|
+
stateBuildings.set(building.key.value, level);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
323
433
|
const capital = derivedStateCapital(victoryPoints);
|
|
324
434
|
return {
|
|
325
435
|
id,
|
|
@@ -331,12 +441,8 @@ export function parseState(file, diagnostics) {
|
|
|
331
441
|
resources: numericMap(resourcesBlock),
|
|
332
442
|
...(owner === undefined ? {} : { owner }),
|
|
333
443
|
...(controller === undefined ? {} : { controller }),
|
|
334
|
-
cores
|
|
335
|
-
|
|
336
|
-
: assignments(historyBlock, 'add_core_of').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []),
|
|
337
|
-
claims: historyBlock === undefined
|
|
338
|
-
? []
|
|
339
|
-
: assignments(historyBlock, 'add_claim_by').flatMap(({ value }) => value.type === 'scalar' ? [value.value] : []),
|
|
444
|
+
cores,
|
|
445
|
+
claims,
|
|
340
446
|
victoryPoints,
|
|
341
447
|
provinceBuildings,
|
|
342
448
|
stateBuildings,
|
|
@@ -825,6 +931,7 @@ export class MapWorkspaceIndex {
|
|
|
825
931
|
/** Shared cross-domain symbol/reference authority for these exact source bytes. */
|
|
826
932
|
sharedIndex;
|
|
827
933
|
activeFiles;
|
|
934
|
+
earliestBookmark;
|
|
828
935
|
diagnostics = [];
|
|
829
936
|
definitions;
|
|
830
937
|
definitionsAcrossRoots;
|
|
@@ -875,6 +982,7 @@ export class MapWorkspaceIndex {
|
|
|
875
982
|
localisation: sourceRoots.localisation.map(normalizeConfiguredRoot),
|
|
876
983
|
};
|
|
877
984
|
this.activeFiles = selectActiveFiles(files);
|
|
985
|
+
this.earliestBookmark = earliestBookmarkDate(this.activeFiles.all);
|
|
878
986
|
const modelBudget = new MapModelBudget();
|
|
879
987
|
modelBudget.assertScriptFiles(this.sourceFiles.filter((file) => inConfiguredRoot(file.relativePath, this.sourceRoots.states) ||
|
|
880
988
|
this.sourceRoots.map.some((root) => normalizedPath(file.relativePath).startsWith(`${root}/strategicregions/`)) ||
|
|
@@ -935,7 +1043,9 @@ export class MapWorkspaceIndex {
|
|
|
935
1043
|
const cached = parsedStates.get(file.displayPath);
|
|
936
1044
|
if (cached !== undefined)
|
|
937
1045
|
return cached ?? undefined;
|
|
938
|
-
const state = parseState(file, activeStatePaths.has(file.displayPath) ? this.diagnostics : []);
|
|
1046
|
+
const state = parseState(file, activeStatePaths.has(file.displayPath) ? this.diagnostics : [], this.earliestBookmark);
|
|
1047
|
+
if (state !== undefined && this.earliestBookmark !== undefined)
|
|
1048
|
+
state.earliestBookmark = this.earliestBookmark;
|
|
939
1049
|
parsedStates.set(file.displayPath, state ?? null);
|
|
940
1050
|
if (state !== undefined) {
|
|
941
1051
|
modelBudget.addDocument(state.document);
|
|
@@ -1075,9 +1185,8 @@ export class MapWorkspaceIndex {
|
|
|
1075
1185
|
this.coastalProvinceIds = this.raster?.coastalProvinceIds ?? new Set();
|
|
1076
1186
|
const portPositionsByStateProvince = new Map();
|
|
1077
1187
|
for (const position of this.buildingPositions) {
|
|
1078
|
-
if (position.building !== 'naval_base_spawn'
|
|
1188
|
+
if (position.building !== 'naval_base_spawn')
|
|
1079
1189
|
continue;
|
|
1080
|
-
}
|
|
1081
1190
|
const provinceId = this.provinceAtMapCoordinate(position.x, position.z);
|
|
1082
1191
|
if (provinceId === undefined)
|
|
1083
1192
|
continue;
|
|
@@ -5,7 +5,7 @@ import { ServiceError } from '../core/result.js';
|
|
|
5
5
|
import { applyReplacements, assignments, encodeSource, parseLocalisation, parseClausewitz, } from '../core/source/index.js';
|
|
6
6
|
import { rgbKey } from './bmp.js';
|
|
7
7
|
import { MAP_MASK_CELL_LIMIT, MAP_POLYGON_WORK_LIMIT, MAP_SELECTED_OFFSET_BYTES, MAP_SELECTED_PIXEL_LIMIT, } from './limits.js';
|
|
8
|
-
import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseTextDocument, } from './model.js';
|
|
8
|
+
import { derivedStateCapital, encodeTextDocument, MapWorkspaceIndex, parseMapDate, parseTextDocument, } from './model.js';
|
|
9
9
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
10
10
|
if (iteration % stride !== 0)
|
|
11
11
|
return;
|
|
@@ -1034,8 +1034,74 @@ function stateMembershipId(index, provinceId) {
|
|
|
1034
1034
|
return states[0]?.id;
|
|
1035
1035
|
}
|
|
1036
1036
|
function commitState(changes, state, data, fields, operationId) {
|
|
1037
|
+
assertDatedStateHistorySafe(state, fields, operationId);
|
|
1037
1038
|
addChange(changes, state.file.relativePath, patchState(state, data, fields), operationId, 'text/plain');
|
|
1038
1039
|
}
|
|
1040
|
+
function assertDatedStateHistorySafe(state, fields, operationId) {
|
|
1041
|
+
if (state.historyBlock === undefined)
|
|
1042
|
+
return;
|
|
1043
|
+
for (const entry of assignments(state.historyBlock)) {
|
|
1044
|
+
if (entry.value.type !== 'block')
|
|
1045
|
+
continue;
|
|
1046
|
+
const datedAt = parseMapDate(entry.key.value);
|
|
1047
|
+
if (datedAt === undefined)
|
|
1048
|
+
continue;
|
|
1049
|
+
const keys = new Set(assignments(entry.value).map(({ key }) => key.value));
|
|
1050
|
+
const applicable = state.earliestBookmark === undefined || datedAt <= state.earliestBookmark.ordinal;
|
|
1051
|
+
const affected = [
|
|
1052
|
+
['owner', fields.owner === true && keys.has('owner') && applicable],
|
|
1053
|
+
['controller', fields.controller === true && keys.has('controller') && applicable],
|
|
1054
|
+
[
|
|
1055
|
+
'cores',
|
|
1056
|
+
fields.cores === true &&
|
|
1057
|
+
(keys.has('add_core_of') || keys.has('remove_core_of')) &&
|
|
1058
|
+
applicable,
|
|
1059
|
+
],
|
|
1060
|
+
[
|
|
1061
|
+
'claims',
|
|
1062
|
+
fields.claims === true &&
|
|
1063
|
+
(keys.has('add_claim_by') || keys.has('remove_claim_by')) &&
|
|
1064
|
+
applicable,
|
|
1065
|
+
],
|
|
1066
|
+
[
|
|
1067
|
+
'victoryPoints',
|
|
1068
|
+
((fields.victoryPoints === true && applicable) || fields.provinces === true) &&
|
|
1069
|
+
keys.has('victory_points'),
|
|
1070
|
+
],
|
|
1071
|
+
[
|
|
1072
|
+
'buildings',
|
|
1073
|
+
((fields.buildings === true && applicable) || fields.provinces === true) &&
|
|
1074
|
+
keys.has('buildings'),
|
|
1075
|
+
],
|
|
1076
|
+
];
|
|
1077
|
+
const conflict = affected.find(([, selected]) => selected);
|
|
1078
|
+
if (conflict === undefined)
|
|
1079
|
+
continue;
|
|
1080
|
+
throw new ServiceError('MAP_DATED_STATE_HISTORY_CONFLICT', `State ${state.id} has a dated ${conflict[0]} change that this map operation cannot safely rewrite`, {
|
|
1081
|
+
operationId,
|
|
1082
|
+
stateId: state.id,
|
|
1083
|
+
field: conflict[0],
|
|
1084
|
+
datedAt: entry.key.value,
|
|
1085
|
+
earliestBookmark: state.earliestBookmark?.value ?? null,
|
|
1086
|
+
sourcePath: state.file.displayPath,
|
|
1087
|
+
sourceOffset: entry.start,
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
function assertNoDatedStateHistory(state, operationId) {
|
|
1092
|
+
if (state.historyBlock === undefined)
|
|
1093
|
+
return;
|
|
1094
|
+
const dated = assignments(state.historyBlock).find((entry) => entry.value.type === 'block' && parseMapDate(entry.key.value) !== undefined);
|
|
1095
|
+
if (dated === undefined)
|
|
1096
|
+
return;
|
|
1097
|
+
throw new ServiceError('MAP_DATED_STATE_DISTRIBUTION_UNSUPPORTED', `State ${state.id} has dated history that cannot be distributed to a new or merged state`, {
|
|
1098
|
+
operationId,
|
|
1099
|
+
stateId: state.id,
|
|
1100
|
+
datedAt: dated.key.value,
|
|
1101
|
+
sourcePath: state.file.displayPath,
|
|
1102
|
+
sourceOffset: dated.start,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1039
1105
|
function commitRegion(changes, region, provinces, operationId) {
|
|
1040
1106
|
addChange(changes, region.file.relativePath, patchRegion(region, provinces), operationId, 'text/plain');
|
|
1041
1107
|
}
|
|
@@ -1219,6 +1285,7 @@ function* applySplitState(base, changes, allocations, operation, signal) {
|
|
|
1219
1285
|
const source = index.statesById.get(sourceStateId);
|
|
1220
1286
|
if (source === undefined)
|
|
1221
1287
|
throw new ServiceError('MAP_STATE_NOT_FOUND', `State ${sourceStateId} does not exist`);
|
|
1288
|
+
assertNoDatedStateHistory(source, operation.id);
|
|
1222
1289
|
const selectedSet = new Set(selected);
|
|
1223
1290
|
if (selected.length === 0 || selected.some((id) => !source.provinces.includes(id))) {
|
|
1224
1291
|
throw new ServiceError('MAP_STATE_PROVINCE_SELECTION_INVALID', 'Every new-state province must belong to the source state');
|
|
@@ -1336,6 +1403,8 @@ function applyMergeStates(base, changes, operation) {
|
|
|
1336
1403
|
if (target === undefined || sourceIds.length === 0 || sources.length !== sourceIds.length) {
|
|
1337
1404
|
throw new ServiceError('MAP_STATE_SELECTION_INVALID', 'Merge target and source states must exist');
|
|
1338
1405
|
}
|
|
1406
|
+
for (const source of sources)
|
|
1407
|
+
assertNoDatedStateHistory(source, operation.id);
|
|
1339
1408
|
const regionIds = new Set([...target.provinces, ...sources.flatMap((state) => state.provinces)].map((id) => regionMembershipId(index, id)));
|
|
1340
1409
|
if (regionIds.size !== 1)
|
|
1341
1410
|
throw new ServiceError('MAP_STATE_REGION_DISTRIBUTION_CONFLICT', 'Merged states must share one strategic region');
|
|
@@ -1964,12 +2033,16 @@ function* applyUpdateProvinceDefinition(base, changes, operation, signal) {
|
|
|
1964
2033
|
if (policy.ports === 'retain-if-valid') {
|
|
1965
2034
|
for (const position of dependentPortPositions) {
|
|
1966
2035
|
const locatedProvince = index.provinceAtMapCoordinate(position.x, position.z);
|
|
1967
|
-
|
|
1968
|
-
(targetType !== '
|
|
1969
|
-
position.
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2036
|
+
const incompatible = position.building === 'floating_harbor'
|
|
2037
|
+
? (locatedProvince === source.id && targetType !== 'sea') ||
|
|
2038
|
+
(position.adjacentSeaProvince === source.id && targetType !== 'land')
|
|
2039
|
+
: (locatedProvince === source.id &&
|
|
2040
|
+
(targetType !== 'land' ||
|
|
2041
|
+
position.stateId !== projectedStateId ||
|
|
2042
|
+
!updated.coastal ||
|
|
2043
|
+
index.definitionsById.get(position.adjacentSeaProvince)?.type !== 'sea')) ||
|
|
2044
|
+
(position.adjacentSeaProvince === source.id && targetType !== 'sea');
|
|
2045
|
+
if (incompatible)
|
|
1973
2046
|
provinceTypeDistributionConflict(source, targetType, 'ports', 'A retained port placement would no longer identify compatible land and sea provinces', {
|
|
1974
2047
|
stateId: position.stateId,
|
|
1975
2048
|
locatedProvince: locatedProvince ?? null,
|
|
@@ -379,6 +379,7 @@ function defaultMapPatterns(roots) {
|
|
|
379
379
|
}
|
|
380
380
|
function sourceTextPatterns(roots) {
|
|
381
381
|
return [
|
|
382
|
+
'common/bookmarks/**/*.txt',
|
|
382
383
|
...roots.map.flatMap((root) => {
|
|
383
384
|
const normalized = normalizeSourceRoot(root);
|
|
384
385
|
return ['map', 'csv', 'txt'].map((extension) => `${normalized}/**/*.${extension}`);
|