@aethervtt/shard-scene 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.
@@ -1,5 +1,5 @@
1
1
  import { AssetStore } from '@aethervtt/shard-assets';
2
- import { type Entity, type JsonValue } from '@aethervtt/shard-core';
2
+ import { type Entity, type JsonValue, type Owner } from '@aethervtt/shard-core';
3
3
  import type { PrefabFile, SceneEntity, SceneFile } from './format.js';
4
4
  /** Marks entities that came from a scene file. */
5
5
  export declare const SceneMember: import("@aethervtt/shard-core").ComponentDef<{
@@ -18,6 +18,8 @@ export interface LoadedScene {
18
18
  loadedResources: Map<string, JsonValue>;
19
19
  /** Guids of every asset the scene referenced, requested at load. */
20
20
  assets: Set<string>;
21
+ /** Who it was loaded for (0061); releasing the owner unloads it. */
22
+ owner?: Owner;
21
23
  }
22
24
  export declare const SceneIndex: import("@aethervtt/shard-core").ResourceDef<Map<string, LoadedScene>>;
23
25
  /** Scene files loaded as assets (a glTF's node tree), by guid. */
@@ -34,15 +34,22 @@ const SceneAssetType = defineAssetType("Scene", {
34
34
  return out;
35
35
  };
36
36
  return resolve(artifact.json);
37
- }
37
+ },
38
+ // A model that failed to load spawns a beveled box in its place (0061).
39
+ fallback: () => ({ version: 1, entities: [MISSING_ENTITY] })
38
40
  });
41
+ const MISSING_ENTITY = {
42
+ name: "missing",
43
+ components: { "render/Mesh3d": { mesh: { path: "procedural:missing-box" } } }
44
+ };
39
45
  const PrefabAssets = defineResource("scene/PrefabAssets", {
40
46
  description: "Prefab assets (resolved entity trees) by guid.",
41
47
  init: () => new AssetStore("Prefab")
42
48
  });
43
49
  const PrefabAssetType = defineAssetType("Prefab", {
44
50
  store: PrefabAssets,
45
- load: (artifact) => artifact.json
51
+ load: (artifact) => artifact.json,
52
+ fallback: () => ({ version: 1, root: MISSING_ENTITY })
46
53
  });
47
54
  const overridesField = () => t.json({
48
55
  default: {},
package/dist/instances.js CHANGED
@@ -31,6 +31,7 @@ import {
31
31
  componentDef,
32
32
  equal,
33
33
  expandAliases,
34
+ notAuthorable,
34
35
  rebase,
35
36
  resolveAssets,
36
37
  serializeComponents
@@ -271,6 +272,10 @@ function applyOverrides(flat, overrides, prefix, report, validate) {
271
272
  );
272
273
  continue;
273
274
  }
275
+ if (def.hostOnly) {
276
+ report(key, notAuthorable(name, where));
277
+ continue;
278
+ }
274
279
  if (!def.serializable || def === ChildOf) {
275
280
  report(
276
281
  key,
@@ -432,7 +437,7 @@ function stateOf(world) {
432
437
  let s = perWorld.get(world);
433
438
  if (!s) {
434
439
  const off = assetServer(world).onEvent((e) => {
435
- if (e.kind !== "loaded" && e.kind !== "modified") return;
440
+ if (e.kind !== "loaded" && e.kind !== "modified" && e.kind !== "failed") return;
436
441
  const type = assetServer(world).entry(e.guid)?.type;
437
442
  if (type === "Scene" || type === "Prefab") updateInstances(world);
438
443
  });
@@ -760,7 +765,7 @@ function updateInstances(world) {
760
765
  server.request(entry.guid);
761
766
  continue;
762
767
  }
763
- if (entry.state !== "loaded") continue;
768
+ if (entry.state !== "loaded" && !server.isFallback(entry.guid)) continue;
764
769
  const state = s.states.get(entity);
765
770
  const o = overridesOf(value.overrides);
766
771
  if (state && state.def === def && state.template.entry.guid === entry.guid && isCurrent(world, state.template) && state.template.flat.missing.size === 0 && state.overridesKey === JSON.stringify(o)) {
@@ -1,5 +1,6 @@
1
1
  import { ShardError } from "@aethervtt/shard-core";
2
2
  import {
3
+ bevelBox,
3
4
  box,
4
5
  capsule,
5
6
  cone,
@@ -20,6 +21,15 @@ const PROCEDURAL_MESHES = {
20
21
  torus: {
21
22
  params: ["radius", "tube", "radialSegments", "tubularSegments"],
22
23
  create: (p) => torus(p)
24
+ },
25
+ // The stand-in for a mesh or model that failed to load (0061).
26
+ "missing-box": {
27
+ params: [],
28
+ create: () => {
29
+ const mesh = bevelBox();
30
+ mesh.missing = true;
31
+ return mesh;
32
+ }
23
33
  }
24
34
  };
25
35
  function parseProcedural(source, params = {}, path) {
package/dist/scene.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type AssetRef, type ComponentDef, type Entity, type JsonValue, type ResolvedAsset, type SchemaContext, ShardError, type World } from '@aethervtt/shard-core';
1
+ import { type AssetRef, type ComponentDef, type Entity, type JsonValue, type Owner, type ResolvedAsset, type SchemaContext, ShardError, type World } from '@aethervtt/shard-core';
2
2
  import { type SceneAsset, type SceneEntity, type SceneFile } from './format.js';
3
3
  export interface LoadedSceneHandle {
4
4
  id: string;
@@ -59,9 +59,14 @@ export declare function validateTree(world: World, file: {
59
59
  }, flat: readonly FlatEntity[], errors: ShardError[], options: TreeOptions): void;
60
60
  /** A specific scene error beats the generic asset-not-found for the same field; drop duplicates. */
61
61
  export declare function dedupeErrors(errors: ShardError[]): ShardError[];
62
- /** Validates, then spawns the scene. Throws `scene/invalid` (with every error in `details`) if invalid. */
62
+ /**
63
+ * Validates, then spawns the scene. Throws `scene/invalid` (with every error in `details`) if invalid.
64
+ * With `owner` (0061), everything in it belongs to that owner, and a scene that would exceed the
65
+ * owner's entity limit throws `core/owner-quota` before anything spawns.
66
+ */
63
67
  export declare function loadScene(world: World, json: unknown, options?: {
64
68
  id?: string;
69
+ owner?: Owner;
65
70
  }): LoadedSceneHandle;
66
71
  /**
67
72
  * Resolves once every asset the scene referenced has loaded or failed (following their
@@ -91,3 +96,5 @@ export declare function saveScene(world: World, id: string): SceneFile;
91
96
  /** Scene JSON as written to disk: 2-space indent, trailing newline. */
92
97
  export declare function stringifyScene(file: SceneFile): string;
93
98
  export type { AssetRef };
99
+ /** A host-only component (ownership, 0061) named in authored data. */
100
+ export declare function notAuthorable(name: string, path: string): ShardError;
package/dist/scene.js CHANGED
@@ -352,6 +352,10 @@ function validateTree(world, file, flat, errors, options) {
352
352
  );
353
353
  continue;
354
354
  }
355
+ if (def.hostOnly) {
356
+ errors.push(notAuthorable(name, base));
357
+ continue;
358
+ }
355
359
  if (!def.serializable || def === ChildOf) {
356
360
  errors.push(
357
361
  new ShardError("scene/derived-component", `"${name}" can't be written in a scene`, {
@@ -517,6 +521,7 @@ function loadScene(world, json, options = {}) {
517
521
  const file = json;
518
522
  const flat = [];
519
523
  flatten(file.entities, "/entities", void 0, flat);
524
+ if (options.owner) world.owners.ensureCapacity(options.owner, "entities", flat.length);
520
525
  const entities = /* @__PURE__ */ new Map();
521
526
  for (const f of flat) entities.set(f.path, world.reserveEntity());
522
527
  const requested = /* @__PURE__ */ new Set();
@@ -556,6 +561,12 @@ function loadScene(world, json, options = {}) {
556
561
  else world.insertResource(def, structuredCloneJson(value));
557
562
  scene.loadedResources.set(name, toJson(world.resource(def)));
558
563
  }
564
+ if (options.owner) {
565
+ for (const f of flat)
566
+ if (f.parent === void 0) world.owners.adopt(options.owner, entities.get(f.path));
567
+ scene.owner = options.owner;
568
+ forgetOnRelease(world);
569
+ }
559
570
  index.set(id, scene);
560
571
  return { id, entities };
561
572
  }
@@ -708,6 +719,22 @@ function stringifyScene(file) {
708
719
  return `${JSON.stringify(file, null, 2)}
709
720
  `;
710
721
  }
722
+ const forgetting = /* @__PURE__ */ new WeakSet();
723
+ function forgetOnRelease(world) {
724
+ if (forgetting.has(world)) return;
725
+ forgetting.add(world);
726
+ world.owners.onRelease((owner) => {
727
+ const index = world.tryResource(SceneIndex);
728
+ if (!index) return;
729
+ for (const [id, scene] of index) if (scene.owner === owner) index.delete(id);
730
+ });
731
+ }
732
+ function notAuthorable(name, path) {
733
+ return new ShardError("core/owner-not-authorable", `"${name}" can't be written in scene files`, {
734
+ path,
735
+ hint: "Ownership is a grant from host code: the host loads the scene for an owner (world.owners)."
736
+ });
737
+ }
711
738
  export {
712
739
  ALIASES,
713
740
  addSceneReadyCheck,
@@ -719,6 +746,7 @@ export {
719
746
  findEntityByPath,
720
747
  flatten,
721
748
  loadScene,
749
+ notAuthorable,
722
750
  pathOfEntity,
723
751
  rebase,
724
752
  reloadScene,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aethervtt/shard-scene",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -16,12 +16,12 @@
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-mesh": "0.0.1",
22
- "@aethervtt/shard-render": "0.0.1",
23
- "@aethervtt/shard-runtime": "0.0.1",
24
- "@aethervtt/shard-transform": "0.0.1"
19
+ "@aethervtt/shard-assets": "0.2.0",
20
+ "@aethervtt/shard-core": "0.2.0",
21
+ "@aethervtt/shard-mesh": "0.2.0",
22
+ "@aethervtt/shard-render": "0.2.0",
23
+ "@aethervtt/shard-runtime": "0.2.0",
24
+ "@aethervtt/shard-transform": "0.2.0"
25
25
  },
26
26
  "repository": {
27
27
  "type": "git",
@@ -33,6 +33,6 @@
33
33
  "provenance": true
34
34
  },
35
35
  "shard": {
36
- "buildSha": "d6c5c6aef4675575c878b02cf2c8f738b4190229"
36
+ "buildSha": "32c22a3944ed23f5d1baa90bf7cc9fae724f5fd8"
37
37
  }
38
38
  }