hoi4-agent-tools 2.6.0 → 2.7.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 +7 -0
- package/dist/hoi4_agent_tools/gui/assets.js +15 -3
- package/dist/hoi4_agent_tools/gui/layout.js +270 -75
- package/dist/hoi4_agent_tools/gui/renderer.js +185 -22
- package/dist/hoi4_agent_tools/gui/source-graph.js +40 -11
- package/dist/hoi4_agent_tools/gui/studio.js +35 -7
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/gui.md +5 -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 +1 -1
- 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
|
@@ -86,10 +86,23 @@ function renderText(element, toolText) {
|
|
|
86
86
|
(glyphLine === undefined || glyphLine.source === 'deterministic-fallback'
|
|
87
87
|
? text.lineHeight * 0.8
|
|
88
88
|
: glyphLine.baseline);
|
|
89
|
+
const horizontalScale = glyphLine !== undefined && glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
|
|
90
|
+
const inlineIconMarkup = (text.inlineIcons ?? [])
|
|
91
|
+
.filter((icon) => icon.lineIndex === index)
|
|
92
|
+
.map((icon) => {
|
|
93
|
+
const iconRect = {
|
|
94
|
+
x: originX + icon.offsetX * horizontalScale,
|
|
95
|
+
y: lineTop + (text.lineHeight - icon.height) / 2,
|
|
96
|
+
width: icon.width * horizontalScale,
|
|
97
|
+
height: icon.height,
|
|
98
|
+
};
|
|
99
|
+
if (icon.sprite?.supported === true && icon.sprite.dataUri !== undefined)
|
|
100
|
+
return `<image data-inline-icon="${escapeXml(icon.token)}" ${rectAttributes(iconRect)} href="${icon.sprite.dataUri}" preserveAspectRatio="xMidYMid meet"/>`;
|
|
101
|
+
return `<g data-inline-icon-missing="${escapeXml(icon.token)}"><rect ${rectAttributes(iconRect)} fill="#331f3f" stroke="#ff42d0" stroke-width="1"/><path d="M ${finite(iconRect.x)} ${finite(iconRect.y)} L ${finite(iconRect.x + iconRect.width)} ${finite(iconRect.y + iconRect.height)} M ${finite(iconRect.x + iconRect.width)} ${finite(iconRect.y)} L ${finite(iconRect.x)} ${finite(iconRect.y + iconRect.height)}" stroke="#ff42d0"/></g>`;
|
|
102
|
+
})
|
|
103
|
+
.join('');
|
|
89
104
|
const colourRuns = text.colourRuns?.[index] ?? [];
|
|
90
105
|
if (colourRuns.length > 0) {
|
|
91
|
-
const runWidth = colourRuns.reduce((maximum, run) => Math.max(maximum, run.offsetX + run.width), 0);
|
|
92
|
-
const horizontalScale = runWidth > 0 && width > 0 ? width / runWidth : 1;
|
|
93
106
|
return `<g data-hoi4-colour-runs="true">${colourRuns
|
|
94
107
|
.map((run) => toolText.render(run.text, {
|
|
95
108
|
x: originX + run.offsetX * horizontalScale,
|
|
@@ -100,23 +113,22 @@ function renderText(element, toolText) {
|
|
|
100
113
|
strokeWidth: 0.6,
|
|
101
114
|
...(run.width > 0 ? { targetWidth: run.width * horizontalScale } : {}),
|
|
102
115
|
}))
|
|
103
|
-
.join('')}</g>`;
|
|
116
|
+
.join('')}${inlineIconMarkup}</g>`;
|
|
104
117
|
}
|
|
105
118
|
if (glyphLine?.source === 'fontkit-path') {
|
|
106
|
-
const horizontalScale = glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
|
|
107
119
|
return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphLine.glyphs
|
|
108
120
|
.filter((glyph) => glyph.kind === 'outline')
|
|
109
121
|
.map((glyph) => `<use href="#${outlineDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y)}) scale(${finite(glyph.scale * horizontalScale)} ${finite(-glyph.scale)})"/>`)
|
|
110
|
-
.join('')}</g>`;
|
|
122
|
+
.join('')}${inlineIconMarkup}</g>`;
|
|
111
123
|
}
|
|
112
124
|
if (glyphLine?.source === 'bmfont-atlas') {
|
|
113
|
-
const horizontalScale = glyphLine.width > 0 && width > 0 ? width / glyphLine.width : 1;
|
|
114
125
|
return `<g data-font-sha256="${glyphLine.sourceHash}">${glyphLine.glyphs
|
|
115
126
|
.filter((glyph) => glyph.kind === 'bitmap')
|
|
116
127
|
.map((glyph) => `<use href="#${bitmapDefinitionId(glyph.key)}" transform="translate(${finite(originX + glyph.x * horizontalScale)} ${finite(baseline + glyph.y - glyphLine.baseline)}) scale(${finite(horizontalScale)} 1)"/>`)
|
|
117
|
-
.join('')}</g>`;
|
|
128
|
+
.join('')}${inlineIconMarkup}</g>`;
|
|
118
129
|
}
|
|
119
|
-
|
|
130
|
+
const visibleLine = line.replace(/[\uE000-\uF8FF]/gu, ' ');
|
|
131
|
+
return `<g>${toolText.render(visibleLine, {
|
|
120
132
|
x: originX,
|
|
121
133
|
y: baseline,
|
|
122
134
|
fontSize: text.fontSize,
|
|
@@ -124,9 +136,153 @@ function renderText(element, toolText) {
|
|
|
124
136
|
stroke: '#12151a',
|
|
125
137
|
strokeWidth: 0.6,
|
|
126
138
|
...(width > 0 ? { targetWidth: width } : {}),
|
|
127
|
-
})
|
|
139
|
+
})}${inlineIconMarkup}</g>`;
|
|
128
140
|
});
|
|
129
|
-
|
|
141
|
+
const textClipId = `gui-text-clip-${sha256Bytes(element.id).slice(0, 20)}`;
|
|
142
|
+
const clipDefinition = text.fixedSize
|
|
143
|
+
? `<defs><clipPath id="${textClipId}"><rect ${rectAttributes(rect)}/></clipPath></defs>`
|
|
144
|
+
: '';
|
|
145
|
+
const clip = text.fixedSize ? ` clip-path="url(#${textClipId})"` : '';
|
|
146
|
+
return `${clipDefinition}<g data-source-id="${escapeXml(element.sourceId)}" fill="#f5f2e8" stroke="#12151a" stroke-width="0.6" paint-order="stroke"${clip}>${lines.join('')}</g>`;
|
|
147
|
+
}
|
|
148
|
+
function renderSpriteImage(sprite, rect) {
|
|
149
|
+
if (!sprite.supported || sprite.dataUri === undefined)
|
|
150
|
+
return '';
|
|
151
|
+
return `<image ${rectAttributes(rect)} href="${sprite.dataUri}" preserveAspectRatio="none"/>`;
|
|
152
|
+
}
|
|
153
|
+
function renderSpriteSlice(sprite, source, destination) {
|
|
154
|
+
if (!sprite.supported ||
|
|
155
|
+
sprite.dataUri === undefined ||
|
|
156
|
+
source.width <= 0 ||
|
|
157
|
+
source.height <= 0 ||
|
|
158
|
+
destination.width <= 0 ||
|
|
159
|
+
destination.height <= 0)
|
|
160
|
+
return '';
|
|
161
|
+
return `<svg ${rectAttributes(destination)} viewBox="${finite(source.x)} ${finite(source.y)} ${finite(source.width)} ${finite(source.height)}" preserveAspectRatio="none" overflow="hidden"><image x="0" y="0" width="${finite(sprite.width)}" height="${finite(sprite.height)}" href="${sprite.dataUri}" preserveAspectRatio="none"/></svg>`;
|
|
162
|
+
}
|
|
163
|
+
function renderTiledSpriteSlice(element, sprite, source, destination) {
|
|
164
|
+
if (!sprite.supported ||
|
|
165
|
+
sprite.dataUri === undefined ||
|
|
166
|
+
source.width <= 0 ||
|
|
167
|
+
source.height <= 0 ||
|
|
168
|
+
destination.width <= 0 ||
|
|
169
|
+
destination.height <= 0)
|
|
170
|
+
return '';
|
|
171
|
+
const tileWidth = Math.max(1, source.width * element.scale);
|
|
172
|
+
const tileHeight = Math.max(1, source.height * element.scale);
|
|
173
|
+
const id = `gui-tile-${sha256Bytes(`${element.id}:${source.x}:${source.y}:${source.width}:${source.height}`).slice(0, 20)}`;
|
|
174
|
+
return `<defs><pattern id="${id}" patternUnits="userSpaceOnUse" x="${finite(destination.x)}" y="${finite(destination.y)}" width="${finite(tileWidth)}" height="${finite(tileHeight)}"><svg width="${finite(tileWidth)}" height="${finite(tileHeight)}" viewBox="${finite(source.x)} ${finite(source.y)} ${finite(source.width)} ${finite(source.height)}" preserveAspectRatio="none" overflow="hidden"><image x="0" y="0" width="${finite(sprite.width)}" height="${finite(sprite.height)}" href="${sprite.dataUri}" preserveAspectRatio="none"/></svg></pattern></defs><rect ${rectAttributes(destination)} fill="url(#${id})"/>`;
|
|
175
|
+
}
|
|
176
|
+
function renderCorneredTile(element) {
|
|
177
|
+
const sprite = element.sprite;
|
|
178
|
+
const rect = element.unclippedRect;
|
|
179
|
+
const border = element.spriteBorderSize;
|
|
180
|
+
if (sprite?.supported !== true ||
|
|
181
|
+
sprite.dataUri === undefined ||
|
|
182
|
+
border === undefined ||
|
|
183
|
+
border.width <= 0 ||
|
|
184
|
+
border.height <= 0)
|
|
185
|
+
return sprite === undefined ? '' : renderSpriteImage(sprite, rect);
|
|
186
|
+
const sourceBorderX = Math.min(border.width, sprite.width / 2);
|
|
187
|
+
const sourceBorderY = Math.min(border.height, sprite.height / 2);
|
|
188
|
+
const destinationBorderX = Math.min(sourceBorderX * element.scale, rect.width / 2);
|
|
189
|
+
const destinationBorderY = Math.min(sourceBorderY * element.scale, rect.height / 2);
|
|
190
|
+
const sourceWidths = [sourceBorderX, sprite.width - sourceBorderX * 2, sourceBorderX];
|
|
191
|
+
const sourceHeights = [sourceBorderY, sprite.height - sourceBorderY * 2, sourceBorderY];
|
|
192
|
+
const destinationWidths = [
|
|
193
|
+
destinationBorderX,
|
|
194
|
+
rect.width - destinationBorderX * 2,
|
|
195
|
+
destinationBorderX,
|
|
196
|
+
];
|
|
197
|
+
const destinationHeights = [
|
|
198
|
+
destinationBorderY,
|
|
199
|
+
rect.height - destinationBorderY * 2,
|
|
200
|
+
destinationBorderY,
|
|
201
|
+
];
|
|
202
|
+
const sourceXs = [0, sourceBorderX, sprite.width - sourceBorderX];
|
|
203
|
+
const sourceYs = [0, sourceBorderY, sprite.height - sourceBorderY];
|
|
204
|
+
const destinationXs = [
|
|
205
|
+
rect.x,
|
|
206
|
+
rect.x + destinationBorderX,
|
|
207
|
+
rect.x + rect.width - destinationBorderX,
|
|
208
|
+
];
|
|
209
|
+
const destinationYs = [
|
|
210
|
+
rect.y,
|
|
211
|
+
rect.y + destinationBorderY,
|
|
212
|
+
rect.y + rect.height - destinationBorderY,
|
|
213
|
+
];
|
|
214
|
+
const parts = [];
|
|
215
|
+
for (let row = 0; row < 3; row += 1) {
|
|
216
|
+
for (let column = 0; column < 3; column += 1) {
|
|
217
|
+
const source = {
|
|
218
|
+
x: sourceXs[column] ?? 0,
|
|
219
|
+
y: sourceYs[row] ?? 0,
|
|
220
|
+
width: sourceWidths[column] ?? 0,
|
|
221
|
+
height: sourceHeights[row] ?? 0,
|
|
222
|
+
};
|
|
223
|
+
const destination = {
|
|
224
|
+
x: destinationXs[column] ?? rect.x,
|
|
225
|
+
y: destinationYs[row] ?? rect.y,
|
|
226
|
+
width: destinationWidths[column] ?? 0,
|
|
227
|
+
height: destinationHeights[row] ?? 0,
|
|
228
|
+
};
|
|
229
|
+
parts.push(row === 1 && column === 1 && element.spriteTilingCenter === true
|
|
230
|
+
? renderTiledSpriteSlice(element, sprite, source, destination)
|
|
231
|
+
: renderSpriteSlice(sprite, source, destination));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return parts.join('');
|
|
235
|
+
}
|
|
236
|
+
function renderProgressbar(element) {
|
|
237
|
+
const rect = element.unclippedRect;
|
|
238
|
+
const sprite = element.sprite;
|
|
239
|
+
const secondary = element.secondarySprite;
|
|
240
|
+
const ratio = Math.max(0, Math.min(1, element.progressRatio ?? 1));
|
|
241
|
+
const background = secondary === undefined ? '' : renderSpriteImage(secondary, rect);
|
|
242
|
+
if (sprite?.supported !== true || sprite.dataUri === undefined)
|
|
243
|
+
return background;
|
|
244
|
+
if (ratio <= 0)
|
|
245
|
+
return background;
|
|
246
|
+
const horizontal = element.progressHorizontal !== false;
|
|
247
|
+
const filled = horizontal
|
|
248
|
+
? { x: rect.x, y: rect.y, width: rect.width * ratio, height: rect.height }
|
|
249
|
+
: {
|
|
250
|
+
x: rect.x,
|
|
251
|
+
y: rect.y + rect.height * (1 - ratio),
|
|
252
|
+
width: rect.width,
|
|
253
|
+
height: rect.height * ratio,
|
|
254
|
+
};
|
|
255
|
+
const source = horizontal
|
|
256
|
+
? { x: 0, y: 0, width: sprite.width * ratio, height: sprite.height }
|
|
257
|
+
: {
|
|
258
|
+
x: 0,
|
|
259
|
+
y: sprite.height * (1 - ratio),
|
|
260
|
+
width: sprite.width,
|
|
261
|
+
height: sprite.height * ratio,
|
|
262
|
+
};
|
|
263
|
+
return `${background}${renderSpriteSlice(sprite, source, filled)}`;
|
|
264
|
+
}
|
|
265
|
+
function renderMaskedShield(element) {
|
|
266
|
+
const rect = element.unclippedRect;
|
|
267
|
+
const sprite = element.sprite;
|
|
268
|
+
const mask = element.secondarySprite;
|
|
269
|
+
if (sprite?.supported !== true || sprite.dataUri === undefined)
|
|
270
|
+
return '';
|
|
271
|
+
if (mask?.supported !== true || mask.dataUri === undefined)
|
|
272
|
+
return renderSpriteImage(sprite, rect);
|
|
273
|
+
const id = `gui-mask-${sha256Bytes(element.id).slice(0, 20)}`;
|
|
274
|
+
return `<defs><mask id="${id}" maskUnits="userSpaceOnUse" ${rectAttributes(rect)}><image ${rectAttributes(rect)} href="${mask.dataUri}" preserveAspectRatio="none"/></mask></defs><image ${rectAttributes(rect)} href="${sprite.dataUri}" preserveAspectRatio="none" mask="url(#${id})"/>`;
|
|
275
|
+
}
|
|
276
|
+
function renderElementSprite(element) {
|
|
277
|
+
if (element.sprite?.supported !== true || element.sprite.dataUri === undefined)
|
|
278
|
+
return '';
|
|
279
|
+
if (element.spriteRenderMode === 'cornered-tile')
|
|
280
|
+
return renderCorneredTile(element);
|
|
281
|
+
if (element.spriteRenderMode === 'progressbar')
|
|
282
|
+
return renderProgressbar(element);
|
|
283
|
+
if (element.spriteRenderMode === 'masked-shield')
|
|
284
|
+
return renderMaskedShield(element);
|
|
285
|
+
return renderSpriteImage(element.sprite, element.unclippedRect);
|
|
130
286
|
}
|
|
131
287
|
function renderBaseElement(element, clipId, toolText) {
|
|
132
288
|
if (!element.visible)
|
|
@@ -135,25 +291,15 @@ function renderBaseElement(element, clipId, toolText) {
|
|
|
135
291
|
const clip = clipId === undefined ? '' : ` clip-path="url(#${clipId})"`;
|
|
136
292
|
const content = [];
|
|
137
293
|
if (element.sprite?.supported === true && element.sprite.dataUri !== undefined) {
|
|
138
|
-
content.push(
|
|
294
|
+
content.push(renderElementSprite(element));
|
|
139
295
|
}
|
|
140
296
|
else if (element.sprite !== undefined) {
|
|
141
297
|
content.push(`<rect ${rectAttributes(rect)} fill="#331f3f" stroke="#ff42d0" stroke-width="1"/><path d="M ${finite(rect.x)} ${finite(rect.y)} L ${finite(rect.x + rect.width)} ${finite(rect.y + rect.height)} M ${finite(rect.x + rect.width)} ${finite(rect.y)} L ${finite(rect.x)} ${finite(rect.y + rect.height)}" stroke="#ff42d0"/>`);
|
|
142
298
|
}
|
|
143
|
-
if (element.progressRatio !== undefined) {
|
|
299
|
+
if (element.progressRatio !== undefined && element.spriteRenderMode !== 'progressbar') {
|
|
144
300
|
content.push(`<rect x="${finite(rect.x)}" y="${finite(rect.y)}" width="${finite(rect.width * element.progressRatio)}" height="${finite(rect.height)}" fill="#5ecf8d" fill-opacity="0.65"/>`);
|
|
145
301
|
}
|
|
146
302
|
content.push(renderText(element, toolText));
|
|
147
|
-
if (element.state === 'hover')
|
|
148
|
-
content.push(`<rect ${rectAttributes(rect)} fill="#fff" opacity="0.08"/>`);
|
|
149
|
-
if (element.state === 'selected' || element.state === 'active')
|
|
150
|
-
content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#ffd166" stroke-width="2"/>`);
|
|
151
|
-
if (element.state === 'locked' || element.state === 'disabled')
|
|
152
|
-
content.push(`<rect ${rectAttributes(rect)} fill="#111820" opacity="0.5"/>`);
|
|
153
|
-
if (element.state === 'warning')
|
|
154
|
-
content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#ff5d5d" stroke-width="3"/>`);
|
|
155
|
-
if (element.state === 'completed')
|
|
156
|
-
content.push(`<rect ${rectAttributes(rect)} fill="none" stroke="#5ecf8d" stroke-width="2"/>`);
|
|
157
303
|
return `<g id="${escapeXml(element.id)}" data-source="${escapeXml(element.sourcePath)}" data-source-id="${escapeXml(element.sourceId)}"${clip}>${content.join('')}</g>`;
|
|
158
304
|
}
|
|
159
305
|
function renderOverlay(element, variant, toolText) {
|
|
@@ -299,6 +445,23 @@ function sceneLayoutEvidence(scene) {
|
|
|
299
445
|
...(element.sprite.reason === undefined ? {} : { reason: element.sprite.reason }),
|
|
300
446
|
},
|
|
301
447
|
}),
|
|
448
|
+
...(element.secondarySprite === undefined
|
|
449
|
+
? {}
|
|
450
|
+
: {
|
|
451
|
+
secondarySprite: {
|
|
452
|
+
spriteName: element.secondarySprite.spriteName,
|
|
453
|
+
texturePath: element.secondarySprite.texturePath,
|
|
454
|
+
frame: element.secondarySprite.frame,
|
|
455
|
+
frameCount: element.secondarySprite.frameCount,
|
|
456
|
+
width: element.secondarySprite.width,
|
|
457
|
+
height: element.secondarySprite.height,
|
|
458
|
+
format: element.secondarySprite.format,
|
|
459
|
+
supported: element.secondarySprite.supported,
|
|
460
|
+
...(element.secondarySprite.reason === undefined
|
|
461
|
+
? {}
|
|
462
|
+
: { reason: element.secondarySprite.reason }),
|
|
463
|
+
},
|
|
464
|
+
}),
|
|
302
465
|
...(element.text === undefined
|
|
303
466
|
? {}
|
|
304
467
|
: {
|
|
@@ -54,6 +54,8 @@ const modelledElementAttributes = new Set([
|
|
|
54
54
|
'font',
|
|
55
55
|
'fontSize',
|
|
56
56
|
'maxWidth',
|
|
57
|
+
'maxHeight',
|
|
58
|
+
'fixedsize',
|
|
57
59
|
'format',
|
|
58
60
|
'buttonText',
|
|
59
61
|
'buttonFont',
|
|
@@ -65,11 +67,10 @@ const modelledElementAttributes = new Set([
|
|
|
65
67
|
'minValue',
|
|
66
68
|
'maxValue',
|
|
67
69
|
'startValue',
|
|
70
|
+
'centerposition',
|
|
68
71
|
]);
|
|
69
72
|
const ignoredElementAttributes = new Set([
|
|
70
73
|
'margin',
|
|
71
|
-
'maxHeight',
|
|
72
|
-
'fixedsize',
|
|
73
74
|
'pdx_tooltip',
|
|
74
75
|
'pdx_tooltip_delayed',
|
|
75
76
|
'hint_tag',
|
|
@@ -84,7 +85,6 @@ const ignoredElementAttributes = new Set([
|
|
|
84
85
|
'smooth_scrolling',
|
|
85
86
|
'borderSize',
|
|
86
87
|
'stepSize',
|
|
87
|
-
'centerposition',
|
|
88
88
|
'fullScreen',
|
|
89
89
|
'moveable',
|
|
90
90
|
'show_position',
|
|
@@ -194,6 +194,14 @@ function sizeFrom(block) {
|
|
|
194
194
|
const height = numberScalar(size, 'height', 'y');
|
|
195
195
|
return width === undefined || height === undefined ? undefined : { width, height };
|
|
196
196
|
}
|
|
197
|
+
function nestedSizeFrom(block, key) {
|
|
198
|
+
const nested = assignments(block).find((assignment) => assignment.key.value.toLowerCase() === key.toLowerCase() && assignment.value.type === 'block')?.value;
|
|
199
|
+
if (nested?.type !== 'block')
|
|
200
|
+
return undefined;
|
|
201
|
+
const width = numberScalar(nested, 'width', 'x');
|
|
202
|
+
const height = numberScalar(nested, 'height', 'y');
|
|
203
|
+
return width === undefined || height === undefined ? undefined : { width, height };
|
|
204
|
+
}
|
|
197
205
|
function raw(document, node) {
|
|
198
206
|
return document.text.slice(node.start, node.end);
|
|
199
207
|
}
|
|
@@ -335,7 +343,7 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
335
343
|
const name = firstScalarInsensitive(child, 'name');
|
|
336
344
|
if (name !== undefined) {
|
|
337
345
|
const id = deterministicId('gui_sprite', { path: file.displayPath, name });
|
|
338
|
-
const texturePath = firstScalarInsensitive(child, 'texturefile', 'textureFile');
|
|
346
|
+
const texturePath = firstScalarInsensitive(child, 'texturefile', 'textureFile', 'textureFile1');
|
|
339
347
|
const texturePath2 = firstScalarInsensitive(child, 'textureFile2');
|
|
340
348
|
const staticFallback = firstScalarInsensitive(child, 'static_fallback', 'staticFallback');
|
|
341
349
|
const animationRateFps = numberScalar(child, 'animation_rate_fps');
|
|
@@ -344,6 +352,10 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
344
352
|
const pauseOnLoop = numberScalar(child, 'pause_on_loop');
|
|
345
353
|
const effectFile = firstScalarInsensitive(child, 'effectFile');
|
|
346
354
|
const declaredSize = sizeFrom(child);
|
|
355
|
+
const borderSize = nestedSizeFrom(child, 'borderSize');
|
|
356
|
+
const tilingCenter = boolScalar(child, 'tilingCenter');
|
|
357
|
+
const horizontal = boolScalar(child, 'horizontal');
|
|
358
|
+
const steps = numberScalar(child, 'steps');
|
|
347
359
|
const definition = {
|
|
348
360
|
id,
|
|
349
361
|
name,
|
|
@@ -361,6 +373,10 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
361
373
|
...(effectFile === undefined ? {} : { effectFile }),
|
|
362
374
|
...(staticFallback === undefined ? {} : { staticFallback }),
|
|
363
375
|
...(declaredSize === undefined ? {} : { declaredSize }),
|
|
376
|
+
...(borderSize === undefined ? {} : { borderSize }),
|
|
377
|
+
...(tilingCenter === undefined ? {} : { tilingCenter }),
|
|
378
|
+
...(horizontal === undefined ? {} : { horizontal }),
|
|
379
|
+
...(steps === undefined ? {} : { steps: Math.trunc(steps) }),
|
|
364
380
|
rawSource: raw(document, assignment),
|
|
365
381
|
};
|
|
366
382
|
addDomainEntry(sprites, definition, 'sprite');
|
|
@@ -374,6 +390,19 @@ function indexSpritesAndFonts(document, file, fileNodeId, nodes, edges, sprites,
|
|
|
374
390
|
spriteType: definition.spriteType,
|
|
375
391
|
frameCount: definition.frameCount,
|
|
376
392
|
frameAnimated: definition.frameAnimated,
|
|
393
|
+
...(definition.borderSize === undefined
|
|
394
|
+
? {}
|
|
395
|
+
: {
|
|
396
|
+
borderSize: {
|
|
397
|
+
width: definition.borderSize.width,
|
|
398
|
+
height: definition.borderSize.height,
|
|
399
|
+
},
|
|
400
|
+
}),
|
|
401
|
+
...(definition.tilingCenter === undefined
|
|
402
|
+
? {}
|
|
403
|
+
: { tilingCenter: definition.tilingCenter }),
|
|
404
|
+
...(definition.horizontal === undefined ? {} : { horizontal: definition.horizontal }),
|
|
405
|
+
...(definition.steps === undefined ? {} : { steps: definition.steps }),
|
|
377
406
|
...(definition.animationRateFps === undefined
|
|
378
407
|
? {}
|
|
379
408
|
: { animationRateFps: definition.animationRateFps }),
|
|
@@ -607,9 +636,9 @@ function indexAnimationSourceManifest(file, fileNodeId, nodes, edges, manifests,
|
|
|
607
636
|
addEdge(edges, 'contains', fileNodeId, id, true, {}, location);
|
|
608
637
|
}
|
|
609
638
|
function linkAnimationSources(nodes, edges, manifests, sprites, textureNodes, frameNodes) {
|
|
610
|
-
const spritesByName = new Map(sprites.map((sprite) => [sprite.name, sprite]));
|
|
639
|
+
const spritesByName = new Map(sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
|
|
611
640
|
for (const manifest of manifests) {
|
|
612
|
-
const sprite = spritesByName.get(manifest.sprite);
|
|
641
|
+
const sprite = spritesByName.get(manifest.sprite.toLocaleLowerCase('en-US'));
|
|
613
642
|
addEdge(edges, 'animation_provenance', sprite?.id ?? `sprite:${manifest.sprite}`, manifest.id, sprite !== undefined, { sprite: manifest.sprite }, manifest.location);
|
|
614
643
|
const sheet = textureNodes.get(normalizeAssetPath(manifest.sheet.path));
|
|
615
644
|
addEdge(edges, 'animation_sheet', manifest.id, sheet ?? `texture:${normalizeAssetPath(manifest.sheet.path)}`, sheet !== undefined, { texturePath: manifest.sheet.path }, manifest.location);
|
|
@@ -688,8 +717,8 @@ function indexDecisionEntries(document, file, nodes, pendingDecisionEdges) {
|
|
|
688
717
|
walk(document.root);
|
|
689
718
|
}
|
|
690
719
|
function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scriptedLocalisation, localisation, textureNodes, fontAssetNodes, pendingDecisionEdges) {
|
|
691
|
-
const spriteByName = new Map(sprites.map((sprite) => [sprite.name, sprite]));
|
|
692
|
-
const fontByName = new Map(fonts.map((font) => [font.name, font]));
|
|
720
|
+
const spriteByName = new Map(sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
|
|
721
|
+
const fontByName = new Map(fonts.map((font) => [font.name.toLocaleLowerCase('en-US'), font]));
|
|
693
722
|
const elementByName = new Map();
|
|
694
723
|
for (const element of [...elements].sort((a, b) => a.definitionOrder - b.definitionOrder)) {
|
|
695
724
|
if (!elementByName.has(element.name))
|
|
@@ -719,7 +748,7 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
719
748
|
`${sprite.name}_fallback`,
|
|
720
749
|
].filter((value) => value !== undefined);
|
|
721
750
|
const fallback = candidates
|
|
722
|
-
.map((name) => spriteByName.get(name))
|
|
751
|
+
.map((name) => spriteByName.get(name.toLocaleLowerCase('en-US')))
|
|
723
752
|
.find((value) => value !== undefined);
|
|
724
753
|
if (fallback !== undefined)
|
|
725
754
|
addEdge(edges, 'static_fallback', sprite.id, fallback.id, true, {}, sprite.location);
|
|
@@ -739,12 +768,12 @@ function linkGraph(nodes, edges, elements, sprites, fonts, scriptedGuis, scripte
|
|
|
739
768
|
for (const element of elements) {
|
|
740
769
|
const spriteName = [element.attributes.spriteType, element.attributes.quadTextureSprite].find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
741
770
|
if (spriteName !== undefined) {
|
|
742
|
-
const sprite = spriteByName.get(spriteName);
|
|
771
|
+
const sprite = spriteByName.get(spriteName.toLocaleLowerCase('en-US'));
|
|
743
772
|
addEdge(edges, 'uses_sprite', element.id, sprite?.id ?? `sprite:${spriteName}`, sprite !== undefined, { spriteName }, element.location);
|
|
744
773
|
}
|
|
745
774
|
const fontName = [element.attributes.font, element.attributes.buttonFont].find((value) => typeof value === 'string' && value.trim().length > 0);
|
|
746
775
|
if (fontName !== undefined) {
|
|
747
|
-
const font = fontByName.get(fontName);
|
|
776
|
+
const font = fontByName.get(fontName.toLocaleLowerCase('en-US'));
|
|
748
777
|
addEdge(edges, 'uses_font', element.id, font?.id ?? `font:${fontName}`, font !== undefined, { fontName }, element.location);
|
|
749
778
|
}
|
|
750
779
|
for (const key of localisationAttributeNames) {
|
|
@@ -195,7 +195,10 @@ function guiLocalisationPatterns(workspace, languages = ['l_english']) {
|
|
|
195
195
|
].sort((left, right) => compareCodeUnits(left, right));
|
|
196
196
|
}
|
|
197
197
|
function normalizedAssetReference(value) {
|
|
198
|
-
const normalized = value
|
|
198
|
+
const normalized = value
|
|
199
|
+
.replaceAll('\\', '/')
|
|
200
|
+
.replace(/^\/+|^\.\//u, '')
|
|
201
|
+
.replace(/\/+/gu, '/');
|
|
199
202
|
return isSafeAnimationSourcePath(normalized) ? normalized : undefined;
|
|
200
203
|
}
|
|
201
204
|
function referenceVariants(value) {
|
|
@@ -264,29 +267,54 @@ function collectNamedAttributes(value, keys, output) {
|
|
|
264
267
|
collectNamedAttributes(entry, keys, output);
|
|
265
268
|
}
|
|
266
269
|
}
|
|
270
|
+
function localisationIconTokens(value) {
|
|
271
|
+
return [...value.matchAll(/£([A-Za-z0-9_.:+-]+)/gu)]
|
|
272
|
+
.map((match) => match[1])
|
|
273
|
+
.filter((token) => token !== undefined && token.length > 0);
|
|
274
|
+
}
|
|
275
|
+
function localisationIconSpriteCandidates(token) {
|
|
276
|
+
const normalized = token.startsWith('GFX_') ? token : `GFX_${token}`;
|
|
277
|
+
return [...new Set([normalized, `${normalized}_texticon`, `${normalized}_text_icon`])];
|
|
278
|
+
}
|
|
267
279
|
function referencedAssetPatternsForWindow(graph, windowName) {
|
|
268
280
|
const spriteNames = new Set();
|
|
269
281
|
const fontNames = new Set();
|
|
282
|
+
const textValues = new Set();
|
|
270
283
|
for (const element of selectedElements(graph, windowName)) {
|
|
271
284
|
collectNamedAttributes(element.attributes, new Set(['spriteType', 'quadTextureSprite']), spriteNames);
|
|
272
285
|
collectNamedAttributes(element.attributes, new Set(['font', 'buttonFont']), fontNames);
|
|
286
|
+
collectNamedAttributes(element.attributes, new Set(['text', 'buttonText']), textValues);
|
|
287
|
+
}
|
|
288
|
+
for (const localisation of graph.localisation) {
|
|
289
|
+
if (textValues.has(localisation.key))
|
|
290
|
+
textValues.add(localisation.value);
|
|
273
291
|
}
|
|
274
|
-
const
|
|
292
|
+
for (const value of textValues) {
|
|
293
|
+
for (const token of localisationIconTokens(value)) {
|
|
294
|
+
for (const candidate of localisationIconSpriteCandidates(token))
|
|
295
|
+
spriteNames.add(candidate);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const spritesByName = new Map(graph.sprites.map((sprite) => [sprite.name.toLocaleLowerCase('en-US'), sprite]));
|
|
275
299
|
const selectedSprites = new Map();
|
|
276
300
|
const pendingSprites = [...spriteNames];
|
|
277
301
|
while (pendingSprites.length > 0) {
|
|
278
302
|
const name = pendingSprites.pop();
|
|
279
|
-
if (name === undefined
|
|
303
|
+
if (name === undefined)
|
|
304
|
+
continue;
|
|
305
|
+
const key = name.toLocaleLowerCase('en-US');
|
|
306
|
+
if (selectedSprites.has(key))
|
|
280
307
|
continue;
|
|
281
|
-
const sprite = spritesByName.get(
|
|
308
|
+
const sprite = spritesByName.get(key);
|
|
282
309
|
if (sprite === undefined)
|
|
283
310
|
continue;
|
|
284
|
-
selectedSprites.set(
|
|
311
|
+
selectedSprites.set(key, sprite);
|
|
285
312
|
if (sprite.staticFallback !== undefined)
|
|
286
313
|
pendingSprites.push(sprite.staticFallback);
|
|
287
314
|
}
|
|
288
|
-
const selectedManifests = graph.animationSources.filter(({ sprite }) => selectedSprites.has(sprite));
|
|
289
|
-
const
|
|
315
|
+
const selectedManifests = graph.animationSources.filter(({ sprite }) => selectedSprites.has(sprite.toLocaleLowerCase('en-US')));
|
|
316
|
+
const selectedFontNames = new Set([...fontNames].map((name) => name.toLocaleLowerCase('en-US')));
|
|
317
|
+
const selectedFonts = graph.fonts.filter(({ name }) => selectedFontNames.has(name.toLocaleLowerCase('en-US')));
|
|
290
318
|
const references = [
|
|
291
319
|
...[...selectedSprites.values()].flatMap(({ texturePath, texturePath2 }) => [texturePath, texturePath2].filter((value) => value !== undefined)),
|
|
292
320
|
...selectedFonts.flatMap(({ assetPaths }) => assetPaths),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
export const PACKAGE_NAME = 'hoi4-agent-tools';
|
|
3
|
-
export const PACKAGE_VERSION = '2.
|
|
3
|
+
export const PACKAGE_VERSION = '2.7.0';
|
|
4
4
|
export const MCP_PROTOCOL_VERSION = LATEST_PROTOCOL_VERSION;
|
|
5
5
|
export const CONFIG_VERSION = 1;
|
|
6
6
|
export const TRANSACTION_VERSION = 1;
|
package/docs/gui.md
CHANGED
|
@@ -31,6 +31,10 @@ Rewrite once, then inspect and render the result.
|
|
|
31
31
|
|
|
32
32
|
Inspection checks missing assets and localisation, invalid sizes, overlap, clipping, overflow, button-label centering, content crossing a background boundary, conflicting click regions, invisible blockers, broken parents or contexts, list-row cuts, state conflicts, trigger/effect gaps, and resolution drift. `hoi4.gui_render` returns a scripted-scenario gallery and JSON delta report alongside the generic state and resolution galleries.
|
|
33
33
|
|
|
34
|
+
The renderer resolves sprites and fonts through the complete mod, dependency, and installed-game load order. Mod windows therefore render ordinary vanilla sprites, tiled vanilla panels, progress bars, masked shields, button frames, and localisation icons such as `£command_power` without copying game assets into the mod. Case differences in GFX and font identifiers do not prevent a valid asset from resolving.
|
|
35
|
+
|
|
36
|
+
Layout follows Clausewitz orientation and element-origin anchors, including `CENTER_LEFT`, `CENTER_RIGHT`, `CENTER_UP`, `CENTER_DOWN`, `origo`, `centerposition`, inherited container coordinate origins, local scale, and both `%` and `%%` relative dimensions. Text boxes use native font metrics together with `maxWidth`, `maxHeight`, alignment, wrapping, and `fixedsize` clipping. Cornered tile sprites use fixed nine-slice borders and optional center tiling; progress bars composite their filled and background textures; masked shields composite their background and mask textures.
|
|
37
|
+
|
|
34
38
|
Preview scenarios can include exact layout expectations:
|
|
35
39
|
|
|
36
40
|
```json
|
|
@@ -60,4 +64,4 @@ The renderer does not run the game engine. Each render includes a fidelity repor
|
|
|
60
64
|
|
|
61
65
|
Unresolved numeric runtime values such as `[?variable|format]` render as `[X]`. Unresolved text-returning scripted or scoped localisation such as `[GetStatusText]`, `[FROM.GetName]`, and `[?leader_scope.GetName]` renders as `[dynamic_loc]`. Provide scenario values when the exact value matters. Supported HOI4 `§` localisation colour controls are applied to preview text and `§!` restores the default colour.
|
|
62
66
|
|
|
63
|
-
A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous.
|
|
67
|
+
A rewrite stops if malformed or unsupported GUI script makes the requested change ambiguous. Frame-sheet animation, nine-slice tiling, progress composition, masking, and the primary and secondary textures referenced by vanilla GFX definitions render offline. Engine shader programs, hardcoded controls, and dynamic values that are not supplied by a scenario remain fidelity-report entries; an unexecuted shader never hides its resolved base texture.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hoi4-agent-tools",
|
|
3
3
|
"mcpName": "io.github.klimPaskov/hoi4-agent-tools",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.7.0",
|
|
5
5
|
"description": "Analyze HOI4 events, tech trees, AI weights, and MTTH; build focus trees, GUIs, and maps.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/configuration.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/continuous-focus-palette.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/custom-weighted-pool.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/event-feature-manifest.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-plan.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/focus-planning-sidecar.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-animation-source.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-helper.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/gui-scenario.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/map-operation.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"oneOf": [
|
|
5
5
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/operation-result.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-analysis-result.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-compare-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.
|
|
2
|
+
"$id": "https://github.com/klimPaskov/hoi4-agent-tools/blob/v2.7.0/schemas/probability-evaluate-input.schema.json",
|
|
3
3
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
4
|
"type": "object",
|
|
5
5
|
"properties": {
|