@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,74 @@
|
|
|
1
|
+
import type { Commands } from '../ecs/commands.js';
|
|
2
|
+
import type { EventReader } from '../ecs/events.js';
|
|
3
|
+
import type { World } from '../ecs/world.js';
|
|
4
|
+
import type { EventDef } from '../schema/resource.js';
|
|
5
|
+
export interface SystemContext {
|
|
6
|
+
/** Deferred changes, applied right after this system runs. */
|
|
7
|
+
readonly commands: Commands;
|
|
8
|
+
/** World tick when this system last ran (0 before its first run). Use for added/changed. */
|
|
9
|
+
readonly lastRunTick: number;
|
|
10
|
+
/** World tick of the current run. */
|
|
11
|
+
readonly thisRunTick: number;
|
|
12
|
+
/** This system's own reader for an event (created on first use, then reused). */
|
|
13
|
+
reader<T>(event: EventDef<T>): EventReader<T>;
|
|
14
|
+
}
|
|
15
|
+
/** A run condition. Give it a `label` (see `condition`) so it shows up readably in `describe()`. */
|
|
16
|
+
export type Condition = ((world: World) => boolean) & {
|
|
17
|
+
readonly label?: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function condition(label: string, fn: (world: World) => boolean): Condition;
|
|
20
|
+
export declare function not(inner: Condition): Condition;
|
|
21
|
+
export declare function conditionLabel(c: Condition): string;
|
|
22
|
+
export interface SystemSpec<S> {
|
|
23
|
+
/** Unique, namespaced (`game/movement`). Shows up in the profiler, logs, and agent API. */
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Runs once before the first run. Returns the system's local state (queries, scratch). */
|
|
27
|
+
setup?(world: World): S;
|
|
28
|
+
run(state: S, world: World, ctx: SystemContext): void;
|
|
29
|
+
}
|
|
30
|
+
export type Label = SystemDef<unknown> | SystemSetDef;
|
|
31
|
+
interface Orderable<Self> {
|
|
32
|
+
after(...labels: Label[]): Self;
|
|
33
|
+
before(...labels: Label[]): Self;
|
|
34
|
+
runIf(...conditions: Condition[]): Self;
|
|
35
|
+
}
|
|
36
|
+
export interface SystemDef<S = unknown> extends Orderable<SystemConfig> {
|
|
37
|
+
readonly kind: 'system';
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly description: string | undefined;
|
|
40
|
+
setup?(world: World): S;
|
|
41
|
+
run(state: S, world: World, ctx: SystemContext): void;
|
|
42
|
+
inSet(...sets: SystemSetDef[]): SystemConfig;
|
|
43
|
+
}
|
|
44
|
+
export interface SystemSetDef extends Orderable<SystemSetConfig> {
|
|
45
|
+
readonly kind: 'set';
|
|
46
|
+
readonly name: string;
|
|
47
|
+
}
|
|
48
|
+
/** A system plus its scheduling constraints. Immutable; every method returns a new config. */
|
|
49
|
+
export declare class SystemConfig implements Orderable<SystemConfig> {
|
|
50
|
+
readonly system: SystemDef<unknown>;
|
|
51
|
+
readonly afterLabels: readonly Label[];
|
|
52
|
+
readonly beforeLabels: readonly Label[];
|
|
53
|
+
readonly conditions: readonly Condition[];
|
|
54
|
+
readonly sets: readonly SystemSetDef[];
|
|
55
|
+
constructor(system: SystemDef<unknown>, after?: readonly Label[], before?: readonly Label[], conditions?: readonly Condition[], sets?: readonly SystemSetDef[]);
|
|
56
|
+
after(...labels: Label[]): SystemConfig;
|
|
57
|
+
before(...labels: Label[]): SystemConfig;
|
|
58
|
+
runIf(...more: Condition[]): SystemConfig;
|
|
59
|
+
inSet(...more: SystemSetDef[]): SystemConfig;
|
|
60
|
+
}
|
|
61
|
+
/** Constraints applied to every system in a set. */
|
|
62
|
+
export declare class SystemSetConfig implements Orderable<SystemSetConfig> {
|
|
63
|
+
readonly set: SystemSetDef;
|
|
64
|
+
readonly afterLabels: readonly Label[];
|
|
65
|
+
readonly beforeLabels: readonly Label[];
|
|
66
|
+
readonly conditions: readonly Condition[];
|
|
67
|
+
constructor(set: SystemSetDef, after?: readonly Label[], before?: readonly Label[], conditions?: readonly Condition[]);
|
|
68
|
+
after(...labels: Label[]): SystemSetConfig;
|
|
69
|
+
before(...labels: Label[]): SystemSetConfig;
|
|
70
|
+
runIf(...more: Condition[]): SystemSetConfig;
|
|
71
|
+
}
|
|
72
|
+
export declare function defineSystem<S = undefined>(spec: SystemSpec<S>): SystemDef<S>;
|
|
73
|
+
export declare function defineSystemSet(name: string): SystemSetDef;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function condition(label, fn) {
|
|
2
|
+
return Object.assign((world) => fn(world), { label });
|
|
3
|
+
}
|
|
4
|
+
function not(inner) {
|
|
5
|
+
return condition(`not(${conditionLabel(inner)})`, (world) => !inner(world));
|
|
6
|
+
}
|
|
7
|
+
function conditionLabel(c) {
|
|
8
|
+
return c.label ?? (c.name || "anonymous");
|
|
9
|
+
}
|
|
10
|
+
class SystemConfig {
|
|
11
|
+
system;
|
|
12
|
+
afterLabels;
|
|
13
|
+
beforeLabels;
|
|
14
|
+
conditions;
|
|
15
|
+
sets;
|
|
16
|
+
constructor(system, after = [], before = [], conditions = [], sets = []) {
|
|
17
|
+
this.system = system;
|
|
18
|
+
this.afterLabels = after;
|
|
19
|
+
this.beforeLabels = before;
|
|
20
|
+
this.conditions = conditions;
|
|
21
|
+
this.sets = sets;
|
|
22
|
+
}
|
|
23
|
+
after(...labels) {
|
|
24
|
+
const { system, afterLabels, beforeLabels, conditions, sets } = this;
|
|
25
|
+
return new SystemConfig(system, [...afterLabels, ...labels], beforeLabels, conditions, sets);
|
|
26
|
+
}
|
|
27
|
+
before(...labels) {
|
|
28
|
+
const { system, afterLabels, beforeLabels, conditions, sets } = this;
|
|
29
|
+
return new SystemConfig(system, afterLabels, [...beforeLabels, ...labels], conditions, sets);
|
|
30
|
+
}
|
|
31
|
+
runIf(...more) {
|
|
32
|
+
const { system, afterLabels, beforeLabels, conditions, sets } = this;
|
|
33
|
+
return new SystemConfig(system, afterLabels, beforeLabels, [...conditions, ...more], sets);
|
|
34
|
+
}
|
|
35
|
+
inSet(...more) {
|
|
36
|
+
const { system, afterLabels, beforeLabels, conditions, sets } = this;
|
|
37
|
+
return new SystemConfig(system, afterLabels, beforeLabels, conditions, [...sets, ...more]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
class SystemSetConfig {
|
|
41
|
+
set;
|
|
42
|
+
afterLabels;
|
|
43
|
+
beforeLabels;
|
|
44
|
+
conditions;
|
|
45
|
+
constructor(set, after = [], before = [], conditions = []) {
|
|
46
|
+
this.set = set;
|
|
47
|
+
this.afterLabels = after;
|
|
48
|
+
this.beforeLabels = before;
|
|
49
|
+
this.conditions = conditions;
|
|
50
|
+
}
|
|
51
|
+
after(...labels) {
|
|
52
|
+
return new SystemSetConfig(
|
|
53
|
+
this.set,
|
|
54
|
+
[...this.afterLabels, ...labels],
|
|
55
|
+
this.beforeLabels,
|
|
56
|
+
this.conditions
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
before(...labels) {
|
|
60
|
+
return new SystemSetConfig(
|
|
61
|
+
this.set,
|
|
62
|
+
this.afterLabels,
|
|
63
|
+
[...this.beforeLabels, ...labels],
|
|
64
|
+
this.conditions
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
runIf(...more) {
|
|
68
|
+
return new SystemSetConfig(this.set, this.afterLabels, this.beforeLabels, [
|
|
69
|
+
...this.conditions,
|
|
70
|
+
...more
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function defineSystem(spec) {
|
|
75
|
+
const def = {
|
|
76
|
+
kind: "system",
|
|
77
|
+
name: spec.name,
|
|
78
|
+
description: spec.description,
|
|
79
|
+
run: spec.run,
|
|
80
|
+
after: (...labels) => new SystemConfig(def).after(...labels),
|
|
81
|
+
before: (...labels) => new SystemConfig(def).before(...labels),
|
|
82
|
+
runIf: (...conditions) => new SystemConfig(def).runIf(...conditions),
|
|
83
|
+
inSet: (...sets) => new SystemConfig(def).inSet(...sets)
|
|
84
|
+
};
|
|
85
|
+
if (spec.setup) def.setup = spec.setup;
|
|
86
|
+
return def;
|
|
87
|
+
}
|
|
88
|
+
function defineSystemSet(name) {
|
|
89
|
+
const def = {
|
|
90
|
+
kind: "set",
|
|
91
|
+
name,
|
|
92
|
+
after: (...labels) => new SystemSetConfig(def).after(...labels),
|
|
93
|
+
before: (...labels) => new SystemSetConfig(def).before(...labels),
|
|
94
|
+
runIf: (...conditions) => new SystemSetConfig(def).runIf(...conditions)
|
|
95
|
+
};
|
|
96
|
+
return def;
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
SystemConfig,
|
|
100
|
+
SystemSetConfig,
|
|
101
|
+
condition,
|
|
102
|
+
conditionLabel,
|
|
103
|
+
defineSystem,
|
|
104
|
+
defineSystemSet,
|
|
105
|
+
not
|
|
106
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function srgbToLinear(c: number): number;
|
|
2
|
+
export declare function linearToSrgb(c: number): number;
|
|
3
|
+
export declare const HEX_COLOR: RegExp;
|
|
4
|
+
/** Parses `#rrggbb` or `#rrggbbaa` (sRGB) into linear RGBA. */
|
|
5
|
+
export declare function hexToLinear(hex: string): [number, number, number, number];
|
|
6
|
+
/** Formats linear RGBA as sRGB hex, or returns undefined if hex can't represent it exactly. */
|
|
7
|
+
export declare function linearToHex(color: readonly number[]): string | undefined;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
function srgbToLinear(c) {
|
|
2
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
3
|
+
}
|
|
4
|
+
function linearToSrgb(c) {
|
|
5
|
+
return c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
6
|
+
}
|
|
7
|
+
const HEX_COLOR = /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
8
|
+
function hexToLinear(hex) {
|
|
9
|
+
const byte = (i) => Number.parseInt(hex.slice(i, i + 2), 16) / 255;
|
|
10
|
+
const a = hex.length === 9 ? byte(7) : 1;
|
|
11
|
+
return [srgbToLinear(byte(1)), srgbToLinear(byte(3)), srgbToLinear(byte(5)), a];
|
|
12
|
+
}
|
|
13
|
+
function linearToHex(color) {
|
|
14
|
+
const channels = [];
|
|
15
|
+
for (let i = 0; i < 4; i++) {
|
|
16
|
+
const c = color[i];
|
|
17
|
+
if (c < 0 || c > 1) return void 0;
|
|
18
|
+
channels.push(Math.round((i === 3 ? c : linearToSrgb(c)) * 255));
|
|
19
|
+
}
|
|
20
|
+
const alpha = channels[3] === 255 ? "" : channels[3].toString(16).padStart(2, "0");
|
|
21
|
+
const hex = `#${channels.slice(0, 3).map((c) => c.toString(16).padStart(2, "0")).join("")}${alpha}`;
|
|
22
|
+
const back = hexToLinear(hex);
|
|
23
|
+
for (let i = 0; i < 4; i++) {
|
|
24
|
+
if (Math.fround(back[i]) !== Math.fround(color[i])) return void 0;
|
|
25
|
+
}
|
|
26
|
+
return hex;
|
|
27
|
+
}
|
|
28
|
+
export {
|
|
29
|
+
HEX_COLOR,
|
|
30
|
+
hexToLinear,
|
|
31
|
+
linearToHex,
|
|
32
|
+
linearToSrgb,
|
|
33
|
+
srgbToLinear
|
|
34
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ShardError } from '../error.js';
|
|
2
|
+
import type { JsonObject, JsonSchema } from '../json.js';
|
|
3
|
+
import { type AnyField, type Fields, type InferFields, type SchemaContext, type Storage } from './field.js';
|
|
4
|
+
export interface ColumnLayout {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly field: AnyField;
|
|
7
|
+
readonly storage: Storage;
|
|
8
|
+
readonly stride: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ComponentOptions {
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Defaults to 1. Versions above 1 need `migrate`. */
|
|
13
|
+
version?: number;
|
|
14
|
+
/** Upgrades JSON written at version `from` to version `from + 1`. */
|
|
15
|
+
migrate?: (from: number, json: unknown) => unknown;
|
|
16
|
+
/**
|
|
17
|
+
* Components added automatically (with defaults) whenever this one is added, transitively.
|
|
18
|
+
* Spawning `Mesh3d` brings `Transform`, which brings `GlobalTransform`.
|
|
19
|
+
*/
|
|
20
|
+
requires?: readonly ComponentDef[];
|
|
21
|
+
/** False for derived components (computed each frame), which scene files never contain. */
|
|
22
|
+
serialize?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* False leaves the component out of saved games (0038) while scenes still write it: state that
|
|
25
|
+
* rebuilds itself, or that belongs to the session rather than the playthrough.
|
|
26
|
+
*/
|
|
27
|
+
save?: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ComponentDef<F extends Fields = Fields> {
|
|
30
|
+
readonly kind: 'component';
|
|
31
|
+
/** Process-wide numeric id, used internally by the ECS. Never serialized. */
|
|
32
|
+
readonly id: number;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly fields: F;
|
|
35
|
+
readonly isTag: boolean;
|
|
36
|
+
readonly description: string | undefined;
|
|
37
|
+
readonly version: number;
|
|
38
|
+
/** Column descriptors, in field order. ECS tables are built from this alone. */
|
|
39
|
+
readonly layout: readonly ColumnLayout[];
|
|
40
|
+
/** Components added along with this one. */
|
|
41
|
+
readonly requires: readonly ComponentDef[];
|
|
42
|
+
/** Whether scene files and the protocol write this component. */
|
|
43
|
+
readonly serializable: boolean;
|
|
44
|
+
/** Whether saved games include it: serializable, and not `save: false`. */
|
|
45
|
+
readonly saved: boolean;
|
|
46
|
+
defaults(): InferFields<F>;
|
|
47
|
+
serialize(value: InferFields<F>): JsonObject;
|
|
48
|
+
/** Validates, then converts. Throws the first validation error. */
|
|
49
|
+
deserialize(json: unknown, ctx?: SchemaContext): InferFields<F>;
|
|
50
|
+
validate(json: unknown, ctx?: SchemaContext): ShardError[];
|
|
51
|
+
jsonSchema(): JsonSchema;
|
|
52
|
+
/** Runs migrations from `fromVersion` up to the current version. */
|
|
53
|
+
upgrade(json: unknown, fromVersion: number): unknown;
|
|
54
|
+
}
|
|
55
|
+
export type TagDef = ComponentDef<Record<never, never>>;
|
|
56
|
+
/**
|
|
57
|
+
* The component defined under `name`, or undefined. Throws `schema/ambiguous-name` if two different
|
|
58
|
+
* definitions share the name (a world's registry would reject that too).
|
|
59
|
+
*/
|
|
60
|
+
export declare function findComponent(name: string): ComponentDef | undefined;
|
|
61
|
+
/** All component definitions, sorted by name. */
|
|
62
|
+
export declare function allComponents(): ComponentDef[];
|
|
63
|
+
export type Infer<C> = C extends ComponentDef<infer F> ? InferFields<F> : never;
|
|
64
|
+
export declare function defineComponent<const F extends Fields>(name: string, fields: F, options?: ComponentOptions): ComponentDef<F>;
|
|
65
|
+
/**
|
|
66
|
+
* A schema for data that isn't an entity component (file formats, protocol parameters, settings):
|
|
67
|
+
* validation, defaults, (de)serialization, and JSON Schema, without joining the component catalog.
|
|
68
|
+
*/
|
|
69
|
+
export declare function defineSchema<const F extends Fields>(name: string, fields: F, options?: Pick<ComponentOptions, 'description' | 'version' | 'migrate'>): ComponentDef<F>;
|
|
70
|
+
/** A component with no fields. Takes part in queries; stores nothing. */
|
|
71
|
+
export declare function defineTag(name: string, options?: Omit<ComponentOptions, 'version' | 'migrate'>): TagDef;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { ShardError } from "../error.js";
|
|
2
|
+
import {
|
|
3
|
+
defaultsOf,
|
|
4
|
+
objectFromJson,
|
|
5
|
+
objectSchema,
|
|
6
|
+
objectToJson,
|
|
7
|
+
validateObject
|
|
8
|
+
} from "./field.js";
|
|
9
|
+
import { allocateId, assertName, isRedefinable, recordRedefinition } from "./names.js";
|
|
10
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
11
|
+
function findComponent(name) {
|
|
12
|
+
const defs = definitions.get(name);
|
|
13
|
+
if (!defs || defs.length === 0) return void 0;
|
|
14
|
+
if (defs.length > 1) {
|
|
15
|
+
throw new ShardError("schema/ambiguous-name", `Several components are defined as "${name}"`, {
|
|
16
|
+
hint: "Two modules define the same name. Rename one, or import the shared definition."
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return defs[0];
|
|
20
|
+
}
|
|
21
|
+
function allComponents() {
|
|
22
|
+
return [...definitions.values()].flat().sort((a, b) => a.name.localeCompare(b.name));
|
|
23
|
+
}
|
|
24
|
+
function defineComponent(name, fields, options = {}) {
|
|
25
|
+
const existing = definitions.get(name);
|
|
26
|
+
const previous = existing && isRedefinable(name) ? existing.at(-1) : void 0;
|
|
27
|
+
const def = buildSchema(name, fields, options, previous?.id);
|
|
28
|
+
if (previous) {
|
|
29
|
+
definitions.set(name, [def]);
|
|
30
|
+
recordRedefinition({
|
|
31
|
+
kind: "component",
|
|
32
|
+
name,
|
|
33
|
+
previous,
|
|
34
|
+
next: def,
|
|
35
|
+
undo: () => definitions.set(name, [previous])
|
|
36
|
+
});
|
|
37
|
+
} else if (existing) existing.push(def);
|
|
38
|
+
else definitions.set(name, [def]);
|
|
39
|
+
return def;
|
|
40
|
+
}
|
|
41
|
+
function defineSchema(name, fields, options = {}) {
|
|
42
|
+
return buildSchema(name, fields, { ...options, serialize: false });
|
|
43
|
+
}
|
|
44
|
+
function buildSchema(name, fields, options, reuseId) {
|
|
45
|
+
assertName("component", name);
|
|
46
|
+
const version = options.version ?? 1;
|
|
47
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
48
|
+
throw new ShardError("schema/invalid-version", `Component "${name}" has invalid version`, {
|
|
49
|
+
hint: "Versions are integers starting at 1."
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (version > 1 && !options.migrate) {
|
|
53
|
+
throw new ShardError(
|
|
54
|
+
"schema/missing-migration",
|
|
55
|
+
`Component "${name}" is at version ${version} but has no migrate function`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const layout = Object.entries(fields).map(([fieldName, field]) => ({
|
|
59
|
+
name: fieldName,
|
|
60
|
+
field,
|
|
61
|
+
storage: field.storage,
|
|
62
|
+
stride: field.stride
|
|
63
|
+
}));
|
|
64
|
+
const validate = (json, ctx) => {
|
|
65
|
+
const errors = [];
|
|
66
|
+
validateObject(fields, json, "", errors, ctx);
|
|
67
|
+
return errors;
|
|
68
|
+
};
|
|
69
|
+
const def = {
|
|
70
|
+
kind: "component",
|
|
71
|
+
id: reuseId ?? allocateId(),
|
|
72
|
+
name,
|
|
73
|
+
fields,
|
|
74
|
+
isTag: layout.length === 0,
|
|
75
|
+
description: options.description,
|
|
76
|
+
version,
|
|
77
|
+
layout,
|
|
78
|
+
requires: options.requires ?? [],
|
|
79
|
+
serializable: options.serialize ?? true,
|
|
80
|
+
saved: (options.serialize ?? true) && (options.save ?? true),
|
|
81
|
+
defaults: () => defaultsOf(fields),
|
|
82
|
+
serialize: (value) => objectToJson(fields, value),
|
|
83
|
+
deserialize(json, ctx) {
|
|
84
|
+
const errors = validate(json, ctx);
|
|
85
|
+
if (errors.length > 0) throw errors[0];
|
|
86
|
+
return objectFromJson(fields, json, ctx);
|
|
87
|
+
},
|
|
88
|
+
validate,
|
|
89
|
+
jsonSchema: () => {
|
|
90
|
+
const schema = {
|
|
91
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
92
|
+
title: name
|
|
93
|
+
};
|
|
94
|
+
if (options.description) schema.description = options.description;
|
|
95
|
+
return { ...schema, ...objectSchema(fields), "x-version": version };
|
|
96
|
+
},
|
|
97
|
+
upgrade(json, fromVersion) {
|
|
98
|
+
if (fromVersion > version) {
|
|
99
|
+
throw new ShardError(
|
|
100
|
+
"schema/future-version",
|
|
101
|
+
`"${name}" data is version ${fromVersion}, newer than this build's version ${version}`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
let current = json;
|
|
105
|
+
for (let v = fromVersion; v < version; v++) current = options.migrate(v, current);
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
return def;
|
|
110
|
+
}
|
|
111
|
+
function defineTag(name, options = {}) {
|
|
112
|
+
return defineComponent(name, {}, options);
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
allComponents,
|
|
116
|
+
defineComponent,
|
|
117
|
+
defineSchema,
|
|
118
|
+
defineTag,
|
|
119
|
+
findComponent
|
|
120
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Entity } from '../ecs/entity.js';
|
|
2
|
+
import { ShardError } from '../error.js';
|
|
3
|
+
import { type JsonSchema, type JsonValue } from '../json.js';
|
|
4
|
+
export type NumericStorage = 'f32' | 'f64' | 'i8' | 'i16' | 'i32' | 'u8' | 'u16' | 'u32';
|
|
5
|
+
export type Storage = NumericStorage | 'object';
|
|
6
|
+
export type TypedArrayFor<S extends Storage> = S extends 'f32' ? Float32Array : S extends 'f64' ? Float64Array : S extends 'i8' ? Int8Array : S extends 'i16' ? Int16Array : S extends 'i32' ? Int32Array : S extends 'u8' ? Uint8Array : S extends 'u16' ? Uint16Array : S extends 'u32' ? Uint32Array : never;
|
|
7
|
+
export type TypedArray = TypedArrayFor<NumericStorage>;
|
|
8
|
+
export type Column = TypedArray | unknown[];
|
|
9
|
+
export type FieldKind = NumericStorage | 'bool' | 'vec2' | 'vec3' | 'vec4'
|
|
10
|
+
/** Three i32 components (grid cells). */
|
|
11
|
+
| 'ivec3' | 'quat' | 'mat3' | 'mat4' | 'affine3x4' | 'color' | 'enum' | 'entity' | 'string' | 'handle' | 'list' | 'struct' | 'json'
|
|
12
|
+
/** A size with a unit: pixels, a percent, or auto (`@aethervtt/shard-ui`'s `uiLength`). */
|
|
13
|
+
| 'length';
|
|
14
|
+
export interface FieldOptions<V> {
|
|
15
|
+
/** Written for agents and tools: what the field means, not its type. */
|
|
16
|
+
description?: string;
|
|
17
|
+
default?: V;
|
|
18
|
+
/** Numeric fields and each component of vectors. */
|
|
19
|
+
min?: number;
|
|
20
|
+
max?: number;
|
|
21
|
+
unit?: string;
|
|
22
|
+
hidden?: boolean;
|
|
23
|
+
readonly?: boolean;
|
|
24
|
+
/** Missing values are an error instead of taking the default. */
|
|
25
|
+
required?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Named values for number fields. Files may write the name (`"illuminance": "daylight"`); it
|
|
28
|
+
* resolves to the number on load. The JSON Schema lists the names as alternatives.
|
|
29
|
+
*/
|
|
30
|
+
presets?: Readonly<Record<string, number>>;
|
|
31
|
+
}
|
|
32
|
+
/** A reference to an asset. `guid` is the identity; `path` is for readers. */
|
|
33
|
+
export interface AssetRef<T extends string = string> {
|
|
34
|
+
readonly type: T;
|
|
35
|
+
guid: string | undefined;
|
|
36
|
+
path: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
export interface ResolvedAsset {
|
|
39
|
+
readonly guid: string;
|
|
40
|
+
readonly path: string;
|
|
41
|
+
readonly type: string;
|
|
42
|
+
}
|
|
43
|
+
/** Optional hooks for things the schema can't know on its own. */
|
|
44
|
+
export interface SchemaContext {
|
|
45
|
+
/** Resolves an entity path (e.g. `lobby/sofa_02`) to an entity. */
|
|
46
|
+
resolveEntity?(path: string): Entity | undefined;
|
|
47
|
+
/** Looks up an asset by guid or path, for validation and to fill in the other half. */
|
|
48
|
+
resolveAsset?(ref: {
|
|
49
|
+
guid?: string;
|
|
50
|
+
path?: string;
|
|
51
|
+
}): ResolvedAsset | undefined;
|
|
52
|
+
}
|
|
53
|
+
export interface FieldType<V = unknown, S extends Storage = Storage> {
|
|
54
|
+
readonly kind: FieldKind;
|
|
55
|
+
readonly storage: S;
|
|
56
|
+
/** Values per row in the column (3 for vec3). */
|
|
57
|
+
readonly stride: number;
|
|
58
|
+
readonly options: FieldOptions<V>;
|
|
59
|
+
/** A struct's fields. */
|
|
60
|
+
readonly fields?: Fields;
|
|
61
|
+
/** A list's item type. */
|
|
62
|
+
readonly item?: AnyField;
|
|
63
|
+
/** The asset type a handle refers to. */
|
|
64
|
+
readonly assetType?: string;
|
|
65
|
+
/** Phantom, for type inference only. */
|
|
66
|
+
readonly __value?: V;
|
|
67
|
+
defaultValue(): V;
|
|
68
|
+
read(column: Column, row: number): V;
|
|
69
|
+
write(column: Column, row: number, value: V): void;
|
|
70
|
+
validate(json: unknown, path: string, errors: ShardError[], ctx: SchemaContext | undefined): void;
|
|
71
|
+
toJson(value: V): JsonValue;
|
|
72
|
+
fromJson(json: unknown, ctx: SchemaContext | undefined): V;
|
|
73
|
+
jsonSchema(): JsonSchema;
|
|
74
|
+
}
|
|
75
|
+
export type AnyField = FieldType<unknown, Storage>;
|
|
76
|
+
export type Fields = Record<string, AnyField>;
|
|
77
|
+
export type FieldValue<T> = T extends FieldType<infer V, Storage> ? V : never;
|
|
78
|
+
export type InferFields<F extends Fields> = {
|
|
79
|
+
-readonly [K in keyof F]: FieldValue<F[K]>;
|
|
80
|
+
};
|
|
81
|
+
/** What you may pass in: arrays can be readonly, since values are copied into columns. */
|
|
82
|
+
export type FieldInput<V> = V extends unknown[] ? Readonly<V> : V;
|
|
83
|
+
/** Input for spawn/add/set: any subset of fields; missing ones take defaults or stay unchanged. */
|
|
84
|
+
export type InitFields<F extends Fields> = {
|
|
85
|
+
[K in keyof F]?: FieldInput<FieldValue<F[K]>>;
|
|
86
|
+
};
|
|
87
|
+
type Factory<V, S extends Storage> = FieldType<V, S> & ((options?: FieldOptions<V>) => FieldType<V, S>);
|
|
88
|
+
/**
|
|
89
|
+
* The shortest decimal that reads back as the same 32-bit float, so f32 values serialize as `0.4`
|
|
90
|
+
* rather than `0.4000000059604645` (what a Float32Array hands back).
|
|
91
|
+
*/
|
|
92
|
+
export declare function f32ToJson(v: number): number;
|
|
93
|
+
export type Vec2 = [number, number];
|
|
94
|
+
export type Vec3 = [number, number, number];
|
|
95
|
+
export type Vec4 = [number, number, number, number];
|
|
96
|
+
export type IVec3 = [number, number, number];
|
|
97
|
+
export type Quat = [number, number, number, number];
|
|
98
|
+
export type Color = [number, number, number, number];
|
|
99
|
+
type Tuple<N extends number, Acc extends number[] = []> = Acc['length'] extends N ? Acc : Tuple<N, [...Acc, number]>;
|
|
100
|
+
/** Column-major 3x3. */
|
|
101
|
+
export type Mat3 = Tuple<9>;
|
|
102
|
+
/** Column-major 4x4. */
|
|
103
|
+
export type Mat4 = Tuple<16>;
|
|
104
|
+
/** Top three rows of an affine 4x4, row by row (see `affine` in math). */
|
|
105
|
+
export type Affine3x4 = Tuple<12>;
|
|
106
|
+
declare function enumField<const T extends readonly string[]>(values: T, options?: FieldOptions<T[number]>): FieldType<T[number], 'u8'>;
|
|
107
|
+
declare function handle<const T extends string>(type: T, options?: FieldOptions<AssetRef<T> | null>): FieldType<AssetRef<T> | null, 'object'>;
|
|
108
|
+
declare function list<I extends AnyField>(inner: I, options?: FieldOptions<FieldValue<I>[]>): FieldType<FieldValue<I>[], 'object'>;
|
|
109
|
+
declare function struct<F extends Fields>(fields: F, options?: FieldOptions<InferFields<F>>): FieldType<InferFields<F>, 'object'>;
|
|
110
|
+
export declare function defaultsOf<F extends Fields>(fields: F): InferFields<F>;
|
|
111
|
+
export declare function validateObject(fields: Fields, json: unknown, path: string, errors: ShardError[], ctx: SchemaContext | undefined): void;
|
|
112
|
+
export declare function objectToJson(fields: Fields, value: Record<string, unknown>): {
|
|
113
|
+
[key: string]: JsonValue;
|
|
114
|
+
};
|
|
115
|
+
export declare function objectFromJson<F extends Fields>(fields: F, json: Record<string, unknown>, ctx: SchemaContext | undefined): InferFields<F>;
|
|
116
|
+
export declare function objectSchema(fields: Fields): JsonSchema;
|
|
117
|
+
/** Field type constructors. Bare (`t.f32`) or with options (`t.f32({ default: 1 })`). */
|
|
118
|
+
export declare const t: {
|
|
119
|
+
f32: Factory<number, "f32">;
|
|
120
|
+
f64: Factory<number, "f64">;
|
|
121
|
+
i8: Factory<number, "i8">;
|
|
122
|
+
i16: Factory<number, "i16">;
|
|
123
|
+
i32: Factory<number, "i32">;
|
|
124
|
+
u8: Factory<number, "u8">;
|
|
125
|
+
u16: Factory<number, "u16">;
|
|
126
|
+
u32: Factory<number, "u32">;
|
|
127
|
+
bool: Factory<boolean, "u8">;
|
|
128
|
+
vec2: Factory<Vec2, "f32">;
|
|
129
|
+
vec3: Factory<Vec3, "f32">;
|
|
130
|
+
vec4: Factory<Vec4, "f32">;
|
|
131
|
+
ivec3: Factory<IVec3, "i32">;
|
|
132
|
+
quat: Factory<Quat, "f32">;
|
|
133
|
+
mat3: Factory<[number, number, number, number, number, number, number, number, number], "f32">;
|
|
134
|
+
mat4: Factory<[number, number, number, number, number, number, number, number, number, number, number, number, number, number, number, number], "f32">;
|
|
135
|
+
affine3x4: Factory<[number, number, number, number, number, number, number, number, number, number, number, number], "f32">;
|
|
136
|
+
color: Factory<Color, "f32">;
|
|
137
|
+
enum: typeof enumField;
|
|
138
|
+
entity: Factory<number | null, "f64">;
|
|
139
|
+
string: Factory<string, "object">;
|
|
140
|
+
handle: typeof handle;
|
|
141
|
+
list: typeof list;
|
|
142
|
+
struct: typeof struct;
|
|
143
|
+
json: Factory<JsonValue, "object">;
|
|
144
|
+
};
|
|
145
|
+
export {};
|