hoi4-agent-tools 3.2.0 → 3.4.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 +19 -0
- package/README.md +4 -0
- package/dist/hoi4_agent_tools/core/analysis-job-operations.js +11 -1
- package/dist/hoi4_agent_tools/core/condition-evaluator.js +18 -9
- package/dist/hoi4_agent_tools/core/helper-expansion.js +3 -0
- package/dist/hoi4_agent_tools/core/job-worker-host.js +3 -0
- package/dist/hoi4_agent_tools/core/operation-tasks.js +7 -0
- package/dist/hoi4_agent_tools/core/scenario-model.js +85 -0
- package/dist/hoi4_agent_tools/event/comparison-selection.js +78 -0
- package/dist/hoi4_agent_tools/event/helper-expansion.js +48 -0
- package/dist/hoi4_agent_tools/event/operations.js +16 -0
- package/dist/hoi4_agent_tools/event/service.js +21 -2
- package/dist/hoi4_agent_tools/focus/compact.js +241 -27
- package/dist/hoi4_agent_tools/focus/layout.js +119 -8
- package/dist/hoi4_agent_tools/focus/render.js +62 -0
- package/dist/hoi4_agent_tools/focus/scenario.js +99 -0
- package/dist/hoi4_agent_tools/focus/tool-operations.js +30 -4
- package/dist/hoi4_agent_tools/gui/assets.js +54 -4
- package/dist/hoi4_agent_tools/gui/layout.js +935 -104
- package/dist/hoi4_agent_tools/gui/renderer.js +47 -17
- package/dist/hoi4_agent_tools/gui/scenario-conditions.js +2 -23
- package/dist/hoi4_agent_tools/gui/shader.js +515 -0
- package/dist/hoi4_agent_tools/gui/source-graph.js +166 -6
- package/dist/hoi4_agent_tools/gui/studio.js +245 -19
- package/dist/hoi4_agent_tools/gui/tool-operations.js +9 -0
- package/dist/hoi4_agent_tools/gui/validators.js +38 -12
- package/dist/hoi4_agent_tools/map/operations.js +70 -1
- package/dist/hoi4_agent_tools/map/render.js +70 -1
- package/dist/hoi4_agent_tools/map/service.js +30 -6
- package/dist/hoi4_agent_tools/map/tool-operations.js +67 -1
- package/dist/hoi4_agent_tools/mcp/server/instructions.js +1 -1
- package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +3 -0
- package/dist/hoi4_agent_tools/mcp/tools/analysis.js +90 -0
- package/dist/hoi4_agent_tools/mcp/tools/event.js +10 -1
- package/dist/hoi4_agent_tools/mcp/tools/focus.js +14 -4
- package/dist/hoi4_agent_tools/mcp/tools/gui.js +5 -1
- package/dist/hoi4_agent_tools/mcp/tools/map.js +12 -2
- package/dist/hoi4_agent_tools/mcp/tools/probability.js +5 -0
- package/dist/hoi4_agent_tools/mechanic/assertions.js +112 -0
- package/dist/hoi4_agent_tools/mechanic/interpreter.js +619 -0
- package/dist/hoi4_agent_tools/mechanic/service.js +145 -0
- package/dist/hoi4_agent_tools/mechanic/source.js +89 -0
- package/dist/hoi4_agent_tools/package-check/service.js +231 -0
- package/dist/hoi4_agent_tools/probability/evaluation.js +6 -1
- package/dist/hoi4_agent_tools/probability/operations.js +8 -0
- package/dist/hoi4_agent_tools/probability/service.js +33 -12
- package/dist/hoi4_agent_tools/probability/source-analysis.js +1 -0
- package/dist/hoi4_agent_tools/scenario-suite/service.js +341 -0
- package/dist/hoi4_agent_tools/schemas/event.js +8 -0
- package/dist/hoi4_agent_tools/schemas/focus-requests.js +40 -1
- package/dist/hoi4_agent_tools/schemas/gui-requests.js +8 -0
- package/dist/hoi4_agent_tools/schemas/map-requests.js +22 -1
- package/dist/hoi4_agent_tools/schemas/scenarios.js +264 -0
- package/dist/hoi4_agent_tools/schemas/technology.js +7 -0
- package/dist/hoi4_agent_tools/technology/graph.js +25 -10
- package/dist/hoi4_agent_tools/technology/render.js +286 -31
- package/dist/hoi4_agent_tools/technology/service.js +55 -8
- package/dist/hoi4_agent_tools/technology/source-analysis.js +166 -3
- package/dist/hoi4_agent_tools/version.js +1 -1
- package/docs/README.md +1 -0
- package/docs/events.md +3 -1
- package/docs/focus.md +10 -0
- package/docs/gui.md +49 -3
- package/docs/map.md +8 -0
- package/docs/mechanics.md +70 -0
- package/docs/probability.md +3 -1
- package/docs/technology.md +7 -2
- package/package.json +2 -1
- package/schemas/configuration.schema.json +1 -1
- package/schemas/continuous-focus-palette.schema.json +1 -1
- package/schemas/custom-weighted-pool.schema.json +1 -1
- package/schemas/decision-inspect-input.schema.json +1 -1
- package/schemas/event-feature-manifest.schema.json +1 -1
- package/schemas/focus-plan.schema.json +1 -1
- package/schemas/focus-planning-sidecar.schema.json +1 -1
- package/schemas/gui-animation-source.schema.json +1 -1
- package/schemas/gui-helper.schema.json +1 -1
- package/schemas/gui-scenario.schema.json +1 -1
- package/schemas/helper-expansion-request.schema.json +1 -1
- package/schemas/helper-expansion-summary.schema.json +1 -1
- package/schemas/impact-inspect-input.schema.json +1 -1
- package/schemas/map-operation.schema.json +1 -1
- package/schemas/mechanic-test-input.schema.json +650 -0
- package/schemas/operation-result.schema.json +1 -1
- package/schemas/package-check-input.schema.json +164 -0
- package/schemas/package-manifest.schema.json +142 -0
- package/schemas/probability-analysis-result.schema.json +1 -1
- package/schemas/probability-compare-input.schema.json +1 -1
- package/schemas/probability-evaluate-input.schema.json +1 -1
- package/schemas/probability-inspect-input.schema.json +1 -1
- package/schemas/probability-render-input.schema.json +1 -1
- package/schemas/probability-scenario-set.schema.json +1 -1
- package/schemas/probability-sequence-input.schema.json +1 -1
- package/schemas/probability-simulate-input.schema.json +1 -1
- package/schemas/probability-sweep-input.schema.json +1 -1
- package/schemas/scenario-suite.schema.json +1004 -0
- package/schemas/scenario-test-input.schema.json +1057 -0
- package/schemas/shared-scenario.schema.json +237 -0
- package/server.json +2 -2
|
@@ -241,8 +241,8 @@ function renderCorneredTile(element) {
|
|
|
241
241
|
if (sprite?.supported !== true ||
|
|
242
242
|
sprite.dataUri === undefined ||
|
|
243
243
|
border === undefined ||
|
|
244
|
-
border.width
|
|
245
|
-
border.height
|
|
244
|
+
border.width < 0 ||
|
|
245
|
+
border.height < 0)
|
|
246
246
|
return sprite === undefined ? '' : renderSpriteImage(sprite, rect);
|
|
247
247
|
const sourceBorderX = Math.min(border.width, sprite.width / 2);
|
|
248
248
|
const sourceBorderY = Math.min(border.height, sprite.height / 2);
|
|
@@ -299,11 +299,6 @@ function renderProgressbar(element) {
|
|
|
299
299
|
const sprite = element.sprite;
|
|
300
300
|
const secondary = element.secondarySprite;
|
|
301
301
|
const ratio = Math.max(0, Math.min(1, element.progressRatio ?? 1));
|
|
302
|
-
const background = secondary === undefined ? '' : renderSpriteImage(secondary, rect);
|
|
303
|
-
if (sprite?.supported !== true || sprite.dataUri === undefined)
|
|
304
|
-
return background;
|
|
305
|
-
if (ratio <= 0)
|
|
306
|
-
return background;
|
|
307
302
|
const horizontal = element.progressHorizontal !== false;
|
|
308
303
|
const filled = horizontal
|
|
309
304
|
? { x: rect.x, y: rect.y, width: rect.width * ratio, height: rect.height }
|
|
@@ -313,15 +308,36 @@ function renderProgressbar(element) {
|
|
|
313
308
|
width: rect.width,
|
|
314
309
|
height: rect.height * ratio,
|
|
315
310
|
};
|
|
316
|
-
const
|
|
317
|
-
? { x:
|
|
318
|
-
: {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
311
|
+
const remainder = horizontal
|
|
312
|
+
? { x: rect.x + filled.width, y: rect.y, width: rect.width - filled.width, height: rect.height }
|
|
313
|
+
: { x: rect.x, y: rect.y, width: rect.width, height: rect.height - filled.height };
|
|
314
|
+
// A threshold shader selects a texture once per pixel. Painting the filled region
|
|
315
|
+
// over a complete secondary layer would incorrectly blend both sources at partial alpha.
|
|
316
|
+
const layer = (texture, region, colour) => {
|
|
317
|
+
if (region.width <= 0 || region.height <= 0)
|
|
318
|
+
return '';
|
|
319
|
+
if (colour !== undefined)
|
|
320
|
+
return `<rect ${rectAttributes(region)} fill="${escapeXml(colour)}"/>`;
|
|
321
|
+
if (texture?.supported !== true ||
|
|
322
|
+
texture.dataUri === undefined ||
|
|
323
|
+
rect.width <= 0 ||
|
|
324
|
+
rect.height <= 0)
|
|
325
|
+
return '';
|
|
326
|
+
const source = {
|
|
327
|
+
x: ((region.x - rect.x) / rect.width) * texture.width,
|
|
328
|
+
y: ((region.y - rect.y) / rect.height) * texture.height,
|
|
329
|
+
width: (region.width / rect.width) * texture.width,
|
|
330
|
+
height: (region.height / rect.height) * texture.height,
|
|
323
331
|
};
|
|
324
|
-
|
|
332
|
+
return renderSpriteSlice(texture, source, region);
|
|
333
|
+
};
|
|
334
|
+
const content = `${layer(secondary, remainder, element.progressColours?.second)}${layer(sprite, filled, element.progressColours?.first)}`;
|
|
335
|
+
const filtering = element.progressShader?.textureFiltering;
|
|
336
|
+
if (filtering === undefined)
|
|
337
|
+
return content;
|
|
338
|
+
const shrinking = [sprite, secondary].some((texture) => texture !== undefined && (rect.width < texture.width || rect.height < texture.height));
|
|
339
|
+
const mode = filtering[shrinking ? 'minification' : 'magnification'];
|
|
340
|
+
return `<g image-rendering="${mode === 'nearest' ? 'optimizeSpeed' : 'optimizeQuality'}">${content}</g>`;
|
|
325
341
|
}
|
|
326
342
|
function renderMaskedShield(element) {
|
|
327
343
|
const rect = element.unclippedRect;
|
|
@@ -351,8 +367,22 @@ function renderBaseElement(element, clipId, toolText) {
|
|
|
351
367
|
const rect = element.unclippedRect;
|
|
352
368
|
const clip = clipId === undefined ? '' : ` clip-path="url(#${clipId})"`;
|
|
353
369
|
const content = [];
|
|
354
|
-
if (element.
|
|
355
|
-
content.push(
|
|
370
|
+
if (element.spriteRenderMode === 'progressbar' && element.progressColours !== undefined) {
|
|
371
|
+
content.push(renderProgressbar(element));
|
|
372
|
+
}
|
|
373
|
+
else if (element.sprite?.supported === true && element.sprite.dataUri !== undefined) {
|
|
374
|
+
const sprite = renderElementSprite(element);
|
|
375
|
+
if (element.spriteShader === undefined)
|
|
376
|
+
content.push(sprite);
|
|
377
|
+
else {
|
|
378
|
+
const id = `gui-shader-${sha256Bytes(element.id).slice(0, 20)}`;
|
|
379
|
+
const matrix = element.spriteShader.colourMatrix
|
|
380
|
+
.map((coefficient) => Number(coefficient.toFixed(9)))
|
|
381
|
+
.join(' ');
|
|
382
|
+
const shrinking = rect.width < element.sprite.width || rect.height < element.sprite.height;
|
|
383
|
+
const sampling = element.spriteShader.textureFiltering[shrinking ? 'minification' : 'magnification'];
|
|
384
|
+
content.push(`<defs><filter id="${id}" filterUnits="userSpaceOnUse" ${rectAttributes(rect)} color-interpolation-filters="sRGB"><feColorMatrix type="matrix" values="${matrix}"/></filter></defs><g filter="url(#${id})" image-rendering="${sampling === 'nearest' ? 'optimizeSpeed' : 'optimizeQuality'}">${sprite}</g>`);
|
|
385
|
+
}
|
|
356
386
|
}
|
|
357
387
|
else if (element.sprite !== undefined) {
|
|
358
388
|
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"/>`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClausewitzEvaluationDefinitions } from '../core/clausewitz-evaluation.js';
|
|
2
2
|
import { hasBlockingDiagnostics } from '../core/diagnostics.js';
|
|
3
3
|
import { evaluateTriggerBlock } from '../core/condition-evaluator.js';
|
|
4
|
+
import { scenarioFromGui } from '../core/scenario-model.js';
|
|
4
5
|
import { resolveScopeContext, rootScopeContext } from '../core/scenario-state.js';
|
|
5
6
|
import { assignments, firstScalar, parseClausewitz } from '../core/source/index.js';
|
|
6
7
|
/** Keep declared scoped scalars out of exploratory value generation. */
|
|
@@ -30,29 +31,7 @@ function parsedCondition(expression) {
|
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
export function guiConditionScenario(base, values = base.values) {
|
|
33
|
-
|
|
34
|
-
for (const [key, value] of Object.entries(base.flags))
|
|
35
|
-
state[`flag.${key}`] = value;
|
|
36
|
-
const scopes = structuredClone(base.scopes ?? {});
|
|
37
|
-
for (const [key, value] of Object.entries(state)) {
|
|
38
|
-
const match = /^(ROOT|THIS|FROM(?:\.FROM)*|PREV)\.(.+)$/u.exec(key);
|
|
39
|
-
if (match === null)
|
|
40
|
-
continue;
|
|
41
|
-
if (match[1] === 'ROOT' || match[1] === 'THIS')
|
|
42
|
-
state[match[2]] ??= value;
|
|
43
|
-
else {
|
|
44
|
-
const scope = (scopes[match[1]] ??= { id: match[1], state: {} });
|
|
45
|
-
scope.state[match[2]] ??= value;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const actor = base.country?.tag ?? base.country?.countryTag ?? base.country?.GetTag;
|
|
49
|
-
return {
|
|
50
|
-
id: base.id,
|
|
51
|
-
state,
|
|
52
|
-
scopes,
|
|
53
|
-
closedFlags: false,
|
|
54
|
-
...(typeof actor === 'string' ? { actor } : {}),
|
|
55
|
-
};
|
|
34
|
+
return scenarioFromGui(base, values);
|
|
56
35
|
}
|
|
57
36
|
export function evaluateGuiCondition(expression, scenario, subject, definitions = new ClausewitzEvaluationDefinitions(), scope = 'ROOT') {
|
|
58
37
|
const context = resolveScopeContext(scenario, scope, rootScopeContext(scenario));
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
// This is a bounded colour-expression interpreter, never a JavaScript/HLSL executor.
|
|
2
|
+
// Unsupported sampling, geometry, branches and nonlinear colour operations stay explicit.
|
|
3
|
+
export const GUI_SHADER_MAX_BYTES = 262_144;
|
|
4
|
+
const maxTokens = 4_096;
|
|
5
|
+
const maxTokenLength = 256;
|
|
6
|
+
const maxDepth = 64;
|
|
7
|
+
const maxStatements = 128;
|
|
8
|
+
const maxCoefficient = 1_000_000;
|
|
9
|
+
const constant = (value) => [0, 0, 0, 0, value];
|
|
10
|
+
const isConstant = (value) => value.slice(0, 4).every((entry) => entry === 0);
|
|
11
|
+
const identity = [
|
|
12
|
+
[1, 0, 0, 0, 0],
|
|
13
|
+
[0, 1, 0, 0, 0],
|
|
14
|
+
[0, 0, 1, 0, 0],
|
|
15
|
+
[0, 0, 0, 1, 0],
|
|
16
|
+
];
|
|
17
|
+
class UnsupportedShader extends Error {
|
|
18
|
+
}
|
|
19
|
+
function unsupported(reason) {
|
|
20
|
+
throw new UnsupportedShader(reason.length > 512 ? `${reason.slice(0, 509)}...` : reason);
|
|
21
|
+
}
|
|
22
|
+
function checked(value) {
|
|
23
|
+
if (value.length !== 5 ||
|
|
24
|
+
value.some((entry) => !Number.isFinite(entry) || Math.abs(entry) > maxCoefficient))
|
|
25
|
+
unsupported('Shader colour arithmetic exceeds its finite coefficient budget.');
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function binary(left, right, operator) {
|
|
29
|
+
const width = Math.max(left.length, right.length);
|
|
30
|
+
if (![1, width].includes(left.length) || ![1, width].includes(right.length))
|
|
31
|
+
unsupported('Shader vector widths do not agree.');
|
|
32
|
+
return Array.from({ length: width }, (_, index) => {
|
|
33
|
+
const a = left[left.length === 1 ? 0 : index];
|
|
34
|
+
const b = right[right.length === 1 ? 0 : index];
|
|
35
|
+
if (operator === '+' || operator === '-')
|
|
36
|
+
return checked(a.map((entry, component) => entry + (operator === '+' ? 1 : -1) * b[component]));
|
|
37
|
+
if (operator === '*') {
|
|
38
|
+
if (isConstant(a))
|
|
39
|
+
return checked(b.map((entry) => entry * a[4]));
|
|
40
|
+
if (isConstant(b))
|
|
41
|
+
return checked(a.map((entry) => entry * b[4]));
|
|
42
|
+
unsupported('Multiplication of two sampled colour expressions is nonlinear.');
|
|
43
|
+
}
|
|
44
|
+
if (operator === '/' && isConstant(b) && b[4] !== 0)
|
|
45
|
+
return checked(a.map((entry) => entry / b[4]));
|
|
46
|
+
return unsupported('Shader division requires a finite nonzero constant divisor.');
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function components(swizzle, width) {
|
|
50
|
+
const alphabet = /^[rgba]+$/u.test(swizzle) ? 'rgba' : 'xyzw';
|
|
51
|
+
const result = Array.from({ length: swizzle.length }, (_, index) => alphabet.indexOf(swizzle.charAt(index)));
|
|
52
|
+
if (result.length === 0 ||
|
|
53
|
+
result.length > 4 ||
|
|
54
|
+
result.some((index) => index < 0 || index >= width))
|
|
55
|
+
unsupported('Shader swizzle is outside its vector.');
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function withoutComments(source) {
|
|
59
|
+
const stripped = source.replace(/\/\*[\s\S]*?\*\//gu, ' ').replace(/\/\/[^\r\n]*/gu, '');
|
|
60
|
+
if (stripped.includes('/*'))
|
|
61
|
+
unsupported('Shader has an unterminated comment.');
|
|
62
|
+
return stripped;
|
|
63
|
+
}
|
|
64
|
+
function inactiveFeatures(source) {
|
|
65
|
+
const stack = [];
|
|
66
|
+
let active = true;
|
|
67
|
+
const output = [];
|
|
68
|
+
for (const line of source.split(/\r?\n/u)) {
|
|
69
|
+
const directive = /^\s*[#@](\w+)(?:\s+([\s\S]*?))?\s*$/u.exec(line);
|
|
70
|
+
if (directive === null) {
|
|
71
|
+
if (active)
|
|
72
|
+
output.push(line);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const [, command, argument] = directive;
|
|
76
|
+
if (command === 'ifdef' || command === 'ifndef') {
|
|
77
|
+
if (!['ANIMATED', 'MASKING'].includes(argument ?? ''))
|
|
78
|
+
unsupported(`Shader feature ${argument ?? '(missing)'} has no supported binding.`);
|
|
79
|
+
if (stack.length >= maxDepth)
|
|
80
|
+
unsupported('Shader preprocessor nesting exceeds its budget.');
|
|
81
|
+
const condition = command === 'ifndef';
|
|
82
|
+
stack.push({ parent: active, condition, alternative: false });
|
|
83
|
+
active = active && condition;
|
|
84
|
+
}
|
|
85
|
+
else if (command === 'else' && argument === undefined) {
|
|
86
|
+
const branch = stack.at(-1);
|
|
87
|
+
if (branch === undefined || branch.alternative)
|
|
88
|
+
unsupported('Shader has an unmatched else.');
|
|
89
|
+
branch.alternative = true;
|
|
90
|
+
active = branch.parent && !branch.condition;
|
|
91
|
+
}
|
|
92
|
+
else if (command === 'endif' && argument === undefined) {
|
|
93
|
+
const branch = stack.pop();
|
|
94
|
+
if (branch === undefined)
|
|
95
|
+
unsupported('Shader has an unmatched endif.');
|
|
96
|
+
active = branch.parent;
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
unsupported(`Shader directive ${command ?? '(missing)'} is not supported.`);
|
|
100
|
+
}
|
|
101
|
+
if (stack.length !== 0)
|
|
102
|
+
unsupported('Shader has an unterminated conditional feature.');
|
|
103
|
+
return output.join('\n');
|
|
104
|
+
}
|
|
105
|
+
function tokenize(source) {
|
|
106
|
+
const tokens = [];
|
|
107
|
+
const pattern = /\s+|(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?[fF]?|[A-Za-z_]\w*|[+*/-]=|[{}()[\],;.=+*/-]/guy;
|
|
108
|
+
let offset = 0;
|
|
109
|
+
while (offset < source.length) {
|
|
110
|
+
pattern.lastIndex = offset;
|
|
111
|
+
const token = pattern.exec(source)?.[0];
|
|
112
|
+
if (token === undefined)
|
|
113
|
+
unsupported('Shader contains unsupported expression syntax.');
|
|
114
|
+
offset += token.length;
|
|
115
|
+
if (token.trim().length === 0)
|
|
116
|
+
continue;
|
|
117
|
+
if (token.length > maxTokenLength)
|
|
118
|
+
unsupported('Shader token length exceeds its budget.');
|
|
119
|
+
tokens.push(token);
|
|
120
|
+
if (tokens.length > maxTokens)
|
|
121
|
+
unsupported('Shader token count exceeds its budget.');
|
|
122
|
+
}
|
|
123
|
+
return tokens;
|
|
124
|
+
}
|
|
125
|
+
class ColourInterpreter {
|
|
126
|
+
tokens;
|
|
127
|
+
inputName;
|
|
128
|
+
offset = 0;
|
|
129
|
+
depth = 0;
|
|
130
|
+
variables = new Map();
|
|
131
|
+
constructor(tokens, inputName, uniforms) {
|
|
132
|
+
this.tokens = tokens;
|
|
133
|
+
this.inputName = inputName;
|
|
134
|
+
this.variables.set('Color', (uniforms.colour ?? [1, 1, 1, 1]).map(constant));
|
|
135
|
+
this.variables.set('Time', [constant(uniforms.time)]);
|
|
136
|
+
this.variables.set('AnimationTime', [constant(uniforms.animationTime)]);
|
|
137
|
+
}
|
|
138
|
+
peek() {
|
|
139
|
+
return this.tokens[this.offset];
|
|
140
|
+
}
|
|
141
|
+
take() {
|
|
142
|
+
const token = this.tokens[this.offset++];
|
|
143
|
+
return token ?? unsupported('Shader expression ended unexpectedly.');
|
|
144
|
+
}
|
|
145
|
+
require(token) {
|
|
146
|
+
if (this.take() !== token)
|
|
147
|
+
unsupported(`Shader expected ${token}.`);
|
|
148
|
+
}
|
|
149
|
+
identifier() {
|
|
150
|
+
const token = this.take();
|
|
151
|
+
return /^[A-Za-z_]\w*$/u.test(token) ? token : unsupported('Shader expected an identifier.');
|
|
152
|
+
}
|
|
153
|
+
functionCall(name) {
|
|
154
|
+
this.require('(');
|
|
155
|
+
if (name === 'tex2D') {
|
|
156
|
+
this.require('MapTexture');
|
|
157
|
+
this.require(',');
|
|
158
|
+
this.require(this.inputName);
|
|
159
|
+
this.require('.');
|
|
160
|
+
this.require('vTexCoord');
|
|
161
|
+
this.require(')');
|
|
162
|
+
return identity;
|
|
163
|
+
}
|
|
164
|
+
const args = [];
|
|
165
|
+
if (this.peek() !== ')') {
|
|
166
|
+
for (;;) {
|
|
167
|
+
args.push(this.expression());
|
|
168
|
+
if (args.length > 4)
|
|
169
|
+
unsupported('Shader call exceeds its argument budget.');
|
|
170
|
+
if (this.peek() !== ',')
|
|
171
|
+
break;
|
|
172
|
+
this.take();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
this.require(')');
|
|
176
|
+
const constructor = /^float([1-4])?$/u.exec(name);
|
|
177
|
+
if (constructor !== null) {
|
|
178
|
+
const width = Number(constructor[1] ?? 1);
|
|
179
|
+
const channels = args.flat();
|
|
180
|
+
if (channels.length === 1)
|
|
181
|
+
return Array.from({ length: width }, () => channels[0]);
|
|
182
|
+
if (channels.length !== width)
|
|
183
|
+
unsupported('Shader vector constructor has the wrong width.');
|
|
184
|
+
return channels;
|
|
185
|
+
}
|
|
186
|
+
if (name === 'dot' && args.length === 2 && args[0].length === args[1].length) {
|
|
187
|
+
const product = binary(args[0], args[1], '*');
|
|
188
|
+
return [product.reduce((sum, channel) => binary([sum], [channel], '+')[0], constant(0))];
|
|
189
|
+
}
|
|
190
|
+
if (name === 'lerp' && args.length === 3)
|
|
191
|
+
return binary(args[0], binary(binary(args[1], args[0], '-'), args[2], '*'), '+');
|
|
192
|
+
if (['saturate', 'abs'].includes(name) && args.length === 1)
|
|
193
|
+
return args[0].map((channel) => {
|
|
194
|
+
if (!isConstant(channel))
|
|
195
|
+
unsupported(`${name} of sampled colour is nonlinear.`);
|
|
196
|
+
return constant(name === 'abs' ? Math.abs(channel[4]) : Math.max(0, Math.min(1, channel[4])));
|
|
197
|
+
});
|
|
198
|
+
if (['min', 'max', 'pow'].includes(name) &&
|
|
199
|
+
args.length === 2 &&
|
|
200
|
+
args.every((arg) => arg.length === 1 && isConstant(arg[0]))) {
|
|
201
|
+
const a = args[0][0][4];
|
|
202
|
+
const b = args[1][0][4];
|
|
203
|
+
return [
|
|
204
|
+
checked([
|
|
205
|
+
0,
|
|
206
|
+
0,
|
|
207
|
+
0,
|
|
208
|
+
0,
|
|
209
|
+
name === 'min' ? Math.min(a, b) : name === 'max' ? Math.max(a, b) : Math.pow(a, b),
|
|
210
|
+
]),
|
|
211
|
+
];
|
|
212
|
+
}
|
|
213
|
+
return unsupported(`Shader call ${name} is not a supported affine colour operation.`);
|
|
214
|
+
}
|
|
215
|
+
primary() {
|
|
216
|
+
const token = this.take();
|
|
217
|
+
let value;
|
|
218
|
+
if (token === '(') {
|
|
219
|
+
value = this.expression();
|
|
220
|
+
this.require(')');
|
|
221
|
+
}
|
|
222
|
+
else if (token === '-' || token === '+') {
|
|
223
|
+
value = binary([constant(token === '-' ? -1 : 1)], this.expression(3), '*');
|
|
224
|
+
}
|
|
225
|
+
else if (/^(?:\d|\.)/u.test(token)) {
|
|
226
|
+
value = [checked([0, 0, 0, 0, Number(token.replace(/[fF]$/u, ''))])];
|
|
227
|
+
}
|
|
228
|
+
else if (this.peek() === '(') {
|
|
229
|
+
value = this.functionCall(token);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
value = this.variables.get(token) ?? unsupported(`Shader value ${token} is unresolved.`);
|
|
233
|
+
}
|
|
234
|
+
while (this.peek() === '.') {
|
|
235
|
+
this.take();
|
|
236
|
+
value = components(this.identifier(), value.length).map((index) => value[index]);
|
|
237
|
+
}
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
expression(minimumPrecedence = 0) {
|
|
241
|
+
this.depth += 1;
|
|
242
|
+
if (this.depth > maxDepth)
|
|
243
|
+
unsupported('Shader expression nesting exceeds its budget.');
|
|
244
|
+
try {
|
|
245
|
+
let value = this.primary();
|
|
246
|
+
for (;;) {
|
|
247
|
+
const operator = this.peek();
|
|
248
|
+
const precedence = operator === '+' || operator === '-' ? 1 : operator === '*' || operator === '/' ? 2 : -1;
|
|
249
|
+
if (precedence < minimumPrecedence)
|
|
250
|
+
return value;
|
|
251
|
+
this.take();
|
|
252
|
+
value = binary(value, this.expression(precedence + 1), operator);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
this.depth -= 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
run() {
|
|
260
|
+
for (let statement = 0; statement < maxStatements; statement += 1) {
|
|
261
|
+
const first = this.identifier();
|
|
262
|
+
if (first === 'return') {
|
|
263
|
+
const value = this.expression();
|
|
264
|
+
this.require(';');
|
|
265
|
+
if (value.length !== 4 || this.offset !== this.tokens.length)
|
|
266
|
+
unsupported('Shader must finish with one four-channel colour return.');
|
|
267
|
+
return value.flat();
|
|
268
|
+
}
|
|
269
|
+
const declaration = /^float([1-4])?$/u.exec(first);
|
|
270
|
+
const name = declaration === null ? first : this.identifier();
|
|
271
|
+
if (['Color', 'Time', 'AnimationTime'].includes(name))
|
|
272
|
+
unsupported('Shader cannot assign to a uniform.');
|
|
273
|
+
if (declaration !== null && this.variables.has(name))
|
|
274
|
+
unsupported('Shader redeclares a value.');
|
|
275
|
+
const existing = this.variables.get(name);
|
|
276
|
+
let swizzle;
|
|
277
|
+
if (this.peek() === '.') {
|
|
278
|
+
this.take();
|
|
279
|
+
swizzle = components(this.identifier(), existing?.length ?? 0);
|
|
280
|
+
if (new Set(swizzle).size !== swizzle.length)
|
|
281
|
+
unsupported('Shader assignment repeats a swizzle component.');
|
|
282
|
+
}
|
|
283
|
+
const operator = this.take();
|
|
284
|
+
if (!['=', '+=', '-=', '*=', '/='].includes(operator))
|
|
285
|
+
unsupported('Shader statement is not an assignment.');
|
|
286
|
+
const width = declaration === null ? (swizzle?.length ?? existing?.length) : Number(declaration[1] ?? 1);
|
|
287
|
+
if (width === undefined)
|
|
288
|
+
unsupported(`Shader assignment to ${name} is undeclared.`);
|
|
289
|
+
let value = this.expression();
|
|
290
|
+
this.require(';');
|
|
291
|
+
if (value.length === 1)
|
|
292
|
+
value = Array.from({ length: width }, () => value[0]);
|
|
293
|
+
if (value.length !== width)
|
|
294
|
+
unsupported('Shader assignment changes vector width.');
|
|
295
|
+
if (operator !== '=') {
|
|
296
|
+
if (existing === undefined || declaration !== null)
|
|
297
|
+
unsupported('Shader compound assignment has no prior value.');
|
|
298
|
+
value = binary(swizzle === undefined ? existing : swizzle.map((index) => existing[index]), value, operator[0]);
|
|
299
|
+
}
|
|
300
|
+
if (swizzle === undefined)
|
|
301
|
+
this.variables.set(name, value);
|
|
302
|
+
else {
|
|
303
|
+
const replaced = [...existing];
|
|
304
|
+
swizzle.forEach((index, component) => {
|
|
305
|
+
replaced[index] = value[component];
|
|
306
|
+
});
|
|
307
|
+
this.variables.set(name, replaced);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return unsupported('Shader statement count exceeds its budget.');
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function mainCodes(source) {
|
|
314
|
+
const codes = new Map();
|
|
315
|
+
for (const match of source.matchAll(/\bMainCode\s+(\w+)\s*\[\[([\s\S]*?)\]\]/gu)) {
|
|
316
|
+
if (codes.has(match[1]))
|
|
317
|
+
unsupported('Shader has duplicate named programs.');
|
|
318
|
+
codes.set(match[1], match[2]);
|
|
319
|
+
}
|
|
320
|
+
return codes;
|
|
321
|
+
}
|
|
322
|
+
function assertNativeVertex(source) {
|
|
323
|
+
const program = inactiveFeatures(source).replace(/\s+/gu, '');
|
|
324
|
+
const match = /^VS_OUTPUTmain\((?:const)?VS_INPUT(\w+)\)\{VS_OUTPUT(\w+);([\s\S]*)\}$/u.exec(program);
|
|
325
|
+
if (match === null)
|
|
326
|
+
unsupported('Shader vertex program is not the supported native sprite transform.');
|
|
327
|
+
const [, input, output, body] = match;
|
|
328
|
+
const prefix = `${output}.vPosition=mul(WorldViewProjectionMatrix,float4(${input}.vPosition.xyz,1));${output}.vTexCoord=${input}.vTexCoord;`;
|
|
329
|
+
const endings = ['', `${output}.vTexCoord+=Offset;`, `${output}.vTexCoord.x+=vXOffset;`];
|
|
330
|
+
if (!endings.some((offset) => body === `${prefix}${offset}return${output};`))
|
|
331
|
+
unsupported('Shader changes sprite geometry or texture coordinates beyond native atlas offsets.');
|
|
332
|
+
}
|
|
333
|
+
function fields(source) {
|
|
334
|
+
const result = new Map();
|
|
335
|
+
const remainder = source.replace(/\b(\w+)\s*=\s*(?:"([^"\r\n]*)"|([A-Za-z_][A-Za-z0-9_]*|[+-]?(?:\d+(?:\.\d*)?|\.\d+)))/gu, (_match, name, quoted, scalar) => {
|
|
336
|
+
if (result.has(name))
|
|
337
|
+
unsupported('Shader render state has a duplicate field.');
|
|
338
|
+
result.set(name, quoted ?? scalar);
|
|
339
|
+
return '';
|
|
340
|
+
});
|
|
341
|
+
if (remainder.trim().length > 0)
|
|
342
|
+
unsupported('Shader render state contains unsupported syntax.');
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
function textureFiltering(source) {
|
|
346
|
+
const samplers = [...source.matchAll(/\bMapTexture\s*=\s*\{([^{}]*)\}/gu)];
|
|
347
|
+
if (samplers.length !== 1)
|
|
348
|
+
unsupported('Shader must bind one primary MapTexture sampler.');
|
|
349
|
+
const sampler = fields(samplers[0][1]);
|
|
350
|
+
if (sampler.get('Index') !== '0' ||
|
|
351
|
+
sampler.get('AddressU') !== 'Clamp' ||
|
|
352
|
+
sampler.get('AddressV') !== 'Clamp' ||
|
|
353
|
+
sampler.get('MipFilter') !== 'None')
|
|
354
|
+
unsupported('Shader sampler requires primary texture slot zero, clamp addressing and no mip filtering.');
|
|
355
|
+
if ([...sampler.keys()].some((key) => ![
|
|
356
|
+
'Index',
|
|
357
|
+
'MagFilter',
|
|
358
|
+
'MinFilter',
|
|
359
|
+
'MipFilter',
|
|
360
|
+
'AddressU',
|
|
361
|
+
'AddressV',
|
|
362
|
+
'MipMapLodBias',
|
|
363
|
+
].includes(key)))
|
|
364
|
+
unsupported('Shader sampler has unsupported fields.');
|
|
365
|
+
const mode = (key) => sampler.get(key) === 'Linear'
|
|
366
|
+
? 'linear'
|
|
367
|
+
: sampler.get(key) === 'Point'
|
|
368
|
+
? 'nearest'
|
|
369
|
+
: unsupported(`Shader ${key} is not a supported texture filter.`);
|
|
370
|
+
return { magnification: mode('MagFilter'), minification: mode('MinFilter') };
|
|
371
|
+
}
|
|
372
|
+
function assertStraightAlphaBlend(source) {
|
|
373
|
+
const blends = [...source.matchAll(/\bBlendState\s+\w+\s*\{([^{}]*)\}/gu)];
|
|
374
|
+
const blendFields = blends.length === 1 ? fields(blends[0][1]) : new Map();
|
|
375
|
+
if (blends.length !== 1 ||
|
|
376
|
+
blendFields.get('BlendEnable') !== 'yes' ||
|
|
377
|
+
blendFields.get('SourceBlend')?.toLowerCase() !== 'src_alpha' ||
|
|
378
|
+
blendFields.get('DestBlend')?.toLowerCase() !== 'inv_src_alpha' ||
|
|
379
|
+
(blendFields.has('AlphaTest') && blendFields.get('AlphaTest') !== 'no') ||
|
|
380
|
+
(blendFields.has('BlendOp') && blendFields.get('BlendOp')?.toLowerCase() !== 'add') ||
|
|
381
|
+
[...blendFields.keys()].some((key) => !['BlendEnable', 'SourceBlend', 'DestBlend', 'AlphaTest', 'BlendOp'].includes(key)))
|
|
382
|
+
unsupported('Shader does not use supported straight-alpha sprite blending.');
|
|
383
|
+
}
|
|
384
|
+
/** Recognize the native two-source threshold operation, never execute arbitrary shader code. */
|
|
385
|
+
export function compileGuiProgressEffect(source, provenance, effect) {
|
|
386
|
+
try {
|
|
387
|
+
if (Buffer.byteLength(source, 'utf8') > GUI_SHADER_MAX_BYTES)
|
|
388
|
+
unsupported('Shader source exceeds its byte budget.');
|
|
389
|
+
const clean = withoutComments(source);
|
|
390
|
+
if (/^\s*[#@]/mu.test(clean) || /\bIncludes\s*=\s*\{\s*[^\s}]/u.test(clean))
|
|
391
|
+
unsupported('Progress shader has unbound includes or feature directives.');
|
|
392
|
+
const effects = [...clean.matchAll(/\bEffect\s+(\w+)\s*\{([^{}]*)\}/gu)].filter((match) => match[1] === effect);
|
|
393
|
+
if (effects.length !== 1)
|
|
394
|
+
unsupported(`Shader must define exactly one ${effect} effect.`);
|
|
395
|
+
const effectFields = fields(effects[0][2]);
|
|
396
|
+
const pixel = effectFields.get('PixelShader');
|
|
397
|
+
const vertex = effectFields.get('VertexShader');
|
|
398
|
+
if (effectFields.size !== 2 || pixel === undefined || vertex === undefined)
|
|
399
|
+
unsupported('Progress shader effect has unsupported render-state overrides.');
|
|
400
|
+
const programs = mainCodes(clean);
|
|
401
|
+
const vertexSource = programs.get(vertex)?.replace(/\s+/gu, '');
|
|
402
|
+
const vertexProgram = vertexSource === undefined
|
|
403
|
+
? null
|
|
404
|
+
: /^VS_OUTPUTmain\((?:const)?VS_INPUT(\w+)\)\{VS_OUTPUT(\w+);([\s\S]*)\}$/u.exec(vertexSource);
|
|
405
|
+
if (vertexProgram === null)
|
|
406
|
+
unsupported('Progress vertex program is not a native transform.');
|
|
407
|
+
const [, input, output, body] = vertexProgram;
|
|
408
|
+
if (body !==
|
|
409
|
+
`${output}.vPosition=mul(WorldViewProjectionMatrix,${input}.vPosition);${output}.vTexCoord0=${input}.vTexCoord;${output}.vTexCoord0.y=-${output}.vTexCoord0.y;return${output};`)
|
|
410
|
+
unsupported('Progress shader changes native geometry or texture coordinates.');
|
|
411
|
+
const pixelSource = programs.get(pixel);
|
|
412
|
+
const program = pixelSource === undefined
|
|
413
|
+
? null
|
|
414
|
+
: /^\s*float4\s+main\(\s*(?:const\s+)?VS_OUTPUT\s+(\w+)\s*\)\s*:\s*PDX_COLOR\s*\{([\s\S]*)\}\s*$/u.exec(pixelSource);
|
|
415
|
+
if (program === null)
|
|
416
|
+
unsupported('Progress shader pixel program is missing or unsupported.');
|
|
417
|
+
const variable = program[1];
|
|
418
|
+
const first = effect === 'Color' ? 'vFirstColor' : `tex2D(TextureOne,${variable}.vTexCoord0.xy)`;
|
|
419
|
+
const second = effect === 'Color' ? 'vSecondColor' : `tex2D(TextureTwo,${variable}.vTexCoord0.xy)`;
|
|
420
|
+
const pixelBody = program[2].replace(/\s+/gu, '');
|
|
421
|
+
if (pixelBody !== `if(${variable}.vTexCoord0.x<=CurrentState)return${first};elsereturn${second};`)
|
|
422
|
+
unsupported('Progress pixel program is not a supported two-source threshold operation.');
|
|
423
|
+
assertStraightAlphaBlend(clean);
|
|
424
|
+
let filtering;
|
|
425
|
+
if (effect === 'Texture') {
|
|
426
|
+
const modes = ['TextureOne', 'TextureTwo'].map((name, index) => {
|
|
427
|
+
const declarations = [
|
|
428
|
+
...clean.matchAll(new RegExp(`\\b${name}\\s*=\\s*\\{([^{}]*)\\}`, 'gu')),
|
|
429
|
+
];
|
|
430
|
+
if (declarations.length !== 1)
|
|
431
|
+
unsupported(`Progress shader must bind one ${name} sampler.`);
|
|
432
|
+
const sampler = fields(declarations[0][1]);
|
|
433
|
+
if (sampler.get('Index') !== String(index) ||
|
|
434
|
+
sampler.get('MipFilter') !== 'None' ||
|
|
435
|
+
sampler.get('AddressU') !== 'Wrap' ||
|
|
436
|
+
sampler.get('AddressV') !== 'Wrap' ||
|
|
437
|
+
[...sampler.keys()].some((key) => !['Index', 'MagFilter', 'MinFilter', 'MipFilter', 'AddressU', 'AddressV'].includes(key)))
|
|
438
|
+
unsupported('Progress samplers require their declared slots, wrap addressing and no mip filtering.');
|
|
439
|
+
const mode = (key) => sampler.get(key) === 'Point'
|
|
440
|
+
? 'nearest'
|
|
441
|
+
: sampler.get(key) === 'Linear'
|
|
442
|
+
? 'linear'
|
|
443
|
+
: unsupported(`Unsupported progress ${key}.`);
|
|
444
|
+
return { magnification: mode('MagFilter'), minification: mode('MinFilter') };
|
|
445
|
+
});
|
|
446
|
+
if (modes[0].magnification !== modes[1].magnification ||
|
|
447
|
+
modes[0].minification !== modes[1].minification)
|
|
448
|
+
unsupported('Progress textures require matching filtering modes.');
|
|
449
|
+
filtering = modes[0];
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
supported: true,
|
|
453
|
+
shader: {
|
|
454
|
+
sourcePath: provenance.path,
|
|
455
|
+
sourceHash: provenance.sha256,
|
|
456
|
+
effect,
|
|
457
|
+
entryPoint: pixel,
|
|
458
|
+
composition: 'threshold',
|
|
459
|
+
...(filtering === undefined ? {} : { textureFiltering: filtering }),
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
if (!(error instanceof UnsupportedShader))
|
|
465
|
+
throw error;
|
|
466
|
+
return { supported: false, reason: error.message };
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
export function compileGuiShaderEffect(source, provenance, effect, uniforms) {
|
|
470
|
+
try {
|
|
471
|
+
if (Buffer.byteLength(source, 'utf8') > GUI_SHADER_MAX_BYTES)
|
|
472
|
+
unsupported('Shader source exceeds its byte budget.');
|
|
473
|
+
if ([uniforms.time, uniforms.animationTime, ...(uniforms.colour ?? [])].some((value) => !Number.isFinite(value) || Math.abs(value) > maxCoefficient))
|
|
474
|
+
unsupported('Shader uniforms must be finite and within their coefficient budget.');
|
|
475
|
+
const clean = withoutComments(source);
|
|
476
|
+
if (/^\s*[#@](?:define|undef|include|if|elif|pragma)\b/mu.test(clean))
|
|
477
|
+
unsupported('Shader contains unbound preprocessor definitions or expressions.');
|
|
478
|
+
const effects = [...clean.matchAll(/\bEffect\s+(\w+)\s*\{([^{}]*)\}/gu)].filter((match) => match[1] === effect);
|
|
479
|
+
if (effects.length !== 1)
|
|
480
|
+
unsupported(`Shader must define exactly one ${effect} effect.`);
|
|
481
|
+
const effectFields = fields(effects[0][2]);
|
|
482
|
+
const pixel = effectFields.get('PixelShader');
|
|
483
|
+
const vertex = effectFields.get('VertexShader');
|
|
484
|
+
if (effectFields.size !== 2 || pixel === undefined || vertex === undefined)
|
|
485
|
+
unsupported('Shader effect has unsupported render-state overrides.');
|
|
486
|
+
const programs = mainCodes(clean);
|
|
487
|
+
const pixelSource = programs.get(pixel);
|
|
488
|
+
const vertexSource = programs.get(vertex);
|
|
489
|
+
if (pixelSource === undefined || vertexSource === undefined)
|
|
490
|
+
unsupported('Shader effect entry point is missing.');
|
|
491
|
+
assertNativeVertex(vertexSource);
|
|
492
|
+
assertStraightAlphaBlend(clean);
|
|
493
|
+
const program = /^\s*float4\s+main\(\s*(?:const\s+)?VS_OUTPUT\s+(\w+)\s*\)\s*:\s*PDX_COLOR\s*\{([^{}]*)\}\s*$/u.exec(inactiveFeatures(pixelSource));
|
|
494
|
+
if (program === null)
|
|
495
|
+
unsupported('Shader pixel program has unsupported control flow.');
|
|
496
|
+
const colourMatrix = new ColourInterpreter(tokenize(program[2]), program[1], uniforms).run();
|
|
497
|
+
const filtering = textureFiltering(clean);
|
|
498
|
+
return {
|
|
499
|
+
supported: true,
|
|
500
|
+
shader: {
|
|
501
|
+
sourcePath: provenance.path,
|
|
502
|
+
sourceHash: provenance.sha256,
|
|
503
|
+
effect,
|
|
504
|
+
entryPoint: pixel,
|
|
505
|
+
textureFiltering: filtering,
|
|
506
|
+
colourMatrix,
|
|
507
|
+
},
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
if (!(error instanceof UnsupportedShader))
|
|
512
|
+
throw error;
|
|
513
|
+
return { supported: false, reason: error.message };
|
|
514
|
+
}
|
|
515
|
+
}
|