hoi4-agent-tools 2.2.0 → 2.3.3
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 +28 -0
- package/README.md +30 -21
- package/dist/hoi4_agent_tools/core/clausewitz-evaluation.js +90 -0
- package/dist/hoi4_agent_tools/core/engine.js +5 -0
- package/dist/hoi4_agent_tools/core/index.js +57 -1
- package/dist/hoi4_agent_tools/core/source/ast-path.js +41 -0
- package/dist/hoi4_agent_tools/core/source/index.js +1 -0
- package/dist/hoi4_agent_tools/event/algorithms.js +178 -1
- package/dist/hoi4_agent_tools/event/graph.js +24 -9
- package/dist/hoi4_agent_tools/event/limits.js +5 -2
- package/dist/hoi4_agent_tools/event/service.js +182 -45
- package/dist/hoi4_agent_tools/mcp/server/create.js +3 -1
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +13 -63
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +413 -0
- package/dist/hoi4_agent_tools/mcp/tools/technology.js +4 -1
- package/dist/hoi4_agent_tools/probability/adapters.js +344 -0
- package/dist/hoi4_agent_tools/probability/compare.js +178 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +649 -0
- package/dist/hoi4_agent_tools/probability/index.js +13 -0
- package/dist/hoi4_agent_tools/probability/model.js +5 -0
- package/dist/hoi4_agent_tools/probability/rational.js +145 -0
- package/dist/hoi4_agent_tools/probability/render.js +307 -0
- package/dist/hoi4_agent_tools/probability/sequence.js +858 -0
- package/dist/hoi4_agent_tools/probability/service.js +1168 -0
- package/dist/hoi4_agent_tools/probability/simulation.js +545 -0
- package/dist/hoi4_agent_tools/probability/source-analysis.js +458 -0
- package/dist/hoi4_agent_tools/probability/sweep.js +265 -0
- package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +280 -0
- package/dist/hoi4_agent_tools/probability/value-evaluator.js +357 -0
- package/dist/hoi4_agent_tools/runtime.js +109 -7
- package/dist/hoi4_agent_tools/schemas/probability.js +930 -0
- package/dist/hoi4_agent_tools/technology/graph.js +30 -3
- package/dist/hoi4_agent_tools/technology/queries.js +70 -3
- package/dist/hoi4_agent_tools/technology/service.js +38 -2
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/development.md +2 -0
- package/docs/events.md +2 -2
- package/docs/probability.md +80 -0
- package/docs/research/probability-adapter-evidence.md +64 -0
- package/docs/setup.md +2 -0
- package/docs/technology.md +4 -2
- package/examples/clients/codex-config.toml +3 -0
- package/examples/clients/generic.json +8 -0
- package/examples/probability/adaptive-event-pool.json +92 -0
- package/examples/probability/adaptive-event-sequence-input.json +57 -0
- package/examples/probability/event-mtth-scenarios.json +57 -0
- package/examples/probability/event-mtth-simulate-input.json +37 -0
- package/examples/probability/focus-route-scenarios.json +61 -0
- package/examples/probability/focus-route-sweep-input.json +36 -0
- package/package.json +19 -8
- package/schemas/configuration.schema.json +747 -0
- package/schemas/continuous-focus-palette.schema.json +325 -0
- package/schemas/custom-weighted-pool.schema.json +298 -0
- package/schemas/event-feature-manifest.schema.json +63 -0
- package/schemas/focus-plan.schema.json +927 -0
- package/schemas/focus-planning-sidecar.schema.json +407 -0
- package/schemas/gui-animation-source.schema.json +176 -0
- package/schemas/gui-helper.schema.json +301 -0
- package/schemas/gui-scenario.schema.json +298 -0
- package/schemas/map-operation.schema.json +2284 -0
- package/schemas/operation-result.schema.json +327 -0
- package/schemas/probability-analysis-result.schema.json +2211 -0
- package/schemas/probability-compare-input.schema.json +664 -0
- package/schemas/probability-evaluate-input.schema.json +665 -0
- package/schemas/probability-inspect-input.schema.json +97 -0
- package/schemas/probability-render-input.schema.json +141 -0
- package/schemas/probability-scenario-set.schema.json +430 -0
- package/schemas/probability-sequence-input.schema.json +886 -0
- package/schemas/probability-simulate-input.schema.json +691 -0
- package/schemas/probability-sweep-input.schema.json +702 -0
- package/server.json +3 -3
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { assignments, astPathFor, nodeLocation, } from '../core/source/index.js';
|
|
2
|
+
import { Rational } from './rational.js';
|
|
3
|
+
import { evaluateTriggerBlock } from './trigger-evaluator.js';
|
|
4
|
+
function appendReference(evaluated, reference) {
|
|
5
|
+
return {
|
|
6
|
+
...evaluated,
|
|
7
|
+
referencedProvenance: [reference, ...(evaluated.referencedProvenance ?? [])],
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function interval(value) {
|
|
11
|
+
return { min: value, max: value };
|
|
12
|
+
}
|
|
13
|
+
function nodeProvenance(candidate, node, symbol) {
|
|
14
|
+
const source = candidate.provenance[0];
|
|
15
|
+
if (source === undefined || candidate.document === undefined)
|
|
16
|
+
return source;
|
|
17
|
+
const astPath = astPathFor(candidate.document, node);
|
|
18
|
+
return {
|
|
19
|
+
...source,
|
|
20
|
+
location: nodeLocation(candidate.document, node, symbol),
|
|
21
|
+
...(astPath === undefined ? {} : { astPath }),
|
|
22
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function conditionSignature(block) {
|
|
26
|
+
const encode = (value) => assignments(value).map((assignment) => [
|
|
27
|
+
assignment.key.value,
|
|
28
|
+
assignment.operator.text,
|
|
29
|
+
assignment.value.type === 'scalar' ? assignment.value.value : encode(assignment.value),
|
|
30
|
+
]);
|
|
31
|
+
const encoded = encode(block);
|
|
32
|
+
return encoded.length === 0 ? 'always' : JSON.stringify(encoded);
|
|
33
|
+
}
|
|
34
|
+
function multiplyIntervals(left, right) {
|
|
35
|
+
const products = [
|
|
36
|
+
left.min.multiply(right.min),
|
|
37
|
+
left.min.multiply(right.max),
|
|
38
|
+
left.max.multiply(right.min),
|
|
39
|
+
left.max.multiply(right.max),
|
|
40
|
+
].sort((a, b) => a.compare(b));
|
|
41
|
+
return { min: products[0], max: products.at(-1) };
|
|
42
|
+
}
|
|
43
|
+
function addIntervals(left, right) {
|
|
44
|
+
return { min: left.min.add(right.min), max: left.max.add(right.max) };
|
|
45
|
+
}
|
|
46
|
+
function scenarioNumber(scenario, expression) {
|
|
47
|
+
if (Object.hasOwn(scenario.state, expression))
|
|
48
|
+
return scenario.state[expression];
|
|
49
|
+
if (Object.hasOwn(scenario.state, `variable.${expression}`))
|
|
50
|
+
return scenario.state[`variable.${expression}`];
|
|
51
|
+
if (expression.startsWith('var:') &&
|
|
52
|
+
Object.hasOwn(scenario.state, `variable.${expression.slice(4)}`))
|
|
53
|
+
return scenario.state[`variable.${expression.slice(4)}`];
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function expressionValue(expression, candidate, scenario, definitions, mtthStack) {
|
|
57
|
+
const numeric = Rational.parse(expression);
|
|
58
|
+
if (numeric !== undefined && Number.isFinite(numeric.toNumber()))
|
|
59
|
+
return { value: numeric, interval: interval(numeric), trace: [], unresolved: [] };
|
|
60
|
+
if (numeric !== undefined || /^(?:[+-]?(?:inf(?:inity)?|nan))$/iu.test(expression.trim()))
|
|
61
|
+
return {
|
|
62
|
+
trace: [],
|
|
63
|
+
unresolved: [
|
|
64
|
+
{
|
|
65
|
+
code: 'VALUE_NON_FINITE',
|
|
66
|
+
message: `Numeric expression ${expression} is outside finite analyzer range`,
|
|
67
|
+
path: expression,
|
|
68
|
+
candidateId: candidate.id,
|
|
69
|
+
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
if (expression.startsWith('@')) {
|
|
74
|
+
const definition = definitions.localConstants
|
|
75
|
+
.get(candidate.provenance[0]?.path ?? '')
|
|
76
|
+
?.get(expression);
|
|
77
|
+
if (definition !== undefined)
|
|
78
|
+
return appendReference(expressionValue(definition.value, candidate, scenario, definitions, mtthStack), {
|
|
79
|
+
path: definition.file.displayPath,
|
|
80
|
+
rootKind: definition.file.rootKind,
|
|
81
|
+
loadOrder: definition.file.loadOrder,
|
|
82
|
+
sourceHash: definition.file.sha256,
|
|
83
|
+
location: nodeLocation(definition.document, definition.node, expression),
|
|
84
|
+
...(astPathFor(definition.document, definition.node) === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { astPath: astPathFor(definition.document, definition.node) }),
|
|
87
|
+
symbol: expression,
|
|
88
|
+
});
|
|
89
|
+
const inline = candidate.document === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: assignments(candidate.document.root, expression).find(({ value }) => value.type === 'scalar');
|
|
92
|
+
if (inline?.value.type === 'scalar')
|
|
93
|
+
return appendReference(expressionValue(inline.value.value, candidate, scenario, definitions, mtthStack), {
|
|
94
|
+
path: candidate.provenance[0]?.path ?? 'proposed:inline-probability-source.txt',
|
|
95
|
+
rootKind: candidate.provenance[0]?.rootKind ?? 'fixture',
|
|
96
|
+
loadOrder: candidate.provenance[0]?.loadOrder ?? 0,
|
|
97
|
+
sourceHash: candidate.provenance[0]?.sourceHash ?? '',
|
|
98
|
+
location: nodeLocation(candidate.document, inline, expression),
|
|
99
|
+
...(astPathFor(candidate.document, inline) === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: { astPath: astPathFor(candidate.document, inline) }),
|
|
102
|
+
symbol: expression,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (expression.startsWith('constant:')) {
|
|
106
|
+
const id = expression.slice('constant:'.length);
|
|
107
|
+
const definition = definitions.scriptConstants.get(id);
|
|
108
|
+
if (definition !== undefined)
|
|
109
|
+
return appendReference(expressionValue(definition.value, candidate, scenario, definitions, mtthStack), {
|
|
110
|
+
path: definition.file.displayPath,
|
|
111
|
+
rootKind: definition.file.rootKind,
|
|
112
|
+
loadOrder: definition.file.loadOrder,
|
|
113
|
+
sourceHash: definition.file.sha256,
|
|
114
|
+
location: nodeLocation(definition.document, definition.node, id),
|
|
115
|
+
...(astPathFor(definition.document, definition.node) === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: { astPath: astPathFor(definition.document, definition.node) }),
|
|
118
|
+
symbol: id,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (expression.startsWith('mtth:')) {
|
|
122
|
+
const id = expression.slice('mtth:'.length);
|
|
123
|
+
const definition = definitions.mtthVariables.get(id);
|
|
124
|
+
if (definition !== undefined && !mtthStack.includes(id)) {
|
|
125
|
+
const definitionCandidate = {
|
|
126
|
+
...candidate,
|
|
127
|
+
document: definition.document,
|
|
128
|
+
provenance: [
|
|
129
|
+
{
|
|
130
|
+
path: definition.file.displayPath,
|
|
131
|
+
rootKind: definition.file.rootKind,
|
|
132
|
+
loadOrder: definition.file.loadOrder,
|
|
133
|
+
sourceHash: definition.file.sha256,
|
|
134
|
+
location: nodeLocation(definition.document, definition.node, id),
|
|
135
|
+
...(astPathFor(definition.document, definition.node) === undefined
|
|
136
|
+
? {}
|
|
137
|
+
: { astPath: astPathFor(definition.document, definition.node) }),
|
|
138
|
+
symbol: id,
|
|
139
|
+
helperChain: [...mtthStack, id],
|
|
140
|
+
},
|
|
141
|
+
...candidate.provenance,
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
return appendReference(evaluateValueBlock(definition.value, definitionCandidate, scenario, definitions, Rational.one, [...mtthStack, id]), definitionCandidate.provenance[0]);
|
|
145
|
+
}
|
|
146
|
+
if (mtthStack.includes(id))
|
|
147
|
+
return {
|
|
148
|
+
trace: [],
|
|
149
|
+
unresolved: [
|
|
150
|
+
{
|
|
151
|
+
code: 'MTTH_RECURSION',
|
|
152
|
+
message: `MTTH variable recursion detected at ${id}`,
|
|
153
|
+
candidateId: candidate.id,
|
|
154
|
+
...(candidate.provenance[0] === undefined
|
|
155
|
+
? {}
|
|
156
|
+
: { provenance: candidate.provenance[0] }),
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const declared = scenarioNumber(scenario, expression);
|
|
162
|
+
if (typeof declared === 'number' || typeof declared === 'string') {
|
|
163
|
+
const parsed = Rational.parse(declared);
|
|
164
|
+
if (parsed !== undefined)
|
|
165
|
+
return { value: parsed, interval: interval(parsed), trace: [], unresolved: [] };
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
trace: [],
|
|
169
|
+
unresolved: [
|
|
170
|
+
{
|
|
171
|
+
code: 'VALUE_UNRESOLVED',
|
|
172
|
+
message: `Scenario or source definitions do not resolve numeric expression ${expression}`,
|
|
173
|
+
path: expression,
|
|
174
|
+
candidateId: candidate.id,
|
|
175
|
+
...(candidate.provenance[0] === undefined ? {} : { provenance: candidate.provenance[0] }),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function conditionBlock(block) {
|
|
181
|
+
const operationKeys = new Set(['base', 'add', 'factor', 'days', 'months', 'years']);
|
|
182
|
+
return {
|
|
183
|
+
...block,
|
|
184
|
+
entries: block.entries.filter((entry) => entry.type !== 'assignment' || !operationKeys.has(entry.key.value)),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function applyOperation(current, operation, operand) {
|
|
188
|
+
if (operation === 'base')
|
|
189
|
+
return operand;
|
|
190
|
+
return operation === 'add' ? addIntervals(current, operand) : multiplyIntervals(current, operand);
|
|
191
|
+
}
|
|
192
|
+
export function evaluateValueBlock(block, candidate, scenario, definitions, initial, mtthStack = []) {
|
|
193
|
+
let current = interval(initial);
|
|
194
|
+
const trace = [];
|
|
195
|
+
const unresolved = [];
|
|
196
|
+
const referencedProvenance = [];
|
|
197
|
+
if (block === undefined)
|
|
198
|
+
return { value: initial, interval: current, trace, unresolved };
|
|
199
|
+
for (const assignment of assignments(block)) {
|
|
200
|
+
if (assignment.value.type === 'scalar') {
|
|
201
|
+
let operation;
|
|
202
|
+
let unit = Rational.one;
|
|
203
|
+
if (assignment.key.value === 'base')
|
|
204
|
+
operation = 'base';
|
|
205
|
+
else if (assignment.key.value === 'add')
|
|
206
|
+
operation = 'add';
|
|
207
|
+
else if (assignment.key.value === 'factor')
|
|
208
|
+
operation = 'factor';
|
|
209
|
+
else if (assignment.key.value === 'days')
|
|
210
|
+
operation = 'add';
|
|
211
|
+
else if (assignment.key.value === 'months') {
|
|
212
|
+
operation = 'add';
|
|
213
|
+
unit = new Rational(30n);
|
|
214
|
+
}
|
|
215
|
+
else if (assignment.key.value === 'years') {
|
|
216
|
+
operation = 'add';
|
|
217
|
+
unit = new Rational(365n);
|
|
218
|
+
}
|
|
219
|
+
if (operation === undefined)
|
|
220
|
+
continue;
|
|
221
|
+
const operand = expressionValue(assignment.value.value, candidate, scenario, definitions, mtthStack);
|
|
222
|
+
unresolved.push(...operand.unresolved);
|
|
223
|
+
referencedProvenance.push(...(operand.referencedProvenance ?? []));
|
|
224
|
+
if (operand.interval === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
const scaled = multiplyIntervals(operand.interval, interval(unit));
|
|
227
|
+
const before = current;
|
|
228
|
+
current = applyOperation(current, operation, scaled);
|
|
229
|
+
if (!Number.isFinite(current.min.toNumber()) || !Number.isFinite(current.max.toNumber())) {
|
|
230
|
+
unresolved.push({
|
|
231
|
+
code: 'VALUE_NON_FINITE',
|
|
232
|
+
message: `${candidate.id} modifier chain exceeds finite analyzer range`,
|
|
233
|
+
candidateId: candidate.id,
|
|
234
|
+
...(nodeProvenance(candidate, assignment, assignment.key.value) === undefined
|
|
235
|
+
? {}
|
|
236
|
+
: { provenance: nodeProvenance(candidate, assignment, assignment.key.value) }),
|
|
237
|
+
});
|
|
238
|
+
return { trace, unresolved, referencedProvenance };
|
|
239
|
+
}
|
|
240
|
+
trace.push({
|
|
241
|
+
operation,
|
|
242
|
+
expression: assignment.value.value,
|
|
243
|
+
applied: 'true',
|
|
244
|
+
...(scaled.min.compare(scaled.max) === 0 ? { value: scaled.min.toJSON() } : {}),
|
|
245
|
+
...(before.min.compare(before.max) === 0 ? { before: before.min.toJSON() } : {}),
|
|
246
|
+
...(current.min.compare(current.max) === 0 ? { after: current.min.toJSON() } : {}),
|
|
247
|
+
...(nodeProvenance(candidate, assignment, assignment.key.value) === undefined
|
|
248
|
+
? {}
|
|
249
|
+
: { provenance: nodeProvenance(candidate, assignment, assignment.key.value) }),
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (assignment.key.value !== 'modifier')
|
|
254
|
+
continue;
|
|
255
|
+
const modifier = assignment.value;
|
|
256
|
+
const condition = evaluateTriggerBlock(conditionBlock(modifier), scenario, candidate, definitions);
|
|
257
|
+
const conditionExpression = conditionSignature(conditionBlock(modifier));
|
|
258
|
+
unresolved.push(...condition.unresolved);
|
|
259
|
+
referencedProvenance.push(...(condition.helperProvenance ?? []));
|
|
260
|
+
for (const modifierAssignment of assignments(modifier)) {
|
|
261
|
+
if (modifierAssignment.value.type !== 'scalar' ||
|
|
262
|
+
!['base', 'add', 'factor'].includes(modifierAssignment.key.value))
|
|
263
|
+
continue;
|
|
264
|
+
const operation = modifierAssignment.key.value;
|
|
265
|
+
const expression = modifierAssignment.value.value;
|
|
266
|
+
const operand = expressionValue(expression, candidate, scenario, definitions, mtthStack);
|
|
267
|
+
unresolved.push(...operand.unresolved);
|
|
268
|
+
referencedProvenance.push(...(operand.referencedProvenance ?? []));
|
|
269
|
+
if (operand.interval === undefined || condition.state === 'false') {
|
|
270
|
+
trace.push({
|
|
271
|
+
operation,
|
|
272
|
+
expression,
|
|
273
|
+
applied: condition.state,
|
|
274
|
+
conditionExpression,
|
|
275
|
+
...(nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value) ===
|
|
276
|
+
undefined
|
|
277
|
+
? {}
|
|
278
|
+
: {
|
|
279
|
+
provenance: nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value),
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const before = current;
|
|
285
|
+
const applied = applyOperation(current, operation, operand.interval);
|
|
286
|
+
current =
|
|
287
|
+
condition.state === 'true'
|
|
288
|
+
? applied
|
|
289
|
+
: {
|
|
290
|
+
min: current.min.min(applied.min),
|
|
291
|
+
max: current.max.max(applied.max),
|
|
292
|
+
};
|
|
293
|
+
if (!Number.isFinite(current.min.toNumber()) || !Number.isFinite(current.max.toNumber())) {
|
|
294
|
+
unresolved.push({
|
|
295
|
+
code: 'VALUE_NON_FINITE',
|
|
296
|
+
message: `${candidate.id} modifier chain exceeds finite analyzer range`,
|
|
297
|
+
candidateId: candidate.id,
|
|
298
|
+
...(nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value) ===
|
|
299
|
+
undefined
|
|
300
|
+
? {}
|
|
301
|
+
: {
|
|
302
|
+
provenance: nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value),
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
return { trace, unresolved, referencedProvenance };
|
|
306
|
+
}
|
|
307
|
+
trace.push({
|
|
308
|
+
operation,
|
|
309
|
+
expression,
|
|
310
|
+
applied: condition.state,
|
|
311
|
+
conditionExpression,
|
|
312
|
+
...(condition.helperProvenance?.length
|
|
313
|
+
? {
|
|
314
|
+
note: `Condition helper chain: ${condition.helperProvenance
|
|
315
|
+
.map(({ symbol }) => symbol)
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.join(' -> ')}`,
|
|
318
|
+
}
|
|
319
|
+
: {}),
|
|
320
|
+
...(operand.interval.min.compare(operand.interval.max) === 0
|
|
321
|
+
? { value: operand.interval.min.toJSON() }
|
|
322
|
+
: {}),
|
|
323
|
+
...(before.min.compare(before.max) === 0 ? { before: before.min.toJSON() } : {}),
|
|
324
|
+
...(current.min.compare(current.max) === 0 ? { after: current.min.toJSON() } : {}),
|
|
325
|
+
...(nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value) ===
|
|
326
|
+
undefined
|
|
327
|
+
? {}
|
|
328
|
+
: {
|
|
329
|
+
provenance: nodeProvenance(candidate, modifierAssignment, modifierAssignment.key.value),
|
|
330
|
+
}),
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
...(current.min.compare(current.max) === 0 ? { value: current.min } : {}),
|
|
336
|
+
interval: current,
|
|
337
|
+
trace,
|
|
338
|
+
unresolved,
|
|
339
|
+
...(referencedProvenance.length === 0 ? {} : { referencedProvenance }),
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
export function evaluateCandidateValue(candidate, scenario, definitions) {
|
|
343
|
+
const initialExpression = candidate.valueExpression ?? candidate.defaultValue;
|
|
344
|
+
const initial = expressionValue(initialExpression, candidate, scenario, definitions, []);
|
|
345
|
+
if (initial.interval === undefined)
|
|
346
|
+
return initial;
|
|
347
|
+
const evaluated = evaluateValueBlock(candidate.weightBlock, candidate, scenario, definitions, initial.interval.min);
|
|
348
|
+
return {
|
|
349
|
+
...evaluated,
|
|
350
|
+
trace: [...initial.trace, ...evaluated.trace],
|
|
351
|
+
unresolved: [...initial.unresolved, ...evaluated.unresolved],
|
|
352
|
+
referencedProvenance: [
|
|
353
|
+
...(initial.referencedProvenance ?? []),
|
|
354
|
+
...(evaluated.referencedProvenance ?? []),
|
|
355
|
+
],
|
|
356
|
+
};
|
|
357
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
1
|
+
import { lstat, readdir, stat } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { loadConfiguration, serverConfigurationSchema, } from './core/configuration.js';
|
|
@@ -39,15 +39,117 @@ async function isFile(candidate) {
|
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Standard content directories that can be the only content in a valid local
|
|
44
|
+
* mod. Keep this list limited to HOI4 roots so an arbitrary project directory
|
|
45
|
+
* is not mistaken for a mod just because it contains a generic `common`
|
|
46
|
+
* folder.
|
|
47
|
+
*/
|
|
48
|
+
const MOD_CONTENT_DIRECTORIES = [
|
|
49
|
+
'events',
|
|
50
|
+
'gfx',
|
|
51
|
+
'history',
|
|
52
|
+
'interface',
|
|
53
|
+
'localisation',
|
|
54
|
+
'localisation_synced',
|
|
55
|
+
'map',
|
|
56
|
+
'music',
|
|
57
|
+
'portraits',
|
|
58
|
+
'sound',
|
|
59
|
+
];
|
|
60
|
+
const MOD_CONTENT_EXTENSIONS = {
|
|
61
|
+
events: ['.txt'],
|
|
62
|
+
gfx: ['.dds', '.gfx', '.png', '.tga'],
|
|
63
|
+
history: ['.txt'],
|
|
64
|
+
interface: ['.gfx', '.gui', '.txt'],
|
|
65
|
+
localisation: ['.yml', '.yaml'],
|
|
66
|
+
localisation_synced: ['.yml', '.yaml'],
|
|
67
|
+
map: ['.bmp', '.csv', '.map', '.txt'],
|
|
68
|
+
music: ['.asset', '.ogg', '.txt', '.wav'],
|
|
69
|
+
portraits: ['.dds', '.jpg', '.jpeg', '.png', '.tga'],
|
|
70
|
+
sound: ['.asset', '.ogg', '.txt', '.wav'],
|
|
71
|
+
};
|
|
72
|
+
const MOD_COMMON_DIRECTORIES = [
|
|
73
|
+
'abilities',
|
|
74
|
+
'ai_focuses',
|
|
75
|
+
'ai_strategy',
|
|
76
|
+
'ai_strategy_plans',
|
|
77
|
+
'bop',
|
|
78
|
+
'buildings',
|
|
79
|
+
'characters',
|
|
80
|
+
'continuous_focus',
|
|
81
|
+
'decisions',
|
|
82
|
+
'doctrines',
|
|
83
|
+
'ideas',
|
|
84
|
+
'mtth',
|
|
85
|
+
'national_focus',
|
|
86
|
+
'on_actions',
|
|
87
|
+
'operations',
|
|
88
|
+
'raids',
|
|
89
|
+
'resistance_compliance_modifiers',
|
|
90
|
+
'script_constants',
|
|
91
|
+
'scripted_effects',
|
|
92
|
+
'scripted_guis',
|
|
93
|
+
'scripted_triggers',
|
|
94
|
+
'special_projects',
|
|
95
|
+
'technology_sharing',
|
|
96
|
+
'technologies',
|
|
97
|
+
'technology_tags',
|
|
98
|
+
'units',
|
|
99
|
+
'unit_tags',
|
|
100
|
+
];
|
|
101
|
+
async function hasEntries(candidate, extensions) {
|
|
102
|
+
try {
|
|
103
|
+
const entries = await readdir(candidate, { withFileTypes: true });
|
|
104
|
+
return entries.some((entry) => entry.isFile() &&
|
|
105
|
+
(extensions === undefined ||
|
|
106
|
+
extensions.some((extension) => entry.name.toLowerCase().endsWith(extension))));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function isRealDirectory(candidate) {
|
|
113
|
+
try {
|
|
114
|
+
const metadata = await lstat(candidate);
|
|
115
|
+
return metadata.isDirectory() && !metadata.isSymbolicLink();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function isLikelyGameRoot(candidate) {
|
|
122
|
+
// Vanilla installations have an executable or launcher marker that a mod
|
|
123
|
+
// root should never contain. The marker check also protects a nested cwd
|
|
124
|
+
// such as `<game>/common` when the server is started from there.
|
|
125
|
+
return ((await isFile(path.join(candidate, 'hoi4.exe'))) ||
|
|
126
|
+
(await isFile(path.join(candidate, 'run_hoi4'))) ||
|
|
127
|
+
(await isFile(path.join(candidate, 'steam_appid.txt'))));
|
|
128
|
+
}
|
|
129
|
+
async function isLikelyModRoot(candidate) {
|
|
130
|
+
if (await isFile(path.join(candidate, 'descriptor.mod')))
|
|
131
|
+
return true;
|
|
132
|
+
if (await isLikelyGameRoot(candidate))
|
|
133
|
+
return false;
|
|
134
|
+
for (const directory of MOD_COMMON_DIRECTORIES) {
|
|
135
|
+
if (await isRealDirectory(path.join(candidate, 'common', directory)))
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (await isFile(path.join(candidate, 'common', 'combat_tactics.txt')))
|
|
139
|
+
return true;
|
|
140
|
+
for (const directory of MOD_CONTENT_DIRECTORIES) {
|
|
141
|
+
if ((await isRealDirectory(path.join(candidate, directory))) &&
|
|
142
|
+
(await hasEntries(path.join(candidate, directory), MOD_CONTENT_EXTENSIONS[directory])))
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
42
147
|
async function findCurrentModRoot(start = process.cwd()) {
|
|
43
148
|
for (let cursor = path.resolve(start);;) {
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
(await isDirectory(path.join(cursor, 'interface'))) ||
|
|
48
|
-
(await isDirectory(path.join(cursor, 'map')))))) {
|
|
149
|
+
if (await isLikelyGameRoot(cursor))
|
|
150
|
+
break;
|
|
151
|
+
if (await isLikelyModRoot(cursor))
|
|
49
152
|
return cursor;
|
|
50
|
-
}
|
|
51
153
|
const parent = path.dirname(cursor);
|
|
52
154
|
if (parent === cursor)
|
|
53
155
|
break;
|