hoi4-agent-tools 3.10.0 → 4.0.1
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 +131 -0
- package/README.md +91 -147
- package/dist/bin/http.js +10 -0
- package/dist/bin/setup.js +3 -2
- package/dist/bin/stdio.js +10 -0
- package/dist/hoi4_agent_tools/core/artifacts.js +40 -5
- package/dist/hoi4_agent_tools/core/canonical.js +18 -4
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -0
- package/dist/hoi4_agent_tools/core/configuration.js +7 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
- package/dist/hoi4_agent_tools/core/domain-tools.js +65 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +5 -2
- package/dist/hoi4_agent_tools/core/engine.js +29 -2
- package/dist/hoi4_agent_tools/core/index.js +2 -2
- package/dist/hoi4_agent_tools/core/job-executor.js +14 -1
- package/dist/hoi4_agent_tools/core/job-operation-registration.js +15 -7
- package/dist/hoi4_agent_tools/core/job-store.js +78 -5
- package/dist/hoi4_agent_tools/core/job-transactions.js +31 -2
- package/dist/hoi4_agent_tools/core/job-worker-host.js +314 -127
- package/dist/hoi4_agent_tools/core/job-worker.js +96 -37
- package/dist/hoi4_agent_tools/core/operation-result.js +123 -10
- package/dist/hoi4_agent_tools/core/operation-tasks.js +49 -89
- package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +25 -21
- package/dist/hoi4_agent_tools/core/server-registry.js +22 -5
- package/dist/hoi4_agent_tools/core/shared-request-capacity.js +26 -1
- package/dist/hoi4_agent_tools/core/source/cache.js +20 -10
- package/dist/hoi4_agent_tools/core/source/lexer.js +11 -3
- package/dist/hoi4_agent_tools/core/temporary-directories.js +293 -0
- package/dist/hoi4_agent_tools/core/transactions.js +114 -23
- package/dist/hoi4_agent_tools/core/worker-exit.js +80 -0
- package/dist/hoi4_agent_tools/core/workspace.js +84 -8
- package/dist/hoi4_agent_tools/decision/ai.js +1 -1
- package/dist/hoi4_agent_tools/decision/service.js +28 -0
- package/dist/hoi4_agent_tools/decision/source-inventory.js +7 -5
- package/dist/hoi4_agent_tools/event/job-operations.js +9 -5
- package/dist/hoi4_agent_tools/event/operations.js +14 -1
- package/dist/hoi4_agent_tools/focus/job-operations.js +1 -2
- package/dist/hoi4_agent_tools/focus/tool-operations.js +1 -1
- package/dist/hoi4_agent_tools/gui/assets.js +3 -0
- package/dist/hoi4_agent_tools/gui/dds.js +8 -4
- package/dist/hoi4_agent_tools/gui/layout.js +45 -5
- package/dist/hoi4_agent_tools/gui/native-state.js +244 -0
- package/dist/hoi4_agent_tools/gui/renderer.js +12 -7
- package/dist/hoi4_agent_tools/gui/source-graph.js +17 -4
- package/dist/hoi4_agent_tools/gui/studio.js +33 -21
- package/dist/hoi4_agent_tools/gui/validators.js +8 -6
- package/dist/hoi4_agent_tools/map/bmp.js +1 -1
- package/dist/hoi4_agent_tools/map/derived.js +748 -0
- package/dist/hoi4_agent_tools/map/job-operations.js +7 -3
- package/dist/hoi4_agent_tools/map/model.js +103 -0
- package/dist/hoi4_agent_tools/map/operations.js +427 -58
- package/dist/hoi4_agent_tools/map/references.js +483 -0
- package/dist/hoi4_agent_tools/map/service.js +136 -61
- package/dist/hoi4_agent_tools/map/tool-operations.js +2 -1
- package/dist/hoi4_agent_tools/map/validation.js +3 -0
- package/dist/hoi4_agent_tools/map/world.js +1280 -0
- package/dist/hoi4_agent_tools/mcp/prompts/probability.js +2 -2
- package/dist/hoi4_agent_tools/mcp/server/argument-errors.js +72 -0
- package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +23 -5
- package/dist/hoi4_agent_tools/mcp/server/create.js +7 -0
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +24 -30
- package/dist/hoi4_agent_tools/mcp/server/tool-listing.js +45 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +28 -8
- package/dist/hoi4_agent_tools/mcp/tools/event.js +19 -30
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +5 -13
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -6
- package/dist/hoi4_agent_tools/mcp/tools/job.js +4 -4
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -18
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +42 -48
- package/dist/hoi4_agent_tools/mcp/tools/reference.js +26 -9
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +17 -30
- package/dist/hoi4_agent_tools/mcp/transports/http.js +28 -4
- package/dist/hoi4_agent_tools/mcp/transports/stdio-lifetime.js +31 -8
- package/dist/hoi4_agent_tools/probability/job-operations.js +4 -9
- package/dist/hoi4_agent_tools/probability/operations.js +175 -13
- package/dist/hoi4_agent_tools/probability/source-analysis.js +24 -1
- package/dist/hoi4_agent_tools/reference/error-log.js +359 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.js +307 -0
- package/dist/hoi4_agent_tools/reference/known-mistakes.json +154 -0
- package/dist/hoi4_agent_tools/reference/mod-index.js +101 -0
- package/dist/hoi4_agent_tools/reference/operations.js +88 -19
- package/dist/hoi4_agent_tools/reference/player-text.js +4 -3
- package/dist/hoi4_agent_tools/reference/script-validation.js +19 -1
- package/dist/hoi4_agent_tools/reference/service.js +6 -0
- package/dist/hoi4_agent_tools/runtime.js +14 -1
- package/dist/hoi4_agent_tools/schemas/event.js +78 -19
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +10 -0
- package/dist/hoi4_agent_tools/schemas/lenient-arguments.js +266 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +63 -20
- package/dist/hoi4_agent_tools/schemas/map.js +122 -3
- package/dist/hoi4_agent_tools/schemas/mod-tools.js +181 -0
- package/dist/hoi4_agent_tools/schemas/probability-requests.js +119 -9
- package/dist/hoi4_agent_tools/schemas/reference.js +13 -0
- package/dist/hoi4_agent_tools/schemas/scenarios.js +1 -3
- package/dist/hoi4_agent_tools/schemas/script-validation.js +13 -6
- package/dist/hoi4_agent_tools/schemas/technology.js +79 -21
- package/dist/hoi4_agent_tools/technology/job-operations.js +3 -4
- package/dist/hoi4_agent_tools/technology/operations.js +14 -1
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +7 -4
- package/docs/analysis.md +15 -2
- package/docs/configuration.md +281 -0
- package/docs/development.md +26 -2
- package/docs/events.md +29 -6
- package/docs/examples.md +13 -28
- package/docs/focus.md +127 -5
- package/docs/gui.md +105 -21
- package/docs/helper-expansion.md +9 -9
- package/docs/http.md +3 -1
- package/docs/jobs.md +23 -14
- package/docs/map.md +199 -7
- package/docs/mechanics.md +6 -2
- package/docs/probability.md +46 -16
- package/docs/reference.md +73 -5
- package/docs/setup.md +45 -14
- package/docs/technology.md +28 -4
- package/docs/third-party-notices.md +11 -0
- package/docs/tools.md +150 -42
- package/docs/troubleshooting.md +61 -0
- package/examples/clients/codex-config.toml +5 -2
- package/examples/clients/generic.json +2 -2
- package/examples/config/config.json +7 -0
- package/npm-shrinkwrap.json +151 -156
- package/package.json +13 -8
- package/schemas/configuration.schema.json +144 -128
- 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 +55 -0
- 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 +597 -126
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +51 -0
- package/schemas/operation-result.schema.json +2 -2
- 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 +3 -5
- package/schemas/scenario-test-input.schema.json +3 -5
- 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 +3 -3
|
@@ -157,7 +157,11 @@ function decodeRgb(bytes, dataOffset, width, height, bitCount, pitch, masks) {
|
|
|
157
157
|
const source = dataOffset + y * rowPitch + x * bytesPerPixel;
|
|
158
158
|
const pixel = bitCount === 32
|
|
159
159
|
? bytes.readUInt32LE(source)
|
|
160
|
-
:
|
|
160
|
+
: bitCount === 16
|
|
161
|
+
? bytes.readUInt16LE(source)
|
|
162
|
+
: byte(bytes, source) |
|
|
163
|
+
(byte(bytes, source + 1) << 8) |
|
|
164
|
+
(byte(bytes, source + 2) << 16);
|
|
161
165
|
const target = (y * width + x) * 4;
|
|
162
166
|
output[target] = extractChannel(pixel, masks[0], 0);
|
|
163
167
|
output[target + 1] = extractChannel(pixel, masks[1], 0);
|
|
@@ -231,8 +235,8 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
231
235
|
if ((flags & DDPF_RGB) === 0)
|
|
232
236
|
return unsupported('non-rgb', 'The DDS is neither RGB nor a recognised compressed format.');
|
|
233
237
|
const bitCount = bytes.readUInt32LE(88);
|
|
234
|
-
if (bitCount !== 24 && bitCount !== 32)
|
|
235
|
-
return unsupported(`rgb${bitCount}`, `RGB DDS bit depth ${bitCount} is unsupported; expected 24 or 32.`);
|
|
238
|
+
if (bitCount !== 16 && bitCount !== 24 && bitCount !== 32)
|
|
239
|
+
return unsupported(`rgb${bitCount}`, `RGB DDS bit depth ${bitCount} is unsupported; expected 16, 24 or 32.`);
|
|
236
240
|
const masks = [
|
|
237
241
|
bytes.readUInt32LE(92),
|
|
238
242
|
bytes.readUInt32LE(96),
|
|
@@ -240,7 +244,7 @@ export function decodeDds(bytes, budget = new RenderBudget(), operationKey = `dd
|
|
|
240
244
|
bytes.readUInt32LE(104),
|
|
241
245
|
];
|
|
242
246
|
const output = decodeRgb(bytes, 128, width, height, bitCount, rowPitch, masks);
|
|
243
|
-
const format = bitCount === 24 ? 'rgb24' : 'rgba32';
|
|
247
|
+
const format = bitCount === 16 ? 'rgb16' : bitCount === 24 ? 'rgb24' : 'rgba32';
|
|
244
248
|
return output === undefined
|
|
245
249
|
? unsupported(format, 'The DDS RGB pixel data is truncated.')
|
|
246
250
|
: { width, height, data: output, format };
|
|
@@ -2,6 +2,7 @@ import { compareCodeUnits, hashCanonical } from '../core/canonical.js';
|
|
|
2
2
|
import { assertRenderDimensions } from '../core/render-budget.js';
|
|
3
3
|
import { sortDiagnostics } from '../core/diagnostics.js';
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
|
+
import { nativeWindowState } from './native-state.js';
|
|
5
6
|
import { GuiAssetCatalog } from './assets.js';
|
|
6
7
|
import { GUI_GRAPH_MAX_EDGES, GUI_GRAPH_MAX_ELEMENTS, GUI_GRAPH_MAX_NODES, GUI_SCENE_MAX_DEPTH, GUI_SCENE_MAX_ELEMENTS, GUI_SCENE_MAX_TEXT_CHARACTERS, GUI_SCENE_MAX_TEXT_LAYOUT_OPERATIONS, GUI_SCENE_MAX_WORK, GUI_TEXT_MAX_CHARACTERS, GUI_TEXT_MAX_MISSING_GLYPH_SAMPLES, } from './limits.js';
|
|
7
8
|
import { guiElementAttributeFidelity } from './source-graph.js';
|
|
@@ -593,6 +594,24 @@ function anchoredCoordinate(start, span, offset, anchor) {
|
|
|
593
594
|
function legacyRowSuffix(instanceSuffix) {
|
|
594
595
|
return instanceSuffix.replace(/#list-[^#]+/gu, '');
|
|
595
596
|
}
|
|
597
|
+
/**
|
|
598
|
+
* Read a visibility value the way the game reads one: booleans, the script words yes/no and
|
|
599
|
+
* true/false, visible/hidden/shown, and the numbers 0 and 1. Anything else is not a decision.
|
|
600
|
+
*/
|
|
601
|
+
function visibilityValue(value) {
|
|
602
|
+
if (typeof value === 'boolean')
|
|
603
|
+
return value;
|
|
604
|
+
if (typeof value === 'number')
|
|
605
|
+
return value === 0 ? false : value === 1 ? true : undefined;
|
|
606
|
+
if (typeof value !== 'string')
|
|
607
|
+
return undefined;
|
|
608
|
+
const word = value.trim().toLowerCase();
|
|
609
|
+
if (['yes', 'true', '1', 'visible', 'shown', 'show'].includes(word))
|
|
610
|
+
return true;
|
|
611
|
+
if (['no', 'false', '0', 'hidden', 'hide'].includes(word))
|
|
612
|
+
return false;
|
|
613
|
+
return undefined;
|
|
614
|
+
}
|
|
596
615
|
function explicitElementVisibility(definition, scenario, rowValues) {
|
|
597
616
|
const scripted = rowValues?.[`${definition.name}.visible`] ??
|
|
598
617
|
scenario.values[`${definition.name}.visible`] ??
|
|
@@ -602,7 +621,14 @@ function explicitElementVisibility(definition, scenario, rowValues) {
|
|
|
602
621
|
(definition.sourceOwnerId === undefined
|
|
603
622
|
? undefined
|
|
604
623
|
: scenario.visibility[definition.sourceOwnerId]) ??
|
|
605
|
-
(
|
|
624
|
+
visibilityValue(scripted));
|
|
625
|
+
}
|
|
626
|
+
/** Source visibility: `visible` and `show` mean shown when true, `hide` and `hidden` when false. */
|
|
627
|
+
function declaredVisibility(attributes) {
|
|
628
|
+
const visible = scalarBoolean(property(attributes, 'visible', 'show'));
|
|
629
|
+
if (visible !== undefined)
|
|
630
|
+
return visible;
|
|
631
|
+
return !(scalarBoolean(property(attributes, 'hide', 'hidden')) ?? false);
|
|
606
632
|
}
|
|
607
633
|
function clippedByChain(chain) {
|
|
608
634
|
let result;
|
|
@@ -1124,8 +1150,7 @@ async function layoutAttachedScrollbars(definition, scrollbars, owner, childOutp
|
|
|
1124
1150
|
...(maximum === 0 ? { enabled: false } : {}),
|
|
1125
1151
|
},
|
|
1126
1152
|
};
|
|
1127
|
-
const sourceShown =
|
|
1128
|
-
!(scalarBoolean(property(bar.attributes, 'hide', 'hidden')) ?? false);
|
|
1153
|
+
const sourceShown = declaredVisibility(bar.attributes);
|
|
1129
1154
|
barLayouts.push({
|
|
1130
1155
|
definition: bar,
|
|
1131
1156
|
rect: { x: coordinate(true), y: coordinate(false), width: barWidth, height: barHeight },
|
|
@@ -1253,8 +1278,7 @@ async function layoutElement(definition, parentRect, inheritedClip, parentScale,
|
|
|
1253
1278
|
const parentUnresolvedVisibility = parentSceneElement?.visibilityStatus === 'unresolved'
|
|
1254
1279
|
? parentSceneElement.visibilityStatusReason
|
|
1255
1280
|
: undefined;
|
|
1256
|
-
const
|
|
1257
|
-
const sourceVisible = scalarBoolean(property(definition.attributes, 'visible')) ?? !declaredHidden;
|
|
1281
|
+
const sourceVisible = declaredVisibility(definition.attributes);
|
|
1258
1282
|
const resolveVisibleAppearance = inheritedVisible && (explicitlyVisible ?? sourceVisible);
|
|
1259
1283
|
const scriptedEnabled = rowValues?.[`${definition.name}.enabled`] ??
|
|
1260
1284
|
scenario.values[`${definition.name}.enabled`] ??
|
|
@@ -2158,13 +2182,29 @@ export async function buildGuiScene(graph, scannedFiles, windowName, scenario, c
|
|
|
2158
2182
|
addFidelity(fidelity, 'unresolved', 'scripted_gui_visibility', `${scripted.name}.visible requires more scenario facts; its source visibility remains unresolved.`);
|
|
2159
2183
|
}
|
|
2160
2184
|
}
|
|
2185
|
+
// Engine-controlled branches and icon frames come from the scenario country; scripted state,
|
|
2186
|
+
// then every explicit scenario value, outranks them.
|
|
2187
|
+
const native = nativeWindowState(windowName, new Set(graph.elements.map(({ name }) => name)), scenario, scannedFiles);
|
|
2188
|
+
if (native !== undefined) {
|
|
2189
|
+
for (const note of native.notes)
|
|
2190
|
+
addFidelity(fidelity, 'modelled', 'native_window_state', note);
|
|
2191
|
+
const { sources } = native.state;
|
|
2192
|
+
if (native.state.tag === undefined ||
|
|
2193
|
+
sources.faction === 'default' ||
|
|
2194
|
+
sources.balanceOfPower === 'default')
|
|
2195
|
+
addFidelity(fidelity, 'approximated', 'native_window_state', native.state.tag === undefined
|
|
2196
|
+
? '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.'
|
|
2197
|
+
: `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.`);
|
|
2198
|
+
}
|
|
2161
2199
|
const layoutScenario = {
|
|
2162
2200
|
...scenario,
|
|
2163
2201
|
values: {
|
|
2164
2202
|
...resolvedScriptedProperties,
|
|
2203
|
+
...(native?.values ?? {}),
|
|
2165
2204
|
...scenario.values,
|
|
2166
2205
|
},
|
|
2167
2206
|
visibility: {
|
|
2207
|
+
...(native?.visibility ?? {}),
|
|
2168
2208
|
...scriptedWindowVisibility,
|
|
2169
2209
|
...constantElementVisibility,
|
|
2170
2210
|
...scenario.visibility,
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { assignments, parseClausewitz } from '../core/source/index.js';
|
|
2
|
+
/** Windows whose branches the engine selects, keyed by the window's root element name. */
|
|
3
|
+
const NATIVE_BRANCHES = {
|
|
4
|
+
countrypoliticsview: [
|
|
5
|
+
{
|
|
6
|
+
elements: ['no_faction'],
|
|
7
|
+
shown: (state) => state.faction === 'none',
|
|
8
|
+
describe: 'the country is not in a faction',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
elements: ['original_in_faction'],
|
|
12
|
+
shown: (state) => state.faction === 'leader',
|
|
13
|
+
describe: 'the country leads its faction',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
elements: ['in_faction'],
|
|
17
|
+
shown: (state) => state.faction === 'member',
|
|
18
|
+
describe: 'the country is a member of a faction it does not lead',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
elements: ['power_balance_button'],
|
|
22
|
+
shown: (state) => state.balanceOfPower,
|
|
23
|
+
describe: 'the country has an active balance of power',
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Icons whose frame the game picks from the ruling ideology, as the ideology group at each
|
|
29
|
+
* one-based frame. `pol_factions_strip` holds fascism, communism, democratic and neutrality.
|
|
30
|
+
*/
|
|
31
|
+
const IDEOLOGY_FRAME_ICONS = {
|
|
32
|
+
pol_faction_icon: ['fascism', 'communism', 'democratic', 'neutrality'],
|
|
33
|
+
};
|
|
34
|
+
const DEFAULT_SCENARIO_DATE = '1936.1.1';
|
|
35
|
+
function scenarioDate(scenario) {
|
|
36
|
+
const parts = (scenario.date ?? DEFAULT_SCENARIO_DATE).split('.').map(Number);
|
|
37
|
+
return dateKey(parts[0] ?? 1936, parts[1] ?? 1, parts[2] ?? 1);
|
|
38
|
+
}
|
|
39
|
+
function dateKey(year, month, day) {
|
|
40
|
+
return year * 10_000 + month * 100 + day;
|
|
41
|
+
}
|
|
42
|
+
function scenarioCountryTag(scenario) {
|
|
43
|
+
const candidate = [
|
|
44
|
+
scenario.country?.tag,
|
|
45
|
+
scenario.country?.countryTag,
|
|
46
|
+
scenario.country?.GetTag,
|
|
47
|
+
].find((value) => typeof value === 'string' && /^[A-Za-z0-9_]{2,64}$/u.test(value));
|
|
48
|
+
return candidate?.toUpperCase();
|
|
49
|
+
}
|
|
50
|
+
function scenarioString(scenario, ...keys) {
|
|
51
|
+
for (const key of keys) {
|
|
52
|
+
const value = scenario.country?.[key];
|
|
53
|
+
if (typeof value === 'string' && /^[A-Za-z0-9_]{2,64}$/u.test(value))
|
|
54
|
+
return value.toLowerCase();
|
|
55
|
+
}
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
function scenarioBoolean(scenario, ...keys) {
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
const value = scenario.country?.[key];
|
|
61
|
+
if (typeof value === 'boolean')
|
|
62
|
+
return value;
|
|
63
|
+
if (typeof value === 'string' && /^(?:yes|true)$/iu.test(value))
|
|
64
|
+
return true;
|
|
65
|
+
if (typeof value === 'string' && /^(?:no|false)$/iu.test(value))
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
function explicitFaction(scenario) {
|
|
71
|
+
const named = scenarioString(scenario, 'faction', 'factionRole');
|
|
72
|
+
if (named === 'none' || named === 'member' || named === 'leader')
|
|
73
|
+
return named;
|
|
74
|
+
const leader = scenarioBoolean(scenario, 'factionLeader', 'isFactionLeader');
|
|
75
|
+
if (leader === true)
|
|
76
|
+
return 'leader';
|
|
77
|
+
const member = scenarioBoolean(scenario, 'inFaction', 'isInFaction', 'faction');
|
|
78
|
+
if (member === false)
|
|
79
|
+
return 'none';
|
|
80
|
+
if (member === true)
|
|
81
|
+
return 'member';
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const FACTION_CREATION = new Set(['create_faction', 'create_faction_from_template']);
|
|
85
|
+
function visitHistory(entries, cutoff, facts, members, owner) {
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.type !== 'assignment')
|
|
88
|
+
continue;
|
|
89
|
+
const key = entry.key.value;
|
|
90
|
+
const dated = /^(\d{1,4})\.(\d{1,2})\.(\d{1,2})$/u.exec(key);
|
|
91
|
+
if (dated !== null) {
|
|
92
|
+
// A dated block applies from its date; later blocks have not happened yet.
|
|
93
|
+
if (entry.value.type === 'block' && dateKey(+dated[1], +dated[2], +dated[3]) <= cutoff)
|
|
94
|
+
visitHistory(entry.value.entries, cutoff, facts, members, owner);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (key === 'set_politics' && entry.value.type === 'block') {
|
|
98
|
+
const party = assignments(entry.value, 'ruling_party')[0]?.value;
|
|
99
|
+
if (party?.type === 'scalar')
|
|
100
|
+
facts.ideology = party.value.toLowerCase();
|
|
101
|
+
}
|
|
102
|
+
else if (FACTION_CREATION.has(key)) {
|
|
103
|
+
facts.createsFaction = true;
|
|
104
|
+
}
|
|
105
|
+
else if (key === 'set_power_balance') {
|
|
106
|
+
facts.powerBalance = true;
|
|
107
|
+
}
|
|
108
|
+
else if (key === 'remove_power_balance') {
|
|
109
|
+
facts.powerBalance = false;
|
|
110
|
+
}
|
|
111
|
+
else if (key === 'add_to_faction' &&
|
|
112
|
+
entry.value.type === 'scalar' &&
|
|
113
|
+
entry.value.value.toUpperCase() !== owner) {
|
|
114
|
+
const tag = entry.value.value.toUpperCase();
|
|
115
|
+
const joined = members.get(tag) ?? new Set();
|
|
116
|
+
joined.add(owner);
|
|
117
|
+
members.set(tag, joined);
|
|
118
|
+
}
|
|
119
|
+
else if ((key === 'if' || key === 'else' || key === 'else_if') &&
|
|
120
|
+
entry.value.type === 'block') {
|
|
121
|
+
// Conditions such as DLC checks cannot be evaluated here; their content is treated as
|
|
122
|
+
// part of the start, which matches a fully owned game.
|
|
123
|
+
visitHistory(entry.value.entries, cutoff, facts, members, owner);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function historyTag(file) {
|
|
128
|
+
const match = /^history\/countries\/([A-Za-z0-9_]{2,64})(?:\s*-[^/]*)?\.txt$/u.exec(file.relativePath.replaceAll('\\', '/'));
|
|
129
|
+
return match?.[1]?.toUpperCase();
|
|
130
|
+
}
|
|
131
|
+
function historyState(tag, files, cutoff) {
|
|
132
|
+
const facts = {
|
|
133
|
+
createsFaction: false,
|
|
134
|
+
powerBalance: false,
|
|
135
|
+
};
|
|
136
|
+
// `add_to_faction = X` in a leader's history adds X to that leader's faction, so a country
|
|
137
|
+
// is a member when another country's history adds it.
|
|
138
|
+
const additions = new Map();
|
|
139
|
+
let found = false;
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
if (file.shadowedBy !== undefined)
|
|
142
|
+
continue;
|
|
143
|
+
const fileTag = historyTag(file);
|
|
144
|
+
if (fileTag === undefined)
|
|
145
|
+
continue;
|
|
146
|
+
const owned = fileTag === tag;
|
|
147
|
+
if (owned)
|
|
148
|
+
found = true;
|
|
149
|
+
// Only the country's own history carries its other facts; every other file can matter
|
|
150
|
+
// solely through the members it adds to a faction.
|
|
151
|
+
else if (!file.bytes.includes('add_to_faction'))
|
|
152
|
+
continue;
|
|
153
|
+
const document = parseClausewitz(file.bytes, file.displayPath);
|
|
154
|
+
const own = {
|
|
155
|
+
createsFaction: false,
|
|
156
|
+
powerBalance: false,
|
|
157
|
+
};
|
|
158
|
+
visitHistory(document.root.entries, cutoff, owned ? facts : own, additions, fileTag);
|
|
159
|
+
}
|
|
160
|
+
const memberOfFaction = (additions.get(tag)?.size ?? 0) > 0;
|
|
161
|
+
return { facts, found, memberOfFaction };
|
|
162
|
+
}
|
|
163
|
+
/** Resolve the country facts the engine would use, explicit scenario values first. */
|
|
164
|
+
export function resolveNativeCountryState(scenario, files) {
|
|
165
|
+
const tag = scenarioCountryTag(scenario);
|
|
166
|
+
const history = tag === undefined
|
|
167
|
+
? undefined
|
|
168
|
+
: historyState(tag, files.filter(({ relativePath }) => relativePath.startsWith('history/countries/')), scenarioDate(scenario));
|
|
169
|
+
const knownHistory = history?.found === true ? history : undefined;
|
|
170
|
+
const ideologyFromScenario = scenarioString(scenario, 'ideology', 'rulingIdeology', 'countryIdeology');
|
|
171
|
+
const factionFromScenario = explicitFaction(scenario);
|
|
172
|
+
const bopFromScenario = scenarioBoolean(scenario, 'balanceOfPower', 'hasBalanceOfPower', 'powerBalance');
|
|
173
|
+
const ideology = ideologyFromScenario ?? knownHistory?.facts.ideology;
|
|
174
|
+
const faction = factionFromScenario ??
|
|
175
|
+
(knownHistory === undefined
|
|
176
|
+
? 'none'
|
|
177
|
+
: knownHistory.facts.createsFaction
|
|
178
|
+
? 'leader'
|
|
179
|
+
: knownHistory.memberOfFaction
|
|
180
|
+
? 'member'
|
|
181
|
+
: 'none');
|
|
182
|
+
return {
|
|
183
|
+
tag,
|
|
184
|
+
ideology,
|
|
185
|
+
faction,
|
|
186
|
+
balanceOfPower: bopFromScenario ?? knownHistory?.facts.powerBalance ?? false,
|
|
187
|
+
sources: {
|
|
188
|
+
ideology: ideologyFromScenario !== undefined
|
|
189
|
+
? 'scenario'
|
|
190
|
+
: knownHistory?.facts.ideology !== undefined
|
|
191
|
+
? 'history'
|
|
192
|
+
: 'unknown',
|
|
193
|
+
faction: factionFromScenario !== undefined
|
|
194
|
+
? 'scenario'
|
|
195
|
+
: knownHistory !== undefined
|
|
196
|
+
? 'history'
|
|
197
|
+
: 'default',
|
|
198
|
+
balanceOfPower: bopFromScenario !== undefined
|
|
199
|
+
? 'scenario'
|
|
200
|
+
: knownHistory !== undefined
|
|
201
|
+
? 'history'
|
|
202
|
+
: 'default',
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** Whether any native decision can apply to this window, so the country state is worth resolving. */
|
|
207
|
+
export function windowHasNativeState(windowName, elementNames) {
|
|
208
|
+
return (NATIVE_BRANCHES[windowName] !== undefined ||
|
|
209
|
+
Object.keys(IDEOLOGY_FRAME_ICONS).some((name) => elementNames.has(name)));
|
|
210
|
+
}
|
|
211
|
+
/** The engine-selected branches and frames for a scene, below any explicit scenario input. */
|
|
212
|
+
export function nativeWindowState(windowName, elementNames, scenario, files) {
|
|
213
|
+
if (!windowHasNativeState(windowName, elementNames))
|
|
214
|
+
return undefined;
|
|
215
|
+
const state = resolveNativeCountryState(scenario, files);
|
|
216
|
+
const visibility = {};
|
|
217
|
+
const values = {};
|
|
218
|
+
const notes = [];
|
|
219
|
+
const subject = state.tag ?? 'the scenario country';
|
|
220
|
+
for (const rule of NATIVE_BRANCHES[windowName] ?? []) {
|
|
221
|
+
const shown = rule.shown(state);
|
|
222
|
+
for (const name of rule.elements)
|
|
223
|
+
visibility[name] = shown;
|
|
224
|
+
notes.push(`${rule.elements.join(', ')} ${shown ? 'is shown' : 'is hidden'} for ${subject}; the game shows it only when ${rule.describe}.`);
|
|
225
|
+
}
|
|
226
|
+
for (const [name, order] of Object.entries(IDEOLOGY_FRAME_ICONS)) {
|
|
227
|
+
if (!elementNames.has(name) || state.ideology === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
const index = order.indexOf(state.ideology);
|
|
230
|
+
if (index < 0)
|
|
231
|
+
continue;
|
|
232
|
+
values[`${name}.frame`] = index + 1;
|
|
233
|
+
notes.push(`${name} shows the ${state.ideology} frame for ${subject}.`);
|
|
234
|
+
}
|
|
235
|
+
return { visibility, values, notes, state };
|
|
236
|
+
}
|
|
237
|
+
/** Whether the scenario needs history to resolve its native state, and so to be scanned. */
|
|
238
|
+
export function scenarioNeedsCountryHistory(scenario) {
|
|
239
|
+
if (scenarioCountryTag(scenario) === undefined)
|
|
240
|
+
return false;
|
|
241
|
+
return (scenarioString(scenario, 'ideology', 'rulingIdeology', 'countryIdeology') === undefined ||
|
|
242
|
+
explicitFaction(scenario) === undefined ||
|
|
243
|
+
scenarioBoolean(scenario, 'balanceOfPower', 'hasBalanceOfPower', 'powerBalance') === undefined);
|
|
244
|
+
}
|
|
@@ -658,15 +658,20 @@ export async function renderGuiScene(scene, variants = defaultVariants, signal,
|
|
|
658
658
|
const basePng = await sharp(Buffer.from(full.svg), { limitInputPixels: RENDER_MAX_PIXELS })
|
|
659
659
|
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
660
660
|
.toBuffer();
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
661
|
+
// Derived variants are rasterized one at a time: each decodes the full-size base raster, so
|
|
662
|
+
// running them together multiplies the peak native memory of one render.
|
|
663
|
+
const rendered = [];
|
|
664
|
+
for (const item of prepared) {
|
|
665
|
+
if (item.variant === 'full') {
|
|
666
|
+
rendered.push({
|
|
664
667
|
variant: item.variant,
|
|
665
668
|
svg: item.svg,
|
|
666
669
|
png: basePng,
|
|
667
670
|
width: item.view.width,
|
|
668
671
|
height: item.view.height,
|
|
669
|
-
};
|
|
672
|
+
});
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
670
675
|
signal?.throwIfAborted();
|
|
671
676
|
budget.reserveRasterOperation(`gui-derived-variant:${sha256Bytes(item.svg)}`, `GUI ${item.variant} derived rasterization`);
|
|
672
677
|
const png = item.variant === 'cropped'
|
|
@@ -684,14 +689,14 @@ export async function renderGuiScene(scene, variants = defaultVariants, signal,
|
|
|
684
689
|
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
685
690
|
.toBuffer();
|
|
686
691
|
signal?.throwIfAborted();
|
|
687
|
-
|
|
692
|
+
rendered.push({
|
|
688
693
|
variant: item.variant,
|
|
689
694
|
svg: item.svg,
|
|
690
695
|
png,
|
|
691
696
|
width: item.view.width,
|
|
692
697
|
height: item.view.height,
|
|
693
|
-
};
|
|
694
|
-
}
|
|
698
|
+
});
|
|
699
|
+
}
|
|
695
700
|
images = rendered;
|
|
696
701
|
}
|
|
697
702
|
return {
|
|
@@ -76,6 +76,7 @@ const modelledElementAttributes = new Set([
|
|
|
76
76
|
'hide',
|
|
77
77
|
'hidden',
|
|
78
78
|
'visible',
|
|
79
|
+
'show',
|
|
79
80
|
'enabled',
|
|
80
81
|
'context_aware_text',
|
|
81
82
|
]);
|
|
@@ -1063,9 +1064,13 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
1063
1064
|
for (const font of fonts) {
|
|
1064
1065
|
const key = font.name.toLocaleLowerCase('en-US');
|
|
1065
1066
|
const existing = fontByName.get(key);
|
|
1067
|
+
const declared = font.kind === 'bitmapfont';
|
|
1068
|
+
const existingDeclared = existing?.kind === 'bitmapfont';
|
|
1066
1069
|
if (existing === undefined ||
|
|
1067
|
-
(
|
|
1068
|
-
(
|
|
1070
|
+
(!existingDeclared && declared) ||
|
|
1071
|
+
(existingDeclared === declared &&
|
|
1072
|
+
((existing.override && !font.override) ||
|
|
1073
|
+
(existing.languages.length > 0 && font.languages.length === 0))))
|
|
1069
1074
|
fontByName.set(key, font);
|
|
1070
1075
|
}
|
|
1071
1076
|
const elementByName = new Map();
|
|
@@ -1126,7 +1131,7 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
1126
1131
|
addEdge(edges, 'uses_font', element.id, font?.id ?? `font:${fontName}`, font !== undefined, { fontName }, element.location);
|
|
1127
1132
|
}
|
|
1128
1133
|
for (const key of localisationAttributeNames) {
|
|
1129
|
-
const value = element.attributes
|
|
1134
|
+
const value = localisationAttributeValue(element.attributes, key);
|
|
1130
1135
|
if (typeof value !== 'string' ||
|
|
1131
1136
|
value.trim().length === 0 ||
|
|
1132
1137
|
!isStaticGuiLocalisationReference(value))
|
|
@@ -1206,6 +1211,14 @@ const localisationAttributeNames = [
|
|
|
1206
1211
|
'pdx_tooltip_delayed',
|
|
1207
1212
|
'hint_tag',
|
|
1208
1213
|
];
|
|
1214
|
+
/** Clausewitz keys are case-insensitive: vanilla spells the button label `buttontext`. */
|
|
1215
|
+
function localisationAttributeValue(attributes, name) {
|
|
1216
|
+
const lowered = name.toLowerCase();
|
|
1217
|
+
for (const [key, value] of Object.entries(attributes))
|
|
1218
|
+
if (key.toLowerCase() === lowered)
|
|
1219
|
+
return value;
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1209
1222
|
function isStaticGuiLocalisationReference(value) {
|
|
1210
1223
|
const trimmed = value.trim();
|
|
1211
1224
|
return !(trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
@@ -1214,7 +1227,7 @@ function referencedGuiLocalisationKeys(elements, scriptedLocalisation) {
|
|
|
1214
1227
|
const keys = new Set(scriptedLocalisation.flatMap(({ localisationKeys }) => localisationKeys));
|
|
1215
1228
|
for (const element of elements) {
|
|
1216
1229
|
for (const attribute of localisationAttributeNames) {
|
|
1217
|
-
const value = element.attributes
|
|
1230
|
+
const value = localisationAttributeValue(element.attributes, attribute);
|
|
1218
1231
|
if (typeof value === 'string' &&
|
|
1219
1232
|
value.trim().length > 0 &&
|
|
1220
1233
|
isStaticGuiLocalisationReference(value))
|
|
@@ -17,6 +17,7 @@ import { TRANSACTION_MAX_DIAGNOSTICS } from '../core/transaction-limits.js';
|
|
|
17
17
|
import { PACKAGE_VERSION } from '../version.js';
|
|
18
18
|
import { isSafeAnimationSourcePath } from './animation-manifest.js';
|
|
19
19
|
import { GuiAssetCatalog, parseBmFont } from './assets.js';
|
|
20
|
+
import { scenarioNeedsCountryHistory } from './native-state.js';
|
|
20
21
|
import { projectGuiGraphForArtifact } from './inspection-artifact.js';
|
|
21
22
|
import { buildGuiScene } from './layout.js';
|
|
22
23
|
import { GUI_VALIDATION_MAX_DIAGNOSTICS } from './limits.js';
|
|
@@ -333,7 +334,13 @@ function assertTargetWindowAvailable(graph, windowName) {
|
|
|
333
334
|
})),
|
|
334
335
|
});
|
|
335
336
|
}
|
|
337
|
+
const lowerCaseKeySets = new WeakMap();
|
|
336
338
|
function collectNamedAttributes(value, keys, output) {
|
|
339
|
+
let lowered = lowerCaseKeySets.get(keys);
|
|
340
|
+
if (lowered === undefined) {
|
|
341
|
+
lowered = new Set([...keys].map((key) => key.toLowerCase()));
|
|
342
|
+
lowerCaseKeySets.set(keys, lowered);
|
|
343
|
+
}
|
|
337
344
|
if (Array.isArray(value)) {
|
|
338
345
|
for (const entry of value)
|
|
339
346
|
collectNamedAttributes(entry, keys, output);
|
|
@@ -342,7 +349,7 @@ function collectNamedAttributes(value, keys, output) {
|
|
|
342
349
|
if (typeof value !== 'object' || value === null)
|
|
343
350
|
return;
|
|
344
351
|
for (const [key, entry] of Object.entries(value)) {
|
|
345
|
-
if (
|
|
352
|
+
if (lowered.has(key.toLowerCase()) && typeof entry === 'string')
|
|
346
353
|
output.add(entry);
|
|
347
354
|
collectNamedAttributes(entry, keys, output);
|
|
348
355
|
}
|
|
@@ -559,6 +566,16 @@ export function scenarioCountryFlagPatterns(scenarios) {
|
|
|
559
566
|
`gfx/flags/${directory}${tag}_*.{bmp,dds,png,tga}`,
|
|
560
567
|
]));
|
|
561
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Engine-controlled windows read the scenario country's faction, balance of power and ruling
|
|
571
|
+
* party from country history; the faction facts also live in other countries' histories.
|
|
572
|
+
*/
|
|
573
|
+
function scenarioCountryHistoryPatterns(scenarios) {
|
|
574
|
+
return scenarios.some(scenarioNeedsCountryHistory) ? ['history/countries/*.txt'] : [];
|
|
575
|
+
}
|
|
576
|
+
function scenarioCountryAssetPatterns(scenarios) {
|
|
577
|
+
return [...scenarioCountryFlagPatterns(scenarios), ...scenarioCountryHistoryPatterns(scenarios)];
|
|
578
|
+
}
|
|
562
579
|
function bmFontPagePatterns(files) {
|
|
563
580
|
const references = files.flatMap((file) => {
|
|
564
581
|
if (!file.relativePath.toLowerCase().endsWith('.fnt') || file.shadowedBy !== undefined)
|
|
@@ -1072,7 +1089,7 @@ export class ScriptedGuiStudio {
|
|
|
1072
1089
|
const generatedOptions = input.generatedScenarios === undefined
|
|
1073
1090
|
? undefined
|
|
1074
1091
|
: parseGeneratedScenarioOptions(input.generatedScenarios);
|
|
1075
|
-
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]),
|
|
1092
|
+
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]), scenarioCountryAssetPatterns([placeholderScenario, ...explicitRelatedScenarios]), scenarioLocalisationKeys([placeholderScenario, ...explicitRelatedScenarios]), [placeholderScenario, ...explicitRelatedScenarios].some(({ tooltip }) => tooltip !== undefined));
|
|
1076
1093
|
let builtDefinitions;
|
|
1077
1094
|
const evaluationDefinitions = () => (builtDefinitions ??= ClausewitzEvaluationDefinitions.build(scanned));
|
|
1078
1095
|
const resolveLocalisation = (target) => resolveScenarioScriptedLocalisation(scanned.graph, input.windowName, target, evaluationDefinitions);
|
|
@@ -1106,15 +1123,14 @@ export class ScriptedGuiStudio {
|
|
|
1106
1123
|
const budget = new RenderBudget();
|
|
1107
1124
|
const beforeScenario = parsePreviewScenario(input.before);
|
|
1108
1125
|
const afterScenario = parsePreviewScenario(input.after);
|
|
1109
|
-
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]),
|
|
1126
|
+
const scanned = await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [beforeScenario.language, afterScenario.language], scenarioSpriteNames([beforeScenario, afterScenario]), scenarioTemplateNames([beforeScenario, afterScenario]), scenarioCountryAssetPatterns([beforeScenario, afterScenario]), scenarioLocalisationKeys([beforeScenario, afterScenario]), [beforeScenario, afterScenario].some(({ tooltip }) => tooltip !== undefined));
|
|
1110
1127
|
const beforeCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, beforeScenario.language);
|
|
1111
1128
|
const afterCatalog = new GuiAssetCatalog(scanned.graph, scanned.files, budget, afterScenario.language);
|
|
1112
1129
|
const before = await buildGuiScene(scanned.graph, scanned.files, input.windowName, beforeScenario, beforeCatalog);
|
|
1113
1130
|
const after = await buildGuiScene(scanned.graph, scanned.files, input.windowName, afterScenario, afterCatalog);
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
]);
|
|
1131
|
+
// One full raster at a time keeps the peak native memory of a comparison to a single render.
|
|
1132
|
+
const beforeRender = await renderGuiScene(before, ['full'], input.signal, budget);
|
|
1133
|
+
const afterRender = await renderGuiScene(after, ['full'], input.signal, budget);
|
|
1118
1134
|
const comparison = await compareGuiImages(fullImage(beforeRender).png, fullImage(afterRender).png, budget, input.signal);
|
|
1119
1135
|
const evidenceJson = `${canonicalJson({
|
|
1120
1136
|
offline: true,
|
|
@@ -1159,7 +1175,7 @@ export class ScriptedGuiStudio {
|
|
|
1159
1175
|
placeholderScenario,
|
|
1160
1176
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1161
1177
|
...explicitRelatedScenarios,
|
|
1162
|
-
]),
|
|
1178
|
+
]), scenarioCountryAssetPatterns([
|
|
1163
1179
|
placeholderScenario,
|
|
1164
1180
|
...(requestedBaselineScenario === undefined ? [] : [requestedBaselineScenario]),
|
|
1165
1181
|
...explicitRelatedScenarios,
|
|
@@ -1316,17 +1332,13 @@ export class ScriptedGuiStudio {
|
|
|
1316
1332
|
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(scenarioGallerySvg)}`, 'GUI scripted scenario gallery SVG rasterization');
|
|
1317
1333
|
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(stateGallerySvg)}`, 'GUI state gallery SVG rasterization');
|
|
1318
1334
|
galleryBudget.reserveRasterOperation(`gui-gallery:${sha256Bytes(resolutionGallerySvg)}`, 'GUI resolution gallery SVG rasterization');
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
sharp(Buffer.from(resolutionGallerySvg), { limitInputPixels: RENDER_MAX_PIXELS })
|
|
1327
|
-
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
1328
|
-
.toBuffer(),
|
|
1329
|
-
]);
|
|
1335
|
+
const rasterizeGallery = (svg) => sharp(Buffer.from(svg), { limitInputPixels: RENDER_MAX_PIXELS })
|
|
1336
|
+
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
1337
|
+
.toBuffer();
|
|
1338
|
+
// Gallery canvases are large and independent; rasterize them one at a time.
|
|
1339
|
+
const scenarioGalleryPng = await rasterizeGallery(scenarioGallerySvg);
|
|
1340
|
+
const stateGalleryPng = await rasterizeGallery(stateGallerySvg);
|
|
1341
|
+
const resolutionGalleryPng = await rasterizeGallery(resolutionGallerySvg);
|
|
1330
1342
|
const slug = safeSlug(input.windowName);
|
|
1331
1343
|
const projectedArtifactGraph = projectGuiGraphForArtifact(scanned.graph, [
|
|
1332
1344
|
...new Set([...scenarioScenes, ...stateScenes, ...resolutionScenes, baselineScene].flatMap(({ elements }) => elements.map(({ sourceId }) => sourceId))),
|
|
@@ -1522,7 +1534,7 @@ export class ScriptedGuiStudio {
|
|
|
1522
1534
|
const previewScenario = input.scenario === undefined ? undefined : parsePreviewScenario(input.scenario);
|
|
1523
1535
|
const initiallyScanned = input.windowName === undefined
|
|
1524
1536
|
? await this.scan(input.workspaceId, input.principal, input.signal)
|
|
1525
|
-
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]),
|
|
1537
|
+
: await this.scanWindow(input.workspaceId, input.windowName, input.principal, input.signal, [previewScenario.language], scenarioSpriteNames([previewScenario]), scenarioTemplateNames([previewScenario]), scenarioCountryAssetPatterns([previewScenario]), scenarioLocalisationKeys([previewScenario]), previewScenario.tooltip !== undefined);
|
|
1526
1538
|
const exactTargets = await this.scanner.scan(workspace, {
|
|
1527
1539
|
patterns: prepared.map(({ relativePath }) => relativePath),
|
|
1528
1540
|
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
@@ -1544,7 +1556,7 @@ export class ScriptedGuiStudio {
|
|
|
1544
1556
|
let proposedGraph = buildGuiSourceGraph(proposedFiles, this.engine.indexFiles(proposedFiles));
|
|
1545
1557
|
const proposedAssetPatterns = [
|
|
1546
1558
|
...referencedAssetPatternsForWindow(proposedGraph, input.windowName, [], [scenario.language], scenarioTemplateNames([scenario])),
|
|
1547
|
-
...
|
|
1559
|
+
...scenarioCountryAssetPatterns([scenario]),
|
|
1548
1560
|
];
|
|
1549
1561
|
const exactProposedAssets = proposedAssetPatterns.length === 0
|
|
1550
1562
|
? []
|
|
@@ -490,15 +490,17 @@ function validateOverlapAndGeometry(scene, diagnostics, work) {
|
|
|
490
490
|
if (overlap === undefined)
|
|
491
491
|
continue;
|
|
492
492
|
const ratio = area(overlap) / Math.max(1, Math.min(area(left.rect), area(right.rect)));
|
|
493
|
+
// Tab pages and alternative panels overlap by design; the game shows one at a time.
|
|
494
|
+
// When the scenario cannot decide which, the preview draws them stacked, and the pair
|
|
495
|
+
// is reported only as missing scenario facts, not as a layout or click conflict.
|
|
496
|
+
if (left.visibilityStatus === 'unresolved' || right.visibilityStatus === 'unresolved') {
|
|
497
|
+
if (left.depth === right.depth && ratio > 0.2)
|
|
498
|
+
diagnostics.push(issue('GUI_UNRESOLVED_VISIBILITY_OVERLAP', 'warning', 'layout', `${left.name} and ${right.name} overlap and the scenario does not decide which is visible, so the preview draws both. Declare the flags or variables their visible triggers read, with closedFlags, to render one of them.`, right, { left: left.id, right: right.id, ratio }));
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
493
501
|
if (left.clickable && right.clickable && ratio > 0.02) {
|
|
494
502
|
diagnostics.push(issue('GUI_CONFLICTING_CLICK_REGIONS', 'error', 'design', `${left.name} and ${right.name} have overlapping click regions (${(ratio * 100).toFixed(1)}%).`, right, { left: left.id, right: right.id, ratio }));
|
|
495
503
|
}
|
|
496
|
-
// Tab pages and alternative panels overlap by design; the game shows one at a time.
|
|
497
|
-
// When the scenario cannot decide which, the preview draws them stacked.
|
|
498
|
-
if (left.depth === right.depth &&
|
|
499
|
-
ratio > 0.2 &&
|
|
500
|
-
(left.visibilityStatus === 'unresolved' || right.visibilityStatus === 'unresolved'))
|
|
501
|
-
diagnostics.push(issue('GUI_UNRESOLVED_VISIBILITY_OVERLAP', 'warning', 'layout', `${left.name} and ${right.name} overlap and the scenario does not decide which is visible, so the preview draws both. Declare the flags or variables their visible triggers read, with closedFlags, to render one of them.`, right, { left: left.id, right: right.id, ratio }));
|
|
502
504
|
if (left.depth === right.depth && ratio > 0.2) {
|
|
503
505
|
diagnostics.push(issue('GUI_VISIBLE_OVERLAP', 'warning', 'layout', `${left.name} and ${right.name} overlap by ${(ratio * 100).toFixed(1)}% of the smaller element.`, right, { left: left.id, right: right.id, ratio }));
|
|
504
506
|
}
|
|
@@ -314,7 +314,7 @@ export function createBmp(options) {
|
|
|
314
314
|
bytes.writeUInt32LE(0, 30);
|
|
315
315
|
bytes.writeUInt32LE(stride * options.height, 34);
|
|
316
316
|
if (options.bitsPerPixel === 8)
|
|
317
|
-
bytes.writeUInt32LE(
|
|
317
|
+
bytes.writeUInt32LE(options.colorsUsed ?? palette.length, 46);
|
|
318
318
|
for (const [index, color] of palette.entries()) {
|
|
319
319
|
const offset = 14 + dibSize + index * 4;
|
|
320
320
|
bytes[offset] = color.b;
|