@jgengine/core 0.19.0 → 0.22.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/CHANGELOG.md +364 -0
- package/dist/ai/decisionGraph.d.ts +2 -0
- package/dist/ai/decisionGraph.js +6 -0
- package/dist/ai/flock.d.ts +1 -1
- package/dist/ai/flock.js +7 -4
- package/dist/anim/animGraph.d.ts +33 -2
- package/dist/anim/animGraph.js +200 -12
- package/dist/anim/locomotionGraph.d.ts +11 -3
- package/dist/anim/locomotionGraph.js +25 -15
- package/dist/audio/audioFalloff.d.ts +4 -0
- package/dist/audio/voiceAllocator.d.ts +46 -0
- package/dist/audio/voiceAllocator.js +76 -0
- package/dist/combat/abilityKit.d.ts +19 -0
- package/dist/combat/abilityKit.js +40 -4
- package/dist/combat/death.d.ts +4 -3
- package/dist/combat/death.js +3 -3
- package/dist/combat/magazine.d.ts +9 -0
- package/dist/combat/magazine.js +24 -2
- package/dist/combat/projectiles.d.ts +94 -2
- package/dist/combat/projectiles.js +282 -61
- package/dist/combat/regenShield.d.ts +14 -0
- package/dist/combat/regenShield.js +23 -7
- package/dist/crafting/recipeCatalog.d.ts +16 -0
- package/dist/crafting/recipeCatalog.js +55 -0
- package/dist/economy/auctionBook.d.ts +1 -1
- package/dist/economy/auctionBook.js +11 -1
- package/dist/economy/listingBook.d.ts +1 -1
- package/dist/economy/listingBook.js +11 -1
- package/dist/economy/resourceLedger.js +3 -3
- package/dist/economy/shopStock.js +7 -6
- package/dist/economy/techTree.js +7 -6
- package/dist/economy/unlockPoints.js +3 -6
- package/dist/editor/commandApply.js +89 -16
- package/dist/editor/commands.d.ts +38 -1
- package/dist/editor/commands.js +320 -3
- package/dist/editor/creatorStorage.d.ts +59 -0
- package/dist/editor/creatorStorage.js +164 -0
- package/dist/editor/document.d.ts +4 -0
- package/dist/editor/document.js +116 -17
- package/dist/editor/index.d.ts +3 -0
- package/dist/editor/index.js +1 -0
- package/dist/editor/liveSync.d.ts +4 -1
- package/dist/editor/liveSync.js +26 -10
- package/dist/editor/materialAuthoring.d.ts +14 -0
- package/dist/editor/materialAuthoring.js +38 -0
- package/dist/editor/modelNodeVisibility.d.ts +11 -0
- package/dist/editor/modelNodeVisibility.js +29 -0
- package/dist/editor/movementCatalog.d.ts +90 -0
- package/dist/editor/movementCatalog.js +144 -0
- package/dist/editor/simulation.d.ts +60 -0
- package/dist/editor/simulation.js +202 -0
- package/dist/editor/staticPrefab.d.ts +17 -0
- package/dist/editor/staticPrefab.js +51 -0
- package/dist/editor/terrainMaterials.d.ts +10 -0
- package/dist/editor/terrainMaterials.js +13 -0
- package/dist/editor/types.d.ts +11 -0
- package/dist/game/cameraConfig.d.ts +3 -1
- package/dist/game/clipRoles.d.ts +2 -1
- package/dist/game/clipRoles.js +7 -3
- package/dist/game/contentValidation.d.ts +69 -0
- package/dist/game/contentValidation.js +118 -0
- package/dist/game/controlGate.d.ts +8 -0
- package/dist/game/controlGate.js +43 -6
- package/dist/game/defineGame.d.ts +32 -3
- package/dist/game/dialogueGraph.d.ts +20 -0
- package/dist/game/dialogueGraph.js +107 -0
- package/dist/game/events.d.ts +5 -0
- package/dist/game/keyValueStore.d.ts +17 -4
- package/dist/game/keyValueStore.js +35 -23
- package/dist/game/lootModifiers.d.ts +17 -0
- package/dist/game/lootModifiers.js +26 -0
- package/dist/game/narrativeAuthoring.d.ts +67 -0
- package/dist/game/narrativeAuthoring.js +122 -0
- package/dist/game/playableGame.d.ts +52 -20
- package/dist/game/quest.d.ts +15 -0
- package/dist/game/quest.js +209 -63
- package/dist/game/questCatalog.d.ts +50 -0
- package/dist/game/questCatalog.js +205 -0
- package/dist/game/saveStore.d.ts +6 -5
- package/dist/game/saveStore.js +57 -57
- package/dist/game/systemRuntime.d.ts +2 -2
- package/dist/game/systemRuntime.js +84 -33
- package/dist/gameplay.d.ts +3 -0
- package/dist/gameplay.js +3 -0
- package/dist/interaction/promptRegistry.d.ts +5 -4
- package/dist/interaction/promptRegistry.js +8 -2
- package/dist/inventory/inventoryModel.js +9 -6
- package/dist/inventory/shapedGrid.d.ts +48 -9
- package/dist/inventory/shapedGrid.js +69 -10
- package/dist/item/durability.js +4 -0
- package/dist/item/itemInstanceRegistry.d.ts +32 -1
- package/dist/item/itemInstanceRegistry.js +111 -7
- package/dist/item/itemgen.d.ts +1 -1
- package/dist/item/itemgen.js +1 -1
- package/dist/item/modularItem.js +7 -1
- package/dist/material/appearanceSignals.d.ts +53 -0
- package/dist/material/appearanceSignals.js +47 -0
- package/dist/material/hairCards.d.ts +47 -0
- package/dist/material/hairCards.js +136 -0
- package/dist/material/materialAsset.d.ts +162 -0
- package/dist/material/materialAsset.js +382 -0
- package/dist/meta/changelog.d.ts +1 -1
- package/dist/meta/changelog.js +348 -1
- package/dist/movement/characterController.d.ts +5 -2
- package/dist/movement/characterController.js +50 -23
- package/dist/movement/freeFlight.d.ts +2 -2
- package/dist/movement/freeFlight.js +3 -3
- package/dist/movement/movementModel.d.ts +24 -1
- package/dist/movement/movementModel.js +74 -7
- package/dist/movement/playerMovement.d.ts +28 -1
- package/dist/movement/playerMovement.js +425 -102
- package/dist/movement/poseState.d.ts +2 -0
- package/dist/movement/poseState.js +29 -1
- package/dist/movement/solidObstacles.d.ts +3 -1
- package/dist/movement/solidObstacles.js +8 -5
- package/dist/movement/terrainGrade.d.ts +13 -0
- package/dist/movement/terrainGrade.js +33 -0
- package/dist/movement/voxelController.d.ts +2 -2
- package/dist/movement/voxelController.js +3 -3
- package/dist/nav/navGrid.d.ts +19 -1
- package/dist/nav/navGrid.js +26 -10
- package/dist/nav/solidRoute.d.ts +50 -0
- package/dist/nav/solidRoute.js +144 -0
- package/dist/physics/ballisticSweep.d.ts +10 -3
- package/dist/physics/ballisticSweep.js +53 -29
- package/dist/physics/forceVolume.d.ts +41 -0
- package/dist/physics/forceVolume.js +88 -0
- package/dist/physics/kinematicVehicle.d.ts +8 -3
- package/dist/physics/kinematicVehicle.js +2 -2
- package/dist/physics/physicsWorldBackend.js +19 -3
- package/dist/physics/spatialGrid.d.ts +1 -1
- package/dist/physics/spatialGrid.js +8 -9
- package/dist/physics/vehicleDynamics.d.ts +3 -2
- package/dist/physics/vehicleDynamics.js +2 -2
- package/dist/physics/vehicleObstacles.d.ts +3 -2
- package/dist/physics/vehicleObstacles.js +29 -5
- package/dist/runtime/cameraDirector.d.ts +39 -2
- package/dist/runtime/cameraDirector.js +162 -0
- package/dist/runtime/commandDispatch.d.ts +14 -0
- package/dist/runtime/commandDispatch.js +19 -0
- package/dist/runtime/context/combat.d.ts +11 -4
- package/dist/runtime/context/combat.js +178 -11
- package/dist/runtime/context/deathLoot.d.ts +9 -8
- package/dist/runtime/context/deathLoot.js +16 -16
- package/dist/runtime/context/scene.d.ts +2 -0
- package/dist/runtime/context/scene.js +13 -21
- package/dist/runtime/descriptors/baseline.js +34 -3
- package/dist/runtime/descriptors/features.d.ts +2 -1
- package/dist/runtime/descriptors/features.js +18 -0
- package/dist/runtime/gameContext.js +79 -13
- package/dist/runtime/gameContextTypes.d.ts +14 -0
- package/dist/runtime/headlessRunner.d.ts +3 -1
- package/dist/runtime/headlessRunner.js +11 -11
- package/dist/runtime/hostedGameRunner.d.ts +24 -1
- package/dist/runtime/hostedGameRunner.js +156 -10
- package/dist/runtime/hostedWorldSession.d.ts +17 -5
- package/dist/runtime/hostedWorldSession.js +154 -16
- package/dist/runtime/hostedWorldStore.d.ts +2 -2
- package/dist/runtime/inputRecorder.d.ts +1 -1
- package/dist/runtime/inputRecorder.js +21 -1
- package/dist/runtime/inputSnapshot.d.ts +16 -4
- package/dist/runtime/inputSnapshot.js +31 -3
- package/dist/runtime/runtimeSave.d.ts +10 -6
- package/dist/runtime/runtimeSave.js +11 -5
- package/dist/runtime/simContext.d.ts +8 -3
- package/dist/runtime/simContext.js +7 -3
- package/dist/runtime/simSnapshot.js +2 -0
- package/dist/runtime/worldReplication.d.ts +2 -0
- package/dist/runtime/worldReplication.js +1 -1
- package/dist/scene/assetCatalog.d.ts +2 -0
- package/dist/scene/behaviorRuntime.d.ts +26 -5
- package/dist/scene/behaviorRuntime.js +263 -16
- package/dist/scene/behaviors.d.ts +30 -1
- package/dist/scene/behaviors.js +29 -0
- package/dist/scene/entityStore.d.ts +14 -3
- package/dist/scene/entityStore.js +45 -9
- package/dist/scene/mount.d.ts +9 -0
- package/dist/scene/mount.js +35 -0
- package/dist/scene/possession.d.ts +2 -0
- package/dist/scene/possession.js +21 -1
- package/dist/scene/sceneRaycast.d.ts +7 -0
- package/dist/scene/sceneRaycast.js +55 -0
- package/dist/scene/spatial.d.ts +20 -1
- package/dist/scene/spatial.js +94 -2
- package/dist/scene/vehicleSeat.d.ts +36 -2
- package/dist/scene/vehicleSeat.js +89 -2
- package/dist/stats/accumulatorMeter.d.ts +10 -0
- package/dist/stats/accumulatorMeter.js +6 -0
- package/dist/stats/eventMeter.d.ts +9 -2
- package/dist/stats/eventMeter.js +5 -2
- package/dist/store/defineStore.d.ts +3 -1
- package/dist/store/defineStore.js +3 -1
- package/dist/store/observableKeyedStore.d.ts +1 -1
- package/dist/store/observableKeyedStore.js +8 -6
- package/dist/time/simClock.d.ts +6 -2
- package/dist/time/simClock.js +5 -0
- package/dist/vfx/particleDirector.d.ts +58 -6
- package/dist/vfx/particleDirector.js +113 -22
- package/dist/vfx/particles.d.ts +68 -5
- package/dist/vfx/particles.js +284 -32
- package/dist/visibility/assetStreaming.d.ts +21 -3
- package/dist/visibility/assetStreaming.js +108 -24
- package/dist/visibility/settings.d.ts +8 -2
- package/dist/visibility/settings.js +3 -0
- package/dist/visibility/visibilitySystem.js +1 -0
- package/dist/work/expedition.d.ts +110 -0
- package/dist/work/expedition.js +195 -0
- package/dist/work/staffedStation.d.ts +29 -0
- package/dist/work/staffedStation.js +27 -0
- package/dist/world/authoredAnimation.d.ts +38 -0
- package/dist/world/authoredAnimation.js +99 -0
- package/dist/world/authoredObjects.d.ts +24 -4
- package/dist/world/authoredObjects.js +43 -16
- package/dist/world/authoredSimulation.d.ts +74 -0
- package/dist/world/authoredSimulation.js +302 -0
- package/dist/world/authoredWeather.d.ts +120 -0
- package/dist/world/authoredWeather.js +314 -0
- package/dist/world/buildingKit.d.ts +8 -3
- package/dist/world/buildingKit.js +21 -8
- package/dist/world/environmentForces.d.ts +21 -0
- package/dist/world/environmentForces.js +42 -0
- package/dist/world/features.d.ts +3 -3
- package/dist/world/footprintGrid.d.ts +55 -0
- package/dist/world/footprintGrid.js +201 -0
- package/dist/world/grassKind.d.ts +10 -0
- package/dist/world/grassKind.js +22 -0
- package/dist/world/pathTerrain.d.ts +1 -1
- package/dist/world/terraform.d.ts +2 -2
- package/dist/world/terraform.js +46 -18
- package/dist/world/weather.d.ts +17 -0
- package/dist/world/weather.js +116 -20
- package/dist/world/worldSolids.d.ts +11 -0
- package/dist/world/worldSolids.js +112 -1
- package/dist/world.d.ts +5 -5
- package/dist/world.js +4 -4
- package/package.json +1 -1
- package/skills/ce-handoff/SKILL.md +1 -1
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +23 -2
- package/skills/jgengine/api.md +28 -17
- package/skills/jgengine/recipes/minimal-game.md +7 -2
- package/skills/jgengine-combat/SKILL.md +8 -2
- package/skills/jgengine-combat/api.md +22 -10
- package/skills/jgengine-combat/recipes/portable-runtime-state.md +60 -6
- package/skills/jgengine-combat/recipes/portable-weapon-plumbing.md +37 -3
- package/skills/jgengine-combat/reference.md +24 -2
- package/skills/jgengine-gameplay/SKILL.md +63 -0
- package/skills/jgengine-gameplay/api.md +131 -37
- package/skills/jgengine-gameplay/capabilities.md +68 -0
- package/skills/jgengine-gameplay/recipes/device-save-status.md +46 -0
- package/skills/jgengine-gameplay/recipes/expeditions.md +108 -0
- package/skills/jgengine-gameplay/recipes/narrative-authoring.md +76 -0
- package/skills/jgengine-gameplay/recipes/paged-item-storage.md +101 -0
- package/skills/jgengine-gameplay/recipes/quest-authoring.md +63 -0
- package/skills/jgengine-gameplay/recipes/shaped-inventory.md +50 -0
- package/skills/jgengine-gameplay/reference-content-validation.md +74 -0
- package/skills/jgengine-gameplay/reference-systems.md +119 -0
- package/skills/jgengine-world/SKILL.md +12 -3
- package/skills/jgengine-world/api.md +201 -58
- package/skills/jgengine-world/capabilities.md +116 -3
- package/skills/jgengine-world/recipes/character-animation.md +31 -8
- package/skills/jgengine-world/recipes/controls.md +4 -0
- package/skills/jgengine-world/recipes/npc-senses.md +12 -0
- package/skills/jgengine-world/recipes/vehicle-feel.md +3 -1
- package/skills/jgengine-world/reference.md +167 -5
- package/skills/jgengine-world/visibility.md +28 -1
package/dist/editor/commands.js
CHANGED
|
@@ -1,6 +1,248 @@
|
|
|
1
|
+
import { parseStaticPrefabBake } from "./staticPrefab.js";
|
|
1
2
|
import { applyDeltaToSnapshot, applySurfaceDeltaToSnapshot, applyWeightDeltaToSnapshot, revertDeltaFromSnapshot, revertSurfaceDeltaFromSnapshot, revertWeightDeltaFromSnapshot, } from "../world/terraform.js";
|
|
2
|
-
import { cloneEditorDocument, findEditorMarker, findEditorVolume, } from "./document.js";
|
|
3
|
+
import { cloneEditorDocument, decodeEditorDocument, findEditorMarker, findEditorVolume, isEditorObjectLocked, wouldCreateCycle, } from "./document.js";
|
|
3
4
|
import { applyMutating, isStructural } from "./commandApply.js";
|
|
5
|
+
function transactionCommandError(document, command) {
|
|
6
|
+
if (command.type === "undo" || command.type === "redo")
|
|
7
|
+
return "history commands cannot run inside a transaction";
|
|
8
|
+
const placeables = [...document.markers, ...document.volumes, ...document.paths, ...document.annotations];
|
|
9
|
+
const requireObjects = (ids, allowLocked = false) => {
|
|
10
|
+
if (!Array.isArray(ids) || ids.length === 0)
|
|
11
|
+
return "expected at least one object id";
|
|
12
|
+
for (const id of ids) {
|
|
13
|
+
if (typeof id !== "string" || !placeables.some((item) => item.id === id))
|
|
14
|
+
return `object not found: ${String(id)}`;
|
|
15
|
+
if (!allowLocked && isEditorObjectLocked(document, id))
|
|
16
|
+
return `object is locked: ${id}`;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
};
|
|
20
|
+
switch (command.type) {
|
|
21
|
+
case "setMarker":
|
|
22
|
+
case "setVolume":
|
|
23
|
+
case "setPath":
|
|
24
|
+
case "setNote": {
|
|
25
|
+
const list = command.type === "setMarker" ? document.markers : command.type === "setVolume" ? document.volumes : command.type === "setPath" ? document.paths : document.annotations;
|
|
26
|
+
if (!list.some((item) => item.id === command.id))
|
|
27
|
+
return `${command.type} target not found: ${command.id}`;
|
|
28
|
+
return requireObjects([command.id]);
|
|
29
|
+
}
|
|
30
|
+
case "setTransform":
|
|
31
|
+
return [...document.markers, ...document.volumes, ...document.annotations].some((item) => item.id === command.id) ? requireObjects([command.id]) : `transform target not found: ${command.id}`;
|
|
32
|
+
case "remove": return requireObjects([command.id]);
|
|
33
|
+
case "translate":
|
|
34
|
+
case "removeMany":
|
|
35
|
+
case "batchSetProperties":
|
|
36
|
+
case "assignMaterial":
|
|
37
|
+
return requireObjects(command.ids);
|
|
38
|
+
case "assignMaterialAsset":
|
|
39
|
+
case "clearMaterialAssets":
|
|
40
|
+
for (const id of command.ids)
|
|
41
|
+
if (!document.markers.some((marker) => marker.id === id))
|
|
42
|
+
return `material target is not a marker: ${id}`;
|
|
43
|
+
return requireObjects(command.ids);
|
|
44
|
+
case "duplicate": return requireObjects(command.ids, true);
|
|
45
|
+
case "select": return command.ids.length === 0 ? null : requireObjects(command.ids, true);
|
|
46
|
+
case "setObjectFlags": return requireObjects(command.ids, true);
|
|
47
|
+
case "setParent": {
|
|
48
|
+
const error = requireObjects(command.ids);
|
|
49
|
+
if (error !== null)
|
|
50
|
+
return error;
|
|
51
|
+
if (command.parentId !== null && !placeables.some((item) => item.id === command.parentId))
|
|
52
|
+
return `parent not found: ${command.parentId}`;
|
|
53
|
+
for (const id of command.ids) {
|
|
54
|
+
if (wouldCreateCycle(document, id, command.parentId))
|
|
55
|
+
return `parent cycle: ${id}`;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
case "addMarker":
|
|
60
|
+
case "addVolume":
|
|
61
|
+
case "addPath":
|
|
62
|
+
case "addNote": {
|
|
63
|
+
const item = command.type === "addMarker" ? command.marker : command.type === "addVolume" ? command.volume : command.type === "addPath" ? command.path : command.note;
|
|
64
|
+
if (typeof item.id !== "string" || item.id.trim().length === 0)
|
|
65
|
+
return "expected a nonempty object id";
|
|
66
|
+
const own = command.type === "addMarker" ? document.markers : command.type === "addVolume" ? document.volumes : command.type === "addPath" ? document.paths : document.annotations;
|
|
67
|
+
if (placeables.some((entry) => entry.id === item.id) && !own.some((entry) => entry.id === item.id))
|
|
68
|
+
return `object id collision: ${item.id}`;
|
|
69
|
+
return own.some((entry) => entry.id === item.id) ? requireObjects([item.id]) : null;
|
|
70
|
+
}
|
|
71
|
+
case "createPrefab": return requireObjects(command.ids, true);
|
|
72
|
+
case "setPrefabStaticBake":
|
|
73
|
+
if (command.bake !== null)
|
|
74
|
+
parseStaticPrefabBake(command.bake);
|
|
75
|
+
return document.prefabs.some((item) => item.id === command.prefabId) ? null : `prefab not found: ${command.prefabId}`;
|
|
76
|
+
case "insertPrefab":
|
|
77
|
+
case "deletePrefab":
|
|
78
|
+
return document.prefabs.some((item) => item.id === command.prefabId) ? null : `prefab not found: ${command.prefabId}`;
|
|
79
|
+
case "detachPrefabInstance":
|
|
80
|
+
return placeables.some((item) => item.meta?.prefabInstanceId === command.instanceId) ? null : `prefab instance not found: ${command.instanceId}`;
|
|
81
|
+
case "createCollection":
|
|
82
|
+
return command.memberIds === undefined || command.memberIds.length === 0 ? null : requireObjects(command.memberIds, true);
|
|
83
|
+
case "renameCollection":
|
|
84
|
+
case "deleteCollection":
|
|
85
|
+
case "setCollectionFlags":
|
|
86
|
+
case "selectCollection":
|
|
87
|
+
return document.collections.some((item) => item.id === command.id) ? null : `collection not found: ${command.id}`;
|
|
88
|
+
case "setCollectionMembers":
|
|
89
|
+
case "addToCollection":
|
|
90
|
+
case "removeFromCollection": {
|
|
91
|
+
if (!document.collections.some((item) => item.id === command.id))
|
|
92
|
+
return `collection not found: ${command.id}`;
|
|
93
|
+
const ids = command.type === "setCollectionMembers" ? command.memberIds : command.ids;
|
|
94
|
+
return ids.length === 0 ? null : requireObjects(ids, true);
|
|
95
|
+
}
|
|
96
|
+
case "setCatalogEntry":
|
|
97
|
+
case "removeCatalogEntry": {
|
|
98
|
+
const catalog = document.catalogs.find((item) => item.id === command.catalogId);
|
|
99
|
+
return catalog?.entries.some((item) => item.id === command.entryId) ? null : `catalog entry not found: ${command.catalogId}/${command.entryId}`;
|
|
100
|
+
}
|
|
101
|
+
case "addCatalogEntry":
|
|
102
|
+
return document.catalogs.find((item) => item.id === command.catalogId)?.entries.some((item) => item.id === command.entry.id) ? `catalog entry id collision: ${command.entry.id}` : null;
|
|
103
|
+
case "removeCatalog":
|
|
104
|
+
case "setCatalogSchema":
|
|
105
|
+
return document.catalogs.some((item) => item.id === command.id) ? null : `catalog not found: ${command.id}`;
|
|
106
|
+
case "removeGridLayer":
|
|
107
|
+
case "setGridLayer":
|
|
108
|
+
case "paintGridCells":
|
|
109
|
+
case "fillGridRect":
|
|
110
|
+
case "floodFillGrid":
|
|
111
|
+
case "resizeGridLayer":
|
|
112
|
+
return document.grids?.some((item) => item.id === command.id) ? null : `grid not found: ${command.id}`;
|
|
113
|
+
case "convertScatterToObjects":
|
|
114
|
+
return document.paths.some((item) => item.id === command.pathId) ? requireObjects([command.pathId]) : `path not found: ${command.pathId}`;
|
|
115
|
+
case "sculptTerrain":
|
|
116
|
+
case "paintTerrain":
|
|
117
|
+
case "blendTerrain":
|
|
118
|
+
case "setTerrainLayers":
|
|
119
|
+
case "clearTerrain":
|
|
120
|
+
return document.terrain === undefined ? "terrain not found" : null;
|
|
121
|
+
default: return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function assertTransactionValue(value, path = "$", ancestors = new Set()) {
|
|
125
|
+
if (value === undefined || value === null || typeof value === "string" || typeof value === "boolean")
|
|
126
|
+
return;
|
|
127
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
128
|
+
return;
|
|
129
|
+
if (typeof value !== "object")
|
|
130
|
+
throw new Error(`${path}: expected serializable finite values`);
|
|
131
|
+
if (ancestors.has(value))
|
|
132
|
+
throw new Error(`${path}: cyclic value`);
|
|
133
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)
|
|
134
|
+
throw new Error(`${path}: expected a plain object`);
|
|
135
|
+
ancestors.add(value);
|
|
136
|
+
for (const [key, child] of Object.entries(value))
|
|
137
|
+
assertTransactionValue(child, `${path}.${key}`, ancestors);
|
|
138
|
+
ancestors.delete(value);
|
|
139
|
+
}
|
|
140
|
+
function assertObjectFields(value, nullableParent = false) {
|
|
141
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
142
|
+
throw new Error("expected an object payload");
|
|
143
|
+
const fields = value;
|
|
144
|
+
for (const key of ["id", "kind", "color", "label", "catalogId", "parentId", "text", "name", "materialId"]) {
|
|
145
|
+
if (key === "parentId" && nullableParent && fields[key] === null)
|
|
146
|
+
continue;
|
|
147
|
+
if (fields[key] !== undefined && typeof fields[key] !== "string")
|
|
148
|
+
throw new Error(`${key}: expected a string`);
|
|
149
|
+
}
|
|
150
|
+
for (const key of ["rotationY", "radius", "height", "width", "cellSize", "cols", "rows"]) {
|
|
151
|
+
if (fields[key] !== undefined && typeof fields[key] !== "number")
|
|
152
|
+
throw new Error(`${key}: expected a number`);
|
|
153
|
+
}
|
|
154
|
+
for (const key of ["locked", "hidden", "visible"]) {
|
|
155
|
+
if (fields[key] !== undefined && typeof fields[key] !== "boolean")
|
|
156
|
+
throw new Error(`${key}: expected a boolean`);
|
|
157
|
+
}
|
|
158
|
+
for (const key of ["position", "center", "halfExtents", "offset", "at"]) {
|
|
159
|
+
const vector = fields[key];
|
|
160
|
+
if (vector !== undefined && (vector === null || typeof vector !== "object" || !["x", "y", "z"].every((axis) => typeof vector[axis] === "number")))
|
|
161
|
+
throw new Error(`${key}: expected {x,y,z} numbers`);
|
|
162
|
+
}
|
|
163
|
+
for (const key of ["ids", "memberIds"]) {
|
|
164
|
+
if (fields[key] !== undefined && (!Array.isArray(fields[key]) || fields[key].some((id) => typeof id !== "string")))
|
|
165
|
+
throw new Error(`${key}: expected string ids`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const transactionFields = {
|
|
169
|
+
select: "ids", clearSelection: "", setTransform: "id ?position ?rotationY", translate: "ids delta", setParent: "ids parentId",
|
|
170
|
+
addMarker: "marker", addVolume: "volume", addPath: "path", addNote: "note",
|
|
171
|
+
setMarker: "id patch", setVolume: "id patch", setPath: "id patch", setNote: "id patch",
|
|
172
|
+
setCatalogEntry: "catalogId entryId patch", addCatalogEntry: "catalogId entry", removeCatalogEntry: "catalogId entryId",
|
|
173
|
+
addCatalog: "id ?label ?schema", removeCatalog: "id", setCatalogSchema: "id schema ?label",
|
|
174
|
+
remove: "id", removeMany: "ids", duplicate: "ids ?offset", addFragment: "fragment ?offset",
|
|
175
|
+
importDocument: "document", importJson: "json", replaceDocument: "document", setTerrain: "terrain",
|
|
176
|
+
sculptTerrain: "delta", paintTerrain: "delta", blendTerrain: "delta", setTerrainLayers: "layers", clearTerrain: "",
|
|
177
|
+
setMinimapBake: "minimap", setBake: "bake", setEnvironment: "?environment", setSimulation: "?simulation", convertScatterToObjects: "pathId markers",
|
|
178
|
+
setPrefabStaticBake: "prefabId bake", createPrefab: "id name ids", insertPrefab: "prefabId at ?instanceId", detachPrefabInstance: "instanceId", deletePrefab: "prefabId",
|
|
179
|
+
createCollection: "id name ?memberIds", renameCollection: "id name", deleteCollection: "id",
|
|
180
|
+
setCollectionMembers: "id memberIds", addToCollection: "id ids", removeFromCollection: "id ids", setCollectionFlags: "id patch",
|
|
181
|
+
setObjectFlags: "ids patch", selectCollection: "id", batchSetProperties: "ids patch", assignMaterial: "ids materialId",
|
|
182
|
+
upsertMaterialAsset: "asset", removeMaterialAsset: "id", assignMaterialAsset: "ids materialId selector", clearMaterialAssets: "ids",
|
|
183
|
+
addGridLayer: "layer", removeGridLayer: "id", setGridLayer: "id patch", paintGridCells: "id cells",
|
|
184
|
+
fillGridRect: "id col0 row0 col1 row1 value", floodFillGrid: "id col row value", resizeGridLayer: "id cols rows",
|
|
185
|
+
setUiPanel: "id patch", removeUiPanel: "id", setUi: "?ui", undo: "", redo: "",
|
|
186
|
+
};
|
|
187
|
+
const placeableFields = {
|
|
188
|
+
marker: "id kind position rotationY color label catalogId parentId locked hidden meta",
|
|
189
|
+
volume: "id kind shape center radius height halfExtents color label parentId locked hidden meta",
|
|
190
|
+
path: "id kind points width color label parentId locked hidden meta",
|
|
191
|
+
note: "id text position color parentId locked hidden meta",
|
|
192
|
+
};
|
|
193
|
+
function assertPlacementFields(command, key, payload) {
|
|
194
|
+
const kind = command.type === "addMarker" || command.type === "setMarker" ? "marker"
|
|
195
|
+
: command.type === "addVolume" || command.type === "setVolume" ? "volume"
|
|
196
|
+
: command.type === "addPath" || command.type === "setPath" ? "path"
|
|
197
|
+
: command.type === "addNote" || command.type === "setNote" ? "note" : null;
|
|
198
|
+
if (kind === null || (key !== kind && key !== "patch"))
|
|
199
|
+
return;
|
|
200
|
+
const allowed = new Set(placeableFields[kind].split(" "));
|
|
201
|
+
if (key === "patch")
|
|
202
|
+
allowed.delete("id");
|
|
203
|
+
for (const field of Object.keys(payload)) {
|
|
204
|
+
if (!allowed.has(field))
|
|
205
|
+
throw new Error(`unknown ${key} field: ${field}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function assertCommandFields(command) {
|
|
209
|
+
const spec = transactionFields[command.type];
|
|
210
|
+
if (spec === undefined)
|
|
211
|
+
throw new Error(`unknown command: ${String(command.type)}`);
|
|
212
|
+
const fields = spec.split(" ").filter(Boolean);
|
|
213
|
+
const allowed = new Set(["type", ...fields.map((field) => field.replace(/^\?/, ""))]);
|
|
214
|
+
const record = command;
|
|
215
|
+
for (const key of Object.keys(record)) {
|
|
216
|
+
if (!allowed.has(key))
|
|
217
|
+
throw new Error(`unknown command field: ${key}`);
|
|
218
|
+
}
|
|
219
|
+
for (const field of fields) {
|
|
220
|
+
if (!field.startsWith("?") && record[field] === undefined)
|
|
221
|
+
throw new Error(`missing command field: ${field}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function sameTransactionValue(left, right) {
|
|
225
|
+
if (left === right)
|
|
226
|
+
return true;
|
|
227
|
+
if (left === null || right === null || typeof left !== "object" || typeof right !== "object")
|
|
228
|
+
return false;
|
|
229
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
230
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => sameTransactionValue(value, right[index]));
|
|
231
|
+
}
|
|
232
|
+
const a = left;
|
|
233
|
+
const b = right;
|
|
234
|
+
const keys = Object.keys(a).filter((key) => a[key] !== undefined);
|
|
235
|
+
return keys.length === Object.keys(b).filter((key) => b[key] !== undefined).length && keys.every((key) => sameTransactionValue(a[key], b[key]));
|
|
236
|
+
}
|
|
237
|
+
function isUnchangedPlacement(document, command) {
|
|
238
|
+
switch (command.type) {
|
|
239
|
+
case "addMarker": return sameTransactionValue(document.markers.find((item) => item.id === command.marker.id), command.marker);
|
|
240
|
+
case "addVolume": return sameTransactionValue(document.volumes.find((item) => item.id === command.volume.id), command.volume);
|
|
241
|
+
case "addPath": return sameTransactionValue(document.paths.find((item) => item.id === command.path.id), command.path);
|
|
242
|
+
case "addNote": return sameTransactionValue(document.annotations.find((item) => item.id === command.note.id), command.note);
|
|
243
|
+
default: return false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
4
246
|
function snapshotState(state) {
|
|
5
247
|
return {
|
|
6
248
|
document: cloneEditorDocument(state.document),
|
|
@@ -23,9 +265,11 @@ const terrainStrokeKind = {
|
|
|
23
265
|
blendTerrain: "blend",
|
|
24
266
|
};
|
|
25
267
|
/** Creates an editor session with undo/redo history seeded from an initial document.
|
|
268
|
+
* Environment commands validate and copy their candidate bag before publishing state or history.
|
|
26
269
|
* @internal
|
|
27
270
|
*/
|
|
28
|
-
export function createEditorSession(initial, historyLimit = 100) {
|
|
271
|
+
export function createEditorSession(initial, historyLimit = 100, validateDocument) {
|
|
272
|
+
validateDocument?.(initial);
|
|
29
273
|
let state = {
|
|
30
274
|
document: cloneEditorDocument(initial),
|
|
31
275
|
selection: [],
|
|
@@ -46,12 +290,14 @@ export function createEditorSession(initial, historyLimit = 100) {
|
|
|
46
290
|
return { document: { ...state.document, terrain: next }, selection };
|
|
47
291
|
};
|
|
48
292
|
const pushTerrainStroke = (entry) => {
|
|
293
|
+
const next = applyStroke(entry, "apply", entry.selection);
|
|
294
|
+
validateDocument?.(next.document);
|
|
49
295
|
past.push(entry);
|
|
50
296
|
if (past.length > historyLimit)
|
|
51
297
|
past.shift();
|
|
52
298
|
future.length = 0;
|
|
53
299
|
lastCoalesce = null;
|
|
54
|
-
state =
|
|
300
|
+
state = next;
|
|
55
301
|
};
|
|
56
302
|
return {
|
|
57
303
|
getState: () => state,
|
|
@@ -63,8 +309,74 @@ export function createEditorSession(initial, historyLimit = 100) {
|
|
|
63
309
|
},
|
|
64
310
|
canUndo: () => past.length > 0,
|
|
65
311
|
canRedo: () => future.length > 0,
|
|
312
|
+
transaction(commands) {
|
|
313
|
+
if (!Array.isArray(commands) || commands.length === 0)
|
|
314
|
+
return { ok: false, error: "commands transaction is empty" };
|
|
315
|
+
let staged = snapshotState(state);
|
|
316
|
+
for (let commandIndex = 0; commandIndex < commands.length; commandIndex += 1) {
|
|
317
|
+
try {
|
|
318
|
+
const input = commands[commandIndex];
|
|
319
|
+
assertTransactionValue(input);
|
|
320
|
+
assertObjectFields(input, input.type === "setParent");
|
|
321
|
+
assertCommandFields(input);
|
|
322
|
+
if (input.type === "translate")
|
|
323
|
+
assertObjectFields({ position: input.delta });
|
|
324
|
+
const command = JSON.parse(JSON.stringify(input));
|
|
325
|
+
for (const key of ["marker", "volume", "path", "note", "patch", "entry", "layer"]) {
|
|
326
|
+
const payload = command[key];
|
|
327
|
+
if (payload !== undefined) {
|
|
328
|
+
assertObjectFields(payload);
|
|
329
|
+
assertPlacementFields(command, key, payload);
|
|
330
|
+
if (key === "patch" && ["setMarker", "setVolume", "setPath", "setNote", "setGridLayer"].includes(command.type) && "id" in payload)
|
|
331
|
+
throw new Error("patch cannot change a stable id");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const error = transactionCommandError(staged.document, command);
|
|
335
|
+
if (error !== null)
|
|
336
|
+
throw new Error(error);
|
|
337
|
+
if (isUnchangedPlacement(staged.document, command))
|
|
338
|
+
continue;
|
|
339
|
+
const next = applyMutating(staged, command);
|
|
340
|
+
if (next === null)
|
|
341
|
+
throw new Error(`${command.type} rejected`);
|
|
342
|
+
const decoded = decodeEditorDocument(next.document);
|
|
343
|
+
if (!decoded.ok)
|
|
344
|
+
throw new Error(decoded.errors.map((diagnostic) => `${diagnostic.path} ${diagnostic.message}`).join("; "));
|
|
345
|
+
validateDocument?.(next.document);
|
|
346
|
+
for (const item of [...next.document.markers, ...next.document.volumes, ...next.document.paths, ...next.document.annotations]) {
|
|
347
|
+
if (item.parentId !== undefined && wouldCreateCycle(next.document, item.id, item.parentId))
|
|
348
|
+
throw new Error(`parent cycle: ${item.id}`);
|
|
349
|
+
}
|
|
350
|
+
staged = next;
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
354
|
+
return { ok: false, commandIndex, error: `commands[${commandIndex}]: ${message}` };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const documentChanged = !sameTransactionValue(staged.document, state.document);
|
|
358
|
+
const selectionChanged = !sameTransactionValue(staged.selection, state.selection);
|
|
359
|
+
if (!documentChanged && !selectionChanged)
|
|
360
|
+
return { ok: true, state, changed: false };
|
|
361
|
+
if (documentChanged) {
|
|
362
|
+
past.push({ kind: "snapshot", state: snapshotState(state) });
|
|
363
|
+
if (past.length > historyLimit)
|
|
364
|
+
past.shift();
|
|
365
|
+
future.length = 0;
|
|
366
|
+
lastCoalesce = null;
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
staged.document = state.document;
|
|
370
|
+
}
|
|
371
|
+
state = staged;
|
|
372
|
+
emit();
|
|
373
|
+
return { ok: true, state, changed: true };
|
|
374
|
+
},
|
|
66
375
|
dispatch(command, options) {
|
|
67
376
|
if (command.type === "undo") {
|
|
377
|
+
const candidate = past[past.length - 1];
|
|
378
|
+
if (candidate !== undefined)
|
|
379
|
+
validateDocument?.(candidate.kind === "snapshot" ? candidate.state.document : applyStroke(candidate, "revert", candidate.selection).document);
|
|
68
380
|
const entry = past.pop();
|
|
69
381
|
if (entry === undefined)
|
|
70
382
|
return state;
|
|
@@ -81,6 +393,9 @@ export function createEditorSession(initial, historyLimit = 100) {
|
|
|
81
393
|
return state;
|
|
82
394
|
}
|
|
83
395
|
if (command.type === "redo") {
|
|
396
|
+
const candidate = future[future.length - 1];
|
|
397
|
+
if (candidate !== undefined)
|
|
398
|
+
validateDocument?.(candidate.kind === "snapshot" ? candidate.state.document : applyStroke(candidate, "apply", candidate.selection).document);
|
|
84
399
|
const entry = future.pop();
|
|
85
400
|
if (entry === undefined)
|
|
86
401
|
return state;
|
|
@@ -110,6 +425,8 @@ export function createEditorSession(initial, historyLimit = 100) {
|
|
|
110
425
|
const next = applyMutating(state, command);
|
|
111
426
|
if (next === null)
|
|
112
427
|
return state;
|
|
428
|
+
if (next.document !== state.document)
|
|
429
|
+
validateDocument?.(next.document);
|
|
113
430
|
if (isStructural(command)) {
|
|
114
431
|
const coalesce = options?.coalesce;
|
|
115
432
|
const merge = coalesce !== undefined && coalesce === lastCoalesce && past.length > 0;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { KeyValueStorage } from "../game/keyValueStore.js";
|
|
2
|
+
import type { EditorDocument } from "./types.js";
|
|
3
|
+
/** Bounds and catalog permissions for player-authored documents. */
|
|
4
|
+
export interface CreatorPolicy {
|
|
5
|
+
maxDocuments: number;
|
|
6
|
+
maxBytes: number;
|
|
7
|
+
maxObjects: number;
|
|
8
|
+
maxPathPoints: number;
|
|
9
|
+
maxGridCells: number;
|
|
10
|
+
maxTerrainVertices: number;
|
|
11
|
+
allowedKinds: readonly string[];
|
|
12
|
+
allowedAssets: readonly string[];
|
|
13
|
+
allowedCatalogIds?: readonly string[];
|
|
14
|
+
/** Total standing/weather pools plus capacity for 32 concurrent collision child bursts. */
|
|
15
|
+
maxSimulationParticles?: number;
|
|
16
|
+
maxFireCells?: number;
|
|
17
|
+
maxPopulation?: number;
|
|
18
|
+
maxForceFields?: number;
|
|
19
|
+
/** Additional game-specific constraints; throw to reject the candidate atomically. */
|
|
20
|
+
validate?: (document: EditorDocument) => void;
|
|
21
|
+
}
|
|
22
|
+
/** A named scene save with a storage-format version and optimistic revision. */
|
|
23
|
+
export interface CreatorDocument {
|
|
24
|
+
version: 1;
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
revision: number;
|
|
28
|
+
document: EditorDocument;
|
|
29
|
+
}
|
|
30
|
+
/** Injected durable storage. Save rejects stale revisions; null creates a new id. */
|
|
31
|
+
export interface CreatorDocumentStorage {
|
|
32
|
+
list(): Promise<readonly Omit<CreatorDocument, "document">[]>;
|
|
33
|
+
load(id: string): Promise<CreatorDocument | null>;
|
|
34
|
+
save(value: CreatorDocument, expectedRevision: number | null): Promise<CreatorDocument>;
|
|
35
|
+
}
|
|
36
|
+
/** Creator configuration shared by a host and its lazily loaded editor. */
|
|
37
|
+
export interface CreatorConfig<TPlayable> {
|
|
38
|
+
storage: CreatorDocumentStorage;
|
|
39
|
+
policy: CreatorPolicy;
|
|
40
|
+
initialDocument: () => EditorDocument;
|
|
41
|
+
createPlayable: (document: EditorDocument) => TPlayable;
|
|
42
|
+
createId?: () => string;
|
|
43
|
+
}
|
|
44
|
+
/** Validates schema, finite work budgets, and approved catalog references before cloning. */
|
|
45
|
+
export declare function validateCreatorDocument(value: unknown, policy: CreatorPolicy): EditorDocument;
|
|
46
|
+
/** Imports scene JSON using the same validation as commands and durable saves. */
|
|
47
|
+
export declare function importCreatorDocument(json: string, policy: CreatorPolicy): EditorDocument;
|
|
48
|
+
/** Exports validated, schema-versioned scene JSON.
|
|
49
|
+
* @capability creator-export export validated player scenes with approved catalogs and finite budgets
|
|
50
|
+
*/
|
|
51
|
+
export declare function exportCreatorDocument(document: EditorDocument, policy: CreatorPolicy): string;
|
|
52
|
+
/** Durable browser-compatible adapter over injected key/value storage; one atomic catalog write.
|
|
53
|
+
* @capability creator-storage save and reopen named versioned player creations through injected durable storage
|
|
54
|
+
*/
|
|
55
|
+
export declare function createCreatorDocumentStorage(config: {
|
|
56
|
+
storage: KeyValueStorage;
|
|
57
|
+
key: string;
|
|
58
|
+
policy: CreatorPolicy;
|
|
59
|
+
}): CreatorDocumentStorage;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { cloneEditorDocument, decodeEditorDocument, editorDocumentSize } from "./document.js";
|
|
2
|
+
import { isScatterPath } from "../world/scatterRegion.js";
|
|
3
|
+
function byteSize(value) {
|
|
4
|
+
return new TextEncoder().encode(value).length;
|
|
5
|
+
}
|
|
6
|
+
/** Validates schema, finite work budgets, and approved catalog references before cloning. */
|
|
7
|
+
export function validateCreatorDocument(value, policy) {
|
|
8
|
+
if (value === null || typeof value !== "object" || value.version !== 1)
|
|
9
|
+
throw new Error("Unsupported creator scene version");
|
|
10
|
+
for (const key of ["maxDocuments", "maxBytes", "maxObjects", "maxPathPoints", "maxGridCells", "maxTerrainVertices"]) {
|
|
11
|
+
if (!Number.isSafeInteger(policy[key]) || policy[key] < 0)
|
|
12
|
+
throw new Error(`Invalid creator limit: ${key}`);
|
|
13
|
+
}
|
|
14
|
+
const json = JSON.stringify(value, null, 2);
|
|
15
|
+
if (json === undefined || byteSize(json) > policy.maxBytes)
|
|
16
|
+
throw new Error("Scene exceeds byte budget");
|
|
17
|
+
const decoded = decodeEditorDocument(value);
|
|
18
|
+
if (!decoded.ok)
|
|
19
|
+
throw new Error(decoded.errors.map((error) => `${error.path} ${error.message}`).join("; "));
|
|
20
|
+
const document = decoded.document;
|
|
21
|
+
if (editorDocumentSize(document) > policy.maxObjects)
|
|
22
|
+
throw new Error("Scene exceeds object budget");
|
|
23
|
+
const kinds = new Set(policy.allowedKinds);
|
|
24
|
+
const assets = new Set(policy.allowedAssets);
|
|
25
|
+
const catalogs = new Set(policy.allowedCatalogIds ?? []);
|
|
26
|
+
const nodes = [...document.markers, ...document.volumes, ...document.paths];
|
|
27
|
+
for (const node of nodes)
|
|
28
|
+
if (!kinds.has(node.kind))
|
|
29
|
+
throw new Error(`Unavailable scene kind: ${node.kind}`);
|
|
30
|
+
if (document.paths.some(isScatterPath))
|
|
31
|
+
throw new Error("Creator documents require explicit placements instead of scatter paths");
|
|
32
|
+
if (document.paths.reduce((count, path) => count + path.points.length, 0) > policy.maxPathPoints)
|
|
33
|
+
throw new Error("Scene exceeds path point budget");
|
|
34
|
+
let gridCells = 0;
|
|
35
|
+
for (const grid of document.grids ?? []) {
|
|
36
|
+
if (!kinds.has(grid.kind))
|
|
37
|
+
throw new Error(`Unavailable scene kind: ${grid.kind}`);
|
|
38
|
+
gridCells += grid.cols * grid.rows;
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isSafeInteger(gridCells) || gridCells > policy.maxGridCells)
|
|
41
|
+
throw new Error("Scene exceeds grid budget");
|
|
42
|
+
if (document.terrain !== undefined && document.terrain.cols * document.terrain.rows > policy.maxTerrainVertices)
|
|
43
|
+
throw new Error("Scene exceeds terrain budget");
|
|
44
|
+
// Procedural directives and prefab duplication can expand a tiny save into unbounded runtime work.
|
|
45
|
+
if ((document.directives?.length ?? 0) > 0 || document.prefabs.length > 0)
|
|
46
|
+
throw new Error("Creator documents require explicit placements");
|
|
47
|
+
for (const key of ["maxSimulationParticles", "maxFireCells", "maxPopulation", "maxForceFields"]) {
|
|
48
|
+
if (policy[key] !== undefined && (!Number.isSafeInteger(policy[key]) || policy[key] < 0))
|
|
49
|
+
throw new Error(`Invalid creator limit: ${key}`);
|
|
50
|
+
}
|
|
51
|
+
const simulation = document.simulation;
|
|
52
|
+
if (simulation !== undefined) {
|
|
53
|
+
if ((simulation.forces?.length ?? 0) > (policy.maxForceFields ?? 0))
|
|
54
|
+
throw new Error("Scene exceeds force field budget");
|
|
55
|
+
// Default precipitation pools plus the supported 256-slot rain-impact maximum.
|
|
56
|
+
const weatherParticles = simulation.weather === undefined ? 0 : 5556;
|
|
57
|
+
let maxChildParticles = 0;
|
|
58
|
+
const emitterParticles = (simulation.emitters ?? []).reduce((total, emitter) => {
|
|
59
|
+
const child = emitter.options?.collisionEffect;
|
|
60
|
+
if (child !== undefined)
|
|
61
|
+
maxChildParticles = Math.max(maxChildParticles, Math.min(child.count, child.config.max ?? child.count));
|
|
62
|
+
return total + (emitter.config.max ?? 512);
|
|
63
|
+
}, 0);
|
|
64
|
+
// The renderer retains up to 32 concurrent child bursts, shared by all standing emitters.
|
|
65
|
+
if (weatherParticles + emitterParticles + 32 * maxChildParticles > (policy.maxSimulationParticles ?? 0))
|
|
66
|
+
throw new Error("Scene exceeds particle budget");
|
|
67
|
+
if ((simulation.fires ?? []).reduce((total, fire) => total + fire.config.cols * fire.config.rows, 0) > (policy.maxFireCells ?? 0))
|
|
68
|
+
throw new Error("Scene exceeds fire budget");
|
|
69
|
+
if ((simulation.habitats ?? []).reduce((total, habitat) => total + habitat.count, 0) > (policy.maxPopulation ?? 0))
|
|
70
|
+
throw new Error("Scene exceeds population budget");
|
|
71
|
+
}
|
|
72
|
+
const inspect = (item) => {
|
|
73
|
+
if (Array.isArray(item)) {
|
|
74
|
+
for (const child of item)
|
|
75
|
+
inspect(child);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (item === null || typeof item !== "object")
|
|
79
|
+
return;
|
|
80
|
+
for (const [key, child] of Object.entries(item)) {
|
|
81
|
+
if (["url", "modelUrl", "textureUrl", "urls"].includes(key))
|
|
82
|
+
throw new Error("Creator documents cannot import external asset URLs");
|
|
83
|
+
if (["assetId", "modelId", "materialId"].includes(key) && (typeof child !== "string" || !assets.has(child)))
|
|
84
|
+
throw new Error(`Unavailable asset: ${String(child)}`);
|
|
85
|
+
if (key === "catalogId" && (typeof child !== "string" || !catalogs.has(child)))
|
|
86
|
+
throw new Error(`Unavailable catalog entry: ${String(child)}`);
|
|
87
|
+
inspect(child);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
inspect(document);
|
|
91
|
+
policy.validate?.(document);
|
|
92
|
+
return cloneEditorDocument(document);
|
|
93
|
+
}
|
|
94
|
+
/** Imports scene JSON using the same validation as commands and durable saves. */
|
|
95
|
+
export function importCreatorDocument(json, policy) {
|
|
96
|
+
if (byteSize(json) > policy.maxBytes)
|
|
97
|
+
throw new Error("Scene exceeds byte budget");
|
|
98
|
+
return validateCreatorDocument(JSON.parse(json), policy);
|
|
99
|
+
}
|
|
100
|
+
/** Exports validated, schema-versioned scene JSON.
|
|
101
|
+
* @capability creator-export export validated player scenes with approved catalogs and finite budgets
|
|
102
|
+
*/
|
|
103
|
+
export function exportCreatorDocument(document, policy) {
|
|
104
|
+
return JSON.stringify(validateCreatorDocument(document, policy), null, 2);
|
|
105
|
+
}
|
|
106
|
+
function identity(value) {
|
|
107
|
+
if (value.version !== 1)
|
|
108
|
+
throw new Error("Unsupported creator save version");
|
|
109
|
+
if (!/^[a-zA-Z0-9_-]{1,80}$/.test(value.id))
|
|
110
|
+
throw new Error("Invalid scene id");
|
|
111
|
+
if (typeof value.name !== "string" || value.name.trim().length === 0 || value.name.length > 80)
|
|
112
|
+
throw new Error("Scene name must contain 1–80 characters");
|
|
113
|
+
if (!Number.isSafeInteger(value.revision) || value.revision < 0)
|
|
114
|
+
throw new Error("Invalid scene revision");
|
|
115
|
+
}
|
|
116
|
+
/** Durable browser-compatible adapter over injected key/value storage; one atomic catalog write.
|
|
117
|
+
* @capability creator-storage save and reopen named versioned player creations through injected durable storage
|
|
118
|
+
*/
|
|
119
|
+
export function createCreatorDocumentStorage(config) {
|
|
120
|
+
const read = () => {
|
|
121
|
+
const json = config.storage.getItem(config.key);
|
|
122
|
+
if (json === null)
|
|
123
|
+
return [];
|
|
124
|
+
if (byteSize(json) > config.policy.maxDocuments * (config.policy.maxBytes + 1024) + 128)
|
|
125
|
+
throw new Error("Creator catalog exceeds byte budget");
|
|
126
|
+
const value = JSON.parse(json);
|
|
127
|
+
if (value === null || typeof value !== "object" || value.version !== 1)
|
|
128
|
+
throw new Error("Unsupported creator catalog version");
|
|
129
|
+
const documents = value.documents;
|
|
130
|
+
if (!Array.isArray(documents) || documents.length > config.policy.maxDocuments)
|
|
131
|
+
throw new Error("Creator catalog exceeds document budget");
|
|
132
|
+
const ids = new Set();
|
|
133
|
+
return documents.map((item) => {
|
|
134
|
+
identity(item);
|
|
135
|
+
if (ids.has(item.id))
|
|
136
|
+
throw new Error("Duplicate scene id");
|
|
137
|
+
ids.add(item.id);
|
|
138
|
+
return { ...item, document: validateCreatorDocument(item.document, config.policy) };
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
async list() { return read().map(({ document: _document, ...summary }) => summary); },
|
|
143
|
+
async load(id) { return read().find((item) => item.id === id) ?? null; },
|
|
144
|
+
async save(value, expectedRevision) {
|
|
145
|
+
identity(value);
|
|
146
|
+
const document = validateCreatorDocument(value.document, config.policy);
|
|
147
|
+
const documents = read();
|
|
148
|
+
const index = documents.findIndex((item) => item.id === value.id);
|
|
149
|
+
const previous = documents[index];
|
|
150
|
+
if ((previous?.revision ?? null) !== expectedRevision)
|
|
151
|
+
throw new Error("Scene changed in storage; reopen it before saving");
|
|
152
|
+
if (previous === undefined && documents.length >= config.policy.maxDocuments)
|
|
153
|
+
throw new Error("Creator catalog is full");
|
|
154
|
+
const next = { version: 1, id: value.id, name: value.name.trim(), revision: (previous?.revision ?? 0) + 1, document };
|
|
155
|
+
identity(next);
|
|
156
|
+
if (index < 0)
|
|
157
|
+
documents.push(next);
|
|
158
|
+
else
|
|
159
|
+
documents[index] = next;
|
|
160
|
+
config.storage.setItem(config.key, JSON.stringify({ version: 1, documents }));
|
|
161
|
+
return { ...next, document: cloneEditorDocument(document) };
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { type EditorSimulation } from "./simulation.js";
|
|
2
|
+
import { type MaterialAsset } from "../material/materialAsset.js";
|
|
1
3
|
import { type EditorGridLayer } from "./grid.js";
|
|
2
4
|
import type { EditorCatalogData, EditorBake, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
|
|
3
5
|
/** Spread-ready non-placeable fields preserved across document rebuilds. @internal */
|
|
4
6
|
export declare function editorDocumentExtras(doc: EditorDocument): {
|
|
7
|
+
materialAssets?: MaterialAsset[];
|
|
5
8
|
prefabs: EditorPrefab[];
|
|
6
9
|
collections: EditorCollection[];
|
|
7
10
|
catalogs: EditorCatalogData[];
|
|
@@ -12,6 +15,7 @@ export declare function editorDocumentExtras(doc: EditorDocument): {
|
|
|
12
15
|
minimap?: EditorMinimapBake;
|
|
13
16
|
bakes?: EditorBake[];
|
|
14
17
|
environment?: EditorEnvironment;
|
|
18
|
+
simulation?: EditorSimulation;
|
|
15
19
|
};
|
|
16
20
|
/** Builds a fresh, empty editor document to start authoring a scene from scratch.
|
|
17
21
|
* @internal
|