@aethervtt/shard-protocol 0.0.1 → 0.2.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.
@@ -0,0 +1,2 @@
1
+ import type { MethodDef } from './server.js';
2
+ export declare const ABLATE_METHODS: MethodDef[];
package/dist/ablate.js ADDED
@@ -0,0 +1,62 @@
1
+ import { defineSchema, Last, PostUpdate, t } from "@aethervtt/shard-core";
2
+ import { ablatePasses, Gpu, Graph, Views } from "@aethervtt/shard-render";
3
+ import { AppControlResource } from "@aethervtt/shard-runtime";
4
+ function renderOnly(app) {
5
+ app.runSchedule(PostUpdate);
6
+ app.runSchedule(Last);
7
+ }
8
+ function nextFrame(app) {
9
+ return new Promise((resolve) => {
10
+ const off = app.onFrame(() => {
11
+ off();
12
+ resolve();
13
+ });
14
+ });
15
+ }
16
+ const ABLATE_METHODS = [
17
+ {
18
+ name: "perf.ablate",
19
+ description: "Each render-graph pass's GPU cost, measured honestly on any GPU: rounds alternate the pass disabled and enabled, and its cost is the median drop in gpu:frame (first pass start to last pass end). Use it where per-pass timestamps overlap (tile-based GPUs such as Apple's, where pass times add up to more than the frame). A disabled pass still begins and ends, so what reads its textures stays valid; the image is wrong while it runs. Needs timestamp-query. It runs (passes + 2) \xD7 rounds \xD7 (frames + 4) frames: three passes at the defaults take about 50 s at 60 fps.",
20
+ params: defineSchema("protocol/AblateParams", {
21
+ passes: t.list(t.string, {
22
+ description: "Render-graph nodes to measure (render.describe lists them). Default: every node the last frame ran."
23
+ }),
24
+ frames: t.u32({ default: 120, min: 1, max: 1e4, description: "Frames per measurement." }),
25
+ rounds: t.u32({ default: 5, min: 1, max: 100, description: "Interleaved rounds." }),
26
+ together: t.bool({ description: "Also measure every pass disabled at once." })
27
+ }),
28
+ handler: async ({ app, world, options }, p) => {
29
+ const manual = (options.frames ?? "manual") === "manual" || world.resource(AppControlResource).paused;
30
+ const frame = () => manual ? renderOnly(app) : nextFrame(app);
31
+ const graph = world.resource(Graph);
32
+ const gpu = world.resource(Gpu);
33
+ for (let i = 0; i < 20; i++) {
34
+ await frame();
35
+ await gpu.pipelines.whenIdle();
36
+ if (i > 0 && gpu.pipelines.pending === 0 && gpu.pipelines.skipped === 0) break;
37
+ }
38
+ let passes = p.passes ?? [];
39
+ if (passes.length === 0) {
40
+ const ran = /* @__PURE__ */ new Set();
41
+ const perView = graph.describe().perView;
42
+ for (const view of world.resource(Views).list) {
43
+ for (const name of perView[view.name]?.order ?? []) ran.add(name);
44
+ }
45
+ passes = [...ran];
46
+ }
47
+ return ablatePasses(
48
+ world,
49
+ {
50
+ passes,
51
+ frames: p.frames,
52
+ rounds: p.rounds,
53
+ together: p.together
54
+ },
55
+ frame
56
+ );
57
+ }
58
+ }
59
+ ];
60
+ export {
61
+ ABLATE_METHODS
62
+ };
@@ -0,0 +1,2 @@
1
+ import type { MethodDef } from './server.js';
2
+ export declare const BUDGET_METHODS: MethodDef[];
@@ -0,0 +1,20 @@
1
+ import { defineSchema, t } from "@aethervtt/shard-core";
2
+ import { describeBudgets } from "@aethervtt/shard-runtime";
3
+ const BUDGET_METHODS = [
4
+ {
5
+ name: "perf.budgets",
6
+ description: "Performance budgets (0075, bench/perf/budgets.json) for this app: the named machine it runs on (detected from the CPU model where the host exposes it, and the GPU adapter; null with a perf/unknown-machine warning when it can't tell, and then numbers are the closest machine's), every budget resolved for it with the profiler's latest p95 against it, over budget first; and the scenario's slices (the app's App.perfScenario, or `scenario`): each slice's measured share of the frame (from the profiler's aggregates, gpu:frame for GPU slices, frame for CPU) next to its budget share, over first. Hosts supply the files: the CLI loads them from the repo or project, shard dev serves them to its page.",
7
+ params: defineSchema("protocol/BudgetsParams", {
8
+ scenario: t.string({
9
+ description: "A scenario of budgets.json to measure (scatter-walk, crowd, planet-descent, tabletop-max). Default: the app's own (App.perfScenario)."
10
+ })
11
+ }),
12
+ handler: ({ world }, p) => {
13
+ const scenario = p.scenario;
14
+ return describeBudgets(world, { scenario: scenario ? scenario : void 0 });
15
+ }
16
+ }
17
+ ];
18
+ export {
19
+ BUDGET_METHODS
20
+ };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ export { ABLATE_METHODS } from './ablate.js';
2
+ export { BUDGET_METHODS } from './budgets.js';
1
3
  export { decodePng, encodePng, toBase64 } from './png.js';
2
4
  export { type PreviewImage, previewAsset, previewKtx2 } from './preview.js';
3
5
  export { createProtocolServer, type HandlerContext, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, METHODS, type MethodDef, type ProtocolServer, type ProtocolServerOptions, type Topic, } from './server.js';
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { ABLATE_METHODS } from "./ablate.js";
2
+ import { BUDGET_METHODS } from "./budgets.js";
1
3
  import { decodePng, encodePng, toBase64 } from "./png.js";
2
4
  import { previewAsset, previewKtx2 } from "./preview.js";
3
5
  import {
@@ -6,6 +8,8 @@ import {
6
8
  } from "./server.js";
7
9
  import { connectToHub, DEFAULT_HUB_PORT } from "./transport.js";
8
10
  export {
11
+ ABLATE_METHODS,
12
+ BUDGET_METHODS,
9
13
  DEFAULT_HUB_PORT,
10
14
  METHODS,
11
15
  connectToHub,
package/dist/preview.js CHANGED
@@ -191,6 +191,7 @@ async function renderPreview(world, path, type, width, height) {
191
191
  return { width: image.width, height: image.height, data: image.data };
192
192
  } finally {
193
193
  releaseSceneHooks(app.world);
194
+ await app.dispose();
194
195
  target.destroy();
195
196
  }
196
197
  }
package/dist/server.d.ts CHANGED
@@ -43,6 +43,12 @@ export interface ProtocolServerOptions {
43
43
  platform?: Platform;
44
44
  /** Extra methods from the host (e.g. `project.reload`), added to the built-in ones. */
45
45
  methods?: readonly MethodDef[];
46
+ /**
47
+ * The only methods this server answers (0061): everything else, `subscribe` included, fails with
48
+ * `protocol/method-not-allowed`. A host that forwards requests from hosted mods passes the few
49
+ * it allows; omit it for agents and tools, which get every method.
50
+ */
51
+ allow?: readonly string[];
46
52
  }
47
53
  export declare const METHODS: MethodDef[];
48
54
  export type Topic = 'log' | 'error' | 'frame' | 'project';
package/dist/server.js CHANGED
@@ -9,12 +9,14 @@ import {
9
9
  isPlainObject,
10
10
  Last,
11
11
  PostUpdate,
12
+ ProfilerResource,
12
13
  ShardError,
13
14
  t
14
15
  } from "@aethervtt/shard-core";
15
16
  import {
16
17
  describeInput,
17
18
  injectInput,
19
+ simulateGestures,
18
20
  startRecording,
19
21
  startReplay,
20
22
  stopRecording
@@ -41,6 +43,8 @@ import {
41
43
  } from "@aethervtt/shard-render";
42
44
  import {
43
45
  AppControlResource,
46
+ capturePerf,
47
+ describePerf,
44
48
  LogResource,
45
49
  Time
46
50
  } from "@aethervtt/shard-runtime";
@@ -73,6 +77,8 @@ import {
73
77
  Transform,
74
78
  worldPosition64
75
79
  } from "@aethervtt/shard-transform";
80
+ import { ABLATE_METHODS } from "./ablate.js";
81
+ import { BUDGET_METHODS } from "./budgets.js";
76
82
  import { encodePng, toBase64 } from "./png.js";
77
83
  const CAPTURE_DEBUG_VIEWS = ["clusters", "cascades", "lod", "culling", "none"];
78
84
  import { previewAsset } from "./preview.js";
@@ -87,7 +93,7 @@ function resolveEntity(world, ref) {
87
93
  hint: 'Pass an entity id from world.query, or a scene path like "ship/camera".'
88
94
  });
89
95
  }
90
- function requireComponent(name) {
96
+ function knownComponent(name) {
91
97
  const def = findComponent(name);
92
98
  if (!def) {
93
99
  throw new ShardError("protocol/unknown-component", `Unknown component "${name}"`, {
@@ -96,6 +102,28 @@ function requireComponent(name) {
96
102
  }
97
103
  return def;
98
104
  }
105
+ function requireOwner(world, name) {
106
+ const owner = world.owners.find(name);
107
+ if (!owner) {
108
+ throw new ShardError("protocol/unknown-owner", `No live owner "${name}"`, {
109
+ hint: "owners.describe with no name lists every live owner."
110
+ });
111
+ }
112
+ return owner;
113
+ }
114
+ function requireComponent(name) {
115
+ const def = knownComponent(name);
116
+ if (def.hostOnly) {
117
+ throw new ShardError(
118
+ "core/owner-not-authorable",
119
+ `"${name}" can't be written through the protocol`,
120
+ {
121
+ hint: "Ownership comes from host code holding an Owner (world.owners)."
122
+ }
123
+ );
124
+ }
125
+ return def;
126
+ }
99
127
  function toJson(value) {
100
128
  const seen = /* @__PURE__ */ new WeakSet();
101
129
  return JSON.parse(
@@ -346,7 +374,7 @@ const METHODS = [
346
374
  params: s("SchemaGetParams", {
347
375
  name: t.string({ required: true, description: 'Component name, e.g. "render/Camera3d".' })
348
376
  }),
349
- handler: (_, p) => requireComponent(p.name).jsonSchema()
377
+ handler: (_, p) => knownComponent(p.name).jsonSchema()
350
378
  },
351
379
  {
352
380
  name: "world.stats",
@@ -439,6 +467,23 @@ const METHODS = [
439
467
  return entityJson(world, entity);
440
468
  }
441
469
  },
470
+ {
471
+ name: "owners.describe",
472
+ description: "Owners (0061). With a name: its usage (entities, triangles, textures, bytes), limits, child owners, asset leases and GPU objects. Without: every live owner.",
473
+ params: s("OwnersDescribeParams", { name: t.string() }),
474
+ handler: ({ world }, p) => p.name ? world.owners.describe(requireOwner(world, p.name)) : { owners: world.owners.list() }
475
+ },
476
+ {
477
+ name: "owners.release",
478
+ description: "Releases an owner and its child owners: despawns their entities, drops their asset leases (unloading assets nobody else leases, with their GPU objects).",
479
+ params: s("OwnersReleaseParams", { name: t.string({ required: true }) }),
480
+ handler: ({ world }, p) => {
481
+ const owner = requireOwner(world, p.name);
482
+ const entities = world.owners.describe(owner).usage.entities;
483
+ world.owners.release(owner);
484
+ return { released: owner.name, entities };
485
+ }
486
+ },
442
487
  {
443
488
  name: "entity.despawn",
444
489
  description: "Despawns an entity (and its children unless recursive is false).",
@@ -467,10 +512,19 @@ const METHODS = [
467
512
  },
468
513
  {
469
514
  name: "resource.set",
470
- description: "Merges fields into a plain-object resource (e.g. render/AmbientLight).",
515
+ description: "Merges fields into a plain-object resource (e.g. render/AmbientLight), marking it changed and waking an on-demand app.",
471
516
  params: s("ResourceSetParams", { name: t.string({ required: true }), value: t.json() }),
472
517
  handler: ({ world }, p) => {
473
518
  const def = findResource(p.name);
519
+ if (def?.hostOnly) {
520
+ throw new ShardError(
521
+ "core/owner-not-authorable",
522
+ `Resource "${p.name}" is written by the host only`,
523
+ {
524
+ hint: "Host code sets it; the protocol can read it with resource.get."
525
+ }
526
+ );
527
+ }
474
528
  const current = def && world.tryResource(def);
475
529
  if (!def || !isPlainObject(current)) {
476
530
  throw new ShardError(
@@ -485,7 +539,7 @@ const METHODS = [
485
539
  throw new ShardError("schema/type-mismatch", '"value" must be an object', {
486
540
  path: "/value"
487
541
  });
488
- Object.assign(current, p.value);
542
+ world.patchResource(def, p.value);
489
543
  return toJson(current);
490
544
  }
491
545
  },
@@ -502,10 +556,11 @@ const METHODS = [
502
556
  name: "time.resume",
503
557
  description: "Resumes a paused game.",
504
558
  params: none,
505
- handler: ({ world }) => {
559
+ handler: ({ app, world }) => {
506
560
  const control = world.resource(AppControlResource);
507
561
  control.paused = false;
508
562
  control.pausedByError = false;
563
+ app.requestFrame();
509
564
  return { paused: false, frame: world.resource(Time).frame };
510
565
  }
511
566
  },
@@ -549,7 +604,7 @@ const METHODS = [
549
604
  description: "Limit overlays to entities with all these components."
550
605
  }),
551
606
  buffer: t.string({
552
- description: "Raw floats of a render buffer instead of the display image: 'hdr' (luminance in cd/m\xB2, before post-processing and tonemapping), 'post-hdr' (after the HDR effects, pre-exposed), 'depth', 'velocity' (screen motion in uv units, TAA and motion blur), 'ssao' (ambient occlusion), 'bloom' (the glow's first level), 'dof-half' (color and signed CoC in pixels), or another graph texture. Returned as base64 little-endian rgba32float."
607
+ description: "Raw floats of a render buffer instead of the display image: 'hdr' (luminance in cd/m\xB2, before post-processing and tonemapping), 'post-hdr' (after the HDR effects, pre-exposed; bloom's glow is added by the tonemap, so it's not in it), 'depth', 'velocity' (screen motion in uv units, TAA and motion blur), 'ssao' (ambient occlusion and view depth, at half resolution), 'bloom' (the glow's first level), 'dof-half' (color and signed CoC in pixels), or another graph texture. Returned as base64 little-endian rgba32float."
553
608
  })
554
609
  }),
555
610
  handler: async ({ app, world, options }, p) => {
@@ -732,6 +787,20 @@ const METHODS = [
732
787
  return { queued: true };
733
788
  }
734
789
  },
790
+ {
791
+ name: "input.simulate",
792
+ description: "Plays pointer gestures a step per frame, in CSS pixels, so controls and drags run headless (0060): [{ drag: { from: [x, y], to: [x, y], button, pointer, modifiers, frames, cancel } }, { pinch: { center, from, to, twist, pan, frames } }, { wheel: { at, dy, dx, modifiers } }, { tap: [x, y] }, { wait: frames }]. Needs gesturesPlugin. Returns how many frames they take.",
793
+ params: s("SimulateParams", {
794
+ gestures: t.json({ required: true, description: "The gestures, played one after another." })
795
+ }),
796
+ handler: ({ world }, p) => {
797
+ if (!Array.isArray(p.gestures))
798
+ throw new ShardError("protocol/invalid-params", "gestures must be a list", {
799
+ path: "/gestures"
800
+ });
801
+ return { frames: simulateGestures(world, p.gestures) };
802
+ }
803
+ },
735
804
  {
736
805
  name: "input.record",
737
806
  description: "Starts recording input, or stops and returns the recording.",
@@ -974,6 +1043,87 @@ const METHODS = [
974
1043
  };
975
1044
  }
976
1045
  },
1046
+ {
1047
+ name: "perf.describe",
1048
+ description: 'Profiler aggregates (0074) over the last 120 runs of each span (last, avg, p95, max ms): the frame, each schedule and its command application, systems and other main-thread spans, GPU passes and the GPU frame ("unavailable" without timestamp queries), worker jobs, async spans; memory (GPU ledger by category, JS heap, ECS tables); and the clock (resolution, isolated, timings under four steps are "coarse").',
1049
+ params: s("PerfDescribeParams", {
1050
+ spans: t.list(t.string, {
1051
+ description: 'Only spans these keys cover: a key covers a span it equals or prefixes up to a "/" ("render" covers "render/opaque").'
1052
+ }),
1053
+ top: t.u32({ default: 30, min: 1, max: 1e3, description: "Most spans listed per track." })
1054
+ }),
1055
+ handler: ({ world }, p) => {
1056
+ const spans = p.spans;
1057
+ return toJson(
1058
+ describePerf(world, {
1059
+ spans: spans && spans.length > 0 ? spans : void 0,
1060
+ top: p.top
1061
+ })
1062
+ );
1063
+ }
1064
+ },
1065
+ {
1066
+ name: "perf.capture",
1067
+ description: 'Captures a span timeline (0074) over the next `frames` frames, or with `until.frameMs` a flight recorder that keeps `before` frames and stops `after` frames past the first frame over it. Returns a summary: frame CPU/GPU/interval p50/p95/max, the top spans, and the 5 worst frames, each with the spans most above their median ("over"): the lead for a hitch. The Chrome trace (Perfetto, chrome://tracing) goes to .shard/captures/ (tracePath) where the host can write, else inline (trace).',
1068
+ params: s("PerfCaptureParams", {
1069
+ frames: t.u32({ min: 0, max: 1e5, description: "Frames to capture (default 300)." }),
1070
+ until: t.struct({
1071
+ frameMs: t.f32({
1072
+ min: 0,
1073
+ description: "Flight recorder: stop after the first frame whose CPU time is over this."
1074
+ })
1075
+ }),
1076
+ before: t.u32({
1077
+ default: 120,
1078
+ max: 1e5,
1079
+ description: "Flight recorder: frames kept before."
1080
+ }),
1081
+ after: t.u32({ default: 30, max: 1e5, description: "Flight recorder: frames after." }),
1082
+ timeout: t.f32({
1083
+ default: 60,
1084
+ min: 0,
1085
+ description: "Flight recorder: seconds to wait for a slow frame."
1086
+ }),
1087
+ sample: t.bool({
1088
+ description: "Also sample JavaScript (V8's profiler in Node, JS Self-Profiling in browsers): the hottest functions join the summary."
1089
+ }),
1090
+ devtools: t.bool({
1091
+ description: "Also show spans in Chrome's Performance panel (performance.measure)."
1092
+ }),
1093
+ write: t.bool({ default: true, description: "Write the trace file when the host can." })
1094
+ }),
1095
+ handler: async ({ app, world, options }, p) => {
1096
+ const frameMs = p.until?.frameMs ?? 0;
1097
+ const manual = (options.frames ?? "manual") === "manual";
1098
+ const gpu = world.tryResource(Gpu) !== void 0;
1099
+ return toJson(
1100
+ await capturePerf(world, {
1101
+ frames: p.frames || 300,
1102
+ until: frameMs > 0 ? { frameMs } : void 0,
1103
+ before: p.before,
1104
+ after: p.after,
1105
+ timeout: p.timeout,
1106
+ sample: p.sample,
1107
+ devtools: p.devtools,
1108
+ write: p.write,
1109
+ // Headless: run the frames here, letting GPU readbacks land between them.
1110
+ step: manual ? async () => {
1111
+ app.update(1 / app.fixedHz);
1112
+ if (gpu) await new Promise((resolve) => setTimeout(resolve, 0));
1113
+ } : void 0
1114
+ })
1115
+ );
1116
+ }
1117
+ },
1118
+ {
1119
+ name: "perf.reset",
1120
+ description: "Clears the profiler's aggregates: perf.describe starts over.",
1121
+ params: none,
1122
+ handler: ({ world }) => {
1123
+ world.resource(ProfilerResource).reset();
1124
+ return null;
1125
+ }
1126
+ },
977
1127
  {
978
1128
  name: "log.tail",
979
1129
  description: "Recent log entries (errors from systems, the GPU, and shaders land here).",
@@ -988,7 +1138,9 @@ const METHODS = [
988
1138
  description: "Recent errors with code, path, and hint.",
989
1139
  params: s("ErrorsParams", { count: t.u32({ default: 20, min: 1 }) }),
990
1140
  handler: ({ world }, p) => world.resource(LogResource).errors(p.count)
991
- }
1141
+ },
1142
+ ...ABLATE_METHODS,
1143
+ ...BUDGET_METHODS
992
1144
  ];
993
1145
  function createProtocolServer(app, options = {}) {
994
1146
  const ctx = { app, world: app.world, options };
@@ -1021,10 +1173,12 @@ function createProtocolServer(app, options = {}) {
1021
1173
  }
1022
1174
  };
1023
1175
  byName.set(subscribe.name, subscribe);
1024
- const lookup = (name) => byName.get(name) ?? app.methods.find((m) => m.name === name);
1025
- return {
1176
+ const allow = options.allow ? new Set(options.allow) : void 0;
1177
+ const lookup = (name) => allow && !allow.has(name) ? void 0 : byName.get(name) ?? app.methods.find((m) => m.name === name);
1178
+ const allowed = (m) => !allow || allow.has(m.name);
1179
+ const server = {
1026
1180
  get methods() {
1027
- return [...byName.values(), ...app.methods.filter((m) => !byName.has(m.name))];
1181
+ return [...byName.values(), ...app.methods.filter((m) => !byName.has(m.name))].filter(allowed);
1028
1182
  },
1029
1183
  async handle(request) {
1030
1184
  const id = request.id ?? null;
@@ -1033,13 +1187,21 @@ function createProtocolServer(app, options = {}) {
1033
1187
  if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
1034
1188
  return reply({ error: { code: ERROR.invalid, message: "Invalid JSON-RPC request" } });
1035
1189
  }
1190
+ if (allow && !allow.has(request.method)) {
1191
+ const err = new ShardError(
1192
+ "protocol/method-not-allowed",
1193
+ `"${request.method}" isn't allowed on this server`,
1194
+ { hint: `This host allows: ${[...allow].join(", ")}.` }
1195
+ );
1196
+ return reply({ error: { code: ERROR.shard, message: err.message, data: err.toJSON() } });
1197
+ }
1036
1198
  const method = lookup(request.method);
1037
1199
  if (!method) {
1038
1200
  return reply({
1039
1201
  error: {
1040
1202
  code: ERROR.notFound,
1041
1203
  message: `Unknown method "${request.method}"`,
1042
- data: { methods: [...byName.keys(), ...app.methods.map((m) => m.name)] }
1204
+ data: { methods: server.methods.map((m) => m.name) }
1043
1205
  }
1044
1206
  });
1045
1207
  }
@@ -1079,6 +1241,7 @@ function createProtocolServer(app, options = {}) {
1079
1241
  listeners.clear();
1080
1242
  }
1081
1243
  };
1244
+ return server;
1082
1245
  }
1083
1246
  export {
1084
1247
  METHODS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-protocol",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -16,16 +16,16 @@
16
16
  "README.md"
17
17
  ],
18
18
  "dependencies": {
19
- "@aethervtt/shard-assets": "0.0.1",
20
- "@aethervtt/shard-core": "0.0.1",
21
- "@aethervtt/shard-input": "0.0.1",
22
- "@aethervtt/shard-platform": "0.0.1",
23
- "@aethervtt/shard-render": "0.0.1",
24
- "@aethervtt/shard-runtime": "0.0.1",
25
- "@aethervtt/shard-scene": "0.0.1",
26
- "@aethervtt/shard-text": "0.0.1",
27
- "@aethervtt/shard-texture": "0.0.1",
28
- "@aethervtt/shard-transform": "0.0.1"
19
+ "@aethervtt/shard-assets": "0.2.0",
20
+ "@aethervtt/shard-core": "0.2.0",
21
+ "@aethervtt/shard-input": "0.2.0",
22
+ "@aethervtt/shard-platform": "0.2.0",
23
+ "@aethervtt/shard-render": "0.2.0",
24
+ "@aethervtt/shard-runtime": "0.2.0",
25
+ "@aethervtt/shard-scene": "0.2.0",
26
+ "@aethervtt/shard-text": "0.2.0",
27
+ "@aethervtt/shard-texture": "0.2.0",
28
+ "@aethervtt/shard-transform": "0.2.0"
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
@@ -37,6 +37,6 @@
37
37
  "provenance": true
38
38
  },
39
39
  "shard": {
40
- "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
40
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
41
41
  }
42
42
  }