@aethervtt/shard-protocol 0.0.1 → 0.1.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/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
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  describeInput,
17
17
  injectInput,
18
+ simulateGestures,
18
19
  startRecording,
19
20
  startReplay,
20
21
  stopRecording
@@ -87,7 +88,7 @@ function resolveEntity(world, ref) {
87
88
  hint: 'Pass an entity id from world.query, or a scene path like "ship/camera".'
88
89
  });
89
90
  }
90
- function requireComponent(name) {
91
+ function knownComponent(name) {
91
92
  const def = findComponent(name);
92
93
  if (!def) {
93
94
  throw new ShardError("protocol/unknown-component", `Unknown component "${name}"`, {
@@ -96,6 +97,28 @@ function requireComponent(name) {
96
97
  }
97
98
  return def;
98
99
  }
100
+ function requireOwner(world, name) {
101
+ const owner = world.owners.find(name);
102
+ if (!owner) {
103
+ throw new ShardError("protocol/unknown-owner", `No live owner "${name}"`, {
104
+ hint: "owners.describe with no name lists every live owner."
105
+ });
106
+ }
107
+ return owner;
108
+ }
109
+ function requireComponent(name) {
110
+ const def = knownComponent(name);
111
+ if (def.hostOnly) {
112
+ throw new ShardError(
113
+ "core/owner-not-authorable",
114
+ `"${name}" can't be written through the protocol`,
115
+ {
116
+ hint: "Ownership comes from host code holding an Owner (world.owners)."
117
+ }
118
+ );
119
+ }
120
+ return def;
121
+ }
99
122
  function toJson(value) {
100
123
  const seen = /* @__PURE__ */ new WeakSet();
101
124
  return JSON.parse(
@@ -346,7 +369,7 @@ const METHODS = [
346
369
  params: s("SchemaGetParams", {
347
370
  name: t.string({ required: true, description: 'Component name, e.g. "render/Camera3d".' })
348
371
  }),
349
- handler: (_, p) => requireComponent(p.name).jsonSchema()
372
+ handler: (_, p) => knownComponent(p.name).jsonSchema()
350
373
  },
351
374
  {
352
375
  name: "world.stats",
@@ -439,6 +462,23 @@ const METHODS = [
439
462
  return entityJson(world, entity);
440
463
  }
441
464
  },
465
+ {
466
+ name: "owners.describe",
467
+ description: "Owners (0061). With a name: its usage (entities, triangles, textures, bytes), limits, child owners, asset leases and GPU objects. Without: every live owner.",
468
+ params: s("OwnersDescribeParams", { name: t.string() }),
469
+ handler: ({ world }, p) => p.name ? world.owners.describe(requireOwner(world, p.name)) : { owners: world.owners.list() }
470
+ },
471
+ {
472
+ name: "owners.release",
473
+ description: "Releases an owner and its child owners: despawns their entities, drops their asset leases (unloading assets nobody else leases, with their GPU objects).",
474
+ params: s("OwnersReleaseParams", { name: t.string({ required: true }) }),
475
+ handler: ({ world }, p) => {
476
+ const owner = requireOwner(world, p.name);
477
+ const entities = world.owners.describe(owner).usage.entities;
478
+ world.owners.release(owner);
479
+ return { released: owner.name, entities };
480
+ }
481
+ },
442
482
  {
443
483
  name: "entity.despawn",
444
484
  description: "Despawns an entity (and its children unless recursive is false).",
@@ -467,10 +507,19 @@ const METHODS = [
467
507
  },
468
508
  {
469
509
  name: "resource.set",
470
- description: "Merges fields into a plain-object resource (e.g. render/AmbientLight).",
510
+ description: "Merges fields into a plain-object resource (e.g. render/AmbientLight), marking it changed and waking an on-demand app.",
471
511
  params: s("ResourceSetParams", { name: t.string({ required: true }), value: t.json() }),
472
512
  handler: ({ world }, p) => {
473
513
  const def = findResource(p.name);
514
+ if (def?.hostOnly) {
515
+ throw new ShardError(
516
+ "core/owner-not-authorable",
517
+ `Resource "${p.name}" is written by the host only`,
518
+ {
519
+ hint: "Host code sets it; the protocol can read it with resource.get."
520
+ }
521
+ );
522
+ }
474
523
  const current = def && world.tryResource(def);
475
524
  if (!def || !isPlainObject(current)) {
476
525
  throw new ShardError(
@@ -485,7 +534,7 @@ const METHODS = [
485
534
  throw new ShardError("schema/type-mismatch", '"value" must be an object', {
486
535
  path: "/value"
487
536
  });
488
- Object.assign(current, p.value);
537
+ world.patchResource(def, p.value);
489
538
  return toJson(current);
490
539
  }
491
540
  },
@@ -502,10 +551,11 @@ const METHODS = [
502
551
  name: "time.resume",
503
552
  description: "Resumes a paused game.",
504
553
  params: none,
505
- handler: ({ world }) => {
554
+ handler: ({ app, world }) => {
506
555
  const control = world.resource(AppControlResource);
507
556
  control.paused = false;
508
557
  control.pausedByError = false;
558
+ app.requestFrame();
509
559
  return { paused: false, frame: world.resource(Time).frame };
510
560
  }
511
561
  },
@@ -732,6 +782,20 @@ const METHODS = [
732
782
  return { queued: true };
733
783
  }
734
784
  },
785
+ {
786
+ name: "input.simulate",
787
+ 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.",
788
+ params: s("SimulateParams", {
789
+ gestures: t.json({ required: true, description: "The gestures, played one after another." })
790
+ }),
791
+ handler: ({ world }, p) => {
792
+ if (!Array.isArray(p.gestures))
793
+ throw new ShardError("protocol/invalid-params", "gestures must be a list", {
794
+ path: "/gestures"
795
+ });
796
+ return { frames: simulateGestures(world, p.gestures) };
797
+ }
798
+ },
735
799
  {
736
800
  name: "input.record",
737
801
  description: "Starts recording input, or stops and returns the recording.",
@@ -1021,10 +1085,12 @@ function createProtocolServer(app, options = {}) {
1021
1085
  }
1022
1086
  };
1023
1087
  byName.set(subscribe.name, subscribe);
1024
- const lookup = (name) => byName.get(name) ?? app.methods.find((m) => m.name === name);
1025
- return {
1088
+ const allow = options.allow ? new Set(options.allow) : void 0;
1089
+ const lookup = (name) => allow && !allow.has(name) ? void 0 : byName.get(name) ?? app.methods.find((m) => m.name === name);
1090
+ const allowed = (m) => !allow || allow.has(m.name);
1091
+ const server = {
1026
1092
  get methods() {
1027
- return [...byName.values(), ...app.methods.filter((m) => !byName.has(m.name))];
1093
+ return [...byName.values(), ...app.methods.filter((m) => !byName.has(m.name))].filter(allowed);
1028
1094
  },
1029
1095
  async handle(request) {
1030
1096
  const id = request.id ?? null;
@@ -1033,13 +1099,21 @@ function createProtocolServer(app, options = {}) {
1033
1099
  if (request?.jsonrpc !== "2.0" || typeof request.method !== "string") {
1034
1100
  return reply({ error: { code: ERROR.invalid, message: "Invalid JSON-RPC request" } });
1035
1101
  }
1102
+ if (allow && !allow.has(request.method)) {
1103
+ const err = new ShardError(
1104
+ "protocol/method-not-allowed",
1105
+ `"${request.method}" isn't allowed on this server`,
1106
+ { hint: `This host allows: ${[...allow].join(", ")}.` }
1107
+ );
1108
+ return reply({ error: { code: ERROR.shard, message: err.message, data: err.toJSON() } });
1109
+ }
1036
1110
  const method = lookup(request.method);
1037
1111
  if (!method) {
1038
1112
  return reply({
1039
1113
  error: {
1040
1114
  code: ERROR.notFound,
1041
1115
  message: `Unknown method "${request.method}"`,
1042
- data: { methods: [...byName.keys(), ...app.methods.map((m) => m.name)] }
1116
+ data: { methods: server.methods.map((m) => m.name) }
1043
1117
  }
1044
1118
  });
1045
1119
  }
@@ -1079,6 +1153,7 @@ function createProtocolServer(app, options = {}) {
1079
1153
  listeners.clear();
1080
1154
  }
1081
1155
  };
1156
+ return server;
1082
1157
  }
1083
1158
  export {
1084
1159
  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.1.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.1.0",
20
+ "@aethervtt/shard-core": "0.1.0",
21
+ "@aethervtt/shard-input": "0.1.0",
22
+ "@aethervtt/shard-platform": "0.1.0",
23
+ "@aethervtt/shard-render": "0.1.0",
24
+ "@aethervtt/shard-runtime": "0.1.0",
25
+ "@aethervtt/shard-scene": "0.1.0",
26
+ "@aethervtt/shard-text": "0.1.0",
27
+ "@aethervtt/shard-texture": "0.1.0",
28
+ "@aethervtt/shard-transform": "0.1.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": "8e14c9896f82a3554b156f1ffdd68f2cc3e7d78a"
41
41
  }
42
42
  }