@aethervtt/shard-core 0.0.1
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/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/ecs/commands.d.ts +35 -0
- package/dist/ecs/commands.js +96 -0
- package/dist/ecs/entity.d.ts +11 -0
- package/dist/ecs/entity.js +12 -0
- package/dist/ecs/events.d.ts +34 -0
- package/dist/ecs/events.js +65 -0
- package/dist/ecs/hierarchy.d.ts +7 -0
- package/dist/ecs/hierarchy.js +29 -0
- package/dist/ecs/index.d.ts +8 -0
- package/dist/ecs/index.js +38 -0
- package/dist/ecs/observers.d.ts +40 -0
- package/dist/ecs/observers.js +27 -0
- package/dist/ecs/query.d.ts +44 -0
- package/dist/ecs/query.js +67 -0
- package/dist/ecs/table.d.ts +91 -0
- package/dist/ecs/table.js +286 -0
- package/dist/ecs/world.d.ts +127 -0
- package/dist/ecs/world.js +513 -0
- package/dist/error.d.ts +26 -0
- package/dist/error.js +29 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/json.d.ts +15 -0
- package/dist/json.js +22 -0
- package/dist/math/aabb.d.ts +13 -0
- package/dist/math/aabb.js +78 -0
- package/dist/math/affine.d.ts +28 -0
- package/dist/math/affine.js +159 -0
- package/dist/math/affine64.d.ts +11 -0
- package/dist/math/affine64.js +58 -0
- package/dist/math/frustum.d.ts +11 -0
- package/dist/math/frustum.js +59 -0
- package/dist/math/hash.d.ts +14 -0
- package/dist/math/hash.js +38 -0
- package/dist/math/index.d.ts +14 -0
- package/dist/math/index.js +33 -0
- package/dist/math/mat3.d.ts +6 -0
- package/dist/math/mat3.js +37 -0
- package/dist/math/mat4.d.ts +23 -0
- package/dist/math/mat4.js +224 -0
- package/dist/math/quat.d.ts +22 -0
- package/dist/math/quat.js +233 -0
- package/dist/math/ray.d.ts +10 -0
- package/dist/math/ray.js +54 -0
- package/dist/math/rng.d.ts +41 -0
- package/dist/math/rng.js +102 -0
- package/dist/math/types.d.ts +8 -0
- package/dist/math/types.js +4 -0
- package/dist/math/vec2.d.ts +11 -0
- package/dist/math/vec2.js +52 -0
- package/dist/math/vec3.d.ts +23 -0
- package/dist/math/vec3.js +135 -0
- package/dist/math/vec4.d.ts +8 -0
- package/dist/math/vec4.js +47 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +39 -0
- package/dist/scheduler/profiler.d.ts +19 -0
- package/dist/scheduler/profiler.js +44 -0
- package/dist/scheduler/schedule.d.ts +52 -0
- package/dist/scheduler/schedule.js +235 -0
- package/dist/scheduler/system.d.ts +74 -0
- package/dist/scheduler/system.js +106 -0
- package/dist/schema/color.d.ts +7 -0
- package/dist/schema/color.js +34 -0
- package/dist/schema/component.d.ts +71 -0
- package/dist/schema/component.js +120 -0
- package/dist/schema/field.d.ts +145 -0
- package/dist/schema/field.js +591 -0
- package/dist/schema/index.d.ts +6 -0
- package/dist/schema/index.js +40 -0
- package/dist/schema/names.d.ts +27 -0
- package/dist/schema/names.js +39 -0
- package/dist/schema/registry.d.ts +38 -0
- package/dist/schema/registry.js +58 -0
- package/dist/schema/resource.d.ts +48 -0
- package/dist/schema/resource.js +79 -0
- package/dist/test-env.d.ts +31 -0
- package/dist/test-env.js +37 -0
- package/package.json +38 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { ShardError } from "../error.js";
|
|
2
|
+
import { cloneData, isPlainObject, pointer } from "../json.js";
|
|
3
|
+
import { HEX_COLOR, hexToLinear, linearToHex } from "./color.js";
|
|
4
|
+
function describeValue(value) {
|
|
5
|
+
if (value === null) return "null";
|
|
6
|
+
if (Array.isArray(value)) return `array(${value.length})`;
|
|
7
|
+
return typeof value;
|
|
8
|
+
}
|
|
9
|
+
function mismatch(path, expected, got) {
|
|
10
|
+
return new ShardError(
|
|
11
|
+
"schema/type-mismatch",
|
|
12
|
+
`Expected ${expected} at ${path || "/"}, got ${describeValue(got)}`,
|
|
13
|
+
{ path, hint: `Use ${expected}.` }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
function outOfRange(path, value, min, max) {
|
|
17
|
+
return new ShardError(
|
|
18
|
+
"schema/out-of-range",
|
|
19
|
+
`Value ${value} at ${path || "/"} is outside [${min}, ${max}]`,
|
|
20
|
+
{ path, hint: `Use a value between ${min} and ${max}.` }
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
function editDistance(a, b) {
|
|
24
|
+
const row = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
25
|
+
for (let i = 1; i <= a.length; i++) {
|
|
26
|
+
let prev = row[0];
|
|
27
|
+
row[0] = i;
|
|
28
|
+
for (let j = 1; j <= b.length; j++) {
|
|
29
|
+
const tmp = row[j];
|
|
30
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
31
|
+
prev = tmp;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return row[b.length];
|
|
35
|
+
}
|
|
36
|
+
function unknownField(path, key, known) {
|
|
37
|
+
const close = known.find((k) => editDistance(k.toLowerCase(), key.toLowerCase()) <= 2);
|
|
38
|
+
const hint = close ? `Did you mean "${close}"?` : known.length > 0 ? `Known fields: ${known.join(", ")}.` : "This type has no fields; use {}.";
|
|
39
|
+
return new ShardError("schema/unknown-field", `Unknown field "${key}" at ${path || "/"}`, {
|
|
40
|
+
path: pointer(path, key),
|
|
41
|
+
hint
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function baseSchema(options, defaultJson) {
|
|
45
|
+
const schema = {};
|
|
46
|
+
if (options.description) schema.description = options.description;
|
|
47
|
+
if (options.readonly) schema.readOnly = true;
|
|
48
|
+
if (options.unit) schema["x-unit"] = options.unit;
|
|
49
|
+
if (options.hidden) schema["x-hidden"] = true;
|
|
50
|
+
schema.default = defaultJson;
|
|
51
|
+
return schema;
|
|
52
|
+
}
|
|
53
|
+
function makeField(spec) {
|
|
54
|
+
const field = {
|
|
55
|
+
...spec,
|
|
56
|
+
jsonSchema: () => ({
|
|
57
|
+
...baseSchema(spec.options, spec.toJson(spec.defaultValue())),
|
|
58
|
+
...spec.schema()
|
|
59
|
+
})
|
|
60
|
+
};
|
|
61
|
+
delete field.schema;
|
|
62
|
+
return field;
|
|
63
|
+
}
|
|
64
|
+
function callable(create) {
|
|
65
|
+
return Object.assign((options = {}) => create(options), create({}));
|
|
66
|
+
}
|
|
67
|
+
function f32ToJson(v) {
|
|
68
|
+
if (!Number.isFinite(v) || Number.isInteger(v)) return v;
|
|
69
|
+
const target = Math.fround(v);
|
|
70
|
+
let best = Number(v.toPrecision(7));
|
|
71
|
+
if (Math.fround(best) !== target) {
|
|
72
|
+
for (let p = 8; p <= 9; p++) {
|
|
73
|
+
const candidate = Number(v.toPrecision(p));
|
|
74
|
+
if (Math.fround(candidate) === target) return candidate;
|
|
75
|
+
}
|
|
76
|
+
return v;
|
|
77
|
+
}
|
|
78
|
+
for (let p = 6; p >= 1; p--) {
|
|
79
|
+
const candidate = Number(v.toPrecision(p));
|
|
80
|
+
if (Math.fround(candidate) !== target) break;
|
|
81
|
+
best = candidate;
|
|
82
|
+
}
|
|
83
|
+
return best;
|
|
84
|
+
}
|
|
85
|
+
const INT_RANGE = {
|
|
86
|
+
i8: [-128, 127],
|
|
87
|
+
i16: [-32768, 32767],
|
|
88
|
+
i32: [-2147483648, 2147483647],
|
|
89
|
+
u8: [0, 255],
|
|
90
|
+
u16: [0, 65535],
|
|
91
|
+
u32: [0, 4294967295]
|
|
92
|
+
};
|
|
93
|
+
function numberField(storage) {
|
|
94
|
+
return callable((options) => {
|
|
95
|
+
const range = INT_RANGE[storage];
|
|
96
|
+
const isInt = range !== void 0;
|
|
97
|
+
const min = Math.max(range?.[0] ?? -Infinity, options.min ?? -Infinity);
|
|
98
|
+
const max = Math.min(range?.[1] ?? Infinity, options.max ?? Infinity);
|
|
99
|
+
return makeField({
|
|
100
|
+
kind: storage,
|
|
101
|
+
storage,
|
|
102
|
+
stride: 1,
|
|
103
|
+
options,
|
|
104
|
+
defaultValue: () => options.default ?? 0,
|
|
105
|
+
read: (c, r) => c[r],
|
|
106
|
+
write: (c, r, v) => {
|
|
107
|
+
;
|
|
108
|
+
c[r] = v;
|
|
109
|
+
},
|
|
110
|
+
validate(json2, path, errors) {
|
|
111
|
+
if (typeof json2 === "string" && options.presets) {
|
|
112
|
+
if (!Object.hasOwn(options.presets, json2)) {
|
|
113
|
+
errors.push(
|
|
114
|
+
new ShardError(
|
|
115
|
+
"schema/unknown-preset",
|
|
116
|
+
`Unknown preset "${json2}" at ${path || "/"}`,
|
|
117
|
+
{
|
|
118
|
+
path,
|
|
119
|
+
hint: `Use a number or one of: ${Object.keys(options.presets).join(", ")}.`
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
} else if (typeof json2 !== "number" || !Number.isFinite(json2)) {
|
|
125
|
+
errors.push(mismatch(path, isInt ? "an integer" : "a number", json2));
|
|
126
|
+
} else if (isInt && !Number.isInteger(json2)) {
|
|
127
|
+
errors.push(mismatch(path, "an integer", json2));
|
|
128
|
+
} else if (json2 < min || json2 > max) {
|
|
129
|
+
errors.push(outOfRange(path, json2, min, max));
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
toJson: storage === "f32" ? f32ToJson : (v) => v,
|
|
133
|
+
fromJson: (json2) => typeof json2 === "string" ? options.presets[json2] : json2,
|
|
134
|
+
schema: () => {
|
|
135
|
+
const s = { type: isInt ? "integer" : "number" };
|
|
136
|
+
if (Number.isFinite(min)) s.minimum = min;
|
|
137
|
+
if (Number.isFinite(max)) s.maximum = max;
|
|
138
|
+
if (!options.presets) return s;
|
|
139
|
+
return {
|
|
140
|
+
anyOf: [s, { type: "string", enum: Object.keys(options.presets) }],
|
|
141
|
+
"x-presets": options.presets
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const bool = callable(
|
|
148
|
+
(options) => makeField({
|
|
149
|
+
kind: "bool",
|
|
150
|
+
storage: "u8",
|
|
151
|
+
stride: 1,
|
|
152
|
+
options,
|
|
153
|
+
defaultValue: () => options.default ?? false,
|
|
154
|
+
read: (c, r) => c[r] === 1,
|
|
155
|
+
write: (c, r, v) => {
|
|
156
|
+
;
|
|
157
|
+
c[r] = v ? 1 : 0;
|
|
158
|
+
},
|
|
159
|
+
validate(json2, path, errors) {
|
|
160
|
+
if (typeof json2 !== "boolean") errors.push(mismatch(path, "a boolean", json2));
|
|
161
|
+
},
|
|
162
|
+
toJson: (v) => v,
|
|
163
|
+
fromJson: (json2) => json2,
|
|
164
|
+
schema: () => ({ type: "boolean" })
|
|
165
|
+
})
|
|
166
|
+
);
|
|
167
|
+
function readStrided(c, r, n) {
|
|
168
|
+
const out = new Array(n);
|
|
169
|
+
const base = r * n;
|
|
170
|
+
for (let i = 0; i < n; i++) out[i] = c[base + i];
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
function writeStrided(c, r, n, v) {
|
|
174
|
+
const base = r * n;
|
|
175
|
+
for (let i = 0; i < n; i++) c[base + i] = v[i];
|
|
176
|
+
}
|
|
177
|
+
function validateNumberArray(json2, n, path, errors, min = -Infinity, max = Infinity) {
|
|
178
|
+
const expected = `an array of ${n} numbers`;
|
|
179
|
+
if (!Array.isArray(json2) || json2.length !== n) {
|
|
180
|
+
errors.push(mismatch(path, expected, json2));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
for (let i = 0; i < n; i++) {
|
|
184
|
+
const v = json2[i];
|
|
185
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
186
|
+
errors.push(mismatch(pointer(path, i), "a number", v));
|
|
187
|
+
} else if (v < min || v > max) {
|
|
188
|
+
errors.push(outOfRange(pointer(path, i), v, min, max));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function numberArraySchema(n, options) {
|
|
193
|
+
const items = { type: "number" };
|
|
194
|
+
if (options.min !== void 0) items.minimum = options.min;
|
|
195
|
+
if (options.max !== void 0) items.maximum = options.max;
|
|
196
|
+
return { type: "array", items, minItems: n, maxItems: n };
|
|
197
|
+
}
|
|
198
|
+
function vectorField(kind, zero) {
|
|
199
|
+
const n = zero.length;
|
|
200
|
+
return callable(
|
|
201
|
+
(options) => makeField({
|
|
202
|
+
kind,
|
|
203
|
+
storage: "f32",
|
|
204
|
+
stride: n,
|
|
205
|
+
options,
|
|
206
|
+
defaultValue: () => [...options.default ?? zero],
|
|
207
|
+
read: (c, r) => readStrided(c, r, n),
|
|
208
|
+
write: (c, r, v) => writeStrided(c, r, n, v),
|
|
209
|
+
validate: (json2, path, errors) => validateNumberArray(json2, n, path, errors, options.min, options.max),
|
|
210
|
+
toJson: (v) => v.map(f32ToJson),
|
|
211
|
+
fromJson: (json2) => [...json2],
|
|
212
|
+
schema: () => numberArraySchema(n, options)
|
|
213
|
+
})
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const I32_MIN = -2147483648;
|
|
217
|
+
const I32_MAX = 2147483647;
|
|
218
|
+
const ivec3 = callable((options) => {
|
|
219
|
+
const min = Math.max(I32_MIN, options.min ?? I32_MIN);
|
|
220
|
+
const max = Math.min(I32_MAX, options.max ?? I32_MAX);
|
|
221
|
+
return makeField({
|
|
222
|
+
kind: "ivec3",
|
|
223
|
+
storage: "i32",
|
|
224
|
+
stride: 3,
|
|
225
|
+
options,
|
|
226
|
+
defaultValue: () => [...options.default ?? [0, 0, 0]],
|
|
227
|
+
read: (c, r) => readStrided(c, r, 3),
|
|
228
|
+
write: (c, r, v) => writeStrided(c, r, 3, v),
|
|
229
|
+
validate(json2, path, errors) {
|
|
230
|
+
validateNumberArray(json2, 3, path, errors, min, max);
|
|
231
|
+
if (!Array.isArray(json2) || json2.length !== 3) return;
|
|
232
|
+
for (let i = 0; i < 3; i++) {
|
|
233
|
+
const v = json2[i];
|
|
234
|
+
if (typeof v === "number" && Number.isFinite(v) && !Number.isInteger(v)) {
|
|
235
|
+
errors.push(mismatch(pointer(path, i), "an integer", v));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
toJson: (v) => [...v],
|
|
240
|
+
fromJson: (json2) => [...json2],
|
|
241
|
+
schema: () => ({
|
|
242
|
+
type: "array",
|
|
243
|
+
items: { type: "integer", minimum: min, maximum: max },
|
|
244
|
+
minItems: 3,
|
|
245
|
+
maxItems: 3
|
|
246
|
+
})
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
const color = callable(
|
|
250
|
+
(options) => makeField({
|
|
251
|
+
kind: "color",
|
|
252
|
+
storage: "f32",
|
|
253
|
+
stride: 4,
|
|
254
|
+
options,
|
|
255
|
+
defaultValue: () => [...options.default ?? [1, 1, 1, 1]],
|
|
256
|
+
read: (c, r) => readStrided(c, r, 4),
|
|
257
|
+
write: (c, r, v) => writeStrided(c, r, 4, v),
|
|
258
|
+
validate(json2, path, errors) {
|
|
259
|
+
if (typeof json2 === "string") {
|
|
260
|
+
if (!HEX_COLOR.test(json2)) {
|
|
261
|
+
errors.push(
|
|
262
|
+
new ShardError("schema/type-mismatch", `Invalid color "${json2}" at ${path || "/"}`, {
|
|
263
|
+
path,
|
|
264
|
+
hint: 'Use "#rrggbb", "#rrggbbaa", or a linear [r, g, b, a] array.'
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
validateNumberArray(json2, 4, path, errors, 0);
|
|
271
|
+
},
|
|
272
|
+
toJson: (v) => linearToHex(v) ?? v.map(f32ToJson),
|
|
273
|
+
fromJson: (json2) => typeof json2 === "string" ? hexToLinear(json2) : [...json2],
|
|
274
|
+
schema: () => ({
|
|
275
|
+
anyOf: [
|
|
276
|
+
{ type: "string", pattern: HEX_COLOR.source },
|
|
277
|
+
{ type: "array", items: { type: "number", minimum: 0 }, minItems: 4, maxItems: 4 }
|
|
278
|
+
]
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
);
|
|
282
|
+
function enumField(values, options = {}) {
|
|
283
|
+
if (values.length === 0 || values.length > 256) {
|
|
284
|
+
throw new ShardError("schema/invalid-field", "Enums need between 1 and 256 values");
|
|
285
|
+
}
|
|
286
|
+
const index = new Map(values.map((v, i) => [v, i]));
|
|
287
|
+
return makeField({
|
|
288
|
+
kind: "enum",
|
|
289
|
+
storage: "u8",
|
|
290
|
+
stride: 1,
|
|
291
|
+
options,
|
|
292
|
+
defaultValue: () => options.default ?? values[0],
|
|
293
|
+
read: (c, r) => values[c[r]],
|
|
294
|
+
write: (c, r, v) => {
|
|
295
|
+
const i = index.get(v);
|
|
296
|
+
if (i === void 0) {
|
|
297
|
+
throw new ShardError("schema/invalid-enum", `"${v}" is not one of ${values.join(", ")}`);
|
|
298
|
+
}
|
|
299
|
+
;
|
|
300
|
+
c[r] = i;
|
|
301
|
+
},
|
|
302
|
+
validate(json2, path, errors) {
|
|
303
|
+
if (typeof json2 !== "string" || !index.has(json2)) {
|
|
304
|
+
errors.push(
|
|
305
|
+
new ShardError(
|
|
306
|
+
"schema/type-mismatch",
|
|
307
|
+
`Expected one of ${values.map((v) => `"${v}"`).join(", ")} at ${path || "/"}, got ${JSON.stringify(json2)}`,
|
|
308
|
+
{ path }
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
toJson: (v) => v,
|
|
314
|
+
fromJson: (json2) => json2,
|
|
315
|
+
schema: () => ({ type: "string", enum: [...values] })
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const entity = callable(
|
|
319
|
+
(options) => makeField({
|
|
320
|
+
kind: "entity",
|
|
321
|
+
storage: "f64",
|
|
322
|
+
stride: 1,
|
|
323
|
+
options,
|
|
324
|
+
defaultValue: () => null,
|
|
325
|
+
read: (c, r) => {
|
|
326
|
+
const v = c[r];
|
|
327
|
+
return v < 0 ? null : v;
|
|
328
|
+
},
|
|
329
|
+
write: (c, r, v) => {
|
|
330
|
+
;
|
|
331
|
+
c[r] = v ?? -1;
|
|
332
|
+
},
|
|
333
|
+
validate(json2, path, errors) {
|
|
334
|
+
const ok = json2 === null || typeof json2 === "number" && Number.isInteger(json2) && json2 >= 0 || typeof json2 === "string" && json2.length > 0;
|
|
335
|
+
if (!ok) errors.push(mismatch(path, "an entity id, an entity path, or null", json2));
|
|
336
|
+
},
|
|
337
|
+
toJson: (v) => v,
|
|
338
|
+
fromJson(json2, ctx) {
|
|
339
|
+
if (typeof json2 !== "string") return json2;
|
|
340
|
+
const resolved = ctx?.resolveEntity?.(json2);
|
|
341
|
+
if (resolved === void 0) {
|
|
342
|
+
throw new ShardError("schema/unresolved-entity", `Entity path "${json2}" did not resolve`, {
|
|
343
|
+
hint: "Entity paths are resolved by the scene loader; check the path exists."
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return resolved;
|
|
347
|
+
},
|
|
348
|
+
schema: () => ({
|
|
349
|
+
anyOf: [{ type: "null" }, { type: "integer", minimum: 0 }, { type: "string", minLength: 1 }]
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
);
|
|
353
|
+
function objectField(spec) {
|
|
354
|
+
return makeField({
|
|
355
|
+
...spec,
|
|
356
|
+
storage: "object",
|
|
357
|
+
stride: 1,
|
|
358
|
+
read: (c, r) => cloneData(c[r]),
|
|
359
|
+
write: (c, r, v) => {
|
|
360
|
+
;
|
|
361
|
+
c[r] = cloneData(v);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const string = callable(
|
|
366
|
+
(options) => objectField({
|
|
367
|
+
kind: "string",
|
|
368
|
+
options,
|
|
369
|
+
defaultValue: () => options.default ?? "",
|
|
370
|
+
validate(json2, path, errors) {
|
|
371
|
+
if (typeof json2 !== "string") errors.push(mismatch(path, "a string", json2));
|
|
372
|
+
},
|
|
373
|
+
toJson: (v) => v,
|
|
374
|
+
fromJson: (json2) => json2,
|
|
375
|
+
schema: () => ({ type: "string" })
|
|
376
|
+
})
|
|
377
|
+
);
|
|
378
|
+
function handle(type, options = {}) {
|
|
379
|
+
return objectField({
|
|
380
|
+
kind: "handle",
|
|
381
|
+
options,
|
|
382
|
+
assetType: type,
|
|
383
|
+
defaultValue: () => cloneData(options.default ?? null),
|
|
384
|
+
validate(json2, path, errors, ctx) {
|
|
385
|
+
if (json2 === null) return;
|
|
386
|
+
if (!isPlainObject(json2)) {
|
|
387
|
+
errors.push(mismatch(path, `a ${type} reference { guid, path } or null`, json2));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const key of Object.keys(json2)) {
|
|
391
|
+
if (key !== "guid" && key !== "path") errors.push(unknownField(path, key, ["guid", "path"]));
|
|
392
|
+
}
|
|
393
|
+
const { guid, path: assetPath } = json2;
|
|
394
|
+
if (guid !== void 0 && (typeof guid !== "string" || guid === "")) {
|
|
395
|
+
errors.push(mismatch(pointer(path, "guid"), "a non-empty string", guid));
|
|
396
|
+
}
|
|
397
|
+
if (assetPath !== void 0 && (typeof assetPath !== "string" || assetPath === "")) {
|
|
398
|
+
errors.push(mismatch(pointer(path, "path"), "a non-empty string", assetPath));
|
|
399
|
+
}
|
|
400
|
+
if (guid === void 0 && assetPath === void 0) {
|
|
401
|
+
errors.push(
|
|
402
|
+
new ShardError("schema/missing-field", `Asset reference at ${path || "/"} is empty`, {
|
|
403
|
+
path,
|
|
404
|
+
hint: 'Give a "guid", a "path", or both.'
|
|
405
|
+
})
|
|
406
|
+
);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (!ctx?.resolveAsset) return;
|
|
410
|
+
const resolved = ctx.resolveAsset(json2);
|
|
411
|
+
if (!resolved) {
|
|
412
|
+
errors.push(
|
|
413
|
+
new ShardError(
|
|
414
|
+
"schema/asset-not-found",
|
|
415
|
+
`No asset matches ${JSON.stringify(json2)} at ${path || "/"}`,
|
|
416
|
+
{ path, hint: "Check the path, or that the asset has been imported." }
|
|
417
|
+
)
|
|
418
|
+
);
|
|
419
|
+
} else if (resolved.type !== type) {
|
|
420
|
+
errors.push(
|
|
421
|
+
new ShardError(
|
|
422
|
+
"schema/asset-type-mismatch",
|
|
423
|
+
`Expected a ${type} at ${path || "/"}, but ${resolved.path} is a ${resolved.type}`,
|
|
424
|
+
{ path }
|
|
425
|
+
)
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
toJson(v) {
|
|
430
|
+
if (v === null) return null;
|
|
431
|
+
const out = {};
|
|
432
|
+
if (v.guid !== void 0) out.guid = v.guid;
|
|
433
|
+
if (v.path !== void 0) out.path = v.path;
|
|
434
|
+
return out;
|
|
435
|
+
},
|
|
436
|
+
fromJson(json2, ctx) {
|
|
437
|
+
if (json2 === null) return null;
|
|
438
|
+
const ref = json2;
|
|
439
|
+
const resolved = ctx?.resolveAsset?.(ref);
|
|
440
|
+
return { type, guid: resolved?.guid ?? ref.guid, path: resolved?.path ?? ref.path };
|
|
441
|
+
},
|
|
442
|
+
schema: () => ({
|
|
443
|
+
anyOf: [
|
|
444
|
+
{ type: "null" },
|
|
445
|
+
{
|
|
446
|
+
type: "object",
|
|
447
|
+
properties: {
|
|
448
|
+
guid: { type: "string", minLength: 1 },
|
|
449
|
+
path: { type: "string", minLength: 1 }
|
|
450
|
+
},
|
|
451
|
+
additionalProperties: false,
|
|
452
|
+
minProperties: 1,
|
|
453
|
+
"x-asset-type": type
|
|
454
|
+
}
|
|
455
|
+
]
|
|
456
|
+
})
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function list(inner, options = {}) {
|
|
460
|
+
return objectField({
|
|
461
|
+
kind: "list",
|
|
462
|
+
options,
|
|
463
|
+
item: inner,
|
|
464
|
+
defaultValue: () => cloneData(options.default ?? []),
|
|
465
|
+
validate(json2, path, errors, ctx) {
|
|
466
|
+
if (!Array.isArray(json2)) {
|
|
467
|
+
errors.push(mismatch(path, "an array", json2));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
for (let i = 0; i < json2.length; i++) inner.validate(json2[i], pointer(path, i), errors, ctx);
|
|
471
|
+
},
|
|
472
|
+
toJson: (v) => v.map((item) => inner.toJson(item)),
|
|
473
|
+
fromJson: (json2, ctx) => json2.map((item) => inner.fromJson(item, ctx)),
|
|
474
|
+
schema: () => ({ type: "array", items: inner.jsonSchema() })
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
function struct(fields, options = {}) {
|
|
478
|
+
return objectField({
|
|
479
|
+
kind: "struct",
|
|
480
|
+
options,
|
|
481
|
+
fields,
|
|
482
|
+
defaultValue: () => cloneData(options.default ?? defaultsOf(fields)),
|
|
483
|
+
validate: (json2, path, errors, ctx) => validateObject(fields, json2, path, errors, ctx),
|
|
484
|
+
toJson: (v) => objectToJson(fields, v),
|
|
485
|
+
fromJson: (json2, ctx) => objectFromJson(fields, json2, ctx),
|
|
486
|
+
schema: () => objectSchema(fields)
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
const json = callable(
|
|
490
|
+
(options) => objectField({
|
|
491
|
+
kind: "json",
|
|
492
|
+
options,
|
|
493
|
+
defaultValue: () => cloneData(options.default ?? null),
|
|
494
|
+
validate() {
|
|
495
|
+
},
|
|
496
|
+
toJson: (v) => cloneData(v),
|
|
497
|
+
fromJson: (json2) => cloneData(json2),
|
|
498
|
+
schema: () => ({})
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
function defaultsOf(fields) {
|
|
502
|
+
const out = {};
|
|
503
|
+
for (const name in fields) out[name] = fields[name].defaultValue();
|
|
504
|
+
return out;
|
|
505
|
+
}
|
|
506
|
+
function validateObject(fields, json2, path, errors, ctx) {
|
|
507
|
+
if (!isPlainObject(json2)) {
|
|
508
|
+
errors.push(mismatch(path, "an object", json2));
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const known = Object.keys(fields);
|
|
512
|
+
for (const key of Object.keys(json2)) {
|
|
513
|
+
if (!Object.hasOwn(fields, key)) errors.push(unknownField(path, key, known));
|
|
514
|
+
}
|
|
515
|
+
for (const name of known) {
|
|
516
|
+
const field = fields[name];
|
|
517
|
+
const value = json2[name];
|
|
518
|
+
if (value === void 0) {
|
|
519
|
+
if (field.options.required) {
|
|
520
|
+
errors.push(
|
|
521
|
+
new ShardError("schema/missing-field", `Missing required field "${name}"`, {
|
|
522
|
+
path: pointer(path, name),
|
|
523
|
+
hint: field.options.description
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
field.validate(value, pointer(path, name), errors, ctx);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function objectToJson(fields, value) {
|
|
533
|
+
const out = {};
|
|
534
|
+
for (const name in fields) out[name] = fields[name].toJson(value[name]);
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
function objectFromJson(fields, json2, ctx) {
|
|
538
|
+
const out = {};
|
|
539
|
+
for (const name in fields) {
|
|
540
|
+
const field = fields[name];
|
|
541
|
+
out[name] = json2[name] === void 0 ? field.defaultValue() : field.fromJson(json2[name], ctx);
|
|
542
|
+
}
|
|
543
|
+
return out;
|
|
544
|
+
}
|
|
545
|
+
function objectSchema(fields) {
|
|
546
|
+
const properties = {};
|
|
547
|
+
const required = [];
|
|
548
|
+
for (const name in fields) {
|
|
549
|
+
properties[name] = fields[name].jsonSchema();
|
|
550
|
+
if (fields[name].options.required) required.push(name);
|
|
551
|
+
}
|
|
552
|
+
const schema = { type: "object", properties, additionalProperties: false };
|
|
553
|
+
if (required.length > 0) schema.required = required;
|
|
554
|
+
return schema;
|
|
555
|
+
}
|
|
556
|
+
const t = {
|
|
557
|
+
f32: numberField("f32"),
|
|
558
|
+
f64: numberField("f64"),
|
|
559
|
+
i8: numberField("i8"),
|
|
560
|
+
i16: numberField("i16"),
|
|
561
|
+
i32: numberField("i32"),
|
|
562
|
+
u8: numberField("u8"),
|
|
563
|
+
u16: numberField("u16"),
|
|
564
|
+
u32: numberField("u32"),
|
|
565
|
+
bool,
|
|
566
|
+
vec2: vectorField("vec2", [0, 0]),
|
|
567
|
+
vec3: vectorField("vec3", [0, 0, 0]),
|
|
568
|
+
vec4: vectorField("vec4", [0, 0, 0, 0]),
|
|
569
|
+
ivec3,
|
|
570
|
+
quat: vectorField("quat", [0, 0, 0, 1]),
|
|
571
|
+
mat3: vectorField("mat3", [1, 0, 0, 0, 1, 0, 0, 0, 1]),
|
|
572
|
+
mat4: vectorField("mat4", [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
573
|
+
affine3x4: vectorField("affine3x4", [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0]),
|
|
574
|
+
color,
|
|
575
|
+
enum: enumField,
|
|
576
|
+
entity,
|
|
577
|
+
string,
|
|
578
|
+
handle,
|
|
579
|
+
list,
|
|
580
|
+
struct,
|
|
581
|
+
json
|
|
582
|
+
};
|
|
583
|
+
export {
|
|
584
|
+
defaultsOf,
|
|
585
|
+
f32ToJson,
|
|
586
|
+
objectFromJson,
|
|
587
|
+
objectSchema,
|
|
588
|
+
objectToJson,
|
|
589
|
+
t,
|
|
590
|
+
validateObject
|
|
591
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { allComponents, type ColumnLayout, type ComponentDef, type ComponentOptions, defineComponent, defineSchema, defineTag, findComponent, type Infer, type TagDef, } from './component.js';
|
|
2
|
+
export type { Affine3x4, AnyField, AssetRef, Color, Column, FieldInput, FieldKind, FieldOptions, Fields, FieldType, FieldValue, InferFields, InitFields, IVec3, Mat3, Mat4, NumericStorage, Quat, ResolvedAsset, SchemaContext, Storage, TypedArray, TypedArrayFor, Vec2, Vec3, Vec4, } from './field.js';
|
|
3
|
+
export { t } from './field.js';
|
|
4
|
+
export { beginRedefinition, endRedefinition, isRedefinable, type Redefinition, } from './names.js';
|
|
5
|
+
export { Registry, type RegistryDescription } from './registry.js';
|
|
6
|
+
export { allEvents, allResources, defineEvent, defineResource, type EventDef, findEvent, findResource, type ResourceDef, type ResourceOptions, } from './resource.js';
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allComponents,
|
|
3
|
+
defineComponent,
|
|
4
|
+
defineSchema,
|
|
5
|
+
defineTag,
|
|
6
|
+
findComponent
|
|
7
|
+
} from "./component.js";
|
|
8
|
+
import { t } from "./field.js";
|
|
9
|
+
import {
|
|
10
|
+
beginRedefinition,
|
|
11
|
+
endRedefinition,
|
|
12
|
+
isRedefinable
|
|
13
|
+
} from "./names.js";
|
|
14
|
+
import { Registry } from "./registry.js";
|
|
15
|
+
import {
|
|
16
|
+
allEvents,
|
|
17
|
+
allResources,
|
|
18
|
+
defineEvent,
|
|
19
|
+
defineResource,
|
|
20
|
+
findEvent,
|
|
21
|
+
findResource
|
|
22
|
+
} from "./resource.js";
|
|
23
|
+
export {
|
|
24
|
+
Registry,
|
|
25
|
+
allComponents,
|
|
26
|
+
allEvents,
|
|
27
|
+
allResources,
|
|
28
|
+
beginRedefinition,
|
|
29
|
+
defineComponent,
|
|
30
|
+
defineEvent,
|
|
31
|
+
defineResource,
|
|
32
|
+
defineSchema,
|
|
33
|
+
defineTag,
|
|
34
|
+
endRedefinition,
|
|
35
|
+
findComponent,
|
|
36
|
+
findEvent,
|
|
37
|
+
findResource,
|
|
38
|
+
isRedefinable,
|
|
39
|
+
t
|
|
40
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare function assertName(kind: string, name: string): void;
|
|
2
|
+
/** Process-wide ids, dense from 0, shared by components, resources, and events. */
|
|
3
|
+
export declare function allocateId(): number;
|
|
4
|
+
export interface Redefinition {
|
|
5
|
+
readonly kind: 'component' | 'resource' | 'event';
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** The definition being replaced; the new one reuses its id. */
|
|
8
|
+
readonly previous: {
|
|
9
|
+
readonly id: number;
|
|
10
|
+
};
|
|
11
|
+
readonly next: {
|
|
12
|
+
readonly id: number;
|
|
13
|
+
};
|
|
14
|
+
/** Puts the previous definition back in the global catalog (a failed reload). */
|
|
15
|
+
undo(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Starts a redefinition scope: until `endRedefinition`, defining a component, resource, or event
|
|
19
|
+
* whose name is in `namespace` and already exists replaces it, keeping its id, instead of creating
|
|
20
|
+
* a second definition. Hot reload evaluates the new project bundle inside one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function beginRedefinition(namespace: string): void;
|
|
23
|
+
/** Ends the scope and returns what was redefined. */
|
|
24
|
+
export declare function endRedefinition(): Redefinition[];
|
|
25
|
+
/** True when `name` may replace an existing definition right now. */
|
|
26
|
+
export declare function isRedefinable(name: string): boolean;
|
|
27
|
+
export declare function recordRedefinition(change: Redefinition): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ShardError } from "../error.js";
|
|
2
|
+
const NAME = /^[a-z][a-z0-9-]*\/[A-Z][A-Za-z0-9]*$/;
|
|
3
|
+
function assertName(kind, name) {
|
|
4
|
+
if (!NAME.test(name)) {
|
|
5
|
+
throw new ShardError("schema/invalid-name", `Invalid ${kind} name "${name}"`, {
|
|
6
|
+
hint: 'Use "namespace/PascalName", e.g. "game/Health".'
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
let nextId = 0;
|
|
11
|
+
function allocateId() {
|
|
12
|
+
return nextId++;
|
|
13
|
+
}
|
|
14
|
+
let scope;
|
|
15
|
+
function beginRedefinition(namespace) {
|
|
16
|
+
if (scope) {
|
|
17
|
+
throw new ShardError("schema/redefinition-active", "A redefinition scope is already open");
|
|
18
|
+
}
|
|
19
|
+
scope = { namespace, changes: [] };
|
|
20
|
+
}
|
|
21
|
+
function endRedefinition() {
|
|
22
|
+
const changes = scope?.changes ?? [];
|
|
23
|
+
scope = void 0;
|
|
24
|
+
return changes;
|
|
25
|
+
}
|
|
26
|
+
function isRedefinable(name) {
|
|
27
|
+
return scope !== void 0 && name.startsWith(`${scope.namespace}/`);
|
|
28
|
+
}
|
|
29
|
+
function recordRedefinition(change) {
|
|
30
|
+
scope?.changes.push(change);
|
|
31
|
+
}
|
|
32
|
+
export {
|
|
33
|
+
allocateId,
|
|
34
|
+
assertName,
|
|
35
|
+
beginRedefinition,
|
|
36
|
+
endRedefinition,
|
|
37
|
+
isRedefinable,
|
|
38
|
+
recordRedefinition
|
|
39
|
+
};
|