hoi4-agent-tools 3.0.0 → 3.0.2
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 +12 -0
- package/README.md +3 -1
- package/dist/bin/stdio.js +4 -0
- package/dist/hoi4_agent_tools/core/domain-scan-patterns.js +12 -0
- package/dist/hoi4_agent_tools/gui/assets.js +26 -14
- package/dist/hoi4_agent_tools/gui/renderer.js +4 -11
- package/dist/hoi4_agent_tools/gui/scenario.js +7 -1
- package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +483 -0
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +6 -1
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +9 -5
- package/dist/hoi4_agent_tools/probability/service.js +48 -6
- package/dist/hoi4_agent_tools/technology/compare.js +6 -1
- package/dist/hoi4_agent_tools/technology/graph.js +67 -2
- package/dist/hoi4_agent_tools/technology/model.js +1 -1
- package/dist/hoi4_agent_tools/technology/queries.js +4 -0
- package/dist/hoi4_agent_tools/technology/render.js +73 -28
- package/dist/hoi4_agent_tools/technology/service.js +4 -1
- package/dist/hoi4_agent_tools/technology/source-analysis.js +102 -19
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +2 -0
- package/docs/images/readme/event-chain-mcp.png +0 -0
- package/docs/images/readme/probability-mcp.png +0 -0
- package/docs/images/readme/technology-tree-mcp.png +0 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +1 -1
- package/package.json +1 -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/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 +59 -55
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/operation-result.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/server.json +2 -2
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
import { publicArtifactLink } from '../../core/artifacts.js';
|
|
5
|
+
import { compareCodeUnits, hashCanonical } from '../../core/canonical.js';
|
|
6
|
+
import { countryAssetDiscoveryPatterns } from '../../core/domain-scan-patterns.js';
|
|
7
|
+
import { emptyServiceResult } from '../../core/result.js';
|
|
8
|
+
import { RenderBudget } from '../../core/render-budget.js';
|
|
9
|
+
import { workspaceIdSchema } from '../../schemas/common.js';
|
|
10
|
+
import { GuiAssetCatalog } from '../../gui/assets.js';
|
|
11
|
+
import { ScriptedGuiStudio } from '../../gui/index.js';
|
|
12
|
+
import { buildGuiSourceGraph } from '../../gui/source-graph.js';
|
|
13
|
+
import { PACKAGE_VERSION } from '../../version.js';
|
|
14
|
+
import { resolveServerWorkspaceId } from '../server/base-tools.js';
|
|
15
|
+
import { compactValidatedInputSchema } from '../server/context-schemas.js';
|
|
16
|
+
import { nonNegativeIntegerSchema, sha256Schema } from '../server/output-schemas.js';
|
|
17
|
+
import { progressReporter } from '../server/progress.js';
|
|
18
|
+
import { errorResult, setInlineFilesScanned, strictOperationResultSchema, toolResult, } from '../server/result.js';
|
|
19
|
+
const countryTagSchema = z
|
|
20
|
+
.string()
|
|
21
|
+
.regex(/^[A-Z0-9]{3}$/u)
|
|
22
|
+
.describe('Exact HOI4 country tag selected by ChaosX');
|
|
23
|
+
const chaosxCountryAssetsInput = compactValidatedInputSchema(z
|
|
24
|
+
.object({
|
|
25
|
+
workspaceId: workspaceIdSchema,
|
|
26
|
+
countryTags: z.array(countryTagSchema).min(1).max(4),
|
|
27
|
+
eventId: z.number().int().min(0).max(999).optional(),
|
|
28
|
+
treeId: z.string().min(1).max(256).optional(),
|
|
29
|
+
})
|
|
30
|
+
.strict(), 'ChaosX-only country tags and optional event/focus identity');
|
|
31
|
+
const chaosxCountryAssetsOutput = strictOperationResultSchema(z
|
|
32
|
+
.object({
|
|
33
|
+
revision: sha256Schema,
|
|
34
|
+
countries: z
|
|
35
|
+
.array(z
|
|
36
|
+
.object({
|
|
37
|
+
tag: countryTagSchema,
|
|
38
|
+
flagArtifactName: z.string().max(128).optional(),
|
|
39
|
+
leaderPortraitArtifactName: z.string().max(128).optional(),
|
|
40
|
+
leaderSprite: z.string().max(256).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict())
|
|
43
|
+
.max(4),
|
|
44
|
+
artifactCount: nonNegativeIntegerSchema,
|
|
45
|
+
})
|
|
46
|
+
.strict());
|
|
47
|
+
const visualGuiSelectorSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
windowName: z.string().min(1).max(256),
|
|
50
|
+
guiId: z.string().min(1).max(256),
|
|
51
|
+
})
|
|
52
|
+
.strict();
|
|
53
|
+
const chaosxVisualRevisionInput = compactValidatedInputSchema(z
|
|
54
|
+
.object({
|
|
55
|
+
workspaceId: workspaceIdSchema,
|
|
56
|
+
guiWindows: z.array(visualGuiSelectorSchema).min(1).max(3),
|
|
57
|
+
})
|
|
58
|
+
.strict(), 'ChaosX-only scripted-GUI selectors for fast cache revision checks');
|
|
59
|
+
const visualRevisionEntrySchema = z
|
|
60
|
+
.object({
|
|
61
|
+
windowName: z.string().min(1).max(256),
|
|
62
|
+
guiId: z.string().min(1).max(256),
|
|
63
|
+
revision: sha256Schema,
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
const chaosxVisualRevisionOutput = strictOperationResultSchema(z
|
|
67
|
+
.object({
|
|
68
|
+
workspaceRevision: sha256Schema,
|
|
69
|
+
guiRevisions: z.array(visualRevisionEntrySchema).max(3),
|
|
70
|
+
dependencyFileCount: nonNegativeIntegerSchema,
|
|
71
|
+
})
|
|
72
|
+
.strict());
|
|
73
|
+
const artifactProducing = {
|
|
74
|
+
readOnlyHint: false,
|
|
75
|
+
destructiveHint: false,
|
|
76
|
+
idempotentHint: true,
|
|
77
|
+
openWorldHint: false,
|
|
78
|
+
};
|
|
79
|
+
const readOnly = { ...artifactProducing, readOnlyHint: true };
|
|
80
|
+
function activeFiles(snapshot) {
|
|
81
|
+
return snapshot.files.filter(({ shadowedBy }) => shadowedBy === undefined);
|
|
82
|
+
}
|
|
83
|
+
function historyFile(snapshot, tag) {
|
|
84
|
+
const prefix = `history/countries/${tag.toLowerCase()} `;
|
|
85
|
+
return activeFiles(snapshot)
|
|
86
|
+
.filter(({ relativePath }) => relativePath.toLowerCase().startsWith(prefix))
|
|
87
|
+
.sort((left, right) => {
|
|
88
|
+
const ownership = Number(right.rootKind === 'mod') - Number(left.rootKind === 'mod');
|
|
89
|
+
return (ownership ||
|
|
90
|
+
right.loadOrder - left.loadOrder ||
|
|
91
|
+
compareCodeUnits(left.displayPath, right.displayPath));
|
|
92
|
+
})[0];
|
|
93
|
+
}
|
|
94
|
+
function pictureFromText(file, tag, requireTagPrefix) {
|
|
95
|
+
const text = file.bytes.toString('utf8');
|
|
96
|
+
const pattern = requireTagPrefix
|
|
97
|
+
? new RegExp(`\\bpicture\\s*=\\s*"?(GFX_portrait_${tag}_[A-Za-z0-9_.:-]+)"?`, 'u')
|
|
98
|
+
: /\bpicture\s*=\s*"?(GFX_[A-Za-z0-9_.:-]+)"?/u;
|
|
99
|
+
return pattern.exec(text)?.[1];
|
|
100
|
+
}
|
|
101
|
+
function eventPictureSource(snapshot, tag, eventId) {
|
|
102
|
+
const eventPrefix = eventId === undefined ? undefined : String(eventId).padStart(3, '0');
|
|
103
|
+
const candidates = activeFiles(snapshot)
|
|
104
|
+
.filter(({ relativePath, bytes }) => {
|
|
105
|
+
const normalized = relativePath.toLowerCase();
|
|
106
|
+
return (normalized.endsWith('.txt') &&
|
|
107
|
+
(normalized.startsWith('events/') || normalized.startsWith('common/scripted_effects/')) &&
|
|
108
|
+
bytes.includes(Buffer.from(`GFX_portrait_${tag}_`, 'utf8')));
|
|
109
|
+
})
|
|
110
|
+
.sort((left, right) => {
|
|
111
|
+
const leftPreferred = eventPrefix !== undefined && path.posix.basename(left.relativePath).startsWith(eventPrefix);
|
|
112
|
+
const rightPreferred = eventPrefix !== undefined &&
|
|
113
|
+
path.posix.basename(right.relativePath).startsWith(eventPrefix);
|
|
114
|
+
return (Number(rightPreferred) - Number(leftPreferred) ||
|
|
115
|
+
compareCodeUnits(left.displayPath, right.displayPath));
|
|
116
|
+
});
|
|
117
|
+
for (const file of candidates) {
|
|
118
|
+
const sprite = pictureFromText(file, tag, true);
|
|
119
|
+
if (sprite !== undefined)
|
|
120
|
+
return { sprite, path: file.displayPath };
|
|
121
|
+
}
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
function leaderSelection(snapshot, tag, eventId) {
|
|
125
|
+
const countryHistory = historyFile(snapshot, tag);
|
|
126
|
+
const historyPicture = countryHistory === undefined ? undefined : pictureFromText(countryHistory, tag, false);
|
|
127
|
+
const eventPicture = historyPicture === undefined ? eventPictureSource(snapshot, tag, eventId) : undefined;
|
|
128
|
+
const fallbackSprite = snapshot.index
|
|
129
|
+
.findAll('sprite')
|
|
130
|
+
.filter(({ overridden, id }) => !overridden && id.startsWith(`GFX_portrait_${tag}_`))
|
|
131
|
+
.sort((left, right) => compareCodeUnits(left.id, right.id))[0];
|
|
132
|
+
const leaderSprite = historyPicture ?? eventPicture?.sprite ?? fallbackSprite?.id;
|
|
133
|
+
if (leaderSprite === undefined)
|
|
134
|
+
return {};
|
|
135
|
+
const sprite = snapshot.index.find('sprite', leaderSprite);
|
|
136
|
+
const texture = typeof sprite?.metadata.texture === 'string' ? sprite.metadata.texture : undefined;
|
|
137
|
+
return {
|
|
138
|
+
leaderSprite,
|
|
139
|
+
...(texture === undefined ? {} : { leaderTexturePath: texture }),
|
|
140
|
+
...(countryHistory !== undefined
|
|
141
|
+
? { discoverySourcePath: countryHistory.displayPath }
|
|
142
|
+
: eventPicture === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { discoverySourcePath: eventPicture.path }),
|
|
145
|
+
...(sprite === undefined ? {} : { spriteSourcePath: sprite.path }),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function flagCandidates(tag) {
|
|
149
|
+
return ['tga', 'dds', 'png'].flatMap((extension) => [
|
|
150
|
+
`gfx/flags/${tag}.${extension}`,
|
|
151
|
+
`gfx/flags/medium/${tag}.${extension}`,
|
|
152
|
+
`gfx/flags/small/${tag}.${extension}`,
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
async function rasterPng(catalog, assetPath) {
|
|
156
|
+
const raster = await catalog.loadRaster(assetPath);
|
|
157
|
+
if (!raster.supported || raster.width < 1 || raster.height < 1)
|
|
158
|
+
return undefined;
|
|
159
|
+
return sharp(raster.data, {
|
|
160
|
+
raw: { width: raster.width, height: raster.height, channels: 4 },
|
|
161
|
+
})
|
|
162
|
+
.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
|
|
163
|
+
.toBuffer();
|
|
164
|
+
}
|
|
165
|
+
async function selectCountryAssets(engine, snapshot, workspaceId, tags, eventId, context, signal) {
|
|
166
|
+
const selections = tags.map((tag) => ({
|
|
167
|
+
tag,
|
|
168
|
+
...leaderSelection(snapshot, tag, eventId),
|
|
169
|
+
}));
|
|
170
|
+
const patterns = [
|
|
171
|
+
...new Set(selections.flatMap(({ tag, leaderTexturePath }) => [
|
|
172
|
+
...flagCandidates(tag),
|
|
173
|
+
...(leaderTexturePath === undefined ? [] : [leaderTexturePath]),
|
|
174
|
+
])),
|
|
175
|
+
].sort(compareCodeUnits);
|
|
176
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
177
|
+
const scannedAssets = patterns.length === 0
|
|
178
|
+
? []
|
|
179
|
+
: await engine.scanner.scan(workspace, {
|
|
180
|
+
patterns,
|
|
181
|
+
...(signal === undefined ? {} : { signal }),
|
|
182
|
+
});
|
|
183
|
+
const graph = buildGuiSourceGraph([], snapshot.index);
|
|
184
|
+
const catalog = new GuiAssetCatalog(graph, scannedAssets, new RenderBudget());
|
|
185
|
+
for (const selection of selections) {
|
|
186
|
+
const flagPath = flagCandidates(selection.tag).find((candidate) => catalog.resolveFile(candidate) !== undefined);
|
|
187
|
+
if (flagPath !== undefined)
|
|
188
|
+
selection.flagPath = flagPath;
|
|
189
|
+
}
|
|
190
|
+
return { selections, catalog };
|
|
191
|
+
}
|
|
192
|
+
function scannerPattern(displayPath) {
|
|
193
|
+
const separator = displayPath.indexOf(':');
|
|
194
|
+
return separator < 0 ? displayPath : displayPath.slice(separator + 1);
|
|
195
|
+
}
|
|
196
|
+
async function cachedGuiRevisionCurrent(engine, context, workspaceId, cached, signal) {
|
|
197
|
+
const expectedPaths = Object.keys(cached.sourceHashes);
|
|
198
|
+
if (expectedPaths.length === 0)
|
|
199
|
+
return true;
|
|
200
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
201
|
+
const expected = new Set(expectedPaths);
|
|
202
|
+
const scanned = await engine.scanner.scan(workspace, {
|
|
203
|
+
patterns: [...new Set(expectedPaths.map(scannerPattern))].sort(compareCodeUnits),
|
|
204
|
+
signal,
|
|
205
|
+
});
|
|
206
|
+
const current = Object.fromEntries(scanned
|
|
207
|
+
.filter(({ displayPath }) => expected.has(displayPath))
|
|
208
|
+
.map(({ displayPath, sha256 }) => [displayPath, sha256]));
|
|
209
|
+
return hashCanonical(current) === hashCanonical(cached.sourceHashes);
|
|
210
|
+
}
|
|
211
|
+
function collectNamedGuiAttributes(value, keys, output) {
|
|
212
|
+
if (Array.isArray(value)) {
|
|
213
|
+
for (const entry of value)
|
|
214
|
+
collectNamedGuiAttributes(entry, keys, output);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (typeof value !== 'object' || value === null)
|
|
218
|
+
return;
|
|
219
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
220
|
+
if (keys.has(key) && typeof entry === 'string' && entry.length > 0)
|
|
221
|
+
output.add(entry);
|
|
222
|
+
collectNamedGuiAttributes(entry, keys, output);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function guiDependencySourceHashes(graph, selector) {
|
|
226
|
+
const window = [...graph.elements]
|
|
227
|
+
.sort((left, right) => left.definitionOrder - right.definitionOrder)
|
|
228
|
+
.find(({ name }) => name === selector.windowName);
|
|
229
|
+
if (window === undefined)
|
|
230
|
+
throw new Error(`GUI window not found: ${selector.windowName}`);
|
|
231
|
+
const scriptedGui = graph.scriptedGuis.find(({ name }) => name === selector.guiId);
|
|
232
|
+
if (scriptedGui === undefined)
|
|
233
|
+
throw new Error(`Scripted GUI not found: ${selector.guiId}`);
|
|
234
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
235
|
+
const elementById = new Map(graph.elements.map((element) => [element.id, element]));
|
|
236
|
+
const spriteByName = new Map(graph.sprites.map((sprite) => [sprite.name, sprite]));
|
|
237
|
+
const fontByName = new Map(graph.fonts.map((font) => [font.name, font]));
|
|
238
|
+
const localisationByKey = new Map();
|
|
239
|
+
for (const localisation of graph.localisation)
|
|
240
|
+
if (!localisationByKey.has(localisation.key))
|
|
241
|
+
localisationByKey.set(localisation.key, localisation);
|
|
242
|
+
const outgoing = new Map();
|
|
243
|
+
for (const edge of graph.edges) {
|
|
244
|
+
if (!edge.resolved)
|
|
245
|
+
continue;
|
|
246
|
+
const targets = outgoing.get(edge.from) ?? [];
|
|
247
|
+
targets.push(edge.to);
|
|
248
|
+
outgoing.set(edge.from, targets);
|
|
249
|
+
}
|
|
250
|
+
const pending = [window.id, scriptedGui.id];
|
|
251
|
+
const visited = new Set();
|
|
252
|
+
const sourcePaths = new Set([window.sourcePath, scriptedGui.sourcePath]);
|
|
253
|
+
while (pending.length > 0) {
|
|
254
|
+
const nodeId = pending.pop();
|
|
255
|
+
if (nodeId === undefined || visited.has(nodeId))
|
|
256
|
+
continue;
|
|
257
|
+
visited.add(nodeId);
|
|
258
|
+
const node = nodeById.get(nodeId);
|
|
259
|
+
if (node !== undefined && graph.sourceHashes[node.path] !== undefined)
|
|
260
|
+
sourcePaths.add(node.path);
|
|
261
|
+
const element = elementById.get(nodeId);
|
|
262
|
+
if (element !== undefined) {
|
|
263
|
+
const spriteNames = new Set();
|
|
264
|
+
const fontNames = new Set();
|
|
265
|
+
const localisationKeys = new Set();
|
|
266
|
+
collectNamedGuiAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), spriteNames);
|
|
267
|
+
collectNamedGuiAttributes(element.attributes, new Set(['font', 'buttonFont']), fontNames);
|
|
268
|
+
collectNamedGuiAttributes(element.attributes, new Set(['text', 'buttonText', 'pdx_tooltip', 'pdx_tooltip_delayed', 'hint_tag']), localisationKeys);
|
|
269
|
+
for (const spriteName of spriteNames) {
|
|
270
|
+
const sprite = spriteByName.get(spriteName);
|
|
271
|
+
if (sprite !== undefined)
|
|
272
|
+
pending.push(sprite.id);
|
|
273
|
+
}
|
|
274
|
+
for (const fontName of fontNames) {
|
|
275
|
+
const font = fontByName.get(fontName);
|
|
276
|
+
if (font !== undefined)
|
|
277
|
+
pending.push(font.id);
|
|
278
|
+
}
|
|
279
|
+
for (const localisationKey of localisationKeys) {
|
|
280
|
+
const localisation = localisationByKey.get(localisationKey);
|
|
281
|
+
if (localisation !== undefined)
|
|
282
|
+
sourcePaths.add(localisation.sourcePath);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
for (const target of outgoing.get(nodeId) ?? [])
|
|
286
|
+
pending.push(target);
|
|
287
|
+
}
|
|
288
|
+
return Object.fromEntries([...sourcePaths]
|
|
289
|
+
.filter((sourcePath) => graph.sourceHashes[sourcePath] !== undefined)
|
|
290
|
+
.sort(compareCodeUnits)
|
|
291
|
+
.map((sourcePath) => [sourcePath, graph.sourceHashes[sourcePath]]));
|
|
292
|
+
}
|
|
293
|
+
async function resolveGuiRevision(engine, context, studio, cache, workspaceId, selector, signal) {
|
|
294
|
+
const cacheKey = hashCanonical({ workspaceId, selector });
|
|
295
|
+
const cached = cache.get(cacheKey);
|
|
296
|
+
if (cached !== undefined &&
|
|
297
|
+
(await cachedGuiRevisionCurrent(engine, context, workspaceId, cached, signal))) {
|
|
298
|
+
return {
|
|
299
|
+
windowName: selector.windowName,
|
|
300
|
+
guiId: selector.guiId,
|
|
301
|
+
revision: cached.sourceRevision,
|
|
302
|
+
workspaceRevision: cached.workspaceRevision,
|
|
303
|
+
filesScanned: Object.keys(cached.sourceHashes).sort(compareCodeUnits),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const linted = await studio.lint({
|
|
307
|
+
workspaceId,
|
|
308
|
+
windowName: selector.windowName,
|
|
309
|
+
scenario: {
|
|
310
|
+
id: 'chaosx-visual-revision',
|
|
311
|
+
resolution: { width: 1920, height: 1080 },
|
|
312
|
+
state: 'active',
|
|
313
|
+
scriptedGui: { [selector.guiId]: true },
|
|
314
|
+
visibility: { [selector.windowName]: true, [selector.guiId]: true },
|
|
315
|
+
},
|
|
316
|
+
...(context.principal === undefined ? {} : { principal: context.principal }),
|
|
317
|
+
signal,
|
|
318
|
+
});
|
|
319
|
+
const sourceHashes = guiDependencySourceHashes(linted.graph, selector);
|
|
320
|
+
const sourceRevision = hashCanonical({ selector, sourceHashes });
|
|
321
|
+
const next = {
|
|
322
|
+
workspaceRevision: linted.scene.sourceRevision,
|
|
323
|
+
sourceRevision,
|
|
324
|
+
sourceHashes,
|
|
325
|
+
};
|
|
326
|
+
cache.set(cacheKey, next);
|
|
327
|
+
return {
|
|
328
|
+
windowName: selector.windowName,
|
|
329
|
+
guiId: selector.guiId,
|
|
330
|
+
revision: sourceRevision,
|
|
331
|
+
workspaceRevision: next.workspaceRevision,
|
|
332
|
+
filesScanned: Object.keys(sourceHashes).sort(compareCodeUnits),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
export function registerChaosxTools(server, engine, context) {
|
|
336
|
+
const guiStudio = new ScriptedGuiStudio(engine);
|
|
337
|
+
const guiRevisionCache = new Map();
|
|
338
|
+
server.registerTool('chaosx.visual_revision', {
|
|
339
|
+
title: 'Check ChaosX visual revisions',
|
|
340
|
+
description: 'Private ChaosX cache-coherency endpoint. Computes exact scripted-GUI source revisions without rendering PNG artifacts.',
|
|
341
|
+
inputSchema: chaosxVisualRevisionInput,
|
|
342
|
+
outputSchema: chaosxVisualRevisionOutput,
|
|
343
|
+
annotations: readOnly,
|
|
344
|
+
}, async (input, extra) => {
|
|
345
|
+
const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
|
|
346
|
+
try {
|
|
347
|
+
const progress = progressReporter(extra);
|
|
348
|
+
await progress.report(0, 2, 'Checking ChaosX scripted-GUI sources');
|
|
349
|
+
const resolvedGuiRevisions = [];
|
|
350
|
+
for (const selector of input.guiWindows)
|
|
351
|
+
resolvedGuiRevisions.push(await resolveGuiRevision(engine, context, guiStudio, guiRevisionCache, workspaceId, selector, progress.signal));
|
|
352
|
+
const filesScanned = new Set(resolvedGuiRevisions.flatMap(({ filesScanned }) => filesScanned));
|
|
353
|
+
const guiRevisions = resolvedGuiRevisions.map(({ windowName, guiId, revision }) => ({
|
|
354
|
+
windowName,
|
|
355
|
+
guiId,
|
|
356
|
+
revision,
|
|
357
|
+
}));
|
|
358
|
+
const workspaceRevision = hashCanonical(resolvedGuiRevisions.map(({ windowName, workspaceRevision: revision }) => ({
|
|
359
|
+
windowName,
|
|
360
|
+
revision,
|
|
361
|
+
})));
|
|
362
|
+
const result = emptyServiceResult(workspaceId, {
|
|
363
|
+
workspaceRevision,
|
|
364
|
+
guiRevisions,
|
|
365
|
+
dependencyFileCount: filesScanned.size,
|
|
366
|
+
});
|
|
367
|
+
result.code = 'CHAOSX_VISUAL_REVISION_CHECKED';
|
|
368
|
+
setInlineFilesScanned(result, [...filesScanned].sort(compareCodeUnits));
|
|
369
|
+
await progress.report(2, 2, 'ChaosX scripted-GUI revisions complete');
|
|
370
|
+
return toolResult(result);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
return errorResult(error, workspaceId);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
server.registerTool('chaosx.focus_country_assets', {
|
|
377
|
+
title: 'Render ChaosX focus country assets',
|
|
378
|
+
description: 'Private ChaosX Discord integration endpoint. Returns country flags and leader portraits to accompany a focus-tree raster; it is not intended for coding-agent workflows.',
|
|
379
|
+
inputSchema: chaosxCountryAssetsInput,
|
|
380
|
+
outputSchema: chaosxCountryAssetsOutput,
|
|
381
|
+
annotations: artifactProducing,
|
|
382
|
+
}, async (input, extra) => {
|
|
383
|
+
const workspaceId = await resolveServerWorkspaceId(engine, context, input.workspaceId, extra.signal);
|
|
384
|
+
try {
|
|
385
|
+
const progress = progressReporter(extra);
|
|
386
|
+
await progress.report(0, 3, 'Resolving ChaosX country assets');
|
|
387
|
+
const discoveryWorkspace = engine.resolver.get(workspaceId, context.principal);
|
|
388
|
+
const snapshot = await engine.scan(workspaceId, { patterns: countryAssetDiscoveryPatterns(discoveryWorkspace) }, context.principal, progress.signal);
|
|
389
|
+
const tags = [...new Set(input.countryTags)];
|
|
390
|
+
const { selections, catalog } = await selectCountryAssets(engine, snapshot, workspaceId, tags, input.eventId, context, progress.signal);
|
|
391
|
+
await progress.report(1, 3, 'Rasterizing ChaosX country assets');
|
|
392
|
+
const writes = [];
|
|
393
|
+
const countries = [];
|
|
394
|
+
const filesScanned = new Set();
|
|
395
|
+
for (const selection of selections) {
|
|
396
|
+
const country = { tag: selection.tag };
|
|
397
|
+
const sourcePaths = [selection.discoverySourcePath, selection.spriteSourcePath].filter((value) => value !== undefined);
|
|
398
|
+
for (const sourcePath of sourcePaths)
|
|
399
|
+
filesScanned.add(sourcePath);
|
|
400
|
+
if (selection.flagPath !== undefined) {
|
|
401
|
+
const png = await rasterPng(catalog, selection.flagPath);
|
|
402
|
+
const source = catalog.resolveFile(selection.flagPath);
|
|
403
|
+
if (png !== undefined && source !== undefined) {
|
|
404
|
+
filesScanned.add(source.displayPath);
|
|
405
|
+
const name = `chaosx-${selection.tag}-flag.png`;
|
|
406
|
+
country.flagArtifactName = name;
|
|
407
|
+
writes.push({
|
|
408
|
+
name,
|
|
409
|
+
mimeType: 'image/png',
|
|
410
|
+
content: png,
|
|
411
|
+
provenance: {
|
|
412
|
+
kind: 'chaosx-country-flag',
|
|
413
|
+
toolVersion: PACKAGE_VERSION,
|
|
414
|
+
schemaVersion: 'chaosx-country-assets.v1',
|
|
415
|
+
sourceHashes: { [source.displayPath]: source.sha256 },
|
|
416
|
+
metadata: {
|
|
417
|
+
tag: selection.tag,
|
|
418
|
+
eventId: input.eventId ?? null,
|
|
419
|
+
treeId: input.treeId ?? null,
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
description: `ChaosX country flag for ${selection.tag}`,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (selection.leaderTexturePath !== undefined) {
|
|
427
|
+
const png = await rasterPng(catalog, selection.leaderTexturePath);
|
|
428
|
+
const source = catalog.resolveFile(selection.leaderTexturePath);
|
|
429
|
+
if (png !== undefined && source !== undefined) {
|
|
430
|
+
filesScanned.add(source.displayPath);
|
|
431
|
+
const name = `chaosx-${selection.tag}-leader.png`;
|
|
432
|
+
country.leaderPortraitArtifactName = name;
|
|
433
|
+
const leaderSprite = selection.leaderSprite;
|
|
434
|
+
if (leaderSprite !== undefined)
|
|
435
|
+
country.leaderSprite = leaderSprite;
|
|
436
|
+
const sourceHashes = { [source.displayPath]: source.sha256 };
|
|
437
|
+
for (const sourcePath of sourcePaths) {
|
|
438
|
+
const file = snapshot.files.find(({ displayPath }) => displayPath === sourcePath);
|
|
439
|
+
if (file !== undefined)
|
|
440
|
+
sourceHashes[sourcePath] = file.sha256;
|
|
441
|
+
}
|
|
442
|
+
writes.push({
|
|
443
|
+
name,
|
|
444
|
+
mimeType: 'image/png',
|
|
445
|
+
content: png,
|
|
446
|
+
provenance: {
|
|
447
|
+
kind: 'chaosx-leader-portrait',
|
|
448
|
+
toolVersion: PACKAGE_VERSION,
|
|
449
|
+
schemaVersion: 'chaosx-country-assets.v1',
|
|
450
|
+
sourceHashes,
|
|
451
|
+
metadata: {
|
|
452
|
+
tag: selection.tag,
|
|
453
|
+
leaderSprite: selection.leaderSprite ?? null,
|
|
454
|
+
eventId: input.eventId ?? null,
|
|
455
|
+
treeId: input.treeId ?? null,
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
description: `ChaosX leader portrait for ${selection.tag}`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
countries.push(country);
|
|
463
|
+
}
|
|
464
|
+
const workspace = engine.resolver.get(workspaceId, context.principal);
|
|
465
|
+
const artifacts = writes.length === 0
|
|
466
|
+
? []
|
|
467
|
+
: await engine.artifacts.withAtomicChunkedWrites(workspace, writes, (stored) => Promise.resolve([...stored]), progress.signal);
|
|
468
|
+
const result = emptyServiceResult(workspaceId, {
|
|
469
|
+
revision: snapshot.revision,
|
|
470
|
+
countries,
|
|
471
|
+
artifactCount: artifacts.length,
|
|
472
|
+
});
|
|
473
|
+
result.code = 'CHAOSX_COUNTRY_ASSETS_RENDERED';
|
|
474
|
+
setInlineFilesScanned(result, [...filesScanned].sort(compareCodeUnits));
|
|
475
|
+
result.artifacts = artifacts.map(publicArtifactLink);
|
|
476
|
+
await progress.report(3, 3, 'ChaosX country assets complete');
|
|
477
|
+
return toolResult(result);
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
return errorResult(error, workspaceId);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
@@ -136,7 +136,12 @@ const resolutionSchema = z
|
|
|
136
136
|
.object({
|
|
137
137
|
width: z.number().int().min(320).max(RENDER_MAX_DIMENSION),
|
|
138
138
|
height: z.number().int().min(200).max(RENDER_MAX_DIMENSION),
|
|
139
|
-
uiScale: z
|
|
139
|
+
uiScale: z
|
|
140
|
+
.number()
|
|
141
|
+
.min(0.25)
|
|
142
|
+
.max(4)
|
|
143
|
+
.optional()
|
|
144
|
+
.describe('In-game UI scale for this resolution. Use 1 at 1920x1080; do not use it to enlarge an artifact.'),
|
|
140
145
|
})
|
|
141
146
|
.strict()
|
|
142
147
|
.superRefine(({ width, height }, context) => {
|
|
@@ -18,7 +18,7 @@ const candidatePool = z.array(z.string().min(1).max(512)).max(100_000).optional(
|
|
|
18
18
|
const outputs = z.array(probabilityOutputSchema).max(10).optional();
|
|
19
19
|
const commonShape = {
|
|
20
20
|
workspaceId: workspaceIdSchema,
|
|
21
|
-
adapter: probabilityAdapterIdSchema,
|
|
21
|
+
adapter: probabilityAdapterIdSchema.optional(),
|
|
22
22
|
scenarioSet: nestedScenarios,
|
|
23
23
|
candidatePool,
|
|
24
24
|
horizonDays: z.number().positive().max(1_000_000).optional(),
|
|
@@ -51,7 +51,9 @@ const inspectInput = z
|
|
|
51
51
|
code: 'custom',
|
|
52
52
|
message: 'An adapter requires a source; provide a source alone to discover compatible adapters',
|
|
53
53
|
});
|
|
54
|
-
if (value.customPoolManifest !== undefined &&
|
|
54
|
+
if (value.customPoolManifest !== undefined &&
|
|
55
|
+
value.adapter !== undefined &&
|
|
56
|
+
value.adapter !== 'custom_weighted_pool')
|
|
55
57
|
context.addIssue({
|
|
56
58
|
code: 'custom',
|
|
57
59
|
path: ['adapter'],
|
|
@@ -71,7 +73,9 @@ const evaluateInput = z
|
|
|
71
73
|
code: 'custom',
|
|
72
74
|
message: 'Provide exactly one source or customPoolManifest',
|
|
73
75
|
});
|
|
74
|
-
if (value.customPoolManifest !== undefined &&
|
|
76
|
+
if (value.customPoolManifest !== undefined &&
|
|
77
|
+
value.adapter !== undefined &&
|
|
78
|
+
value.adapter !== 'custom_weighted_pool')
|
|
75
79
|
context.addIssue({
|
|
76
80
|
code: 'custom',
|
|
77
81
|
path: ['adapter'],
|
|
@@ -120,7 +124,7 @@ const sequenceInput = z
|
|
|
120
124
|
const compareInput = z
|
|
121
125
|
.object({
|
|
122
126
|
workspaceId: workspaceIdSchema,
|
|
123
|
-
adapter: probabilityAdapterIdSchema,
|
|
127
|
+
adapter: probabilityAdapterIdSchema.optional(),
|
|
124
128
|
before: nestedSource.optional(),
|
|
125
129
|
after: nestedSource.optional(),
|
|
126
130
|
beforeManifest: nestedManifest.optional(),
|
|
@@ -149,7 +153,7 @@ const compareInput = z
|
|
|
149
153
|
code: 'custom',
|
|
150
154
|
message: 'Custom-pool comparison requires beforeManifest and afterManifest',
|
|
151
155
|
});
|
|
152
|
-
if (manifestMode && value.adapter !== 'custom_weighted_pool')
|
|
156
|
+
if (manifestMode && value.adapter !== undefined && value.adapter !== 'custom_weighted_pool')
|
|
153
157
|
context.addIssue({
|
|
154
158
|
code: 'custom',
|
|
155
159
|
path: ['adapter'],
|
|
@@ -17,6 +17,43 @@ import { Rational } from './rational.js';
|
|
|
17
17
|
import { discoverWeightedSurface, inventoryWeightedSurfaces, } from './source-analysis.js';
|
|
18
18
|
import { sweepSurface } from './sweep.js';
|
|
19
19
|
const states = new WeakMap();
|
|
20
|
+
function resolveSourceBackedAdapter(snapshot, source, candidatePool, requested) {
|
|
21
|
+
if (requested !== undefined && requested !== 'custom_weighted_pool') {
|
|
22
|
+
try {
|
|
23
|
+
discoverWeightedSurface(snapshot, requested, source, candidatePool);
|
|
24
|
+
return requested;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (!(error instanceof ServiceError) ||
|
|
28
|
+
(error.code !== 'PROBABILITY_SURFACE_EMPTY' &&
|
|
29
|
+
error.code !== 'PROBABILITY_IDENTIFIER_NOT_FOUND'))
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const inventory = inventoryWeightedSurfaces(snapshot, source, candidatePool);
|
|
34
|
+
const viable = inventory.adapters
|
|
35
|
+
.filter(({ candidateCount, identifierMatchCount, candidatePoolMatchCount }) => candidateCount > 0 &&
|
|
36
|
+
(source.identifier === undefined || identifierMatchCount > 0) &&
|
|
37
|
+
(candidatePool.length === 0 || candidatePoolMatchCount > 0))
|
|
38
|
+
.sort((left, right) => right.identifierMatchCount - left.identifierMatchCount ||
|
|
39
|
+
right.candidatePoolMatchCount - left.candidatePoolMatchCount ||
|
|
40
|
+
right.candidateCount - left.candidateCount ||
|
|
41
|
+
compareCodeUnits(left.adapterId, right.adapterId));
|
|
42
|
+
if (viable.length === 1)
|
|
43
|
+
return viable[0].adapterId;
|
|
44
|
+
if (viable.length === 0)
|
|
45
|
+
throw new ServiceError('PROBABILITY_SURFACE_EMPTY', 'No weighted blocks matched this request', {
|
|
46
|
+
requestedAdapter: requested,
|
|
47
|
+
source,
|
|
48
|
+
candidatePool,
|
|
49
|
+
availableAdapters: inventory.adapters.filter(({ candidateCount }) => candidateCount > 0),
|
|
50
|
+
});
|
|
51
|
+
throw new ServiceError('PROBABILITY_ADAPTER_AMBIGUOUS', 'Several weighted adapters match this source; select one adapter or narrow the source by identifier or line', {
|
|
52
|
+
requestedAdapter: requested,
|
|
53
|
+
source,
|
|
54
|
+
matchingAdapters: viable,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
20
57
|
function analyzerState(engine) {
|
|
21
58
|
let state = states.get(engine);
|
|
22
59
|
if (state === undefined) {
|
|
@@ -853,7 +890,9 @@ export class ProbabilityAnalyzer {
|
|
|
853
890
|
return { adapters: probabilityAdapters(), artifacts: [], filesScanned: [] };
|
|
854
891
|
if (customPoolManifest !== undefined && source !== undefined)
|
|
855
892
|
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide either source or customPoolManifest for inspection');
|
|
856
|
-
if (customPoolManifest !== undefined &&
|
|
893
|
+
if (customPoolManifest !== undefined &&
|
|
894
|
+
adapter !== undefined &&
|
|
895
|
+
adapter !== 'custom_weighted_pool')
|
|
857
896
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest inspection requires adapter custom_weighted_pool');
|
|
858
897
|
const snapshot = await this.scan(context, [source?.path], source === undefined ? 'none' : source.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
859
898
|
if (customPoolManifest !== undefined) {
|
|
@@ -1040,15 +1079,18 @@ export class ProbabilityAnalyzer {
|
|
|
1040
1079
|
const usingManifest = request.customPoolManifest !== undefined;
|
|
1041
1080
|
if (usingManifest !== (request.source === undefined))
|
|
1042
1081
|
throw new ServiceError('PROBABILITY_INPUT_AMBIGUOUS', 'Provide exactly one source or customPoolManifest');
|
|
1043
|
-
if (usingManifest &&
|
|
1082
|
+
if (usingManifest &&
|
|
1083
|
+
request.adapter !== undefined &&
|
|
1084
|
+
request.adapter !== 'custom_weighted_pool')
|
|
1044
1085
|
throw new ServiceError('PROBABILITY_ADAPTER_MISMATCH', 'customPoolManifest requires the custom_weighted_pool adapter');
|
|
1045
|
-
if (!usingManifest && request.adapter === 'custom_weighted_pool')
|
|
1046
|
-
throw new ServiceError('PROBABILITY_MANIFEST_REQUIRED', 'custom_weighted_pool requires customPoolManifest');
|
|
1047
|
-
const gameVersionVerification = await this.verifyGameVersion(request, request.adapter);
|
|
1048
1086
|
const snapshot = await this.scan(request, [request.source?.path], usingManifest ? 'none' : request.source?.inlineClausewitz === undefined ? 'domain' : 'shared');
|
|
1087
|
+
const resolvedAdapter = usingManifest
|
|
1088
|
+
? 'custom_weighted_pool'
|
|
1089
|
+
: resolveSourceBackedAdapter(snapshot, request.source, request.candidatePool ?? [], request.adapter);
|
|
1090
|
+
const gameVersionVerification = await this.verifyGameVersion(request, resolvedAdapter);
|
|
1049
1091
|
const surface = usingManifest
|
|
1050
1092
|
? customSurface(request.customPoolManifest, snapshot.revision)
|
|
1051
|
-
: discoverWeightedSurface(snapshot,
|
|
1093
|
+
: discoverWeightedSurface(snapshot, resolvedAdapter, request.source, request.candidatePool ?? []);
|
|
1052
1094
|
const definitions = ClausewitzEvaluationDefinitions.build(snapshot);
|
|
1053
1095
|
const metadata = this.metadata(request, snapshot, surface, request.scenarioSet, usingManifest
|
|
1054
1096
|
? request.customPoolManifest.candidates.map(({ id }) => id)
|
|
@@ -4,12 +4,17 @@ function byId(values) {
|
|
|
4
4
|
}
|
|
5
5
|
function placementSignature(values) {
|
|
6
6
|
return hashCanonical(values
|
|
7
|
-
.map(({ folderId, x, y, xExpression, yExpression }) => ({
|
|
7
|
+
.map(({ folderId, x, y, xExpression, yExpression, layoutSize, layoutWidth, layoutHeight, layoutOffsetX, layoutOffsetY, }) => ({
|
|
8
8
|
folderId,
|
|
9
9
|
x: x ?? null,
|
|
10
10
|
y: y ?? null,
|
|
11
11
|
xExpression: xExpression ?? null,
|
|
12
12
|
yExpression: yExpression ?? null,
|
|
13
|
+
layoutSize: layoutSize ?? null,
|
|
14
|
+
layoutWidth: layoutWidth ?? null,
|
|
15
|
+
layoutHeight: layoutHeight ?? null,
|
|
16
|
+
layoutOffsetX: layoutOffsetX ?? null,
|
|
17
|
+
layoutOffsetY: layoutOffsetY ?? null,
|
|
13
18
|
}))
|
|
14
19
|
.sort((left, right) => compareCodeUnits(left.folderId, right.folderId) ||
|
|
15
20
|
(left.x ?? 0) - (right.x ?? 0) ||
|