@auraindustry/aurajs 0.0.6 → 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/README.md +103 -7
- package/benchmarks/perf-thresholds.json +54 -0
- package/package.json +4 -7
- package/src/asset-pack.mjs +5 -1
- package/src/authored-project.mjs +1449 -0
- package/src/authored-runtime.mjs +2016 -0
- package/src/authoring/avatar-animation-graph.mjs +648 -0
- package/src/bin-integrity.mjs +272 -0
- package/src/build-contract/assets.mjs +130 -0
- package/src/build-contract/capabilities.mjs +116 -0
- package/src/build-contract/constants.mjs +6 -0
- package/src/build-contract/helpers.mjs +44 -0
- package/src/build-contract/web-templates.mjs +5993 -0
- package/src/build-contract.mjs +27 -2910
- package/src/bundler.mjs +188 -55
- package/src/cli.mjs +4825 -1512
- package/src/commands/project-authoring.mjs +434 -0
- package/src/config.mjs +27 -0
- package/src/conformance/cases/app-and-ui-runtime-cases.mjs +3309 -0
- package/src/conformance/cases/core-runtime-cases.mjs +1431 -0
- package/src/conformance/cases/index.mjs +11 -0
- package/src/conformance/cases/scene3d-and-media-cases.mjs +2094 -0
- package/src/conformance/cases/systems-and-gameplay-cases.mjs +1776 -0
- package/src/conformance/shared.mjs +27 -0
- package/src/conformance-runner.mjs +25 -13
- package/src/conformance.mjs +619 -4020
- package/src/cutscene.mjs +362 -5
- package/src/dev-cli-action.mjs +249 -0
- package/src/dev-cli-inspect.mjs +92 -0
- package/src/dev-cli-state.mjs +80 -0
- package/src/external-asset-cache.mjs +587 -0
- package/src/external-asset-policy.mjs +217 -0
- package/src/external-package-surface.mjs +206 -0
- package/src/game-action-runtime.mjs +869 -0
- package/src/game-state-runtime.mjs +206 -6
- package/src/headless-action.mjs +186 -0
- package/src/headless-test/runtime-animation.mjs +1173 -0
- package/src/headless-test/runtime-coordinator.mjs +1514 -0
- package/src/headless-test/runtime-primitives.mjs +320 -0
- package/src/headless-test/runtime-world.mjs +2253 -0
- package/src/headless-test.mjs +392 -4298
- package/src/host-binary.mjs +342 -14
- package/src/icon-discovery.mjs +64 -0
- package/src/make-catalog.mjs +109 -0
- package/src/make.mjs +197 -0
- package/src/package-integrity.mjs +586 -0
- package/src/perf-benchmark.mjs +353 -0
- package/src/postinstall.mjs +5 -5
- package/src/prefabs/index.mjs +34 -0
- package/src/prefabs/scene-serialization.mjs +184 -0
- package/src/project-importer.mjs +620 -0
- package/src/project-registry.mjs +24 -0
- package/src/publish-command.mjs +195 -0
- package/src/publish-env-example.mjs +83 -0
- package/src/publish-validation.mjs +708 -0
- package/src/retro/assets/compile.mjs +232 -0
- package/src/retro/backend-gba/authoring.mjs +1029 -0
- package/src/retro/backend-gba/rom.mjs +363 -0
- package/src/retro/backend-gbc/rom.mjs +85 -0
- package/src/retro/build.mjs +278 -0
- package/src/retro/cli/commands.mjs +292 -0
- package/src/retro/cli/templates.mjs +84 -0
- package/src/retro/diagnostics/catalog.mjs +110 -0
- package/src/retro/diagnostics/emit.mjs +72 -0
- package/src/retro/emulator/case-overlay.mjs +64 -0
- package/src/retro/emulator/discovery.mjs +158 -0
- package/src/retro/emulator/macos-case-overlay.swift +220 -0
- package/src/retro/emulator/profiles.mjs +146 -0
- package/src/retro/emulator/runner.mjs +289 -0
- package/src/retro/frontend/load-project.mjs +98 -0
- package/src/retro/index.mjs +30 -0
- package/src/retro/ir/build-ir.mjs +108 -0
- package/src/retro/runtime-gba/contract.mjs +151 -0
- package/src/retro/runtime-gbc/contract.mjs +117 -0
- package/src/retro/shared/span.mjs +26 -0
- package/src/retro/shared/targets.mjs +64 -0
- package/src/retro/validator/check-project.mjs +114 -0
- package/src/runtime-hotspot-audit.mjs +707 -0
- package/src/scaffold/config.mjs +1000 -0
- package/src/scaffold/fs.mjs +56 -0
- package/src/scaffold/layout.mjs +318 -0
- package/src/scaffold/project-docs.mjs +438 -0
- package/src/scaffold.mjs +93 -596
- package/src/scene-composition/index.mjs +326 -0
- package/src/scene-composition/runtime.mjs +751 -0
- package/src/self-hosted-assets.mjs +604 -0
- package/src/session-client.mjs +750 -0
- package/src/session-native-launcher.mjs +74 -0
- package/src/session-protocol.mjs +75 -0
- package/src/session-runtime.mjs +321 -0
- package/src/session-server.mjs +360 -0
- package/src/shader-kits/index.mjs +773 -0
- package/src/starter-content-registry.mjs +292 -0
- package/src/state-artifacts.mjs +662 -24
- package/src/state-dev-reload.mjs +99 -2
- package/src/terminal-ui.mjs +245 -0
- package/src/web-conformance.mjs +219 -0
- package/templates/create/2d/config/gameplay/shooter.config.js +26 -0
- package/templates/create/2d/content/gameplay/waves.json +26 -0
- package/templates/create/2d/content/registries/.gitkeep +1 -0
- package/templates/create/2d/docs/design/.gitkeep +1 -0
- package/templates/create/2d/docs/design/loop.md +5 -0
- package/templates/create/2d/prefabs/enemies.prefab.js +90 -0
- package/templates/create/2d/prefabs/enemy-basic.prefab.js +18 -0
- package/templates/create/2d/prefabs/player.prefab.js +36 -0
- package/templates/create/2d/prefabs/projectiles.prefab.js +35 -0
- package/templates/create/2d/scenes/boot.scene.js +12 -0
- package/templates/create/2d/scenes/gameplay.scene.js +230 -0
- package/templates/create/2d/scenes/menu.scene.js +9 -0
- package/templates/create/2d/src/main.js +6 -185
- package/templates/create/2d/src/runtime/app.js +49 -0
- package/templates/create/2d/src/runtime/capabilities.js +35 -0
- package/templates/create/2d/ui/hud.screen.js +40 -0
- package/templates/create/2d/ui/pause.screen.js +149 -0
- package/templates/create/2d/ui/settings.screen.js +347 -0
- package/templates/create/2d/ui/title.screen.js +13 -0
- package/templates/create/2d-adventure/aura.config.json +28 -0
- package/templates/create/2d-adventure/config/gameplay/adventure.config.js +14 -0
- package/templates/create/2d-adventure/content/gameplay/world.js +46 -0
- package/templates/create/2d-adventure/content/registries/.gitkeep +1 -0
- package/templates/create/2d-adventure/docs/design/loop.md +5 -0
- package/templates/create/2d-adventure/prefabs/player.prefab.js +54 -0
- package/templates/create/2d-adventure/prefabs/relic.prefab.js +38 -0
- package/templates/create/2d-adventure/prefabs/world.prefab.js +125 -0
- package/templates/create/2d-adventure/scenes/gameplay.scene.js +256 -0
- package/templates/create/2d-adventure/src/runtime/capabilities.js +34 -0
- package/templates/create/2d-adventure/ui/hud.screen.js +60 -0
- package/templates/create/2d-survivor/config/gameplay/survivor.config.js +33 -0
- package/templates/create/2d-survivor/content/gameplay/spawn-zones.json +29 -0
- package/templates/create/2d-survivor/content/registries/.gitkeep +1 -0
- package/templates/create/2d-survivor/docs/design/.gitkeep +1 -0
- package/templates/create/2d-survivor/docs/design/loop.md +5 -0
- package/templates/create/2d-survivor/prefabs/enemies.prefab.js +178 -0
- package/templates/create/2d-survivor/prefabs/enemy-swarm.prefab.js +18 -0
- package/templates/create/2d-survivor/prefabs/player.prefab.js +42 -0
- package/templates/create/2d-survivor/prefabs/projectiles.prefab.js +56 -0
- package/templates/create/2d-survivor/scenes/boot.scene.js +12 -0
- package/templates/create/2d-survivor/scenes/gameplay.scene.js +314 -0
- package/templates/create/2d-survivor/scenes/menu.scene.js +9 -0
- package/templates/create/2d-survivor/src/main.js +5 -332
- package/templates/create/2d-survivor/src/runtime/app.js +49 -0
- package/templates/create/2d-survivor/src/runtime/capabilities.js +35 -0
- package/templates/create/2d-survivor/ui/hud.screen.js +45 -0
- package/templates/create/2d-survivor/ui/title.screen.js +13 -0
- package/templates/create/3d/assets/models/starter-avatar.gltf +184 -0
- package/templates/create/3d/config/gameplay/.gitkeep +1 -0
- package/templates/create/3d/content/gameplay/checkpoints.json +33 -0
- package/templates/create/3d/content/gameplay/course.js +40 -0
- package/templates/create/3d/content/registries/.gitkeep +1 -0
- package/templates/create/3d/docs/design/.gitkeep +1 -0
- package/templates/create/3d/docs/design/loop.md +5 -0
- package/templates/create/3d/prefabs/checkpoint.prefab.js +15 -0
- package/templates/create/3d/prefabs/player.prefab.js +204 -0
- package/templates/create/3d/prefabs/world.prefab.js +112 -0
- package/templates/create/3d/scenes/boot.scene.js +12 -0
- package/templates/create/3d/scenes/checkpoint.scene.js +9 -0
- package/templates/create/3d/scenes/gameplay.scene.js +292 -0
- package/templates/create/3d/src/main.js +6 -295
- package/templates/create/3d/src/runtime/app.js +49 -0
- package/templates/create/3d/src/runtime/capabilities.js +53 -0
- package/templates/create/3d/src/runtime/materials.js +34 -0
- package/templates/create/3d/src/runtime/state.js +39 -0
- package/templates/create/3d/ui/hud.screen.js +75 -0
- package/templates/create/3d/ui/pause.screen.js +166 -0
- package/templates/create/3d/ui/settings.screen.js +387 -0
- package/templates/create/3d-adventure/assets/models/starter-avatar.gltf +184 -0
- package/templates/create/3d-adventure/aura.config.json +28 -0
- package/templates/create/3d-adventure/config/gameplay/adventure.config.js +9 -0
- package/templates/create/3d-adventure/content/gameplay/course.js +62 -0
- package/templates/create/3d-adventure/content/registries/.gitkeep +1 -0
- package/templates/create/3d-adventure/docs/design/loop.md +5 -0
- package/templates/create/3d-adventure/prefabs/player.prefab.js +168 -0
- package/templates/create/3d-adventure/prefabs/relic.prefab.js +35 -0
- package/templates/create/3d-adventure/prefabs/world.prefab.js +119 -0
- package/templates/create/3d-adventure/scenes/gameplay.scene.js +358 -0
- package/templates/create/3d-adventure/src/runtime/capabilities.js +56 -0
- package/templates/create/3d-adventure/src/runtime/materials.js +39 -0
- package/templates/create/3d-adventure/src/runtime/state.js +31 -0
- package/templates/create/3d-adventure/ui/hud.screen.js +70 -0
- package/templates/create/3d-adventure/ui/pause.screen.js +437 -0
- package/templates/create/3d-collectathon/assets/models/starter-avatar.gltf +184 -0
- package/templates/create/3d-collectathon/config/gameplay/.gitkeep +1 -0
- package/templates/create/3d-collectathon/content/gameplay/collectibles.json +26 -0
- package/templates/create/3d-collectathon/content/gameplay/course.js +46 -0
- package/templates/create/3d-collectathon/content/registries/.gitkeep +1 -0
- package/templates/create/3d-collectathon/docs/design/.gitkeep +1 -0
- package/templates/create/3d-collectathon/docs/design/loop.md +5 -0
- package/templates/create/3d-collectathon/prefabs/collectible.prefab.js +15 -0
- package/templates/create/3d-collectathon/prefabs/player.prefab.js +207 -0
- package/templates/create/3d-collectathon/prefabs/world.prefab.js +112 -0
- package/templates/create/3d-collectathon/scenes/boot.scene.js +12 -0
- package/templates/create/3d-collectathon/scenes/checkpoint.scene.js +9 -0
- package/templates/create/3d-collectathon/scenes/gameplay.scene.js +200 -0
- package/templates/create/3d-collectathon/src/main.js +5 -355
- package/templates/create/3d-collectathon/src/runtime/app.js +49 -0
- package/templates/create/3d-collectathon/src/runtime/capabilities.js +53 -0
- package/templates/create/3d-collectathon/src/runtime/materials.js +34 -0
- package/templates/create/3d-collectathon/src/runtime/state.js +27 -0
- package/templates/create/3d-collectathon/ui/hud.screen.js +66 -0
- package/templates/create/3d-collectathon/ui/pause.screen.js +13 -0
- package/templates/create/blank/config/gameplay/.gitkeep +1 -0
- package/templates/create/blank/content/gameplay/.gitkeep +1 -0
- package/templates/create/blank/content/registries/.gitkeep +1 -0
- package/templates/create/blank/docs/design/.gitkeep +1 -0
- package/templates/create/blank/docs/design/loop.md +5 -0
- package/templates/create/blank/prefabs/.gitkeep +1 -0
- package/templates/create/blank/scenes/.gitkeep +1 -0
- package/templates/create/blank/src/runtime/.gitkeep +1 -0
- package/templates/create/blank/ui/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/assets/audio/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/assets/fonts/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/assets/sprites/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/assets/starter/README.md +11 -0
- package/templates/create/deckbuilder-2d/assets/ui/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/aura.config.json +28 -0
- package/templates/create/deckbuilder-2d/config/gameplay/deckbuilder.config.js +26 -0
- package/templates/create/deckbuilder-2d/content/cards/guard.card.js +19 -0
- package/templates/create/deckbuilder-2d/content/cards/spark.card.js +20 -0
- package/templates/create/deckbuilder-2d/content/cards/starter.deck.js +69 -0
- package/templates/create/deckbuilder-2d/content/cards/strike.card.js +19 -0
- package/templates/create/deckbuilder-2d/content/cards/survey.card.js +20 -0
- package/templates/create/deckbuilder-2d/content/encounters/training-battle.encounter.js +14 -0
- package/templates/create/deckbuilder-2d/content/encounters/training-battle.js +65 -0
- package/templates/create/deckbuilder-2d/content/enemies/training-automaton.enemy.js +48 -0
- package/templates/create/deckbuilder-2d/content/gameplay/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/content/registries/cards.registry.js +26 -0
- package/templates/create/deckbuilder-2d/content/registries/encounters.registry.js +20 -0
- package/templates/create/deckbuilder-2d/content/registries/enemies.registry.js +20 -0
- package/templates/create/deckbuilder-2d/content/registries/relics.registry.js +20 -0
- package/templates/create/deckbuilder-2d/content/relics/ember-charm.relic.js +18 -0
- package/templates/create/deckbuilder-2d/docs/design/loop.md +12 -0
- package/templates/create/deckbuilder-2d/prefabs/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/scenes/boot.scene.js +84 -0
- package/templates/create/deckbuilder-2d/scenes/gameplay.scene.js +641 -0
- package/templates/create/deckbuilder-2d/src/components/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/src/main.js +17 -0
- package/templates/create/deckbuilder-2d/src/runtime/capabilities.js +22 -0
- package/templates/create/deckbuilder-2d/src/shared/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/src/systems/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/tests/smoke/.gitkeep +1 -0
- package/templates/create/deckbuilder-2d/ui/hud.screen.js +80 -0
- package/templates/create/deckbuilder-2d/ui/pause.screen.js +146 -0
- package/templates/create/deckbuilder-2d/ui/settings.screen.js +342 -0
- package/templates/create/local-multiplayer/aura.config.json +40 -0
- package/templates/create/local-multiplayer/config/gameplay/local-multiplayer.config.js +26 -0
- package/templates/create/local-multiplayer/content/gameplay/room-layout.js +13 -0
- package/templates/create/local-multiplayer/content/registries/.gitkeep +1 -0
- package/templates/create/local-multiplayer/docs/design/loop.md +14 -0
- package/templates/create/local-multiplayer/prefabs/player.prefab.js +99 -0
- package/templates/create/local-multiplayer/scenes/boot.scene.js +12 -0
- package/templates/create/local-multiplayer/scenes/gameplay.scene.js +443 -0
- package/templates/create/local-multiplayer/src/main.js +17 -0
- package/templates/create/local-multiplayer/src/runtime/capabilities.js +28 -0
- package/templates/create/local-multiplayer/ui/hud.screen.js +60 -0
- package/templates/create/shared/src/runtime/project-inspector.js +105 -0
- package/templates/create/shared/src/runtime/scene-flow.js +290 -0
- package/templates/create/shared/src/runtime/screen-shell.js +222 -0
- package/templates/create/shared/src/runtime/ui-forms.js +209 -0
- package/templates/create/shared/src/runtime/ui-settings.js +237 -0
- package/templates/create/shared/src/runtime/ui-theme.js +352 -0
- package/templates/create/shared/src/starter-utils/adventure-objectives.js +102 -0
- package/templates/create/shared/src/starter-utils/animation-2d.js +337 -0
- package/templates/create/shared/src/starter-utils/avatar-3d.js +404 -0
- package/templates/create/shared/src/starter-utils/combat-feedback-2d.js +320 -0
- package/templates/create/shared/src/starter-utils/core.js +39 -3
- package/templates/create/shared/src/starter-utils/index.js +8 -2
- package/templates/create/shared/src/starter-utils/platformer-3d.js +34 -3
- package/templates/create/shared/src/starter-utils/triggers.js +662 -0
- package/templates/create/shared/src/starter-utils/tween-2d.js +615 -0
- package/templates/create/video-cutscene/assets/video/.gitkeep +0 -0
- package/templates/create/video-cutscene/aura.config.json +28 -0
- package/templates/create/video-cutscene/config/gameplay/.gitkeep +0 -0
- package/templates/create/video-cutscene/content/gameplay/.gitkeep +0 -0
- package/templates/create/video-cutscene/content/registries/.gitkeep +0 -0
- package/templates/create/video-cutscene/docs/design/loop.md +22 -0
- package/templates/create/video-cutscene/prefabs/.gitkeep +0 -0
- package/templates/create/video-cutscene/scenes/boot.scene.js +11 -0
- package/templates/create/video-cutscene/scenes/cutscene.scene.js +113 -0
- package/templates/create/video-cutscene/scenes/gameplay.scene.js +50 -0
- package/templates/create/video-cutscene/src/main.js +17 -0
- package/templates/create/video-cutscene/src/runtime/app.js +52 -0
- package/templates/create/video-cutscene/src/runtime/capabilities.js +35 -0
- package/templates/create/video-cutscene/src/runtime/state.js +13 -0
- package/templates/create/video-cutscene/ui/.gitkeep +0 -0
- package/templates/create-bin/play.js +1187 -0
- package/templates/make/README.md +46 -0
- package/templates/make/catalog.json +51 -0
- package/templates/make/component/files/{{MAKE_NAME}}.component.js +20 -0
- package/templates/make/component/manifest.json +9 -0
- package/templates/make/data/files/{{MAKE_NAME}}.json +14 -0
- package/templates/make/data/manifest.json +9 -0
- package/templates/make/material/files/{{MAKE_NAME}}.material.json +17 -0
- package/templates/make/material/manifest.json +9 -0
- package/templates/make/prefab/files/{{MAKE_NAME}}.prefab.js +20 -0
- package/templates/make/prefab/manifest.json +9 -0
- package/templates/make/scene/files/{{MAKE_NAME}}.scene.js +31 -0
- package/templates/make/scene/manifest.json +9 -0
- package/templates/make/shader/files/{{MAKE_NAME}}.shader.js +23 -0
- package/templates/make/shader/manifest.json +9 -0
- package/templates/make/system/files/{{MAKE_NAME}}.system.js +15 -0
- package/templates/make/system/manifest.json +9 -0
- package/templates/make/ui-screen/files/{{MAKE_NAME}}.screen.js +16 -0
- package/templates/make/ui-screen/files/{{MAKE_NAME}}.screen.json +23 -0
- package/templates/make/ui-screen/manifest.json +10 -0
- package/templates/make-starters/deckbuilder-2d/card/files/{{MAKE_NAME}}.card.js +22 -0
- package/templates/make-starters/deckbuilder-2d/card/manifest.json +9 -0
- package/templates/make-starters/deckbuilder-2d/catalog.json +34 -0
- package/templates/make-starters/deckbuilder-2d/encounter/files/{{MAKE_NAME}}.encounter.js +18 -0
- package/templates/make-starters/deckbuilder-2d/encounter/manifest.json +9 -0
- package/templates/make-starters/deckbuilder-2d/enemy/files/{{MAKE_NAME}}.enemy.js +28 -0
- package/templates/make-starters/deckbuilder-2d/enemy/manifest.json +9 -0
- package/templates/make-starters/deckbuilder-2d/relic/files/{{MAKE_NAME}}.relic.js +23 -0
- package/templates/make-starters/deckbuilder-2d/relic/manifest.json +9 -0
- package/templates/retro/platformer/README.md +10 -0
- package/templates/retro/platformer/assets/retro/assets.json +91 -0
- package/templates/retro/platformer/aura.config.json +7 -0
- package/templates/retro/platformer/package.json +5 -0
- package/templates/retro/platformer/src/main.js +40 -0
- package/templates/retro/puzzle-grid/README.md +10 -0
- package/templates/retro/puzzle-grid/assets/retro/assets.json +90 -0
- package/templates/retro/puzzle-grid/aura.config.json +7 -0
- package/templates/retro/puzzle-grid/package.json +5 -0
- package/templates/retro/puzzle-grid/src/main.js +29 -0
- package/templates/retro/tactics-grid/README.md +10 -0
- package/templates/retro/tactics-grid/assets/retro/assets.json +90 -0
- package/templates/retro/tactics-grid/aura.config.json +7 -0
- package/templates/retro/tactics-grid/package.json +5 -0
- package/templates/retro/tactics-grid/src/main.js +35 -0
- package/templates/retro/topdown-adventure/README.md +10 -0
- package/templates/retro/topdown-adventure/assets/retro/assets.json +95 -0
- package/templates/retro/topdown-adventure/aura.config.json +7 -0
- package/templates/retro/topdown-adventure/package.json +5 -0
- package/templates/retro/topdown-adventure/src/main.js +29 -0
- package/templates/skills/aurajs/SKILL.md +61 -5
package/src/state-artifacts.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
existsSync,
|
|
3
4
|
mkdirSync,
|
|
@@ -8,13 +9,28 @@ import {
|
|
|
8
9
|
} from 'node:fs';
|
|
9
10
|
import { dirname, join, resolve } from 'node:path';
|
|
10
11
|
|
|
12
|
+
import { GAME_STATE_SCHEMA_VERSION } from './game-state-runtime.mjs';
|
|
13
|
+
|
|
11
14
|
export const STATE_ARTIFACT_SCHEMA_VERSION = 'aurajs.state-artifact.v1';
|
|
12
15
|
export const STATE_ARTIFACT_KINDS = Object.freeze(['slot', 'checkpoint']);
|
|
16
|
+
export const STATE_ARTIFACT_VERSION = 2;
|
|
13
17
|
|
|
14
18
|
const STATE_ARTIFACT_DIRECTORY_NAMES = Object.freeze({
|
|
15
19
|
slot: 'slots',
|
|
16
20
|
checkpoint: 'checkpoints',
|
|
17
21
|
});
|
|
22
|
+
const LEGACY_STATE_ARTIFACT_VERSION = 1;
|
|
23
|
+
const LEGACY_GAME_STATE_SCHEMA_VERSION = 'aurajs.game-state.v0';
|
|
24
|
+
const STATE_SECTION_NAMES = Object.freeze([
|
|
25
|
+
'globals',
|
|
26
|
+
'camera',
|
|
27
|
+
'project',
|
|
28
|
+
'scene3d',
|
|
29
|
+
'physics',
|
|
30
|
+
'ecs',
|
|
31
|
+
'tilemap',
|
|
32
|
+
]);
|
|
33
|
+
const DEFAULT_STATE_ARTIFACT_MIGRATION_REASON_CODE = 'state_artifact_migrated';
|
|
18
34
|
|
|
19
35
|
export class StateArtifactError extends Error {
|
|
20
36
|
constructor(reasonCode, message, details = {}) {
|
|
@@ -25,6 +41,525 @@ export class StateArtifactError extends Error {
|
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
|
|
44
|
+
function isPlainObject(value) {
|
|
45
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePositiveInteger(value) {
|
|
49
|
+
if (Number.isInteger(value) && value > 0) {
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === 'string' && /^[0-9]+$/.test(value.trim())) {
|
|
53
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
54
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sanitizeJsonValue(value) {
|
|
60
|
+
if (value === null) return null;
|
|
61
|
+
if (typeof value === 'string' || typeof value === 'boolean') return value;
|
|
62
|
+
if (typeof value === 'number') {
|
|
63
|
+
return Number.isFinite(value) ? Number(value) : null;
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.map((entry) => sanitizeJsonValue(entry));
|
|
67
|
+
}
|
|
68
|
+
if (isPlainObject(value)) {
|
|
69
|
+
const out = {};
|
|
70
|
+
for (const key of Object.keys(value).sort()) {
|
|
71
|
+
const next = sanitizeJsonValue(value[key]);
|
|
72
|
+
if (next === undefined) continue;
|
|
73
|
+
out[key] = next;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sanitizeJsonObject(value) {
|
|
81
|
+
return isPlainObject(value) ? sanitizeJsonValue(value) : {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function stableStringify(value) {
|
|
85
|
+
if (value === null || typeof value !== 'object') {
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
return `[${value.map((entry) => stableStringify(entry)).join(',')}]`;
|
|
90
|
+
}
|
|
91
|
+
const keys = Object.keys(value).sort();
|
|
92
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sha256(value) {
|
|
96
|
+
return createHash('sha256').update(String(value)).digest('hex');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function resolveEnvelopeStateSchemaVersion(envelope) {
|
|
100
|
+
if (typeof envelope?.stateSchemaVersion === 'string' && envelope.stateSchemaVersion.trim().length > 0) {
|
|
101
|
+
return envelope.stateSchemaVersion.trim();
|
|
102
|
+
}
|
|
103
|
+
if (typeof envelope?.payload?.schemaVersion === 'string' && envelope.payload.schemaVersion.trim().length > 0) {
|
|
104
|
+
return envelope.payload.schemaVersion.trim();
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeMigrationSteps(steps) {
|
|
110
|
+
if (!Array.isArray(steps)) return [];
|
|
111
|
+
const seen = new Set();
|
|
112
|
+
const output = [];
|
|
113
|
+
for (const step of steps) {
|
|
114
|
+
if (typeof step !== 'string') continue;
|
|
115
|
+
const normalized = step.trim();
|
|
116
|
+
if (normalized.length === 0 || seen.has(normalized)) continue;
|
|
117
|
+
seen.add(normalized);
|
|
118
|
+
output.push(normalized);
|
|
119
|
+
}
|
|
120
|
+
return output;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizeStateArtifactMigrationMetadata(
|
|
124
|
+
migration,
|
|
125
|
+
{
|
|
126
|
+
artifactVersion = STATE_ARTIFACT_VERSION,
|
|
127
|
+
stateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
128
|
+
} = {},
|
|
129
|
+
) {
|
|
130
|
+
const normalizedArtifactVersion = parsePositiveInteger(artifactVersion) || STATE_ARTIFACT_VERSION;
|
|
131
|
+
const normalizedStateSchemaVersion = typeof stateSchemaVersion === 'string' && stateSchemaVersion.trim().length > 0
|
|
132
|
+
? stateSchemaVersion.trim()
|
|
133
|
+
: GAME_STATE_SCHEMA_VERSION;
|
|
134
|
+
const input = isPlainObject(migration) ? migration : {};
|
|
135
|
+
const sourceArtifactVersion = parsePositiveInteger(input.sourceArtifactVersion) || normalizedArtifactVersion;
|
|
136
|
+
const sourceStateSchemaVersion = typeof input.sourceStateSchemaVersion === 'string'
|
|
137
|
+
&& input.sourceStateSchemaVersion.trim().length > 0
|
|
138
|
+
? input.sourceStateSchemaVersion.trim()
|
|
139
|
+
: normalizedStateSchemaVersion;
|
|
140
|
+
const steps = normalizeMigrationSteps(input.steps);
|
|
141
|
+
const applied = input.applied === true
|
|
142
|
+
|| steps.length > 0
|
|
143
|
+
|| sourceArtifactVersion !== normalizedArtifactVersion
|
|
144
|
+
|| sourceStateSchemaVersion !== normalizedStateSchemaVersion;
|
|
145
|
+
return {
|
|
146
|
+
applied,
|
|
147
|
+
sourceArtifactVersion,
|
|
148
|
+
targetArtifactVersion: normalizedArtifactVersion,
|
|
149
|
+
sourceStateSchemaVersion,
|
|
150
|
+
targetStateSchemaVersion: normalizedStateSchemaVersion,
|
|
151
|
+
steps,
|
|
152
|
+
reasonCode: typeof input.reasonCode === 'string' && input.reasonCode.trim().length > 0
|
|
153
|
+
? input.reasonCode.trim()
|
|
154
|
+
: (applied ? DEFAULT_STATE_ARTIFACT_MIGRATION_REASON_CODE : null),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mergeStateArtifactMigrationMetadata(
|
|
159
|
+
base,
|
|
160
|
+
delta,
|
|
161
|
+
{
|
|
162
|
+
artifactVersion = STATE_ARTIFACT_VERSION,
|
|
163
|
+
stateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
164
|
+
} = {},
|
|
165
|
+
) {
|
|
166
|
+
const targetArtifactVersion = parsePositiveInteger(artifactVersion) || STATE_ARTIFACT_VERSION;
|
|
167
|
+
const targetStateSchemaVersion = typeof stateSchemaVersion === 'string' && stateSchemaVersion.trim().length > 0
|
|
168
|
+
? stateSchemaVersion.trim()
|
|
169
|
+
: GAME_STATE_SCHEMA_VERSION;
|
|
170
|
+
const normalizedBase = normalizeStateArtifactMigrationMetadata(base, {
|
|
171
|
+
artifactVersion: targetArtifactVersion,
|
|
172
|
+
stateSchemaVersion: targetStateSchemaVersion,
|
|
173
|
+
});
|
|
174
|
+
const normalizedDelta = normalizeStateArtifactMigrationMetadata(delta, {
|
|
175
|
+
artifactVersion: targetArtifactVersion,
|
|
176
|
+
stateSchemaVersion: targetStateSchemaVersion,
|
|
177
|
+
});
|
|
178
|
+
const sourceArtifactVersion = Math.min(
|
|
179
|
+
normalizedBase.sourceArtifactVersion,
|
|
180
|
+
normalizedDelta.sourceArtifactVersion,
|
|
181
|
+
);
|
|
182
|
+
const sourceStateSchemaVersion = normalizedBase.applied
|
|
183
|
+
? normalizedBase.sourceStateSchemaVersion
|
|
184
|
+
: normalizedDelta.sourceStateSchemaVersion;
|
|
185
|
+
const steps = normalizeMigrationSteps([
|
|
186
|
+
...normalizedBase.steps,
|
|
187
|
+
...normalizedDelta.steps,
|
|
188
|
+
]);
|
|
189
|
+
const applied = normalizedBase.applied || normalizedDelta.applied || steps.length > 0;
|
|
190
|
+
return {
|
|
191
|
+
applied,
|
|
192
|
+
sourceArtifactVersion,
|
|
193
|
+
targetArtifactVersion,
|
|
194
|
+
sourceStateSchemaVersion,
|
|
195
|
+
targetStateSchemaVersion,
|
|
196
|
+
steps,
|
|
197
|
+
reasonCode: normalizedDelta.reasonCode
|
|
198
|
+
|| normalizedBase.reasonCode
|
|
199
|
+
|| (applied ? DEFAULT_STATE_ARTIFACT_MIGRATION_REASON_CODE : null),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeMigrationHookMap(input, defaults = new Map()) {
|
|
204
|
+
const map = new Map(defaults);
|
|
205
|
+
if (input instanceof Map) {
|
|
206
|
+
for (const [key, handler] of input.entries()) {
|
|
207
|
+
if (typeof handler === 'function') {
|
|
208
|
+
map.set(key, handler);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return map;
|
|
212
|
+
}
|
|
213
|
+
if (isPlainObject(input)) {
|
|
214
|
+
for (const [key, handler] of Object.entries(input)) {
|
|
215
|
+
if (typeof handler === 'function') {
|
|
216
|
+
map.set(key, handler);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return map;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function normalizeMigrationHooks(migrationHooks = null) {
|
|
224
|
+
const input = isPlainObject(migrationHooks) ? migrationHooks : {};
|
|
225
|
+
return {
|
|
226
|
+
artifactVersions: normalizeMigrationHookMap(
|
|
227
|
+
input.artifactVersions,
|
|
228
|
+
new Map([
|
|
229
|
+
[LEGACY_STATE_ARTIFACT_VERSION, migrateArtifactEnvelopeV1ToV2],
|
|
230
|
+
]),
|
|
231
|
+
),
|
|
232
|
+
stateSchemas: normalizeMigrationHookMap(
|
|
233
|
+
input.stateSchemas,
|
|
234
|
+
new Map([
|
|
235
|
+
[LEGACY_GAME_STATE_SCHEMA_VERSION, migrateLegacyGameStatePayloadV0ToV1],
|
|
236
|
+
]),
|
|
237
|
+
),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeStateArtifactReadOptions({
|
|
242
|
+
targetArtifactVersion = STATE_ARTIFACT_VERSION,
|
|
243
|
+
targetStateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
244
|
+
migrationHooks = null,
|
|
245
|
+
exportDefaults = null,
|
|
246
|
+
} = {}) {
|
|
247
|
+
return {
|
|
248
|
+
targetArtifactVersion: parsePositiveInteger(targetArtifactVersion) || STATE_ARTIFACT_VERSION,
|
|
249
|
+
targetStateSchemaVersion: typeof targetStateSchemaVersion === 'string' && targetStateSchemaVersion.trim().length > 0
|
|
250
|
+
? targetStateSchemaVersion.trim()
|
|
251
|
+
: GAME_STATE_SCHEMA_VERSION,
|
|
252
|
+
migrationHooks: normalizeMigrationHooks(migrationHooks),
|
|
253
|
+
exportDefaults: normalizeStateArtifactExportDefaults(exportDefaults),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeStateArtifactExportDefaults(exportDefaults = null) {
|
|
258
|
+
const input = isPlainObject(exportDefaults) ? exportDefaults : {};
|
|
259
|
+
return {
|
|
260
|
+
mode: typeof input.mode === 'string' && input.mode.trim().length > 0
|
|
261
|
+
? input.mode.trim().toLowerCase()
|
|
262
|
+
: null,
|
|
263
|
+
seed: Number.isInteger(input.seed) && input.seed >= 0 ? input.seed : 0,
|
|
264
|
+
frameIndex: Number.isInteger(input.frameIndex) && input.frameIndex >= 0 ? input.frameIndex : 0,
|
|
265
|
+
elapsedSeconds: Number.isFinite(input.elapsedSeconds) && input.elapsedSeconds >= 0
|
|
266
|
+
? Number(input.elapsedSeconds)
|
|
267
|
+
: null,
|
|
268
|
+
capturedAt: typeof input.capturedAt === 'string' && input.capturedAt.trim().length > 0
|
|
269
|
+
? input.capturedAt.trim()
|
|
270
|
+
: null,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function migrateArtifactEnvelopeV1ToV2({ envelope }) {
|
|
275
|
+
const sourceStateSchemaVersion = resolveEnvelopeStateSchemaVersion(envelope) || GAME_STATE_SCHEMA_VERSION;
|
|
276
|
+
return {
|
|
277
|
+
envelope: {
|
|
278
|
+
...envelope,
|
|
279
|
+
artifactVersion: STATE_ARTIFACT_VERSION,
|
|
280
|
+
},
|
|
281
|
+
migration: {
|
|
282
|
+
applied: true,
|
|
283
|
+
sourceArtifactVersion: LEGACY_STATE_ARTIFACT_VERSION,
|
|
284
|
+
targetArtifactVersion: STATE_ARTIFACT_VERSION,
|
|
285
|
+
sourceStateSchemaVersion,
|
|
286
|
+
targetStateSchemaVersion: sourceStateSchemaVersion,
|
|
287
|
+
steps: ['state_artifact_version_backfill'],
|
|
288
|
+
reasonCode: DEFAULT_STATE_ARTIFACT_MIGRATION_REASON_CODE,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function migrateLegacyGameStatePayloadV0ToV1({ envelope, exportDefaults = null }) {
|
|
294
|
+
const rawState = isPlainObject(envelope?.payload?.state)
|
|
295
|
+
? envelope.payload.state
|
|
296
|
+
: (isPlainObject(envelope?.payload) ? envelope.payload : {});
|
|
297
|
+
const normalizedExportDefaults = normalizeStateArtifactExportDefaults(exportDefaults);
|
|
298
|
+
const migratedState = {
|
|
299
|
+
globals: sanitizeJsonObject(rawState.globals),
|
|
300
|
+
};
|
|
301
|
+
for (const sectionName of STATE_SECTION_NAMES) {
|
|
302
|
+
if (sectionName === 'globals') continue;
|
|
303
|
+
if (!Object.prototype.hasOwnProperty.call(rawState, sectionName)) continue;
|
|
304
|
+
const normalizedValue = sanitizeJsonValue(rawState[sectionName]);
|
|
305
|
+
if (normalizedValue == null) continue;
|
|
306
|
+
migratedState[sectionName] = normalizedValue;
|
|
307
|
+
}
|
|
308
|
+
if (isPlainObject(migratedState.camera)) {
|
|
309
|
+
if (typeof migratedState.camera.following !== 'boolean') {
|
|
310
|
+
migratedState.camera.following = false;
|
|
311
|
+
}
|
|
312
|
+
if (!Number.isInteger(migratedState.camera.activeEffects) || migratedState.camera.activeEffects < 0) {
|
|
313
|
+
migratedState.camera.activeEffects = 0;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const exportMode = normalizedExportDefaults.mode
|
|
318
|
+
|| (typeof envelope?.source?.mode === 'string' && envelope.source.mode.trim().length > 0
|
|
319
|
+
? envelope.source.mode.trim().toLowerCase()
|
|
320
|
+
: 'headless');
|
|
321
|
+
const exportElapsedSeconds = normalizedExportDefaults.elapsedSeconds != null
|
|
322
|
+
? normalizedExportDefaults.elapsedSeconds
|
|
323
|
+
: (exportMode === 'headless' ? 0 : null);
|
|
324
|
+
|
|
325
|
+
const migratedPayload = {
|
|
326
|
+
schemaVersion: GAME_STATE_SCHEMA_VERSION,
|
|
327
|
+
export: {
|
|
328
|
+
mode: exportMode,
|
|
329
|
+
seed: normalizedExportDefaults.seed,
|
|
330
|
+
frameIndex: normalizedExportDefaults.frameIndex,
|
|
331
|
+
elapsedSeconds: exportElapsedSeconds,
|
|
332
|
+
fingerprint: sha256(stableStringify(migratedState)),
|
|
333
|
+
capturedAt: normalizedExportDefaults.capturedAt,
|
|
334
|
+
},
|
|
335
|
+
state: migratedState,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
envelope: {
|
|
340
|
+
...envelope,
|
|
341
|
+
stateSchemaVersion: GAME_STATE_SCHEMA_VERSION,
|
|
342
|
+
payload: migratedPayload,
|
|
343
|
+
},
|
|
344
|
+
migration: {
|
|
345
|
+
applied: true,
|
|
346
|
+
sourceArtifactVersion: parsePositiveInteger(envelope?.artifactVersion) || LEGACY_STATE_ARTIFACT_VERSION,
|
|
347
|
+
targetArtifactVersion: parsePositiveInteger(envelope?.artifactVersion) || STATE_ARTIFACT_VERSION,
|
|
348
|
+
sourceStateSchemaVersion: LEGACY_GAME_STATE_SCHEMA_VERSION,
|
|
349
|
+
targetStateSchemaVersion: GAME_STATE_SCHEMA_VERSION,
|
|
350
|
+
steps: ['game_state_payload_v0_to_v1'],
|
|
351
|
+
reasonCode: DEFAULT_STATE_ARTIFACT_MIGRATION_REASON_CODE,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function applyArtifactVersionMigrations(
|
|
357
|
+
envelope,
|
|
358
|
+
{
|
|
359
|
+
filePath = null,
|
|
360
|
+
targetArtifactVersion = STATE_ARTIFACT_VERSION,
|
|
361
|
+
migrationHooks,
|
|
362
|
+
migration,
|
|
363
|
+
} = {},
|
|
364
|
+
) {
|
|
365
|
+
let working = envelope;
|
|
366
|
+
let currentVersion = parsePositiveInteger(working?.artifactVersion) || LEGACY_STATE_ARTIFACT_VERSION;
|
|
367
|
+
let currentMigration = normalizeStateArtifactMigrationMetadata(migration, {
|
|
368
|
+
artifactVersion: currentVersion,
|
|
369
|
+
stateSchemaVersion: resolveEnvelopeStateSchemaVersion(working) || GAME_STATE_SCHEMA_VERSION,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
if (currentVersion > targetArtifactVersion) {
|
|
373
|
+
throw new StateArtifactError(
|
|
374
|
+
'state_artifact_version_unsupported',
|
|
375
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} uses artifactVersion "${currentVersion}", expected <= "${targetArtifactVersion}".`,
|
|
376
|
+
{
|
|
377
|
+
filePath,
|
|
378
|
+
sourceArtifactVersion: currentVersion,
|
|
379
|
+
targetArtifactVersion,
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
while (currentVersion < targetArtifactVersion) {
|
|
385
|
+
const migrate = migrationHooks.artifactVersions.get(currentVersion);
|
|
386
|
+
if (typeof migrate !== 'function') {
|
|
387
|
+
throw new StateArtifactError(
|
|
388
|
+
'state_artifact_migration_unavailable',
|
|
389
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} cannot migrate artifactVersion "${currentVersion}" to "${targetArtifactVersion}".`,
|
|
390
|
+
{
|
|
391
|
+
filePath,
|
|
392
|
+
sourceArtifactVersion: currentVersion,
|
|
393
|
+
targetArtifactVersion,
|
|
394
|
+
},
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
let result;
|
|
399
|
+
try {
|
|
400
|
+
result = migrate({ envelope: working, filePath });
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new StateArtifactError(
|
|
403
|
+
'state_artifact_migration_failed',
|
|
404
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} migration from artifactVersion "${currentVersion}" failed: ${error.message}`,
|
|
405
|
+
{
|
|
406
|
+
filePath,
|
|
407
|
+
sourceArtifactVersion: currentVersion,
|
|
408
|
+
targetArtifactVersion,
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!isPlainObject(result?.envelope)) {
|
|
414
|
+
throw new StateArtifactError(
|
|
415
|
+
'state_artifact_migration_failed',
|
|
416
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} migration from artifactVersion "${currentVersion}" returned an invalid envelope.`,
|
|
417
|
+
{
|
|
418
|
+
filePath,
|
|
419
|
+
sourceArtifactVersion: currentVersion,
|
|
420
|
+
targetArtifactVersion,
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
working = result.envelope;
|
|
426
|
+
const nextVersion = parsePositiveInteger(working.artifactVersion);
|
|
427
|
+
if (!nextVersion || nextVersion <= currentVersion) {
|
|
428
|
+
throw new StateArtifactError(
|
|
429
|
+
'state_artifact_migration_failed',
|
|
430
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} migration from artifactVersion "${currentVersion}" did not advance the artifactVersion.`,
|
|
431
|
+
{
|
|
432
|
+
filePath,
|
|
433
|
+
sourceArtifactVersion: currentVersion,
|
|
434
|
+
targetArtifactVersion,
|
|
435
|
+
nextArtifactVersion: nextVersion,
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
currentVersion = nextVersion;
|
|
441
|
+
currentMigration = mergeStateArtifactMigrationMetadata(currentMigration, result.migration, {
|
|
442
|
+
artifactVersion: currentVersion,
|
|
443
|
+
stateSchemaVersion: resolveEnvelopeStateSchemaVersion(working) || GAME_STATE_SCHEMA_VERSION,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
envelope: working,
|
|
449
|
+
migration: currentMigration,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function applyStateSchemaMigrations(
|
|
454
|
+
envelope,
|
|
455
|
+
{
|
|
456
|
+
filePath = null,
|
|
457
|
+
targetStateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
458
|
+
migrationHooks,
|
|
459
|
+
migration,
|
|
460
|
+
exportDefaults = null,
|
|
461
|
+
} = {},
|
|
462
|
+
) {
|
|
463
|
+
let working = envelope;
|
|
464
|
+
let currentStateSchemaVersion = resolveEnvelopeStateSchemaVersion(working);
|
|
465
|
+
if (!currentStateSchemaVersion) {
|
|
466
|
+
throw new StateArtifactError(
|
|
467
|
+
'invalid_state_artifact_payload',
|
|
468
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} stateSchemaVersion must be a non-empty string.`,
|
|
469
|
+
{ filePath },
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const currentArtifactVersion = parsePositiveInteger(working?.artifactVersion) || STATE_ARTIFACT_VERSION;
|
|
474
|
+
let currentMigration = normalizeStateArtifactMigrationMetadata(migration, {
|
|
475
|
+
artifactVersion: currentArtifactVersion,
|
|
476
|
+
stateSchemaVersion: currentStateSchemaVersion,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const seenSchemas = new Set([currentStateSchemaVersion]);
|
|
480
|
+
while (currentStateSchemaVersion !== targetStateSchemaVersion) {
|
|
481
|
+
const migrate = migrationHooks.stateSchemas.get(currentStateSchemaVersion);
|
|
482
|
+
if (typeof migrate !== 'function') {
|
|
483
|
+
throw new StateArtifactError(
|
|
484
|
+
'state_artifact_state_schema_migration_unavailable',
|
|
485
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} cannot migrate state schema "${currentStateSchemaVersion}" to "${targetStateSchemaVersion}".`,
|
|
486
|
+
{
|
|
487
|
+
filePath,
|
|
488
|
+
sourceArtifactVersion: currentArtifactVersion,
|
|
489
|
+
targetArtifactVersion: currentArtifactVersion,
|
|
490
|
+
sourceStateSchemaVersion: currentStateSchemaVersion,
|
|
491
|
+
targetStateSchemaVersion,
|
|
492
|
+
},
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
let result;
|
|
497
|
+
try {
|
|
498
|
+
result = migrate({
|
|
499
|
+
envelope: working,
|
|
500
|
+
filePath,
|
|
501
|
+
targetStateSchemaVersion,
|
|
502
|
+
exportDefaults,
|
|
503
|
+
});
|
|
504
|
+
} catch (error) {
|
|
505
|
+
throw new StateArtifactError(
|
|
506
|
+
'state_artifact_state_schema_migration_failed',
|
|
507
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} state schema migration from "${currentStateSchemaVersion}" failed: ${error.message}`,
|
|
508
|
+
{
|
|
509
|
+
filePath,
|
|
510
|
+
sourceArtifactVersion: currentArtifactVersion,
|
|
511
|
+
targetArtifactVersion: currentArtifactVersion,
|
|
512
|
+
sourceStateSchemaVersion: currentStateSchemaVersion,
|
|
513
|
+
targetStateSchemaVersion,
|
|
514
|
+
},
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!isPlainObject(result?.envelope)) {
|
|
519
|
+
throw new StateArtifactError(
|
|
520
|
+
'state_artifact_state_schema_migration_failed',
|
|
521
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} state schema migration from "${currentStateSchemaVersion}" returned an invalid envelope.`,
|
|
522
|
+
{
|
|
523
|
+
filePath,
|
|
524
|
+
sourceArtifactVersion: currentArtifactVersion,
|
|
525
|
+
targetArtifactVersion: currentArtifactVersion,
|
|
526
|
+
sourceStateSchemaVersion: currentStateSchemaVersion,
|
|
527
|
+
targetStateSchemaVersion,
|
|
528
|
+
},
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
working = result.envelope;
|
|
533
|
+
const nextStateSchemaVersion = resolveEnvelopeStateSchemaVersion(working);
|
|
534
|
+
if (!nextStateSchemaVersion || seenSchemas.has(nextStateSchemaVersion)) {
|
|
535
|
+
throw new StateArtifactError(
|
|
536
|
+
'state_artifact_state_schema_migration_failed',
|
|
537
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} state schema migration from "${currentStateSchemaVersion}" did not advance toward "${targetStateSchemaVersion}".`,
|
|
538
|
+
{
|
|
539
|
+
filePath,
|
|
540
|
+
sourceArtifactVersion: currentArtifactVersion,
|
|
541
|
+
targetArtifactVersion: currentArtifactVersion,
|
|
542
|
+
sourceStateSchemaVersion: currentStateSchemaVersion,
|
|
543
|
+
targetStateSchemaVersion,
|
|
544
|
+
nextStateSchemaVersion,
|
|
545
|
+
},
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
currentStateSchemaVersion = nextStateSchemaVersion;
|
|
550
|
+
seenSchemas.add(currentStateSchemaVersion);
|
|
551
|
+
currentMigration = mergeStateArtifactMigrationMetadata(currentMigration, result.migration, {
|
|
552
|
+
artifactVersion: parsePositiveInteger(working?.artifactVersion) || currentArtifactVersion,
|
|
553
|
+
stateSchemaVersion: currentStateSchemaVersion,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
envelope: working,
|
|
559
|
+
migration: currentMigration,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
28
563
|
export function normalizeStateArtifactKind(value) {
|
|
29
564
|
const normalized = String(value || '').trim().toLowerCase();
|
|
30
565
|
if (!STATE_ARTIFACT_KINDS.includes(normalized)) {
|
|
@@ -98,6 +633,7 @@ export function createStateArtifactEnvelope({
|
|
|
98
633
|
const output = {
|
|
99
634
|
schemaVersion: STATE_ARTIFACT_SCHEMA_VERSION,
|
|
100
635
|
artifactType: normalizedKind,
|
|
636
|
+
artifactVersion: STATE_ARTIFACT_VERSION,
|
|
101
637
|
name: normalizedName,
|
|
102
638
|
createdAt: typeof createdAt === 'string' && createdAt.trim().length > 0
|
|
103
639
|
? createdAt
|
|
@@ -108,6 +644,10 @@ export function createStateArtifactEnvelope({
|
|
|
108
644
|
: null,
|
|
109
645
|
source: normalizeArtifactSource(source),
|
|
110
646
|
payload,
|
|
647
|
+
migration: normalizeStateArtifactMigrationMetadata(null, {
|
|
648
|
+
artifactVersion: STATE_ARTIFACT_VERSION,
|
|
649
|
+
stateSchemaVersion: payload.schemaVersion,
|
|
650
|
+
}),
|
|
111
651
|
};
|
|
112
652
|
|
|
113
653
|
if (typeof note === 'string' && note.trim().length > 0) {
|
|
@@ -124,7 +664,7 @@ export function writeStateArtifactEnvelope(filePath, envelope) {
|
|
|
124
664
|
return validated;
|
|
125
665
|
}
|
|
126
666
|
|
|
127
|
-
export function readStateArtifactEnvelope(filePath) {
|
|
667
|
+
export function readStateArtifactEnvelope(filePath, options = {}) {
|
|
128
668
|
let text = '';
|
|
129
669
|
try {
|
|
130
670
|
text = readFileSync(filePath, 'utf8');
|
|
@@ -147,10 +687,19 @@ export function readStateArtifactEnvelope(filePath) {
|
|
|
147
687
|
);
|
|
148
688
|
}
|
|
149
689
|
|
|
150
|
-
return normalizeStateArtifactEnvelope(parsed, { filePath });
|
|
690
|
+
return normalizeStateArtifactEnvelope(parsed, { filePath, ...options });
|
|
151
691
|
}
|
|
152
692
|
|
|
153
|
-
export function loadStateArtifact({
|
|
693
|
+
export function loadStateArtifact({
|
|
694
|
+
projectRoot,
|
|
695
|
+
kind,
|
|
696
|
+
name,
|
|
697
|
+
artifactPath = null,
|
|
698
|
+
targetArtifactVersion = STATE_ARTIFACT_VERSION,
|
|
699
|
+
targetStateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
700
|
+
migrationHooks = null,
|
|
701
|
+
exportDefaults = null,
|
|
702
|
+
} = {}) {
|
|
154
703
|
const resolvedPath = artifactPath
|
|
155
704
|
? resolve(projectRoot, artifactPath)
|
|
156
705
|
: resolveStateArtifactPath(projectRoot, kind, name);
|
|
@@ -161,7 +710,15 @@ export function loadStateArtifact({ projectRoot, kind, name, artifactPath = null
|
|
|
161
710
|
{ filePath: resolvedPath },
|
|
162
711
|
);
|
|
163
712
|
}
|
|
164
|
-
const envelope = readStateArtifactEnvelope(
|
|
713
|
+
const envelope = readStateArtifactEnvelope(
|
|
714
|
+
resolvedPath,
|
|
715
|
+
normalizeStateArtifactReadOptions({
|
|
716
|
+
targetArtifactVersion,
|
|
717
|
+
targetStateSchemaVersion,
|
|
718
|
+
migrationHooks,
|
|
719
|
+
exportDefaults,
|
|
720
|
+
}),
|
|
721
|
+
);
|
|
165
722
|
return {
|
|
166
723
|
filePath: resolvedPath,
|
|
167
724
|
envelope,
|
|
@@ -169,9 +726,24 @@ export function loadStateArtifact({ projectRoot, kind, name, artifactPath = null
|
|
|
169
726
|
};
|
|
170
727
|
}
|
|
171
728
|
|
|
172
|
-
export function listStateArtifacts(
|
|
729
|
+
export function listStateArtifacts(
|
|
730
|
+
projectRoot,
|
|
731
|
+
kind,
|
|
732
|
+
{
|
|
733
|
+
targetArtifactVersion = STATE_ARTIFACT_VERSION,
|
|
734
|
+
targetStateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
735
|
+
migrationHooks = null,
|
|
736
|
+
exportDefaults = null,
|
|
737
|
+
} = {},
|
|
738
|
+
) {
|
|
173
739
|
const normalizedKind = normalizeStateArtifactKind(kind);
|
|
174
740
|
const directory = resolveStateArtifactDirectory(projectRoot, normalizedKind);
|
|
741
|
+
const readOptions = normalizeStateArtifactReadOptions({
|
|
742
|
+
targetArtifactVersion,
|
|
743
|
+
targetStateSchemaVersion,
|
|
744
|
+
migrationHooks,
|
|
745
|
+
exportDefaults,
|
|
746
|
+
});
|
|
175
747
|
if (!existsSync(directory)) {
|
|
176
748
|
return {
|
|
177
749
|
kind: normalizedKind,
|
|
@@ -187,7 +759,7 @@ export function listStateArtifacts(projectRoot, kind) {
|
|
|
187
759
|
if (!entry.endsWith('.json')) continue;
|
|
188
760
|
const filePath = join(directory, entry);
|
|
189
761
|
try {
|
|
190
|
-
const envelope = readStateArtifactEnvelope(filePath);
|
|
762
|
+
const envelope = readStateArtifactEnvelope(filePath, readOptions);
|
|
191
763
|
if (envelope.artifactType !== normalizedKind) {
|
|
192
764
|
throw new StateArtifactError(
|
|
193
765
|
'state_artifact_kind_mismatch',
|
|
@@ -203,6 +775,14 @@ export function listStateArtifacts(projectRoot, kind) {
|
|
|
203
775
|
? error.reasonCode
|
|
204
776
|
: 'state_artifact_invalid',
|
|
205
777
|
detail: error instanceof Error ? error.message : String(error),
|
|
778
|
+
migration: error instanceof StateArtifactError && isPlainObject(error.details)
|
|
779
|
+
? {
|
|
780
|
+
sourceArtifactVersion: error.details.sourceArtifactVersion ?? null,
|
|
781
|
+
targetArtifactVersion: error.details.targetArtifactVersion ?? null,
|
|
782
|
+
sourceStateSchemaVersion: error.details.sourceStateSchemaVersion ?? null,
|
|
783
|
+
targetStateSchemaVersion: error.details.targetStateSchemaVersion ?? null,
|
|
784
|
+
}
|
|
785
|
+
: null,
|
|
206
786
|
});
|
|
207
787
|
}
|
|
208
788
|
}
|
|
@@ -215,7 +795,16 @@ export function listStateArtifacts(projectRoot, kind) {
|
|
|
215
795
|
};
|
|
216
796
|
}
|
|
217
797
|
|
|
218
|
-
function normalizeStateArtifactEnvelope(
|
|
798
|
+
function normalizeStateArtifactEnvelope(
|
|
799
|
+
envelope,
|
|
800
|
+
{
|
|
801
|
+
filePath = null,
|
|
802
|
+
targetArtifactVersion = STATE_ARTIFACT_VERSION,
|
|
803
|
+
targetStateSchemaVersion = GAME_STATE_SCHEMA_VERSION,
|
|
804
|
+
migrationHooks = normalizeMigrationHooks(),
|
|
805
|
+
exportDefaults = normalizeStateArtifactExportDefaults(),
|
|
806
|
+
} = {},
|
|
807
|
+
) {
|
|
219
808
|
if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
220
809
|
throw new StateArtifactError(
|
|
221
810
|
'invalid_state_artifact_payload',
|
|
@@ -231,26 +820,58 @@ function normalizeStateArtifactEnvelope(envelope, { filePath = null } = {}) {
|
|
|
231
820
|
);
|
|
232
821
|
}
|
|
233
822
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
823
|
+
let working = envelope;
|
|
824
|
+
let migration = normalizeStateArtifactMigrationMetadata(working.migration, {
|
|
825
|
+
artifactVersion: parsePositiveInteger(working.artifactVersion) || LEGACY_STATE_ARTIFACT_VERSION,
|
|
826
|
+
stateSchemaVersion: resolveEnvelopeStateSchemaVersion(working) || GAME_STATE_SCHEMA_VERSION,
|
|
827
|
+
});
|
|
828
|
+
const artifactMigration = applyArtifactVersionMigrations(working, {
|
|
829
|
+
filePath,
|
|
830
|
+
targetArtifactVersion,
|
|
831
|
+
migrationHooks,
|
|
832
|
+
migration,
|
|
833
|
+
});
|
|
834
|
+
working = artifactMigration.envelope;
|
|
835
|
+
migration = artifactMigration.migration;
|
|
836
|
+
const stateSchemaMigration = applyStateSchemaMigrations(working, {
|
|
837
|
+
filePath,
|
|
838
|
+
targetStateSchemaVersion,
|
|
839
|
+
migrationHooks,
|
|
840
|
+
migration,
|
|
841
|
+
exportDefaults,
|
|
842
|
+
});
|
|
843
|
+
working = stateSchemaMigration.envelope;
|
|
844
|
+
migration = stateSchemaMigration.migration;
|
|
845
|
+
|
|
846
|
+
const normalizedKind = normalizeStateArtifactKind(working.artifactType);
|
|
847
|
+
const normalizedName = normalizeStateArtifactName(working.name, { kind: normalizedKind });
|
|
848
|
+
if (!working.payload || typeof working.payload !== 'object' || Array.isArray(working.payload)) {
|
|
237
849
|
throw new StateArtifactError(
|
|
238
850
|
'invalid_state_artifact_payload',
|
|
239
851
|
`State artifact${filePath ? ` "${filePath}"` : ''} payload must be an object.`,
|
|
240
852
|
{ filePath },
|
|
241
853
|
);
|
|
242
854
|
}
|
|
243
|
-
|
|
855
|
+
const artifactVersion = parsePositiveInteger(working.artifactVersion);
|
|
856
|
+
if (!artifactVersion) {
|
|
857
|
+
throw new StateArtifactError(
|
|
858
|
+
'invalid_state_artifact_payload',
|
|
859
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} artifactVersion must be a positive integer.`,
|
|
860
|
+
{ filePath },
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const stateSchemaVersion = resolveEnvelopeStateSchemaVersion(working);
|
|
864
|
+
if (typeof stateSchemaVersion !== 'string' || stateSchemaVersion.trim().length === 0) {
|
|
244
865
|
throw new StateArtifactError(
|
|
245
866
|
'invalid_state_artifact_payload',
|
|
246
867
|
`State artifact${filePath ? ` "${filePath}"` : ''} stateSchemaVersion must be a non-empty string.`,
|
|
247
868
|
{ filePath },
|
|
248
869
|
);
|
|
249
870
|
}
|
|
250
|
-
if (
|
|
871
|
+
if (working.payload.schemaVersion !== stateSchemaVersion) {
|
|
251
872
|
throw new StateArtifactError(
|
|
252
873
|
'state_artifact_schema_mismatch',
|
|
253
|
-
`State artifact${filePath ? ` "${filePath}"` : ''} payload schema "${
|
|
874
|
+
`State artifact${filePath ? ` "${filePath}"` : ''} payload schema "${working.payload.schemaVersion}" does not match declared stateSchemaVersion "${stateSchemaVersion}".`,
|
|
254
875
|
{ filePath },
|
|
255
876
|
);
|
|
256
877
|
}
|
|
@@ -258,22 +879,34 @@ function normalizeStateArtifactEnvelope(envelope, { filePath = null } = {}) {
|
|
|
258
879
|
const output = {
|
|
259
880
|
schemaVersion: STATE_ARTIFACT_SCHEMA_VERSION,
|
|
260
881
|
artifactType: normalizedKind,
|
|
882
|
+
artifactVersion,
|
|
261
883
|
name: normalizedName,
|
|
262
|
-
createdAt: typeof
|
|
263
|
-
?
|
|
884
|
+
createdAt: typeof working.createdAt === 'string' && working.createdAt.trim().length > 0
|
|
885
|
+
? working.createdAt
|
|
264
886
|
: null,
|
|
265
|
-
stateSchemaVersion
|
|
266
|
-
payloadFingerprint: typeof
|
|
267
|
-
?
|
|
268
|
-
: (typeof
|
|
269
|
-
?
|
|
887
|
+
stateSchemaVersion,
|
|
888
|
+
payloadFingerprint: typeof working?.payload?.export?.fingerprint === 'string'
|
|
889
|
+
? working.payload.export.fingerprint
|
|
890
|
+
: (typeof working.payloadFingerprint === 'string'
|
|
891
|
+
? working.payloadFingerprint
|
|
270
892
|
: null),
|
|
271
|
-
source: normalizeArtifactSource(
|
|
272
|
-
payload:
|
|
893
|
+
source: normalizeArtifactSource(working.source),
|
|
894
|
+
payload: working.payload,
|
|
895
|
+
migration: mergeStateArtifactMigrationMetadata(
|
|
896
|
+
normalizeStateArtifactMigrationMetadata(working.migration, {
|
|
897
|
+
artifactVersion,
|
|
898
|
+
stateSchemaVersion,
|
|
899
|
+
}),
|
|
900
|
+
migration,
|
|
901
|
+
{
|
|
902
|
+
artifactVersion,
|
|
903
|
+
stateSchemaVersion,
|
|
904
|
+
},
|
|
905
|
+
),
|
|
273
906
|
};
|
|
274
907
|
|
|
275
|
-
if (typeof
|
|
276
|
-
output.note =
|
|
908
|
+
if (typeof working.note === 'string' && working.note.trim().length > 0) {
|
|
909
|
+
output.note = working.note.trim();
|
|
277
910
|
}
|
|
278
911
|
|
|
279
912
|
return output;
|
|
@@ -283,12 +916,17 @@ function createArtifactMetadata(filePath, envelope) {
|
|
|
283
916
|
const stat = statSync(filePath);
|
|
284
917
|
return {
|
|
285
918
|
kind: envelope.artifactType,
|
|
919
|
+
artifactVersion: envelope.artifactVersion,
|
|
286
920
|
name: envelope.name,
|
|
287
921
|
path: filePath,
|
|
288
922
|
createdAt: envelope.createdAt,
|
|
289
923
|
note: envelope.note || null,
|
|
290
924
|
stateSchemaVersion: envelope.stateSchemaVersion,
|
|
291
925
|
payloadFingerprint: envelope.payloadFingerprint,
|
|
926
|
+
migration: normalizeStateArtifactMigrationMetadata(envelope.migration, {
|
|
927
|
+
artifactVersion: envelope.artifactVersion,
|
|
928
|
+
stateSchemaVersion: envelope.stateSchemaVersion,
|
|
929
|
+
}),
|
|
292
930
|
mode: typeof envelope?.payload?.export?.mode === 'string' ? envelope.payload.export.mode : null,
|
|
293
931
|
frameIndex: Number.isInteger(envelope?.payload?.export?.frameIndex) ? envelope.payload.export.frameIndex : null,
|
|
294
932
|
elapsedSeconds: Number.isFinite(envelope?.payload?.export?.elapsedSeconds)
|