hoi4-agent-tools 3.4.1 → 3.5.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 +24 -1
- package/README.md +26 -18
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +41 -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/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/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/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/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 +444 -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 +161 -0
- package/dist/hoi4_agent_tools/technology/render.js +9 -4
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +3 -0
- package/docs/gui.md +6 -0
- package/docs/map.md +8 -0
- package/docs/probability.md +7 -0
- package/docs/reference.md +32 -0
- package/docs/research/reference-benchmark.md +42 -0
- package/docs/setup.md +6 -0
- 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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 3.5.1 - 2026-09-27
|
|
6
|
+
|
|
7
|
+
- Preserve installed-game and offline-wiki citations in compact reference context bundles, select question-relevant sections within required sources, and report sources omitted by the result limit separately from missing files.
|
|
8
|
+
- Exercise one-, two-, and eight-result context budgets across six real-source work surfaces while retaining the nine-query discovery benchmark.
|
|
9
|
+
- Allow up to five minutes for npm registry processing before publication verification fails.
|
|
10
|
+
|
|
11
|
+
## 3.5.0 - 2026-09-26
|
|
12
|
+
|
|
13
|
+
- Search and read bounded, revision-bound local offline-wiki, installed-game, and optional generated-script documentation sections; retrieve task citation bundles and exact indexed source definitions and usages through four read-only MCP tools.
|
|
14
|
+
- Compare hash-bound frozen probability source files at their logical gameplay paths while preserving physical source provenance and leaving current files untouched.
|
|
15
|
+
- Keep candidate eligibility overrides separate from weight-modifier conditions so an eligible scenario does not force every modifier to apply.
|
|
16
|
+
- Evaluate hidden and custom-tooltip trigger wrappers with their enclosed conditions while excluding presentation metadata from eligibility logic.
|
|
17
|
+
- Accept explicit GUI scenario dates and state-controller facts, evaluate their source conditions, render requested native windows at their declared settled show position, and resolve native list templates and scenario-only localisation from the correct source.
|
|
18
|
+
- Honor single-line native labels and preserve the first inferred font line, with explicit fidelity reporting for undersized native text-height conventions.
|
|
19
|
+
- Preserve dotted event namespaces so a namespace query selects its own events rather than unrelated systems sharing the first name segment.
|
|
20
|
+
- Compact selected event diagrams without retaining empty row gaps from the rest of the workspace graph.
|
|
21
|
+
- Resolve supported dated state-history values at the earliest active bookmark for map inspection, and refuse state rewrites that would silently conflict with or discard dated commands.
|
|
22
|
+
- Interpret floating-harbor placements as sea-side markers with land targets, preserving vanilla-compatible anomalies as source-linked review warnings instead of thousands of false map errors.
|
|
23
|
+
- Show alternatives within a national focus prerequisite group with dotted connectors, leaving individually required prerequisites solid.
|
|
24
|
+
- Draw wide technology-folder icons at their decoded sprite dimensions and the source GUI's icon anchor, preserving 131×52 chemical cylinders instead of shrinking them to 70×70.
|
|
25
|
+
- Include user-approved in-game focus and technology captures beside current-source and source-matched historical MCP renders, with source revisions and explicit whole-image comparison limits.
|
|
26
|
+
- Include native-size vanilla and Chaos Redux interface references with declared scenarios, full images, diagnostics, and source hashes; keep game imagery outside the npm package.
|
|
27
|
+
|
|
5
28
|
## 3.4.1 - 2026-09-25
|
|
6
29
|
|
|
7
30
|
- Validate technology renders against the requested view's source placement, node coverage, and sprite resolution without inheriting unrelated graph-wide diagnostics, while reporting deferred whole-workspace helper projection as a separate analysis boundary; resolve subtechnology art from the source GUI slot before using a technology-id fallback and include every referenced slot texture in the asset scan.
|
|
@@ -401,4 +424,4 @@
|
|
|
401
424
|
- Removed runtime workspace registration, manual rollback, legacy write-policy inputs, and package-level library/schema exports.
|
|
402
425
|
- Renamed the HTTP discovery grant to `allowDiscoveredMods`; it grants discovered mod IDs only.
|
|
403
426
|
|
|
404
|
-
Earlier release history is
|
|
427
|
+
Earlier release history is preserved in the [commit history](https://github.com/klimPaskov/hoi4-agent-tools/commits/main).
|
package/README.md
CHANGED
|
@@ -11,34 +11,35 @@ HOI4 Agent Tools is an MCP server for coding agents to understand Hearts of Iron
|
|
|
11
11
|
- Cross-system impact and decisions: trace definitions and consumers across source systems, evaluate declared actor and target scenarios, inspect costs and mission paths, and compare in-memory proposals.
|
|
12
12
|
- Technology trees: reconstruct technology and doctrine paths, folder layouts, unlocks, bonuses, grants, metadata, assets, and structural changes.
|
|
13
13
|
- AI and MTTH: evaluate weighted choices and timing across explicit scenarios, bind special scope chains, enumerate dynamic target pools, sweep uncertain inputs, simulate distributions, compare patches, and analyze declared stateful pools.
|
|
14
|
+
- Local references: search and read cited offline wiki pages, installed game documentation, optional script documentation, and exact source symbols without loading whole manuals into the conversation.
|
|
14
15
|
|
|
15
16
|
## Real results
|
|
16
17
|
|
|
17
|
-
The
|
|
18
|
+
The examples use real installed vanilla and Chaos Redux source, supplied game captures, and explicit reproducible display scenarios. They demonstrate source-backed review, not a claim of 99.9% whole-image accuracy. Images, source revisions, hashes, and known differences are in [Visual comparisons](docs/visual-comparisons.md) and [Interface examples](docs/gui-comparisons.md).
|
|
18
19
|
|
|
19
|
-
|
|
|
20
|
-
|
|
|
21
|
-
|  |  |
|
|
22
23
|
|
|
23
|
-
|
|
|
24
|
-
|
|
|
25
|
-
|  |  |
|
|
26
27
|
|
|
27
|
-
The
|
|
28
|
-
|
|
29
|
-
| Trigger Events settings | Advanced settings |
|
|
30
|
-
| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
|
|
31
|
-
|  |  |
|
|
28
|
+
The interface gallery also includes Chaos Redux Settings, Event Log, and Chaos Meter, plus populated vanilla decision and occupation templates. Their manifests separate source-defined layout from declared native runtime fields.
|
|
32
29
|
|
|
33
30
|
The other workbenches return complete source-linked layouts and catalogs rather than prose-only summaries.
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
|
32
|
+
For the supplied focus and technology game captures beside current-source MCP renders, see [Visual examples and comparison limits](https://github.com/klimPaskov/hoi4-agent-tools/blob/main/docs/visual-comparisons.md). These comparisons do not establish 99.9% whole-image accuracy.
|
|
33
|
+
|
|
34
|
+
| Fury focus tree from Chaos Redux | Infantry technology folder from Chaos Redux and vanilla |
|
|
35
|
+
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
36
|
+
|  |  |
|
|
37
|
+
|
|
38
|
+
| Map | Event options |
|
|
39
|
+
| -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
40
|
+
|  |  |
|
|
38
41
|
|
|
39
|
-
|
|
40
|
-
| --------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
41
|
-
|  |  |  |
|
|
42
|
+
These two views retain native output resolution. Their [source manifest](docs/images/comparisons/source/manifest.json) records the map revision and the exact Fury event used for the option view. The event renderer's omitted-node count includes the rest of the workspace graph outside that selection.
|
|
42
43
|
|
|
43
44
|
## Use from a coding agent
|
|
44
45
|
|
|
@@ -51,6 +52,8 @@ npm install --global hoi4-agent-tools@latest
|
|
|
51
52
|
```toml
|
|
52
53
|
[mcp_servers.hoi4_agent_tools]
|
|
53
54
|
command = "hoi4-agent-tools.cmd"
|
|
55
|
+
startup_timeout_sec = 120
|
|
56
|
+
tool_timeout_sec = 600
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repositories can include these steps in their agent template so the coding agent installs and registers the server itself; manual installation is not required in that workflow. Once connected, the server follows the active mod supplied by the client and the agent can call its HOI4 tools directly.
|
|
@@ -69,6 +72,10 @@ On non-Windows systems, use `hoi4-agent-tools` as the command. Agentic HOI4 repo
|
|
|
69
72
|
| `hoi4.map_inspect` | Search, click, navigate, and inspect the complete rendered map and its linked data. |
|
|
70
73
|
| `hoi4.map_render` | Render full-map layers, overlays, names, IDs, coordinates, and source-linked catalogs. |
|
|
71
74
|
| `hoi4.map_rewrite` | Create or update states, provinces, IDs, networks, positions, and connected map data. |
|
|
75
|
+
| `hoi4.reference_context` | Get compact, cited wiki and installed documentation pointers for a modding surface. |
|
|
76
|
+
| `hoi4.reference_search` | Search bounded local documentation sections. |
|
|
77
|
+
| `hoi4.reference_read` | Read one revision-bound section, with line continuation. |
|
|
78
|
+
| `hoi4.source_lookup` | Find exact definitions, overrides, usages, and narrow source blocks. |
|
|
72
79
|
| `hoi4.event_inspect` | Scan, trace, explain, lint, or assess event chains and their state flow. |
|
|
73
80
|
| `hoi4.event_render` | Render source-linked event routes, options, timing, state, scope, and unresolved edges. |
|
|
74
81
|
| `hoi4.event_compare` | Compare event-chain topology and diagnostics between revisions. |
|
|
@@ -114,6 +121,7 @@ Ask your agent in normal task language. A typical workflow is inspect, render, r
|
|
|
114
121
|
- Mechanics and packages: "Test this transfer's conservation and payment," or "Check every declared package link and resume its named scenario suite." See [Mechanic tests and scenario suites](docs/mechanics.md).
|
|
115
122
|
- Technology trees: "Explain everything this technology requires and unlocks," or "Compare this technology patch and render every affected folder and doctrine branch." See [Technology trees](docs/technology.md).
|
|
116
123
|
- AI and MTTH: "Compare these focus weights across peace, defensive-war, and low-stability scenarios," or "Show when this MTTH event becomes likely and which unknown inputs control the result." See [AI and MTTH analysis](docs/probability.md).
|
|
124
|
+
- References: "Find the installed effect signature and an exact vanilla example," or "Show the pertinent wiki and installed documentation for this GUI task." See [Local references](docs/reference.md).
|
|
117
125
|
|
|
118
126
|
## HTTP
|
|
119
127
|
|
|
@@ -176,8 +176,17 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
176
176
|
const malformed = malformedBlock(assignment.value, candidate);
|
|
177
177
|
if (malformed !== undefined)
|
|
178
178
|
return malformed;
|
|
179
|
-
if (key === 'AND'
|
|
180
|
-
|
|
179
|
+
if (key === 'AND' ||
|
|
180
|
+
key === 'hidden_trigger' ||
|
|
181
|
+
key === 'custom_trigger_tooltip' ||
|
|
182
|
+
key === 'custom_override_tooltip')
|
|
183
|
+
return evaluateTriggerBlock(key === 'custom_trigger_tooltip' || key === 'custom_override_tooltip'
|
|
184
|
+
? {
|
|
185
|
+
...assignment.value,
|
|
186
|
+
entries: assignment.value.entries.filter((entry) => entry.type !== 'assignment' ||
|
|
187
|
+
!['tooltip', 'not_tooltip'].includes(entry.key.value)),
|
|
188
|
+
}
|
|
189
|
+
: assignment.value, scenario, candidate, definitions, helperStack, scopeContext);
|
|
181
190
|
if (key === 'OR') {
|
|
182
191
|
const children = assignments(assignment.value).map((child) => evaluateAssignment(child, scenario, candidate, definitions, helperStack, scopeContext));
|
|
183
192
|
return {
|
|
@@ -318,6 +327,36 @@ function evaluateAssignmentCore(assignment, scenario, candidate, definitions, he
|
|
|
318
327
|
unresolved: [],
|
|
319
328
|
};
|
|
320
329
|
}
|
|
330
|
+
if ((key === 'controls_state' || key === 'is_controlled_by') && scenario.controls !== undefined) {
|
|
331
|
+
const targetScope = resolveScopeContext(scenario, right, scopeContext);
|
|
332
|
+
const stateId = key === 'controls_state'
|
|
333
|
+
? /^[1-9][0-9]*$/u.test(right)
|
|
334
|
+
? right
|
|
335
|
+
: targetScope?.binding.id
|
|
336
|
+
: scopeContext.binding.id;
|
|
337
|
+
const country = key === 'controls_state'
|
|
338
|
+
? scopeContext.expression === 'ROOT'
|
|
339
|
+
? scenario.actor
|
|
340
|
+
: scopeIdentity(scopeContext)
|
|
341
|
+
: targetScope === undefined
|
|
342
|
+
? /^[A-Z0-9]{3}$/u.test(right)
|
|
343
|
+
? right
|
|
344
|
+
: undefined
|
|
345
|
+
: targetScope.expression === 'ROOT' && scenario.actor === undefined
|
|
346
|
+
? undefined
|
|
347
|
+
: scopeIdentity(targetScope);
|
|
348
|
+
if (stateId === undefined || !/^[1-9][0-9]*$/u.test(stateId) || country === undefined)
|
|
349
|
+
return unresolved(assignment, candidate, `Scenario does not bind the state and country for ${key}`, 'controls');
|
|
350
|
+
const controller = scenario.controls[stateId];
|
|
351
|
+
if (controller === undefined)
|
|
352
|
+
return unresolved(assignment, candidate, `Scenario does not declare the controller of state ${stateId}`, `controls.${stateId}`);
|
|
353
|
+
if (!['=', '!='].includes(assignment.operator.text))
|
|
354
|
+
return unresolved(assignment, candidate, `Unsupported controller comparison ${assignment.operator.text}`);
|
|
355
|
+
return {
|
|
356
|
+
state: compare(controller, country, assignment.operator.text) ? 'true' : 'false',
|
|
357
|
+
unresolved: [],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
321
360
|
const helper = definitions.scriptedTriggers.get(key);
|
|
322
361
|
if (helper !== undefined && bool(right) !== undefined) {
|
|
323
362
|
if (helperStack.includes(key))
|
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export const ConditionControlMapSchema = z
|
|
3
|
+
.record(z.string().regex(/^[1-9][0-9]{0,8}$/u), z.string().regex(/^[A-Z0-9]{3}$/u))
|
|
4
|
+
.refine((value) => Object.keys(value).length <= 2_048, 'Too many declared state controllers');
|
|
5
|
+
export const GuiScenarioDateSchema = z
|
|
6
|
+
.string()
|
|
7
|
+
.max(16)
|
|
8
|
+
.refine((value) => {
|
|
9
|
+
const match = /^(\d{1,4})[.-](\d{1,2})[.-](\d{1,2})$/u.exec(value);
|
|
10
|
+
if (match === null)
|
|
11
|
+
return false;
|
|
12
|
+
const year = Number(match[1]);
|
|
13
|
+
const month = Number(match[2]);
|
|
14
|
+
const day = Number(match[3]);
|
|
15
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
16
|
+
const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
17
|
+
return year >= 1 && month >= 1 && month <= 12 && day >= 1 && day <= days[month - 1];
|
|
18
|
+
}, 'Use a valid YYYY.M.D or YYYY-MM-DD date')
|
|
19
|
+
.transform((value) => value.split(/[.-]/u).map(Number).join('.'));
|
|
2
20
|
export const ScenarioScalarSchema = z.union([
|
|
3
21
|
z.string().max(16_384),
|
|
4
22
|
z.number(),
|
|
@@ -96,6 +96,16 @@ export const workspaceRegistrationSchema = z
|
|
|
96
96
|
artifactRoot: z.string().min(1).optional(),
|
|
97
97
|
cacheRoot: z.string().min(1).optional(),
|
|
98
98
|
fixtureRoot: z.string().min(1).optional(),
|
|
99
|
+
wikiRoot: z
|
|
100
|
+
.string()
|
|
101
|
+
.min(1)
|
|
102
|
+
.refine((value) => path.isAbsolute(value), 'Wiki root must be absolute')
|
|
103
|
+
.optional(),
|
|
104
|
+
scriptDocsRoot: z
|
|
105
|
+
.string()
|
|
106
|
+
.min(1)
|
|
107
|
+
.refine((value) => path.isAbsolute(value), 'Script documentation root must be absolute')
|
|
108
|
+
.optional(),
|
|
99
109
|
})
|
|
100
110
|
.strict()
|
|
101
111
|
.superRefine((value, context) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { compareCodeUnits } from './canonical.js';
|
|
2
|
+
import { ServiceError } from './result.js';
|
|
2
3
|
function under(roots, glob) {
|
|
3
4
|
return roots.map((root) => `${root.replaceAll('\\', '/').replace(/\/$/u, '')}/${glob}`);
|
|
4
5
|
}
|
|
@@ -39,7 +40,14 @@ export function focusDomainScanPatterns(workspace) {
|
|
|
39
40
|
/** Weighted source surfaces and shared definitions used by probability analysis. */
|
|
40
41
|
export function probabilityDomainScanPatterns(workspace, sourcePaths = []) {
|
|
41
42
|
const roots = workspace.registration.roots;
|
|
42
|
-
const normalizedSourcePaths = sourcePaths.map((sourcePath) =>
|
|
43
|
+
const normalizedSourcePaths = sourcePaths.map((sourcePath) => {
|
|
44
|
+
const normalized = sourcePath.replaceAll('\\', '/').replace(/^\.\//u, '');
|
|
45
|
+
if (normalized.startsWith('/') ||
|
|
46
|
+
normalized.split('/').includes('..') ||
|
|
47
|
+
/[:*?[\]{}!\0]/u.test(normalized))
|
|
48
|
+
throw new ServiceError('PROBABILITY_SOURCE_PATH_INVALID', 'Probability source paths must be exact workspace-relative files');
|
|
49
|
+
return normalized;
|
|
50
|
+
});
|
|
43
51
|
if (normalizedSourcePaths.length > 0)
|
|
44
52
|
return uniquePatterns([...normalizedSourcePaths, ...probabilitySharedDefinitionPatterns()]);
|
|
45
53
|
return uniquePatterns([
|
|
@@ -56,12 +56,18 @@ function stateKeyTarget(assignment, kind) {
|
|
|
56
56
|
function normalized(file) {
|
|
57
57
|
return file.relativePath.replaceAll('\\', '/').toLowerCase();
|
|
58
58
|
}
|
|
59
|
-
function ownerFor(snapshot, file, assignment, ancestors) {
|
|
59
|
+
function ownerFor(snapshot, file, assignment, ancestors, includeOnActions) {
|
|
60
60
|
if (assignment.value.type !== 'block')
|
|
61
61
|
return undefined;
|
|
62
62
|
const key = assignment.key.value;
|
|
63
63
|
const sourcePath = normalized(file);
|
|
64
64
|
let owner;
|
|
65
|
+
if (includeOnActions &&
|
|
66
|
+
sourcePath.startsWith('common/on_actions/') &&
|
|
67
|
+
ancestors.length === 1 &&
|
|
68
|
+
ancestors[0] === 'on_actions') {
|
|
69
|
+
return { kind: 'on_action', id: key };
|
|
70
|
+
}
|
|
65
71
|
if (sourcePath.startsWith('events/') && ancestors.length === 0 && eventKeys.has(key)) {
|
|
66
72
|
const id = firstScalar(assignment.value, 'id')?.value;
|
|
67
73
|
if (id !== undefined)
|
|
@@ -114,7 +120,7 @@ function staticTarget(value) {
|
|
|
114
120
|
!/^(?:ROOT|FROM|PREV|THIS)$/iu.test(value));
|
|
115
121
|
}
|
|
116
122
|
/** Extracts typed script edges while preserving unknown dynamic references. */
|
|
117
|
-
export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000) {
|
|
123
|
+
export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000, options = {}) {
|
|
118
124
|
if (!Number.isSafeInteger(maxReferences) || maxReferences < 1 || maxReferences > 1_000_000)
|
|
119
125
|
throw new RangeError('Semantic reference limit is outside the supported range');
|
|
120
126
|
const references = [];
|
|
@@ -148,7 +154,7 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000)
|
|
|
148
154
|
};
|
|
149
155
|
const walk = (file, document, block, ancestors, owner) => {
|
|
150
156
|
for (const assignment of assignments(block)) {
|
|
151
|
-
const identified = ownerFor(snapshot, file, assignment, ancestors);
|
|
157
|
+
const identified = ownerFor(snapshot, file, assignment, ancestors, options.includeOnActions === true);
|
|
152
158
|
const current = identified ?? owner;
|
|
153
159
|
if (identified?.kind === 'decision' && ancestors.length === 1)
|
|
154
160
|
emit(file, document, assignment, identified, 'decision_category', ancestors[0], 'decision_category_membership');
|
|
@@ -237,7 +243,8 @@ export function scanImpactSemanticReferences(snapshot, maxReferences = 200_000)
|
|
|
237
243
|
path.startsWith('common/technologies/') ||
|
|
238
244
|
path.startsWith('common/scripted_effects/') ||
|
|
239
245
|
path.startsWith('common/scripted_triggers/') ||
|
|
240
|
-
path.startsWith('common/scripted_guis/')
|
|
246
|
+
path.startsWith('common/scripted_guis/') ||
|
|
247
|
+
(options.includeOnActions === true && path.startsWith('common/on_actions/'))))
|
|
241
248
|
continue;
|
|
242
249
|
if (snapshot.index.isSourceSkipped(file.displayPath)) {
|
|
243
250
|
complete = false;
|
|
@@ -180,7 +180,7 @@ export class WorkspaceScanner {
|
|
|
180
180
|
return result;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
-
async function readBoundedFile(handle, remainingBytes, signal) {
|
|
183
|
+
export async function readBoundedFile(handle, remainingBytes, signal) {
|
|
184
184
|
const chunks = [];
|
|
185
185
|
let total = 0;
|
|
186
186
|
for (;;) {
|
|
@@ -34,6 +34,9 @@ export function scenarioFromGui(base, values = base.values) {
|
|
|
34
34
|
for (const [key, value] of Object.entries(base.flags))
|
|
35
35
|
state[`flag.${key}`] = value;
|
|
36
36
|
const scopes = structuredClone(base.scopes ?? {});
|
|
37
|
+
for (const stateId of Object.keys(base.controls ?? {})) {
|
|
38
|
+
scopes[stateId] ??= { id: stateId, type: 'state', state: {} };
|
|
39
|
+
}
|
|
37
40
|
for (const [key, value] of Object.entries(state)) {
|
|
38
41
|
const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
|
|
39
42
|
if (match === null)
|
|
@@ -49,6 +52,8 @@ export function scenarioFromGui(base, values = base.values) {
|
|
|
49
52
|
return {
|
|
50
53
|
schemaVersion: '1.0',
|
|
51
54
|
id: base.id,
|
|
55
|
+
...(base.date === undefined ? {} : { date: base.date }),
|
|
56
|
+
...(base.controls === undefined ? {} : { controls: { ...base.controls } }),
|
|
52
57
|
state,
|
|
53
58
|
scopes,
|
|
54
59
|
closedFlags: false,
|
|
@@ -62,6 +67,7 @@ export function scenarioFromProbability(base) {
|
|
|
62
67
|
id: base.id,
|
|
63
68
|
...(base.actor === undefined ? {} : { actor: base.actor }),
|
|
64
69
|
...(base.date === undefined ? {} : { date: base.date }),
|
|
70
|
+
...(base.controls === undefined ? {} : { controls: { ...base.controls } }),
|
|
65
71
|
state: structuredClone(base.state),
|
|
66
72
|
...(base.flags === undefined ? {} : { flags: [...base.flags] }),
|
|
67
73
|
...(base.eventTargets === undefined ? {} : { eventTargets: { ...base.eventTargets } }),
|
|
@@ -42,6 +42,9 @@ export function resolveScopeContext(scenario, expression, current) {
|
|
|
42
42
|
const direct = explicitScope(scenario, expression, current);
|
|
43
43
|
if (direct !== undefined)
|
|
44
44
|
return direct;
|
|
45
|
+
if (/^[1-9][0-9]*$/u.test(expression) && Object.hasOwn(scenario.controls ?? {}, expression)) {
|
|
46
|
+
return { expression, binding: { id: expression, type: 'state', state: {} }, parent: current };
|
|
47
|
+
}
|
|
45
48
|
const targetMatch = /^(?:event_target|scope):(.+)$/iu.exec(expression);
|
|
46
49
|
const targetId = targetMatch === null
|
|
47
50
|
? undefined
|
|
@@ -488,6 +488,10 @@ export class WorkspaceResolver {
|
|
|
488
488
|
const fixtureRoot = registration.fixtureRoot === undefined
|
|
489
489
|
? undefined
|
|
490
490
|
: await canonicalPath(registration.fixtureRoot);
|
|
491
|
+
const wikiRoot = registration.wikiRoot === undefined ? undefined : await canonicalPath(registration.wikiRoot);
|
|
492
|
+
const scriptDocsRoot = registration.scriptDocsRoot === undefined
|
|
493
|
+
? undefined
|
|
494
|
+
: await canonicalPath(registration.scriptDocsRoot);
|
|
491
495
|
const generatedAllowed = (candidate, generatedKind) => (registration.kind === 'mod' &&
|
|
492
496
|
isWithin(path.join(modRoot, '.hoi4-agent', generatedKind), candidate)) ||
|
|
493
497
|
this.#storageRoots.some((root) => isWithin(root, candidate));
|
|
@@ -524,7 +528,13 @@ export class WorkspaceResolver {
|
|
|
524
528
|
}
|
|
525
529
|
}
|
|
526
530
|
if (this.#serverStateRoot !== undefined &&
|
|
527
|
-
[
|
|
531
|
+
[
|
|
532
|
+
modRoot,
|
|
533
|
+
...sourceRoots,
|
|
534
|
+
artifactRoot,
|
|
535
|
+
cacheRoot,
|
|
536
|
+
...[wikiRoot, scriptDocsRoot].filter((root) => root !== undefined),
|
|
537
|
+
].some((root) => pathsOverlap(this.#serverStateRoot, root))) {
|
|
528
538
|
throw new ServiceError('SERVER_STATE_ROOT_OVERLAP', 'Server state root must not overlap a workspace source or generated-storage root', { workspaceId: registration.id });
|
|
529
539
|
}
|
|
530
540
|
await mkdir(artifactRoot, { recursive: true });
|
|
@@ -595,6 +605,8 @@ export class WorkspaceResolver {
|
|
|
595
605
|
artifactRoot,
|
|
596
606
|
cacheRoot,
|
|
597
607
|
...(fixtureRoot === undefined ? {} : { fixtureRoot }),
|
|
608
|
+
...(wikiRoot === undefined ? {} : { wikiRoot }),
|
|
609
|
+
...(scriptDocsRoot === undefined ? {} : { scriptDocsRoot }),
|
|
598
610
|
writeEnabled: registration.kind === 'mod',
|
|
599
611
|
};
|
|
600
612
|
const workspaceIdentity = resolvedWorkspaceIdentity(workspaceBase);
|
|
@@ -848,7 +848,14 @@ function ensureReferencedNodes(graph) {
|
|
|
848
848
|
id: edge.to,
|
|
849
849
|
kind: 'unresolved',
|
|
850
850
|
label: `Missing ${isEvent ? 'event' : 'helper'}: ${expression}`,
|
|
851
|
-
...(isEvent
|
|
851
|
+
...(isEvent
|
|
852
|
+
? {
|
|
853
|
+
eventId: expression,
|
|
854
|
+
namespace: expression.includes('.')
|
|
855
|
+
? expression.slice(0, expression.lastIndexOf('.'))
|
|
856
|
+
: expression,
|
|
857
|
+
}
|
|
858
|
+
: {}),
|
|
852
859
|
sourcePath: edge.location.path,
|
|
853
860
|
location: edge.location,
|
|
854
861
|
metadata: { expression, unresolvedKind },
|
|
@@ -984,6 +984,7 @@ export class EventChainViewer {
|
|
|
984
984
|
const renderOptions = {
|
|
985
985
|
view: input.view,
|
|
986
986
|
budget,
|
|
987
|
+
compactLayout: input.compactLayout ?? input.selector !== undefined,
|
|
987
988
|
...(input.selector === undefined ? {} : { selector: input.selector }),
|
|
988
989
|
...(input.direction === undefined ? {} : { direction: input.direction }),
|
|
989
990
|
...(input.maxDepth === undefined ? {} : { maxDepth: input.maxDepth }),
|
|
@@ -450,7 +450,7 @@ class FragmentAnalyzer {
|
|
|
450
450
|
kind: 'event',
|
|
451
451
|
label: id,
|
|
452
452
|
eventId: id,
|
|
453
|
-
namespace: id.includes('.') ? id.slice(0, id.
|
|
453
|
+
namespace: id.includes('.') ? id.slice(0, id.lastIndexOf('.')) : id,
|
|
454
454
|
sourcePath: this.file.displayPath,
|
|
455
455
|
location: this.location(assignment, id),
|
|
456
456
|
metadata: {
|
|
@@ -204,7 +204,12 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
204
204
|
diagnosticMap.set(focusId, group);
|
|
205
205
|
}
|
|
206
206
|
const prerequisiteEdges = plan.focuses
|
|
207
|
-
.flatMap((focus) => focus.prerequisites.groups.flatMap((group, groupIndex) => group.focusIds.map((parentId) => ({
|
|
207
|
+
.flatMap((focus) => focus.prerequisites.groups.flatMap((group, groupIndex) => group.focusIds.map((parentId) => ({
|
|
208
|
+
parentId,
|
|
209
|
+
childId: focus.id,
|
|
210
|
+
groupIndex,
|
|
211
|
+
alternative: group.focusIds.length > 1,
|
|
212
|
+
}))))
|
|
208
213
|
.filter(({ parentId, childId }) => pixel.has(parentId) && pixel.has(childId))
|
|
209
214
|
.sort((left, right) => compareCodeUnits(left.parentId, right.parentId) ||
|
|
210
215
|
compareCodeUnits(left.childId, right.childId) ||
|
|
@@ -239,7 +244,10 @@ function svgDocument(plan, layout, diagnostics, options) {
|
|
|
239
244
|
originX: padding - minimumX * horizontal,
|
|
240
245
|
originY: padding - minimumY * vertical,
|
|
241
246
|
});
|
|
242
|
-
|
|
247
|
+
const stroke = edge.alternative
|
|
248
|
+
? 'stroke-width="2.5" stroke-dasharray="2 4" stroke-linecap="round"'
|
|
249
|
+
: 'stroke-width="2"';
|
|
250
|
+
parts.push(`<path d="${focusConnectorSvgPath(curve)}" fill="none" stroke="#8da2bc" ${stroke} marker-end="url(#arrow)" data-parent="${escapeXml(edge.parentId)}" data-child="${escapeXml(edge.childId)}" data-prerequisite-group="${edge.groupIndex}" data-prerequisite-kind="${edge.alternative ? 'or' : 'required'}"/>`);
|
|
243
251
|
}
|
|
244
252
|
parts.push('</g>', '<g id="mutual-exclusions">');
|
|
245
253
|
for (const [leftId, rightId] of exclusionEdges) {
|
|
@@ -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()
|