hoi4-agent-tools 4.0.5 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +2 -0
- package/dist/hoi4_agent_tools/core/bounded-source.js +210 -0
- package/dist/hoi4_agent_tools/core/canonical.js +42 -0
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +12 -8
- package/dist/hoi4_agent_tools/core/dependency-walk.js +21 -2
- package/dist/hoi4_agent_tools/core/diagnostic-rollup.js +91 -0
- package/dist/hoi4_agent_tools/core/domain-validation.js +12 -0
- package/dist/hoi4_agent_tools/core/engine.js +2 -1
- package/dist/hoi4_agent_tools/core/impact-graph.js +6 -2
- package/dist/hoi4_agent_tools/core/impact-semantic.js +12 -2
- package/dist/hoi4_agent_tools/core/index.js +1 -0
- package/dist/hoi4_agent_tools/core/job-executor.js +9 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +12 -2
- package/dist/hoi4_agent_tools/core/managed-outputs.js +72 -0
- package/dist/hoi4_agent_tools/core/operation-result.js +28 -2
- package/dist/hoi4_agent_tools/core/scanner.js +17 -0
- package/dist/hoi4_agent_tools/core/server-state.js +9 -1
- package/dist/hoi4_agent_tools/core/temporary-directories.js +5 -1
- package/dist/hoi4_agent_tools/core/transaction-canonical.js +43 -0
- package/dist/hoi4_agent_tools/core/transaction-limits.js +7 -2
- package/dist/hoi4_agent_tools/core/transactions.js +355 -111
- package/dist/hoi4_agent_tools/decision/service.js +22 -1
- package/dist/hoi4_agent_tools/event/algorithms.js +3 -2
- package/dist/hoi4_agent_tools/event/graph.js +52 -35
- package/dist/hoi4_agent_tools/event/limits.js +4 -0
- package/dist/hoi4_agent_tools/event/operations.js +11 -0
- package/dist/hoi4_agent_tools/event/queries.js +13 -34
- package/dist/hoi4_agent_tools/event/render.js +4 -2
- package/dist/hoi4_agent_tools/event/service.js +93 -33
- package/dist/hoi4_agent_tools/event/source-analysis.js +94 -4
- package/dist/hoi4_agent_tools/event/state-keys.js +63 -0
- package/dist/hoi4_agent_tools/event/summary.js +130 -0
- package/dist/hoi4_agent_tools/impact/service.js +2 -1
- package/dist/hoi4_agent_tools/map/derived.js +21 -6
- package/dist/hoi4_agent_tools/map/job-operations.js +9 -3
- package/dist/hoi4_agent_tools/map/operations.js +72 -21
- package/dist/hoi4_agent_tools/map/reference-labels.js +238 -0
- package/dist/hoi4_agent_tools/map/reference-preparation.js +247 -0
- package/dist/hoi4_agent_tools/map/reference-world.js +1097 -0
- package/dist/hoi4_agent_tools/map/render.js +9 -1
- package/dist/hoi4_agent_tools/map/service.js +222 -18
- package/dist/hoi4_agent_tools/map/tool-operations.js +71 -2
- package/dist/hoi4_agent_tools/map/world-context.js +148 -0
- package/dist/hoi4_agent_tools/map/world-drainage.js +273 -0
- package/dist/hoi4_agent_tools/map/world-outputs.js +55 -0
- package/dist/hoi4_agent_tools/map/world-preview-style.js +157 -0
- package/dist/hoi4_agent_tools/map/world-preview.js +230 -0
- package/dist/hoi4_agent_tools/map/world-provinces.js +524 -0
- package/dist/hoi4_agent_tools/map/world.js +374 -253
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +1 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +66 -0
- package/dist/hoi4_agent_tools/mcp/tools/map.js +15 -2
- package/dist/hoi4_agent_tools/reference/source-lookup.js +4 -2
- package/dist/hoi4_agent_tools/schemas/map-requests.js +4 -0
- package/dist/hoi4_agent_tools/schemas/map.js +103 -10
- package/dist/hoi4_agent_tools/schemas/transaction.js +20 -3
- package/dist/hoi4_agent_tools/version.js +2 -2
- package/docs/analysis.md +4 -0
- package/docs/events.md +12 -0
- package/docs/images/reference-maps/political.png +0 -0
- package/docs/images/reference-maps/sketch.png +0 -0
- package/docs/map.md +45 -0
- package/docs/third-party-notices.md +6 -0
- package/npm-shrinkwrap.json +124 -2
- package/package.json +4 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/error-log-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +373 -70
- package/schemas/mechanic-test-input.schema.json +1 -1
- package/schemas/mod-index-input.schema.json +1 -1
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +1 -1
- package/schemas/package-manifest.schema.json +1 -1
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/reference-context-input.schema.json +1 -1
- package/schemas/reference-read-input.schema.json +1 -1
- package/schemas/reference-search-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1 -1
- package/schemas/scenario-test-input.schema.json +1 -1
- package/schemas/script-validate-input.schema.json +1 -1
- package/schemas/shared-scenario.schema.json +1 -1
- package/schemas/source-lookup-input.schema.json +1 -1
- package/server.json +2 -2
|
@@ -86,6 +86,21 @@ const STATE_RULES = {
|
|
|
86
86
|
multiply_variable: { kind: 'variable', access: 'read_write' },
|
|
87
87
|
divide_variable: { kind: 'variable', access: 'read_write' },
|
|
88
88
|
clamp_variable: { kind: 'variable', access: 'read_write' },
|
|
89
|
+
round_variable: { kind: 'variable', access: 'read_write' },
|
|
90
|
+
modulo_variable: { kind: 'variable', access: 'read_write' },
|
|
91
|
+
randomize_variable: { kind: 'variable', access: 'read_write' },
|
|
92
|
+
set_variable_to_random: { kind: 'variable', access: 'write' },
|
|
93
|
+
// `check_variable` and `has_variable` read a temporary variable of the same name as well.
|
|
94
|
+
set_temp_variable: { kind: 'variable', access: 'write', temporary: true },
|
|
95
|
+
set_temp_variable_to_random: { kind: 'variable', access: 'write', temporary: true },
|
|
96
|
+
add_to_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
97
|
+
subtract_from_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
98
|
+
multiply_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
99
|
+
divide_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
100
|
+
clamp_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
101
|
+
round_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
102
|
+
modulo_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
103
|
+
randomize_temp_variable: { kind: 'variable', access: 'read_write', temporary: true },
|
|
89
104
|
check_variable: { kind: 'variable', access: 'read' },
|
|
90
105
|
has_variable: { kind: 'variable', access: 'read' },
|
|
91
106
|
is_variable_equal: { kind: 'variable', access: 'read' },
|
|
@@ -101,6 +116,16 @@ const STATE_RULES = {
|
|
|
101
116
|
remove_from_array: { kind: 'array', access: 'read_write', nameField: 'array' },
|
|
102
117
|
is_in_array: { kind: 'array', access: 'read', nameField: 'array' },
|
|
103
118
|
clear_array: { kind: 'array', access: 'clear' },
|
|
119
|
+
resize_array: { kind: 'array', access: 'write', nameField: 'array' },
|
|
120
|
+
add_to_temp_array: { kind: 'array', access: 'write', nameField: 'array', temporary: true },
|
|
121
|
+
remove_from_temp_array: {
|
|
122
|
+
kind: 'array',
|
|
123
|
+
access: 'read_write',
|
|
124
|
+
nameField: 'array',
|
|
125
|
+
temporary: true,
|
|
126
|
+
},
|
|
127
|
+
resize_temp_array: { kind: 'array', access: 'write', nameField: 'array', temporary: true },
|
|
128
|
+
clear_temp_array: { kind: 'array', access: 'clear', temporary: true },
|
|
104
129
|
save_event_target_as: { kind: 'event_target', access: 'replace' },
|
|
105
130
|
has_event_target: { kind: 'event_target', access: 'read' },
|
|
106
131
|
save_global_event_target_as: { kind: 'global_event_target', access: 'replace' },
|
|
@@ -153,7 +178,59 @@ const NON_EXECUTING_BLOCKS = new Set([
|
|
|
153
178
|
'mean_time_to_happen',
|
|
154
179
|
'effect_tooltip',
|
|
155
180
|
'custom_effect_tooltip',
|
|
181
|
+
// Condition blocks of decisions, missions, ideas, focuses, raids and characters.
|
|
182
|
+
'visible',
|
|
183
|
+
'available',
|
|
184
|
+
'allowed',
|
|
185
|
+
'allowed_civil_war',
|
|
186
|
+
'allowed_to_remove',
|
|
187
|
+
'cancel_trigger',
|
|
188
|
+
'remove_trigger',
|
|
189
|
+
'target_trigger',
|
|
190
|
+
'target_root_trigger',
|
|
191
|
+
'activation',
|
|
192
|
+
'ai_will_do',
|
|
193
|
+
'bypass',
|
|
194
|
+
'allow_branch',
|
|
195
|
+
'hidden_trigger',
|
|
196
|
+
'custom_trigger_tooltip',
|
|
197
|
+
'and',
|
|
198
|
+
'or',
|
|
199
|
+
'not',
|
|
200
|
+
'nor',
|
|
201
|
+
'nand',
|
|
156
202
|
]);
|
|
203
|
+
/** Native effects whose body holds further effects rather than named arguments. */
|
|
204
|
+
const NATIVE_EFFECT_CONTAINERS = new Set([
|
|
205
|
+
'create_dynamic_country',
|
|
206
|
+
'custom_override_tooltip',
|
|
207
|
+
'effect_tooltip',
|
|
208
|
+
'for_each_loop',
|
|
209
|
+
'for_each_scope_loop',
|
|
210
|
+
'for_loop_effect',
|
|
211
|
+
'hidden_effect',
|
|
212
|
+
'if',
|
|
213
|
+
'random',
|
|
214
|
+
'random_list',
|
|
215
|
+
'random_scope_in_array',
|
|
216
|
+
'while_loop_effect',
|
|
217
|
+
]);
|
|
218
|
+
/** Inside a native effect such as `add_building_construction`, `key = yes` is an argument. */
|
|
219
|
+
function isNativeArgumentBlock(normalizedKey) {
|
|
220
|
+
return (nativeEffectKeys.has(normalizedKey) &&
|
|
221
|
+
!NATIVE_EFFECT_CONTAINERS.has(normalizedKey) &&
|
|
222
|
+
!CONTROL_BLOCKS.has(normalizedKey) &&
|
|
223
|
+
SCOPE_BLOCKS[normalizedKey] === undefined &&
|
|
224
|
+
!/^(?:every|random|global_every)_/u.test(normalizedKey));
|
|
225
|
+
}
|
|
226
|
+
/** Effect blocks of definition files by naming convention: `on_add`, `complete_effect`, `effects`. */
|
|
227
|
+
function isConventionalEffectBlock(normalizedKey) {
|
|
228
|
+
return /^on_[a-z0-9_]+$|(?:^|_)effects?$/u.test(normalizedKey);
|
|
229
|
+
}
|
|
230
|
+
/** Trigger iterators such as `any_owned_state` hold conditions, never effects. */
|
|
231
|
+
function isTriggerIterator(normalizedKey) {
|
|
232
|
+
return /^(?:any|all)_/u.test(normalizedKey);
|
|
233
|
+
}
|
|
157
234
|
const TOOLTIP_ONLY_BLOCKS = new Set(['effect_tooltip', 'custom_effect_tooltip']);
|
|
158
235
|
const COUNTRY_ON_ACTIONS = new Set([
|
|
159
236
|
'on_startup',
|
|
@@ -836,6 +913,7 @@ class FragmentAnalyzer {
|
|
|
836
913
|
helperStack: [],
|
|
837
914
|
semanticPath: ['other'],
|
|
838
915
|
effectContext: true,
|
|
916
|
+
inferMissingHelpers: false,
|
|
839
917
|
});
|
|
840
918
|
if (this.edges.length + this.stateAccesses.length > before) {
|
|
841
919
|
this.addNode({
|
|
@@ -913,6 +991,8 @@ class FragmentAnalyzer {
|
|
|
913
991
|
return;
|
|
914
992
|
}
|
|
915
993
|
if (this.context.inventoryComplete !== false &&
|
|
994
|
+
context.nativeArguments !== true &&
|
|
995
|
+
context.inferMissingHelpers !== false &&
|
|
916
996
|
missingHelperCandidate(key, entry.value, effectContext)) {
|
|
917
997
|
const target = this.unresolvedNode('missing_helper', key, entry, context.ownerId, semanticPath);
|
|
918
998
|
this.addEdge(context.ownerId, target, 'scripted_effect_call', entry, context.conditions, {
|
|
@@ -1039,7 +1119,13 @@ class FragmentAnalyzer {
|
|
|
1039
1119
|
conditions,
|
|
1040
1120
|
sourceScope: scope,
|
|
1041
1121
|
phase,
|
|
1042
|
-
effectContext: NON_EXECUTING_BLOCKS.has(normalizedKey)
|
|
1122
|
+
effectContext: NON_EXECUTING_BLOCKS.has(normalizedKey) || isTriggerIterator(normalizedKey)
|
|
1123
|
+
? false
|
|
1124
|
+
: effectContext,
|
|
1125
|
+
nativeArguments: context.nativeArguments === true || isNativeArgumentBlock(normalizedKey),
|
|
1126
|
+
...(context.inferMissingHelpers === false && isConventionalEffectBlock(normalizedKey)
|
|
1127
|
+
? { inferMissingHelpers: true }
|
|
1128
|
+
: {}),
|
|
1043
1129
|
suppressSemantics: context.suppressSemantics === true || TOOLTIP_ONLY_BLOCKS.has(normalizedKey),
|
|
1044
1130
|
semanticPath,
|
|
1045
1131
|
...(scopeExpression === undefined ? {} : { scopeExpression }),
|
|
@@ -1296,7 +1382,8 @@ class FragmentAnalyzer {
|
|
|
1296
1382
|
return id;
|
|
1297
1383
|
}
|
|
1298
1384
|
recordState(assignment, context, semanticPath) {
|
|
1299
|
-
|
|
1385
|
+
// `event_target:founder.districts` names the target `founder`; the rest is a value in its scope.
|
|
1386
|
+
const keyedTarget = /^(event_target|global_event_target|scope):([A-Za-z0-9_-]+)(?:\.[A-Za-z0-9_.-]+)?$/u.exec(assignment.key.value);
|
|
1300
1387
|
if (keyedTarget !== null) {
|
|
1301
1388
|
this.addState(context, keyedTarget[1] === 'global_event_target'
|
|
1302
1389
|
? 'global_event_target'
|
|
@@ -1307,7 +1394,7 @@ class FragmentAnalyzer {
|
|
|
1307
1394
|
const rule = STATE_RULES[assignment.key.value];
|
|
1308
1395
|
if (rule === undefined) {
|
|
1309
1396
|
const raw = valueText(this.document, assignment.value);
|
|
1310
|
-
for (const match of raw.matchAll(/\b(event_target|global_event_target):([A-Za-z0-9_
|
|
1397
|
+
for (const match of raw.matchAll(/\b(event_target|global_event_target):([A-Za-z0-9_-]+)/gu)) {
|
|
1311
1398
|
this.addState(context, match[1] === 'global_event_target' ? 'global_event_target' : 'event_target', match[2], 'read', assignment, semanticPath, false);
|
|
1312
1399
|
}
|
|
1313
1400
|
return;
|
|
@@ -1336,7 +1423,10 @@ class FragmentAnalyzer {
|
|
|
1336
1423
|
else if (access === 'write') {
|
|
1337
1424
|
this.#seenStateWriters.add(writerKey);
|
|
1338
1425
|
}
|
|
1339
|
-
this.addState(context, kind, name, access, assignment, semanticPath, isDynamic(name),
|
|
1426
|
+
this.addState(context, kind, name, access, assignment, semanticPath, isDynamic(name), {
|
|
1427
|
+
...(rule.storage === undefined ? {} : { storage: rule.storage }),
|
|
1428
|
+
...(rule.temporary === true ? { temporary: true } : {}),
|
|
1429
|
+
});
|
|
1340
1430
|
}
|
|
1341
1431
|
addState(context, kind, name, access, node, _semanticPath, dynamic, metadata = {}) {
|
|
1342
1432
|
const semantic = {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const SCOPE_KINDS = [
|
|
2
|
+
'country',
|
|
3
|
+
'state',
|
|
4
|
+
'province',
|
|
5
|
+
'unit_leader',
|
|
6
|
+
'operative',
|
|
7
|
+
'character',
|
|
8
|
+
'global',
|
|
9
|
+
'unknown',
|
|
10
|
+
];
|
|
11
|
+
/** Identity of one stored value: variables and arrays live per scope, saved scopes per storage. */
|
|
12
|
+
export function stateKey(value) {
|
|
13
|
+
const scope = value.kind === 'variable' || value.kind === 'array' ? `:${value.scope}` : '';
|
|
14
|
+
const storage = value.kind === 'saved_scope'
|
|
15
|
+
? `:${value.metadata.storage === 'global' || value.metadata.storage === 'local'
|
|
16
|
+
? value.metadata.storage
|
|
17
|
+
: 'unknown'}`
|
|
18
|
+
: '';
|
|
19
|
+
return `${value.kind}:${value.name}${scope}${storage}`;
|
|
20
|
+
}
|
|
21
|
+
/** Keys of every write that a read of this value can observe. */
|
|
22
|
+
export function compatibleProducerKeys(value) {
|
|
23
|
+
// `event_target:name` reads local and global targets alike, whereas
|
|
24
|
+
// `global_event_target:name` reads only global ones.
|
|
25
|
+
if (value.kind === 'event_target') {
|
|
26
|
+
return [stateKey(value), `global_event_target:${value.name}`];
|
|
27
|
+
}
|
|
28
|
+
// A helper body runs in whatever scope calls it, so its scope is unknown. A write there can
|
|
29
|
+
// reach a read in any scope, and a read there can observe a write in any scope.
|
|
30
|
+
if (value.kind === 'variable' || value.kind === 'array') {
|
|
31
|
+
const scopes = value.scope === 'unknown' ? SCOPE_KINDS : [value.scope, 'unknown'];
|
|
32
|
+
const keys = scopes.map((scope) => `${value.kind}:${value.name}:${scope}`);
|
|
33
|
+
// `entries^num` and `entries^3` read the array `entries`; `GER.value` and `ROOT.value` read
|
|
34
|
+
// `value` in another scope, which may be written under its bare name in any scope.
|
|
35
|
+
const element = /^([^^]+)\^/u.exec(value.name)?.[1];
|
|
36
|
+
if (element !== undefined)
|
|
37
|
+
keys.push(...SCOPE_KINDS.map((scope) => `array:${unscopedName(element)}:${scope}`));
|
|
38
|
+
const bare = unscopedName(value.name);
|
|
39
|
+
if (bare !== value.name)
|
|
40
|
+
keys.push(...SCOPE_KINDS.map((scope) => `${value.kind}:${bare}:${scope}`));
|
|
41
|
+
return [...new Set(keys)];
|
|
42
|
+
}
|
|
43
|
+
if (value.kind !== 'saved_scope' ||
|
|
44
|
+
value.metadata.storage === 'global' ||
|
|
45
|
+
value.metadata.storage === 'local') {
|
|
46
|
+
return [stateKey(value)];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
`saved_scope:${value.name}:global`,
|
|
50
|
+
`saved_scope:${value.name}:local`,
|
|
51
|
+
`saved_scope:${value.name}:unknown`,
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
/** `GER.value`, `ROOT.FROM.value` or `var:target.value` without its scope prefix. */
|
|
55
|
+
function unscopedName(name) {
|
|
56
|
+
const scoped = /^(?:(?:[A-Z][A-Z0-9]{2}|ROOT|FROM|PREV|THIS|OWNER|CONTROLLER|CAPITAL)\.|(?:var|event_target):[A-Za-z0-9_]+\.)+(.+)$/u.exec(name);
|
|
57
|
+
return scoped?.[1] ?? name;
|
|
58
|
+
}
|
|
59
|
+
export function hasUnknownSavedScopeStorage(value) {
|
|
60
|
+
return (value.kind === 'saved_scope' &&
|
|
61
|
+
value.metadata.storage !== 'global' &&
|
|
62
|
+
value.metadata.storage !== 'local');
|
|
63
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { compareCodeUnits } from '../core/canonical.js';
|
|
2
|
+
/** Findings returned inline, before an agent opens the linked report. */
|
|
3
|
+
export const EVENT_INLINE_FINDINGS = 10;
|
|
4
|
+
const EVENT_INLINE_CODES = 10;
|
|
5
|
+
const EVENT_INLINE_EVENT_IDS = 20;
|
|
6
|
+
const EVENT_INLINE_MESSAGE_LENGTH = 300;
|
|
7
|
+
const CLASSIFICATION_ORDER = {
|
|
8
|
+
confirmed_error: 0,
|
|
9
|
+
probable_defect: 1,
|
|
10
|
+
design_warning: 2,
|
|
11
|
+
unresolved_analysis: 3,
|
|
12
|
+
};
|
|
13
|
+
const CONFIDENCE_ORDER = {
|
|
14
|
+
confirmed: 0,
|
|
15
|
+
high: 1,
|
|
16
|
+
medium: 2,
|
|
17
|
+
low: 3,
|
|
18
|
+
unresolved: 4,
|
|
19
|
+
};
|
|
20
|
+
function countBy(values, key) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
for (const value of values)
|
|
23
|
+
counts.set(key(value), (counts.get(key(value)) ?? 0) + 1);
|
|
24
|
+
return Object.fromEntries([...counts].sort(([left], [right]) => compareCodeUnits(left, right)));
|
|
25
|
+
}
|
|
26
|
+
function layer(issue) {
|
|
27
|
+
const path = issue.location?.path;
|
|
28
|
+
return path === undefined ? 'unknown' : (/^([a-z]+):/u.exec(path)?.[1] ?? 'unknown');
|
|
29
|
+
}
|
|
30
|
+
/** The mod's own files first, then most severe: classification, confidence, source order. */
|
|
31
|
+
function issueOrder(left, right) {
|
|
32
|
+
const leftMod = layer(left) === 'mod' ? 0 : 1;
|
|
33
|
+
const rightMod = layer(right) === 'mod' ? 0 : 1;
|
|
34
|
+
return (leftMod - rightMod ||
|
|
35
|
+
CLASSIFICATION_ORDER[left.classification] - CLASSIFICATION_ORDER[right.classification] ||
|
|
36
|
+
CONFIDENCE_ORDER[left.confidence] - CONFIDENCE_ORDER[right.confidence] ||
|
|
37
|
+
compareCodeUnits(left.location?.path ?? '', right.location?.path ?? '') ||
|
|
38
|
+
(left.location?.start.line ?? 0) - (right.location?.start.line ?? 0) ||
|
|
39
|
+
compareCodeUnits(left.code, right.code) ||
|
|
40
|
+
compareCodeUnits(left.message, right.message));
|
|
41
|
+
}
|
|
42
|
+
/** Counts and the most severe findings of a selection, small enough to return inline. */
|
|
43
|
+
export function summarizeEventIssues(issues) {
|
|
44
|
+
const byCode = Object.entries(countBy(issues, ({ code }) => code))
|
|
45
|
+
.sort(([leftCode, left], [rightCode, right]) => right - left || compareCodeUnits(leftCode, rightCode))
|
|
46
|
+
.slice(0, EVENT_INLINE_CODES);
|
|
47
|
+
return {
|
|
48
|
+
issues: issues.length,
|
|
49
|
+
byClassification: countBy(issues, ({ classification }) => classification),
|
|
50
|
+
// `mod` findings are the workspace's own; `game` and `dependency` ones are context.
|
|
51
|
+
byLayer: countBy(issues, layer),
|
|
52
|
+
topCodes: Object.fromEntries(byCode),
|
|
53
|
+
findings: [...issues]
|
|
54
|
+
.sort(issueOrder)
|
|
55
|
+
.slice(0, EVENT_INLINE_FINDINGS)
|
|
56
|
+
.map((issue) => ({
|
|
57
|
+
code: issue.code,
|
|
58
|
+
classification: issue.classification,
|
|
59
|
+
confidence: issue.confidence,
|
|
60
|
+
message: issue.message.length > EVENT_INLINE_MESSAGE_LENGTH
|
|
61
|
+
? `${issue.message.slice(0, EVENT_INLINE_MESSAGE_LENGTH - 1)}…`
|
|
62
|
+
: issue.message,
|
|
63
|
+
...(issue.location === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { path: issue.location.path, line: issue.location.start.line }),
|
|
66
|
+
...(typeof issue.details.name === 'string' ? { subject: issue.details.name } : {}),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Size of a traced chain and the events it reaches. */
|
|
71
|
+
export function summarizeEventTrace(trace) {
|
|
72
|
+
const events = trace.nodes
|
|
73
|
+
.flatMap(({ kind, eventId }) => (kind === 'event' && eventId !== undefined ? [eventId] : []))
|
|
74
|
+
.sort(compareCodeUnits);
|
|
75
|
+
return {
|
|
76
|
+
nodes: trace.nodes.length,
|
|
77
|
+
edges: trace.edges.length,
|
|
78
|
+
events: events.length,
|
|
79
|
+
eventIds: events.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
80
|
+
boundaryNodes: trace.boundaryNodeIds.length,
|
|
81
|
+
truncated: trace.truncated,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** How many chains start where, in a form an agent can read without the report. */
|
|
85
|
+
export function summarizeEventRoots(roots) {
|
|
86
|
+
const nodeLayer = ({ sourcePath }) => sourcePath === undefined ? 'unknown' : (/^([a-z]+):/u.exec(sourcePath)?.[1] ?? 'unknown');
|
|
87
|
+
// The mod's own events first: vanilla callerless events are context, not the mod's defects.
|
|
88
|
+
const ids = (nodes) => [...nodes]
|
|
89
|
+
.sort((left, right) => (nodeLayer(left) === 'mod' ? 0 : 1) - (nodeLayer(right) === 'mod' ? 0 : 1) ||
|
|
90
|
+
compareCodeUnits(left.eventId ?? '', right.eventId ?? ''))
|
|
91
|
+
.flatMap(({ eventId }) => (eventId === undefined ? [] : [eventId]))
|
|
92
|
+
.slice(0, EVENT_INLINE_EVENT_IDS);
|
|
93
|
+
return {
|
|
94
|
+
entryPoints: roots.entryPoints.length,
|
|
95
|
+
entryKinds: countBy(roots.entryPoints, ({ metadata }) => typeof metadata.entryKind === 'string' ? metadata.entryKind : 'unknown'),
|
|
96
|
+
automaticEvents: roots.automaticEvents.length,
|
|
97
|
+
callerlessTriggeredEvents: roots.callerlessTriggeredEvents.length,
|
|
98
|
+
callerlessTriggeredByLayer: countBy(roots.callerlessTriggeredEvents, nodeLayer),
|
|
99
|
+
callerlessTriggeredEventIds: ids(roots.callerlessTriggeredEvents),
|
|
100
|
+
cycles: roots.stronglyConnectedComponents.filter(({ cyclic }) => cyclic).length,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/** Whether a route exists and, if so, the events it passes through in order. */
|
|
104
|
+
export function summarizeEventPath(path) {
|
|
105
|
+
return {
|
|
106
|
+
found: path.found,
|
|
107
|
+
steps: path.steps.length,
|
|
108
|
+
eventIds: path.steps
|
|
109
|
+
.flatMap(({ node }) => node.kind === 'event' && node.eventId !== undefined ? [node.eventId] : [])
|
|
110
|
+
.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
111
|
+
...(path.failureReason === undefined ? {} : { failureReason: path.failureReason }),
|
|
112
|
+
...(path.failureMessage === undefined ? {} : { failureMessage: path.failureMessage }),
|
|
113
|
+
unresolvedAssumptions: path.unresolvedAssumptions.length,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** How far a change to the subject reaches, by count. */
|
|
117
|
+
export function summarizeEventImpact(impact) {
|
|
118
|
+
return {
|
|
119
|
+
direct: impact.directNodeIds.length,
|
|
120
|
+
upstream: impact.upstreamNodeIds.length,
|
|
121
|
+
downstream: impact.downstreamNodeIds.length,
|
|
122
|
+
stateAccesses: impact.stateAccessIds.length,
|
|
123
|
+
affectedRoots: impact.affectedRootIds.length,
|
|
124
|
+
affectedTerminals: impact.affectedTerminalIds.length,
|
|
125
|
+
wouldDisconnect: impact.wouldDisconnectNodeIds.length,
|
|
126
|
+
wouldDisconnectTerminals: impact.wouldDisconnectTerminalIds.length,
|
|
127
|
+
unresolved: impact.unresolvedNodeIds.length,
|
|
128
|
+
sampleDownstream: impact.downstreamNodeIds.slice(0, EVENT_INLINE_EVENT_IDS),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -94,6 +94,8 @@ export class ImpactAnalyzer {
|
|
|
94
94
|
directConsumers: graph.directConsumers.length,
|
|
95
95
|
transitiveConsumers: graph.transitiveConsumers.length,
|
|
96
96
|
affectedFiles: graph.affectedFiles.length,
|
|
97
|
+
// Affected files are evidence, not writes: this read-only analysis never changes a file.
|
|
98
|
+
affectedFilePaths: graph.affectedFiles.slice(0, 64),
|
|
97
99
|
cycles: graph.cycles.length,
|
|
98
100
|
scenarioSuites: scenarioSuites.scannedSuites,
|
|
99
101
|
affectedCases: scenarioSuites.matches.length,
|
|
@@ -108,7 +110,6 @@ export class ImpactAnalyzer {
|
|
|
108
110
|
result.code = complete ? 'IMPACT_ANALYZED' : 'IMPACT_ANALYZED_PARTIAL';
|
|
109
111
|
result.artifacts = [publicArtifactLink(artifact)];
|
|
110
112
|
result.proposedFiles = overlay?.changes.map(({ path }) => path).slice(0, 64) ?? [];
|
|
111
|
-
result.changedFiles = graph.affectedFiles.slice(0, 64);
|
|
112
113
|
setInlineFilesScanned(result, before.files.map(({ displayPath }) => displayPath));
|
|
113
114
|
result.validation = {
|
|
114
115
|
passed: complete,
|
|
@@ -136,9 +136,9 @@ function coastPoint(raster, shape, isSea) {
|
|
|
136
136
|
[0, 1],
|
|
137
137
|
[0, -1],
|
|
138
138
|
]) {
|
|
139
|
-
const nx = x + dx;
|
|
139
|
+
const nx = (x + dx + raster.width) % raster.width;
|
|
140
140
|
const ny = y + dy;
|
|
141
|
-
if (
|
|
141
|
+
if (ny < 0 || ny >= raster.height)
|
|
142
142
|
continue;
|
|
143
143
|
const other = raster.provinceIds[ny * raster.width + nx];
|
|
144
144
|
if (other < 0 || !isSea(other))
|
|
@@ -154,6 +154,19 @@ function coastPoint(raster, shape, isSea) {
|
|
|
154
154
|
const middle = points[Math.floor(points.length / 2)];
|
|
155
155
|
return { ...middle, sea };
|
|
156
156
|
}
|
|
157
|
+
/** A sea pixel next to the landing province, staying inside narrow inlets and map edges. */
|
|
158
|
+
function floatingHarborPoint(raster, coast) {
|
|
159
|
+
for (const distance of [2, 1]) {
|
|
160
|
+
const x = (coast.x + coast.dx * distance + raster.width) % raster.width;
|
|
161
|
+
const y = coast.y + coast.dy * distance;
|
|
162
|
+
if (y < 0 || y >= raster.height)
|
|
163
|
+
continue;
|
|
164
|
+
// Keep the same sea association selected from the real coastal contact.
|
|
165
|
+
if (raster.provinceIds[y * raster.width + x] === coast.sea)
|
|
166
|
+
return { x, y };
|
|
167
|
+
}
|
|
168
|
+
throw new ServiceError('MAP_FLOATING_HARBOR_WATER_UNAVAILABLE', 'The selected coastal contact has no adjacent sea pixel for a floating harbor');
|
|
169
|
+
}
|
|
157
170
|
function rotationFor(dx, dy) {
|
|
158
171
|
// Bitmap y grows downward; map z grows upward.
|
|
159
172
|
return Math.atan2(-dy, dx);
|
|
@@ -315,9 +328,8 @@ function planBuildingPositions(index, request) {
|
|
|
315
328
|
continue;
|
|
316
329
|
for (let copy = 0; copy < perOwner && produced < wanted - have; copy += 1, produced += 1) {
|
|
317
330
|
const floating = slot.name === 'floating_harbor';
|
|
318
|
-
//
|
|
319
|
-
const x = floating ?
|
|
320
|
-
const y = floating ? coast.y + coast.dy * 2 : coast.y;
|
|
331
|
+
// Prefer two pixels offshore, with a verified adjacent-water fallback.
|
|
332
|
+
const { x, y } = floating ? floatingHarborPoint(raster, coast) : coast;
|
|
321
333
|
added.push([
|
|
322
334
|
state.id,
|
|
323
335
|
slot.name,
|
|
@@ -350,7 +362,10 @@ function planBuildingPositions(index, request) {
|
|
|
350
362
|
}
|
|
351
363
|
return {
|
|
352
364
|
relativePath: document?.file.relativePath ?? `${mapRoot(index)}/buildings.txt`,
|
|
353
|
-
|
|
365
|
+
// The native building-position reader treats a trailing newline as an empty CSV record.
|
|
366
|
+
content: Buffer.from(rewriteLines(document, drop, added)
|
|
367
|
+
.toString('utf8')
|
|
368
|
+
.replace(/\r?\n$/u, ''), 'utf8'),
|
|
354
369
|
};
|
|
355
370
|
}
|
|
356
371
|
function planUnitPositions(index, request) {
|
|
@@ -4,7 +4,7 @@ import { errorResult, toolResult } from '../core/operation-result.js';
|
|
|
4
4
|
import { ServiceError } from '../core/result.js';
|
|
5
5
|
import { MAP_VIEW_FIELDS, mapInspectRequestSchema, mapRewriteRequestSchema, } from '../schemas/map-requests.js';
|
|
6
6
|
import { AgentNudger } from './service.js';
|
|
7
|
-
import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, renderMapView, } from './tool-operations.js';
|
|
7
|
+
import { inspectMap, completeMapRewrite, normalizeMapInspectRequest, normalizeMapRenderRequest, normalizeMapRewriteRequest, prepareMapRewrite, previewMapRewrite, renderMapView, } from './tool-operations.js';
|
|
8
8
|
const wireResult = z.record(z.string(), z.json());
|
|
9
9
|
/** Registers read-only map operations without invoking transport handlers. */
|
|
10
10
|
export function registerMapJobs(operations, engine) {
|
|
@@ -54,12 +54,18 @@ export function registerMapJobs(operations, engine) {
|
|
|
54
54
|
throw new ServiceError('JOB_ARGUMENT_SCOPE_MISMATCH', 'The request workspace does not match the authorized job workspace');
|
|
55
55
|
const release = lifetime.begin();
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
const operationContext = {
|
|
58
58
|
workspaceId: context.workspaceId,
|
|
59
59
|
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
60
60
|
signal: context.signal,
|
|
61
61
|
progress: (completed, total, message) => context.progress({ completed, total, message }),
|
|
62
|
-
}
|
|
62
|
+
};
|
|
63
|
+
const normalized = normalizeMapRewriteRequest(input);
|
|
64
|
+
if (normalized.preview)
|
|
65
|
+
return {
|
|
66
|
+
readOnlyPreview: wireResult.parse(JSON.parse(JSON.stringify(toolResult(await previewMapRewrite(nudger, normalized, operationContext))))),
|
|
67
|
+
};
|
|
68
|
+
return await prepareMapRewrite(nudger, normalized, operationContext);
|
|
63
69
|
}
|
|
64
70
|
finally {
|
|
65
71
|
release();
|
|
@@ -9,6 +9,8 @@ import { derivedStateCapital, encodeTextDocument, LEVEL_STATE_BUILDINGS, MapWork
|
|
|
9
9
|
import { planReferenceRemap, ReferenceRemapError } from './references.js';
|
|
10
10
|
import { generateWorld } from './world.js';
|
|
11
11
|
import { planMapPositions, planSupplyRebuild } from './derived.js';
|
|
12
|
+
import { buildWorldContext } from './world-context.js';
|
|
13
|
+
import { reconcileWorldOutputs } from './world-outputs.js';
|
|
12
14
|
function* cancellationCheckpoint(signal, iteration = 0, stride = 1) {
|
|
13
15
|
if (iteration % stride !== 0)
|
|
14
16
|
return;
|
|
@@ -3250,6 +3252,14 @@ export const WORLD_REPLACED_ROOTS = [
|
|
|
3250
3252
|
'map/supplyareas',
|
|
3251
3253
|
'common/bookmarks',
|
|
3252
3254
|
];
|
|
3255
|
+
const STARTING_SCENARIO_REPLACED_ROOTS = [
|
|
3256
|
+
'common/on_actions',
|
|
3257
|
+
'common/ai_areas',
|
|
3258
|
+
'common/ai_strategy_plans',
|
|
3259
|
+
'common/ai_strategy',
|
|
3260
|
+
'common/decisions',
|
|
3261
|
+
'events',
|
|
3262
|
+
];
|
|
3253
3263
|
/** Add replace_path lines to descriptor.mod, creating a minimal descriptor when none exists. */
|
|
3254
3264
|
function descriptorWithReplacePaths(index, roots) {
|
|
3255
3265
|
const existing = index.sourceFiles.find((file) => file.rootKind === 'mod' && normalizeRelative(file.relativePath) === 'descriptor.mod');
|
|
@@ -3262,36 +3272,56 @@ function descriptorWithReplacePaths(index, roots) {
|
|
|
3262
3272
|
const body = textValue.endsWith('\n') ? textValue : `${textValue}${newline}`;
|
|
3263
3273
|
return Buffer.from(`${body}${missing.map((root) => `replace_path="${root}"`).join(newline)}${missing.length > 0 ? newline : ''}`, 'utf8');
|
|
3264
3274
|
}
|
|
3265
|
-
function applyCreateWorld(base, changes, operation) {
|
|
3275
|
+
function applyCreateWorld(base, changes, operation, context) {
|
|
3276
|
+
const reference = context.referenceWorlds?.get(operation.id);
|
|
3277
|
+
if (operation.reference !== undefined && reference === undefined)
|
|
3278
|
+
throw new ServiceError('MAP_REFERENCE_NOT_PREPARED', 'Reference worlds must be prepared by the authorized workspace service');
|
|
3266
3279
|
const mapRoot = base.sourceRoots.map[0] ?? 'map';
|
|
3280
|
+
const startingScenario = operation.startingScenario ?? reference !== undefined;
|
|
3267
3281
|
const world = generateWorld({
|
|
3268
|
-
|
|
3269
|
-
|
|
3282
|
+
startingScenario,
|
|
3283
|
+
...(operation.worldName === undefined ? {} : { worldName: operation.worldName }),
|
|
3284
|
+
width: operation.width ?? reference?.surface.width ?? 1_024,
|
|
3285
|
+
height: operation.height ?? reference?.surface.height ?? 512,
|
|
3270
3286
|
seed: operation.seed ?? 1,
|
|
3271
3287
|
landCoverage: operation.landCoverage ?? 0.35,
|
|
3272
3288
|
continents: operation.continents ?? 3,
|
|
3273
3289
|
...(operation.landPolygons === undefined ? {} : { landPolygons: operation.landPolygons }),
|
|
3274
3290
|
landProvincePixels: operation.landProvincePixels ?? 400,
|
|
3275
3291
|
seaProvincePixels: operation.seaProvincePixels ?? 2_500,
|
|
3276
|
-
provincesPerState: operation.provincesPerState ?? 5,
|
|
3277
|
-
landProvincesPerRegion: operation.landProvincesPerRegion ?? 25,
|
|
3278
|
-
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? 10,
|
|
3279
|
-
countries: operation.countries
|
|
3292
|
+
provincesPerState: operation.provincesPerState ?? (reference === undefined ? 5 : 12),
|
|
3293
|
+
landProvincesPerRegion: operation.landProvincesPerRegion ?? (reference === undefined ? 25 : 60),
|
|
3294
|
+
seaProvincesPerRegion: operation.seaProvincesPerRegion ?? (reference === undefined ? 10 : 30),
|
|
3295
|
+
countries: operation.countries ??
|
|
3296
|
+
reference?.countries ?? [
|
|
3297
|
+
{ tag: 'X01', name: 'Aster' },
|
|
3298
|
+
{ tag: 'X02', name: 'Brum' },
|
|
3299
|
+
{ tag: 'X03', name: 'Corvel' },
|
|
3300
|
+
{ tag: 'X04', name: 'Doran' },
|
|
3301
|
+
],
|
|
3302
|
+
...(reference === undefined
|
|
3303
|
+
? {}
|
|
3304
|
+
: {
|
|
3305
|
+
referenceSurface: reference.surface,
|
|
3306
|
+
referenceCities: reference.cities,
|
|
3307
|
+
referenceLabels: reference.labels,
|
|
3308
|
+
}),
|
|
3280
3309
|
...(operation.continentNames === undefined
|
|
3281
3310
|
? {}
|
|
3282
3311
|
: { continentNames: operation.continentNames }),
|
|
3283
3312
|
startDate: operation.startDate ?? '1936.1.1',
|
|
3284
|
-
rivers: operation.rivers ?? 8,
|
|
3285
|
-
},
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
localisationRoot: base.sourceRoots.localisation[0] ?? 'localisation',
|
|
3290
|
-
});
|
|
3291
|
-
const roots = operation.replaceBaseGame === false ? [] : [...WORLD_REPLACED_ROOTS];
|
|
3313
|
+
rivers: operation.rivers ?? (reference === undefined ? 8 : 80),
|
|
3314
|
+
}, buildWorldContext(base));
|
|
3315
|
+
const roots = operation.replaceBaseGame === false
|
|
3316
|
+
? []
|
|
3317
|
+
: [...WORLD_REPLACED_ROOTS, ...(startingScenario ? STARTING_SCENARIO_REPLACED_ROOTS : [])];
|
|
3292
3318
|
replacedRootsByPlan.set(changes, roots);
|
|
3293
3319
|
for (const file of world.files)
|
|
3294
3320
|
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType);
|
|
3321
|
+
// Remove only unchanged outputs recorded by a previous generation, before indexing new IDs.
|
|
3322
|
+
for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
|
|
3323
|
+
if (file.content === null)
|
|
3324
|
+
addChange(changes, file.relativePath, null, operation.id, 'text/plain');
|
|
3295
3325
|
// Start from empty derived files so no base-game position or supply line survives.
|
|
3296
3326
|
for (const name of [
|
|
3297
3327
|
'buildings.txt',
|
|
@@ -3309,7 +3339,28 @@ function applyCreateWorld(base, changes, operation) {
|
|
|
3309
3339
|
const positioned = currentIndex(base, changes);
|
|
3310
3340
|
for (const file of planSupplyRebuild(positioned, { mode: 'replace' }))
|
|
3311
3341
|
addChange(changes, file.relativePath, file.content, operation.id, 'text/plain');
|
|
3342
|
+
for (const file of reconcileWorldOutputs(base.sourceFiles, proposedChanges(changes), operation.id, mapRoot, base.sourceRoots.states, base.sourceRoots.localisation))
|
|
3343
|
+
addChange(changes, file.relativePath, file.content, operation.id, file.mediaType ?? 'text/plain');
|
|
3312
3344
|
return [
|
|
3345
|
+
...(reference?.diagnostics ?? []).map((message) => ({
|
|
3346
|
+
code: 'MAP_REFERENCE_INTERPRETATION',
|
|
3347
|
+
severity: 'warning',
|
|
3348
|
+
category: 'map',
|
|
3349
|
+
operationId: operation.id,
|
|
3350
|
+
message,
|
|
3351
|
+
})),
|
|
3352
|
+
...(world.warnings.length === 0
|
|
3353
|
+
? []
|
|
3354
|
+
: [
|
|
3355
|
+
{
|
|
3356
|
+
code: 'MAP_WORLD_GENERATION_REVIEW',
|
|
3357
|
+
severity: 'warning',
|
|
3358
|
+
category: 'map',
|
|
3359
|
+
operationId: operation.id,
|
|
3360
|
+
message: `Drainage generation reported ${world.warnings.length} review notes`,
|
|
3361
|
+
details: { warningCount: world.warnings.length, samples: world.warnings.slice(0, 5) },
|
|
3362
|
+
},
|
|
3363
|
+
]),
|
|
3313
3364
|
{
|
|
3314
3365
|
code: 'MAP_WORLD_CREATED',
|
|
3315
3366
|
severity: 'info',
|
|
@@ -3405,7 +3456,7 @@ function applyEntityLocator(base, changes, operation) {
|
|
|
3405
3456
|
]);
|
|
3406
3457
|
addChange(changes, locator.file.relativePath, output, operation.id, 'text/plain');
|
|
3407
3458
|
}
|
|
3408
|
-
function* planMapOperationSteps(base, operations, signal) {
|
|
3459
|
+
function* planMapOperationSteps(base, operations, signal, context = {}) {
|
|
3409
3460
|
yield* cancellationCheckpoint(signal);
|
|
3410
3461
|
const changes = new Map();
|
|
3411
3462
|
const diagnostics = [];
|
|
@@ -3443,7 +3494,7 @@ function* planMapOperationSteps(base, operations, signal) {
|
|
|
3443
3494
|
const boundsCheckpoint = expectedChangedBounds;
|
|
3444
3495
|
try {
|
|
3445
3496
|
if (operation.kind === 'create_world')
|
|
3446
|
-
diagnostics.push(...applyCreateWorld(base, changes, operation));
|
|
3497
|
+
diagnostics.push(...applyCreateWorld(base, changes, operation, context));
|
|
3447
3498
|
else if (operation.kind === 'move_state_provinces')
|
|
3448
3499
|
applyMoveStateProvinces(base, changes, operation);
|
|
3449
3500
|
else if (operation.kind === 'update_state')
|
|
@@ -3522,12 +3573,12 @@ function* planMapOperationSteps(base, operations, signal) {
|
|
|
3522
3573
|
operations: manifestOperations,
|
|
3523
3574
|
};
|
|
3524
3575
|
}
|
|
3525
|
-
export function planMapOperations(base, operations, signal) {
|
|
3526
|
-
return completeSynchronously(planMapOperationSteps(base, operations, signal));
|
|
3576
|
+
export function planMapOperations(base, operations, signal, context = {}) {
|
|
3577
|
+
return completeSynchronously(planMapOperationSteps(base, operations, signal, context));
|
|
3527
3578
|
}
|
|
3528
3579
|
/** Plans map mutations while periodically yielding so protocol cancellation can be delivered. */
|
|
3529
|
-
export async function planMapOperationsAsync(base, operations, signal) {
|
|
3530
|
-
return completeCooperatively(planMapOperationSteps(base, operations, signal), signal);
|
|
3580
|
+
export async function planMapOperationsAsync(base, operations, signal, context = {}) {
|
|
3581
|
+
return completeCooperatively(planMapOperationSteps(base, operations, signal, context), signal);
|
|
3531
3582
|
}
|
|
3532
3583
|
export function mapOperationId(operation) {
|
|
3533
3584
|
return deterministicId('map_operation', operation);
|