@underpostnet/cyberia 3.3.0 → 3.3.76
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/.dockerignore +1 -0
- package/.env.example +17 -5
- package/.github/workflows/coverall.cyberia.ci.yml +39 -14
- package/.github/workflows/cyberia-client.cd.yml +5 -0
- package/.github/workflows/cyberia-server.cd.yml +5 -0
- package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
- package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
- package/.github/workflows/engine-cyberia.cd.yml +3 -0
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +35 -20
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/publish.cyberia.ci.yml +44 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +1 -0
- package/.prettierignore +1 -0
- package/AGENTS.md +96 -0
- package/CHANGELOG.md +653 -2513
- package/CLI-HELP.md +323 -157
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +5 -5
- package/README.md +48 -28
- package/bin/build.js +86 -41
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +1494 -1403
- package/bin/deploy.js +18 -104
- package/bin/index.js +1494 -1403
- package/compose.env +17 -5
- package/conf.js +98 -7
- package/deploy/cyberia-client/deploy.sh +46 -0
- package/deploy/cyberia-client/package.sh +17 -0
- package/deploy/cyberia-client/state.sh +15 -0
- package/deploy/cyberia-server/deploy.sh +46 -0
- package/deploy/cyberia-server/package.sh +17 -0
- package/deploy/cyberia-server/state.sh +15 -0
- package/deploy/dd-cyberia/deploy.sh +9 -29
- package/deploy/dd-cyberia/init.sh +38 -45
- package/deploy/dd-cyberia/package.sh +16 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +133 -147
- package/deploy/lib/config.sh +29 -0
- package/deploy/lib/github-actions-logging.sh +610 -0
- package/deploy/lib/host.sh +194 -0
- package/deploy/lib/state.sh +65 -0
- package/deploy/pwa-microservices-template/deploy.sh +17 -42
- package/deploy/release/deploy.sh +9 -41
- package/deployment.yaml +7 -2
- package/docker-compose.yml +23 -17
- package/examples/static-page/README.md +19 -19
- package/examples/static-page/ssr-components/CustomPage.js +2 -2
- package/hardhat/hardhat.config.js +1 -6
- package/hardhat/package-lock.json +101 -40
- package/hardhat/package.json +2 -2
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +32 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +32 -7
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/mongo-express/deployment.yaml +46 -27
- package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
- package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
- package/manifests/deployment/mongo-express/service.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
- package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
- package/manifests/grafana/deployment.yaml +26 -7
- package/manifests/grafana/kustomization.yaml +1 -0
- package/manifests/grafana/service.yaml +6 -1
- package/manifests/mariadb/pv.yaml +1 -0
- package/manifests/mongodb/statefulset.yaml +9 -2
- package/manifests/mysql/pv-pvc.yaml +14 -1
- package/manifests/postgresql/pv.yaml +12 -1
- package/manifests/postgresql/pvc.yaml +1 -0
- package/manifests/prometheus/deployment.yaml +18 -22
- package/package.json +87 -12
- package/scripts/event-monitor.sh +56 -0
- package/scripts/k3s-node-setup.sh +27 -14
- package/scripts/kubeadm-node-setup.sh +31 -11
- package/scripts/maas-setup.sh +8 -8
- package/scripts/selinux-normalize.sh +907 -0
- package/scripts/shutdown-machine.sh +843 -0
- package/scripts/test-monitor.sh +5 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +15 -10
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +7 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +90 -186
- package/src/api/core/core.controller.js +1 -1
- package/src/api/core/core.router.js +2 -2
- package/src/api/core/core.service.js +2 -2
- package/src/api/crypto/crypto.controller.js +1 -1
- package/src/api/crypto/crypto.service.js +1 -1
- package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
- package/src/api/cyberia-action/cyberia-action.router.js +3 -3
- package/src/api/cyberia-action/cyberia-action.service.js +5 -6
- package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
- package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
- package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
- package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +2 -4
- package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +13 -35
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +17 -34
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
- package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.service.js +3 -3
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +17 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +468 -5
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +502 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +29 -40
- package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
- package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +35 -142
- package/src/api/cyberia-instance/cyberia-instance.controller.js +2 -5
- package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
- package/src/api/cyberia-instance/cyberia-instance.router.js +5 -18
- package/src/api/cyberia-instance/cyberia-instance.service.js +5 -45
- package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +76 -3
- package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
- package/src/api/cyberia-map/cyberia-map.router.js +1 -1
- package/src/api/cyberia-map/cyberia-map.service.js +8 -4
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
- package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
- package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +4 -4
- package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
- package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -214
- package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
- package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
- package/src/api/default/default.controller.js +1 -1
- package/src/api/default/default.router.js +1 -1
- package/src/api/default/default.service.js +2 -2
- package/src/api/document/document.controller.js +1 -1
- package/src/api/document/document.router.js +1 -1
- package/src/api/document/document.service.js +2 -2
- package/src/api/file/file.controller.js +1 -1
- package/src/api/file/file.ref.js +153 -0
- package/src/api/file/file.ref.json +11 -2
- package/src/api/file/file.router.js +1 -1
- package/src/api/file/file.service.js +2 -2
- package/src/api/instance/instance.controller.js +1 -1
- package/src/api/instance/instance.router.js +1 -1
- package/src/api/instance/instance.service.js +2 -2
- package/src/api/ipfs/ipfs.controller.js +1 -1
- package/src/api/ipfs/ipfs.router.js +2 -2
- package/src/api/ipfs/ipfs.service.js +2 -2
- package/src/api/object-layer/object-layer.controller.js +1 -1
- package/src/api/object-layer/object-layer.model.js +47 -12
- package/src/api/object-layer/object-layer.router.js +12 -5
- package/src/api/object-layer/object-layer.service.js +116 -123
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
- package/src/api/test/test.controller.js +1 -1
- package/src/api/test/test.service.js +1 -1
- package/src/api/user/guest.service.js +3 -3
- package/src/api/user/user.controller.js +1 -1
- package/src/api/user/user.router.js +2 -2
- package/src/api/user/user.service.js +4 -4
- package/src/api.js +5 -9
- package/src/cli/app.js +402 -0
- package/src/cli/baremetal.js +30 -35
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +191 -82
- package/src/cli/db.js +120 -68
- package/src/cli/deploy.js +157 -75
- package/src/cli/docker-compose.js +5 -5
- package/src/cli/domains.js +184 -0
- package/src/cli/dotenv-store.js +143 -0
- package/src/cli/event.js +2281 -0
- package/src/cli/fs-selection.js +115 -0
- package/src/cli/fs.js +159 -343
- package/src/cli/host.js +672 -0
- package/src/cli/image.js +4 -6
- package/src/cli/index.js +343 -151
- package/src/cli/ipfs.js +34 -30
- package/src/cli/kickstart.js +1 -1
- package/src/cli/kubectl.js +218 -41
- package/src/cli/lxd.js +3 -3
- package/src/cli/monitor.js +1359 -26
- package/src/cli/package.js +90 -0
- package/src/cli/release.js +16 -12
- package/src/cli/repository.js +586 -363
- package/src/cli/run.js +398 -843
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +478 -279
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +1 -1
- package/src/cli/test.js +203 -125
- package/src/cli/vultr.js +61 -21
- package/src/cli/wireguard.js +1112 -306
- package/src/client/components/core/CalendarCore.js +53 -96
- package/src/client/components/core/Docs.js +327 -303
- package/src/client/components/core/FileExplorer.js +0 -21
- package/src/client/components/core/Modal.js +56 -1
- package/src/client/components/core/PanelForm.js +4 -4
- package/src/client/components/core/Repository.js +56 -0
- package/src/client/components/core/Translate.js +7 -0
- package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
- package/src/client/components/core/Wallet.js +0 -11
- package/src/client/components/core/Worker.js +7 -1
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
- package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
- package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
- package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
- package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
- package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
- package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
- package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +2 -35
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -1
- package/src/client/components/default/SettingsDefault.js +2 -2
- package/src/client/components/itemledger/SettingsItemledger.js +2 -2
- package/src/client/components/underpost/AppShellUnderpost.js +3 -2
- package/src/client/components/underpost/SettingsUnderpost.js +2 -2
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +313 -30
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
- package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
- package/src/client/public/cyberia-docs/CYBERIA.md +48 -28
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
- package/src/client/public/default/android-chrome-144x144.png +0 -0
- package/src/client/public/default/android-chrome-192x192.png +0 -0
- package/src/client/public/default/android-chrome-256x256.png +0 -0
- package/src/client/public/default/android-chrome-36x36.png +0 -0
- package/src/client/public/default/android-chrome-384x384.png +0 -0
- package/src/client/public/default/android-chrome-48x48.png +0 -0
- package/src/client/public/default/android-chrome-512x512.png +0 -0
- package/src/client/public/default/android-chrome-72x72.png +0 -0
- package/src/client/public/default/android-chrome-96x96.png +0 -0
- package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/default/assets/background/white.jpg +0 -0
- package/src/client/public/default/browserconfig.xml +1 -1
- package/src/client/public/default/favicon-16x16.png +0 -0
- package/src/client/public/default/favicon-32x32.png +0 -0
- package/src/client/public/default/favicon-48x48.png +0 -0
- package/src/client/public/default/favicon.ico +0 -0
- package/src/client/public/default/manifest.webmanifest +5 -5
- package/src/client/public/default/mstile-144x144.png +0 -0
- package/src/client/public/default/mstile-150x150.png +0 -0
- package/src/client/public/default/mstile-310x150.png +0 -0
- package/src/client/public/default/mstile-310x310.png +0 -0
- package/src/client/public/default/mstile-70x70.png +0 -0
- package/src/client/public/default/yandex-browser-50x50.png +0 -0
- package/src/client/public/default/yandex-browser-manifest.json +1 -1
- package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
- package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
- package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
- package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
- package/src/client/services/object-layer/object-layer.management.js +6 -10
- package/src/client/services/object-layer/object-layer.service.js +3 -1
- package/src/client/ssr/RootDocument.js +1 -1
- package/src/client/ssr/body/404.js +1 -1
- package/src/client/ssr/body/500.js +1 -1
- package/src/client/ssr/body/CacheControl.js +1 -1
- package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
- package/src/client/ssr/head/CryptokoynScripts.js +1 -1
- package/src/client/ssr/head/Css.js +1 -1
- package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
- package/src/client/ssr/head/DefaultScripts.js +1 -1
- package/src/client/ssr/head/ItemledgerScripts.js +1 -1
- package/src/client/ssr/head/Microdata.js +1 -1
- package/src/client/ssr/head/Production.js +1 -1
- package/src/client/ssr/head/Pwa.js +1 -1
- package/src/client/ssr/head/PwaItemledger.js +1 -1
- package/src/client/ssr/head/Seo.js +1 -1
- package/src/client/ssr/head/UnderpostScripts.js +1 -1
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
- package/src/client/ssr/views/Cyberia404.js +7 -5
- package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/views/Maintenance.js +1 -1
- package/src/client/ssr/views/NoNetworkConnection.js +1 -1
- package/src/client/ssr/views/Test.js +1 -1
- package/src/client/sw/core.sw.js +20 -14
- package/src/client-builder/client-build-docs.js +92 -77
- package/src/client-builder/client-build-live.js +2 -2
- package/src/client-builder/client-build.js +105 -74
- package/src/client-builder/client-bundle.js +347 -0
- package/src/client-builder/client-dev-server.js +3 -3
- package/src/client-builder/client-icons.js +2 -2
- package/src/client-builder/ssr.js +3 -3
- package/src/client.build.js +3 -6
- package/src/client.dev.js +3 -6
- package/src/db/DataBaseProvider.js +186 -3
- package/src/db/mariadb/MariaDB.js +3 -2
- package/src/db/mongo/MongoBootstrap.js +273 -101
- package/src/db/mongo/MongoExpress.js +187 -0
- package/src/db/mongo/MongooseDB.js +7 -8
- package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
- package/src/grpc/cyberia/grpc-server.js +6 -8
- package/src/index.js +79 -23
- package/src/mailer/EmailRender.js +2 -2
- package/src/mailer/MailerInterceptor.js +87 -0
- package/src/mailer/MailerProvider.js +50 -6
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +144 -147
- package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
- package/src/projects/cyberia/besu-genesis-generator.js +8 -17
- package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
- package/src/projects/cyberia/catalog-cyberia.js +54 -10
- package/src/projects/cyberia/gemini-client.js +4 -4
- package/src/projects/cyberia/generate-saga.js +5 -12
- package/src/projects/cyberia/hot-reload-trigger.js +18 -29
- package/src/projects/cyberia/instance-backup.js +210 -0
- package/src/projects/cyberia/instance-data.js +178 -258
- package/src/projects/cyberia/ipfs-client.js +4 -4
- package/src/projects/cyberia/map-preview-generator.js +33 -69
- package/src/projects/cyberia/object-layer.js +153 -320
- package/src/projects/cyberia/seed-audio.js +139 -0
- package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
- package/src/projects/cyberia/semantic-layer-generator.js +3 -19
- package/src/projects/cyberia/stat-balance.js +81 -0
- package/src/projects/cyberia/stat-commands.js +23 -0
- package/src/projects/cyberia/stat-contract-generator.js +101 -0
- package/src/projects/underpost/catalog-underpost.js +9 -1
- package/src/proxy.js +4 -7
- package/src/runtime/cyberia-client/Dockerfile +17 -8
- package/src/runtime/cyberia-client/Dockerfile.dev +13 -4
- package/src/runtime/cyberia-server/Dockerfile +18 -6
- package/src/runtime/cyberia-server/Dockerfile.dev +16 -3
- package/src/runtime/engine-cyberia/Dockerfile +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.test +5 -5
- package/src/runtime/engine-cyberia/compose.env +17 -5
- package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
- package/src/runtime/express/Express.js +32 -27
- package/src/runtime/lampp/Lampp.js +22 -70
- package/src/runtime/nginx/Nginx.js +1 -1
- package/src/runtime/wp/Wp.js +16 -48
- package/src/server/{catalog.js → build/catalog.js} +15 -5
- package/src/server/build/coverage.js +188 -0
- package/src/server/build/execution.js +402 -0
- package/src/server/build/package.js +508 -0
- package/src/server/build/testing.js +578 -0
- package/src/server/{dns.js → network/dns.js} +144 -11
- package/src/server/{forward-proxy.js → network/forward-proxy.js} +10 -18
- package/src/server/{middlewares.js → network/middlewares.js} +11 -5
- package/src/server/network/node-capability.js +98 -0
- package/src/server/{peer.js → network/peer.js} +3 -3
- package/src/server/{proxy.js → network/proxy.js} +7 -13
- package/src/server/network/router.js +356 -0
- package/src/server/{tls.js → network/tls.js} +2 -2
- package/src/server/{underpost-compression.js → network/underpost-compression.js} +1 -1
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +7 -7
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +2 -2
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/{cron.js → ops/cron.js} +349 -74
- package/src/server/ops/event-notification.js +284 -0
- package/src/server/{logger.js → ops/logger.js} +143 -23
- package/src/server/ops/monitoring.js +1724 -0
- package/src/server/{systemd.js → ops/systemd.js} +55 -1
- package/src/server/{conf.js → runtime/conf.js} +516 -423
- package/src/server/runtime/config-scope.js +215 -0
- package/src/server/runtime/environment.js +149 -0
- package/src/server/{process.js → runtime/process.js} +153 -12
- package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
- package/src/server/{runtime.js → runtime/runtime.js} +12 -8
- package/src/server/{start.js → runtime/start.js} +128 -39
- package/src/server/{auth.js → security/auth.js} +7 -8
- package/src/server/security/container-storage.js +382 -0
- package/src/server/{crypto.js → security/crypto.js} +1 -1
- package/src/server/{selinux.js → security/selinux.js} +1 -1
- package/src/server/{backup.js → storage/backup.js} +17 -8
- package/src/server/{data-query.js → storage/data-query.js} +17 -1
- package/src/server/storage/downloader.js +121 -0
- package/src/server/storage/repository.js +68 -0
- package/src/server/storage/zip.js +153 -0
- package/src/server.js +4 -7
- package/src/ws/IoInterface.js +1 -1
- package/src/ws/IoServer.js +1 -1
- package/src/ws/core/core.ws.connection.js +1 -1
- package/src/ws/core/core.ws.emit.js +1 -1
- package/src/ws/core/core.ws.server.js +1 -1
- package/src/ws/default/default.ws.connection.js +1 -1
- package/src/ws/default/default.ws.emit.js +1 -1
- package/src/ws/default/default.ws.server.js +1 -1
- package/test/e2e/event-e2e-public-ingress-down.js +45 -0
- package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
- package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
- package/test/{api.test.js → integration/app/api.test.js} +33 -14
- package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
- package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +64 -0
- package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
- package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
- package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +17 -23
- package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
- package/test/integration/app/cyberia/cyberia-load.test.js +466 -0
- package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
- package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +338 -0
- package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
- package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
- package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
- package/test/integration/app/cyberia/seed-audio.test.js +275 -0
- package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
- package/test/integration/infra/1-security/config-scope.test.js +305 -0
- package/test/integration/infra/1-security/container-storage.test.js +356 -0
- package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
- package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
- package/test/{selinux.test.js → integration/infra/1-security/selinux.test.js} +2 -4
- package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
- package/test/integration/infra/1-security/systemd-service.test.js +70 -0
- package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
- package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
- package/test/integration/infra/2-network/node-capability.test.js +182 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +1187 -0
- package/test/{wireguard-edge.test.js → integration/infra/2-network/wireguard-edge.test.js} +649 -55
- package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
- package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
- package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
- package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
- package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
- package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
- package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
- package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
- package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +6 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +4 -4
- package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
- package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
- package/test/integration/infra/5-observability/event-notification.test.js +197 -0
- package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
- package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
- package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
- package/test/support/shell-harness.js +73 -0
- package/test/unit/build-template.test.js +97 -0
- package/test/unit/build-workflow-offline.test.js +132 -0
- package/test/unit/catalog.test.js +71 -0
- package/test/unit/client-build-docs.test.js +361 -0
- package/test/unit/client-bundle.test.js +313 -0
- package/test/unit/conf-loading.test.js +303 -0
- package/test/unit/conf-resolution.test.js +996 -0
- package/test/unit/coverage-artifact.test.js +221 -0
- package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
- package/test/unit/cyberia/instance-backup.test.js +183 -0
- package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
- package/test/unit/cyberia/publish-workflow.test.js +22 -0
- package/test/unit/cyberia/stat-balance.test.js +69 -0
- package/test/unit/deploy-env-resolution.test.js +38 -0
- package/test/unit/deploy-log-table.test.js +518 -0
- package/test/unit/downloader-integrity.test.js +98 -0
- package/test/unit/execution-profiles.test.js +219 -0
- package/test/unit/file-reference-registry.test.js +141 -0
- package/test/unit/fleet-sync-source.test.js +149 -0
- package/test/unit/fs-storage-paths.test.js +596 -0
- package/test/unit/logger-redaction.test.js +141 -0
- package/test/unit/package.test.js +457 -0
- package/test/unit/prepare-host.test.js +382 -0
- package/test/unit/process-environment.test.js +18 -0
- package/test/unit/propagation-message.test.js +78 -0
- package/test/unit/redeploy-plan.test.js +24 -0
- package/test/unit/release-bump.test.js +474 -0
- package/test/unit/ssh-output-redaction.test.js +28 -0
- package/test/unit/start-options.test.js +46 -0
- package/test/unit/test-tiers.test.js +220 -0
- package/test/unit/zip-archive.test.js +142 -0
- package/vitest.config.js +66 -0
- package/.nycrc +0 -9
- package/deploy/lib/logging.sh +0 -96
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/coverall-test-cyberia.sh +0 -24
- package/scripts/coverall-test.sh +0 -24
- package/scripts/link-local-underpost-cli.sh +0 -6
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
- package/src/api/user/user.build.js +0 -16
- package/src/cli/env.js +0 -177
- package/src/client/components/core/Blockchain.js +0 -41
- package/src/client/components/core/Blog.js +0 -9
- package/src/client/components/core/KeyboardAvoidance.js +0 -145
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
- package/src/client/public/default/assets/background/dark.jpg +0 -0
- package/src/client/public/default/assets/background/white0-min.jpg +0 -0
- package/src/client/public/default/assets/background/white0.jpg +0 -0
- package/src/client/public/doc/favicon.ico +0 -0
- package/src/client/public/doc/sitemap +0 -148
- package/src/server/downloader.js +0 -73
- package/src/server/environment.js +0 -98
package/bin/index.js
CHANGED
|
@@ -10,22 +10,47 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import dotenv from 'dotenv';
|
|
13
|
-
import {
|
|
13
|
+
import { registerStatCommands } from '../src/projects/cyberia/stat-commands.js';
|
|
14
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
14
15
|
import fs from 'fs-extra';
|
|
15
16
|
import stringify from 'fast-json-stable-stringify';
|
|
16
|
-
import { shellExec } from '../src/server/process.js';
|
|
17
|
-
import {
|
|
17
|
+
import { shellExec } from '../src/server/runtime/process.js';
|
|
18
|
+
import { cli } from '../src/server/build/execution.js';
|
|
19
|
+
import { loggerFactory } from '../src/server/ops/logger.js';
|
|
18
20
|
import { generateBesuManifests, deployBesu, removeBesu } from '../src/projects/cyberia/besu-genesis-generator.js';
|
|
19
21
|
import { DataBaseProviderService } from '../src/db/DataBaseProvider.js';
|
|
20
|
-
import {
|
|
22
|
+
import { CyberiaAudioService } from '../src/api/cyberia-audio/cyberia-audio.service.js';
|
|
23
|
+
import { CyberiaEntityTypeDefaultService } from '../src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js';
|
|
24
|
+
import { CyberiaInstanceConfService } from '../src/api/cyberia-instance-conf/cyberia-instance-conf.service.js';
|
|
25
|
+
import {
|
|
26
|
+
collectInstanceItemIds,
|
|
27
|
+
collectSummonedItemIds,
|
|
28
|
+
selectInstanceSkills,
|
|
29
|
+
} from '../src/api/cyberia-instance/cyberia-instance-items.js';
|
|
30
|
+
import { prepareFallbackAudio, seedFallbackAudio, seedInstanceAudio } from '../src/projects/cyberia/seed-audio.js';
|
|
31
|
+
import {
|
|
32
|
+
CyberiaMapAudioConfService,
|
|
33
|
+
parseEventAudioBinding,
|
|
34
|
+
} from '../src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js';
|
|
35
|
+
import {
|
|
36
|
+
deployEnvFilePath,
|
|
37
|
+
etcHostFactory,
|
|
38
|
+
instanceProjectPathFactory,
|
|
39
|
+
loadConfServerJson,
|
|
40
|
+
normalizeInstanceTopology,
|
|
41
|
+
} from '../src/server/runtime/conf.js';
|
|
21
42
|
import {
|
|
22
43
|
ObjectLayerEngine,
|
|
23
44
|
resolveCanonicalCid,
|
|
24
45
|
pngDirectoryIteratorByObjectLayerType,
|
|
25
46
|
buildImgFromTile,
|
|
26
47
|
} from '../src/projects/cyberia/object-layer.js';
|
|
48
|
+
import { fetchInstanceObjectLayerItemIds, getInstanceModels } from '../src/projects/cyberia/instance-data.js';
|
|
49
|
+
import { atlasBackupFileKey, restoreObjectLayerBackup } from '../src/projects/cyberia/instance-backup.js';
|
|
27
50
|
import { getKeyframeDirectionsByCode } from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
|
|
28
|
-
import {
|
|
51
|
+
import { DEFAULT_ATLAS_UPSCALE_FACTOR } from '../src/projects/cyberia/atlas-sprite-sheet-generator.js';
|
|
52
|
+
import { ATLAS_FILE_FIELDS, AtlasSpriteSheetStore } from '../src/projects/cyberia/atlas-sprite-sheet-store.js';
|
|
53
|
+
import { fileRefFields } from '../src/api/file/file.ref.js';
|
|
29
54
|
import {
|
|
30
55
|
generateMultiFrame,
|
|
31
56
|
lookupSemantic,
|
|
@@ -45,17 +70,23 @@ import {
|
|
|
45
70
|
DefaultCyberiaActions,
|
|
46
71
|
DefaultCyberiaQuests,
|
|
47
72
|
ENTITY_TYPE_DEFAULTS,
|
|
48
|
-
fillInstanceConfDefaults,
|
|
49
|
-
DOCKER_SCRIPTS,
|
|
50
|
-
CyberiaDependencies,
|
|
51
73
|
} from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
74
|
+
import cyberiaCatalog from '../src/projects/cyberia/catalog-cyberia.js';
|
|
52
75
|
|
|
53
76
|
import {
|
|
54
77
|
DEFAULT_INSTANCE_CODE,
|
|
55
78
|
ITEM_TYPES as itemTypes,
|
|
56
79
|
DefaultCyberiaItems,
|
|
57
80
|
} from '../src/client/components/cyberia/SharedDefaultsCyberia.js';
|
|
58
|
-
import {
|
|
81
|
+
import { balanceStats, resolveStatBounds, statPolicyActive } from '../src/projects/cyberia/stat-balance.js';
|
|
82
|
+
import { loadDeployCatalog } from '../src/server/build/catalog.js';
|
|
83
|
+
import {
|
|
84
|
+
DEPLOY_MANIFEST_INDENT,
|
|
85
|
+
STAGED_CLI_PACKAGE,
|
|
86
|
+
buildDeployPackageJson,
|
|
87
|
+
deployPackagePathFactory,
|
|
88
|
+
stageCliPackage,
|
|
89
|
+
} from '../src/server/build/package.js';
|
|
59
90
|
|
|
60
91
|
/**
|
|
61
92
|
* Connect to the project MongoDB instance using the standard env / conf layout.
|
|
@@ -93,9 +124,193 @@ async function connectDbForChain({ envPath, mongoHost }) {
|
|
|
93
124
|
return { ObjectLayer, host, path };
|
|
94
125
|
}
|
|
95
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Rewrites a conf backup's `entityDefaults` into the reference shape the schema now stores.
|
|
129
|
+
*
|
|
130
|
+
* Backups written before the collection became the single owner embedded whole documents in the
|
|
131
|
+
* conf. Those files are the only place that shape still exists, so it is converted here, at the
|
|
132
|
+
* boundary where they enter — nothing downstream understands anything but an id.
|
|
133
|
+
*
|
|
134
|
+
* Each embedded entry is matched against the entity-type-default documents travelling in the same
|
|
135
|
+
* backup, by entity type and live-item set. That lookup is safe precisely because it cannot see
|
|
136
|
+
* the database: it can only ever resolve to a document this instance exported. An entry with no
|
|
137
|
+
* counterpart is written to the collection so the reference it gets resolves to something.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} confData - Parsed cyberia-instance-conf.json.
|
|
140
|
+
* @param {string} backupDir - Backup root, holding cyberia-entity-type-defaults/.
|
|
141
|
+
* @param {import('mongoose').Model} CyberiaEntityTypeDefault
|
|
142
|
+
* @returns {Promise<object>} The conf, with `entityDefaults` as ids.
|
|
143
|
+
*/
|
|
144
|
+
const adoptEntityTypeDefaultRefs = async (confData, backupDir, CyberiaEntityTypeDefault) => {
|
|
145
|
+
const entries = confData.entityDefaults || [];
|
|
146
|
+
const embedded = entries.filter((entry) => entry && 'object' === typeof entry && entry.entityType);
|
|
147
|
+
if (0 === embedded.length) return confData;
|
|
148
|
+
|
|
149
|
+
const dir = `${backupDir}/cyberia-entity-type-defaults`;
|
|
150
|
+
const exported = fs.existsSync(dir)
|
|
151
|
+
? fs
|
|
152
|
+
.readdirSync(dir)
|
|
153
|
+
.filter((file) => file.endsWith('.json'))
|
|
154
|
+
.map((file) => fs.readJsonSync(`${dir}/${file}`))
|
|
155
|
+
: [];
|
|
156
|
+
const liveKey = (doc) => `${doc.entityType}::${[...(doc.liveItemIds || [])].sort().join(',')}`;
|
|
157
|
+
const byKey = new Map(exported.map((doc) => [liveKey(doc), doc]));
|
|
158
|
+
|
|
159
|
+
const ids = [];
|
|
160
|
+
let created = 0;
|
|
161
|
+
for (const entry of entries) {
|
|
162
|
+
if (!entry || 'object' !== typeof entry || !entry.entityType) {
|
|
163
|
+
if (entry) ids.push(entry);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const match = byKey.get(liveKey(entry));
|
|
167
|
+
if (match?._id) {
|
|
168
|
+
ids.push(match._id);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const doc = await CyberiaEntityTypeDefault.create({
|
|
172
|
+
entityType: entry.entityType,
|
|
173
|
+
liveItemIds: entry.liveItemIds || [],
|
|
174
|
+
deadItemIds: entry.deadItemIds || [],
|
|
175
|
+
dropItemIds: entry.dropItemIds || [],
|
|
176
|
+
inventoryItemsIds: entry.inventoryItemsIds || [],
|
|
177
|
+
overrideItemsIdsState: entry.overrideItemsIdsState || [],
|
|
178
|
+
behavior: entry.behavior || '',
|
|
179
|
+
});
|
|
180
|
+
ids.push(doc._id);
|
|
181
|
+
created++;
|
|
182
|
+
}
|
|
183
|
+
confData.entityDefaults = ids;
|
|
184
|
+
logger.info('Migrated embedded conf entityDefaults to collection references', {
|
|
185
|
+
instanceCode: confData.instanceCode,
|
|
186
|
+
references: ids.length,
|
|
187
|
+
created,
|
|
188
|
+
});
|
|
189
|
+
return confData;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** Default source of recorded `<name>.wav` + `<name>.json` pairs for `cyberia audio --import`. */
|
|
193
|
+
const DEFAULT_AUDIO_RECORDS_PATH = './cyberia-audio/records';
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Commander parser for the repeatable `<logic-event-id>:<audio-code>` flag.
|
|
197
|
+
*
|
|
198
|
+
* @function eventAudioBindingFactory
|
|
199
|
+
* @param {string} flag - Flag name, used in the usage error.
|
|
200
|
+
* @returns {(value: string, previous: Array<{event: string, code: string}>) => Array<{event: string, code: string}>} Accumulating parser.
|
|
201
|
+
* @memberof CyberiaCLI
|
|
202
|
+
*/
|
|
203
|
+
const eventAudioBindingFactory =
|
|
204
|
+
(flag) =>
|
|
205
|
+
(value, previous = []) => {
|
|
206
|
+
try {
|
|
207
|
+
return previous.concat([parseEventAudioBinding(value)]);
|
|
208
|
+
} catch {
|
|
209
|
+
throw new InvalidArgumentError(`${flag} expects <logic-event-id>:<audio-code>`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
96
213
|
/** @type {Function} */
|
|
97
214
|
const logger = loggerFactory(import.meta);
|
|
98
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Reads the comma-separated item-id argument of the `ol` command.
|
|
218
|
+
*
|
|
219
|
+
* @param {string} [itemId] - The raw command argument.
|
|
220
|
+
* @returns {string[]} Trimmed, non-empty item ids.
|
|
221
|
+
*/
|
|
222
|
+
const parseItemIds = (itemId) =>
|
|
223
|
+
itemId
|
|
224
|
+
? itemId
|
|
225
|
+
.split(',')
|
|
226
|
+
.map((id) => id.trim())
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
: [];
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Applies the `ol` stat policy (`--normalize-stats`, `--random-stats`,
|
|
232
|
+
* `--min-stat`, `--max-stat`) to one object layer before it is written.
|
|
233
|
+
* Marks the path on a Mongoose document so the save carries it; a plain
|
|
234
|
+
* payload needs no mark. A policy that changes nothing leaves the stats alone.
|
|
235
|
+
*
|
|
236
|
+
* @param {{ data: { item: { id: string, type: string }, stats: Object }, markModified?: Function }} objectLayer
|
|
237
|
+
* @param {import('../src/projects/cyberia/stat-balance.js').StatPolicy} policy
|
|
238
|
+
* @returns {boolean} Whether the stats were rewritten.
|
|
239
|
+
*/
|
|
240
|
+
const applyStatPolicy = (objectLayer, policy) => {
|
|
241
|
+
if (!statPolicyActive(policy)) return false;
|
|
242
|
+
const { stats, item } = objectLayer.data;
|
|
243
|
+
objectLayer.data.stats = balanceStats({
|
|
244
|
+
stats: typeof stats?.toObject === 'function' ? stats.toObject() : stats,
|
|
245
|
+
itemType: item.type,
|
|
246
|
+
policy,
|
|
247
|
+
});
|
|
248
|
+
if (typeof objectLayer.markModified === 'function') objectLayer.markModified('data.stats');
|
|
249
|
+
logger.info(
|
|
250
|
+
`Stats for '${objectLayer.data.item.id}' (${objectLayer.data.item.type}): ${JSON.stringify(objectLayer.data.stats)}`,
|
|
251
|
+
);
|
|
252
|
+
return true;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Finds the asset type directory that holds one item id.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} itemId - Object layer item id.
|
|
259
|
+
* @returns {{ type: string, folder: string }|null} The type and folder, or null when absent.
|
|
260
|
+
*/
|
|
261
|
+
const findAssetFolder = (itemId) => {
|
|
262
|
+
for (const type of Object.keys(itemTypes)) {
|
|
263
|
+
const folder = `./src/client/public/cyberia/assets/${type}/${itemId}`;
|
|
264
|
+
if (fs.existsSync(folder) && fs.statSync(folder).isDirectory()) return { type, folder };
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Resolves the stored item ids one `ol` action works on.
|
|
271
|
+
*
|
|
272
|
+
* The scope is the item-id argument, one instance, or the whole ObjectLayer
|
|
273
|
+
* collection. Only ids the collection holds survive, so an action can never
|
|
274
|
+
* create an object layer. Exits when the scope resolves to nothing.
|
|
275
|
+
*
|
|
276
|
+
* @param {Object} params
|
|
277
|
+
* @param {import('mongoose').Model} params.ObjectLayer - Mongoose ObjectLayer model.
|
|
278
|
+
* @param {string} [params.itemId] - The comma-separated item-id argument.
|
|
279
|
+
* @param {string} [params.instance] - Instance code from `--instance`.
|
|
280
|
+
* @param {string} params.host - Deploy host.
|
|
281
|
+
* @param {string} params.path - Deploy path.
|
|
282
|
+
* @param {string} params.action - The flag being served, for the log lines.
|
|
283
|
+
* @returns {Promise<string[]>} Item ids to work on.
|
|
284
|
+
*/
|
|
285
|
+
const selectScopedItemIds = async ({ ObjectLayer, itemId, instance, host, path, action }) => {
|
|
286
|
+
let storedItemIds;
|
|
287
|
+
if (instance) {
|
|
288
|
+
try {
|
|
289
|
+
storedItemIds = await fetchInstanceObjectLayerItemIds(getInstanceModels({ host, path }), instance);
|
|
290
|
+
} catch (instanceError) {
|
|
291
|
+
logger.error(instanceError.message);
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
logger.info(`Instance '${instance}' runs on ${storedItemIds.length} stored object layer(s)`);
|
|
295
|
+
} else {
|
|
296
|
+
const storedDocs = await ObjectLayer.find({}, { 'data.item.id': 1 }).lean();
|
|
297
|
+
storedItemIds = storedDocs.map((doc) => doc?.data?.item?.id);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { itemIds, missingItemIds } = ObjectLayerEngine.selectStoredItemIds({
|
|
301
|
+
storedItemIds,
|
|
302
|
+
requestedItemIds: parseItemIds(itemId),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const scope = instance ? `instance '${instance}'` : 'the ObjectLayer collection';
|
|
306
|
+
if (missingItemIds.length > 0) logger.warn(`Not in ${scope}, skipped: ${missingItemIds.join(', ')}`);
|
|
307
|
+
if (itemIds.length === 0) {
|
|
308
|
+
logger.error(`No object layer of ${scope} matches the requested item-id(s) for ${action}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
return itemIds;
|
|
312
|
+
};
|
|
313
|
+
|
|
99
314
|
const CYBERIA_DOCKER_HOST_ALIASES = ['cyberia-client', 'cyberia-server', 'engine-cyberia'];
|
|
100
315
|
|
|
101
316
|
const installCyberiaDockerHostAliases = () => {
|
|
@@ -114,6 +329,7 @@ const installCyberiaDockerHostAliases = () => {
|
|
|
114
329
|
|
|
115
330
|
try {
|
|
116
331
|
const program = new Command();
|
|
332
|
+
registerStatCommands(program);
|
|
117
333
|
|
|
118
334
|
/** @type {string} */
|
|
119
335
|
const version = Underpost.version;
|
|
@@ -130,14 +346,52 @@ try {
|
|
|
130
346
|
.command('ol [item-id]')
|
|
131
347
|
.option(
|
|
132
348
|
'--to-atlas-sprite-sheet [dim]',
|
|
133
|
-
'
|
|
349
|
+
'Rebuild both atlas renders of stored object layers, optionally capped to a dimension (default: auto-calculated based on frame count)',
|
|
134
350
|
)
|
|
135
351
|
.option('--show-atlas-sprite-sheet', 'Show consolidated atlas sprite sheet PNG for given item-id')
|
|
136
352
|
.option(
|
|
137
353
|
'--import',
|
|
138
|
-
'Import specific item-id(s) passed as comma-separated command argument (e.g. ol hatchet,sword --import)',
|
|
354
|
+
'Import specific item-id(s) passed as comma-separated command argument (e.g. ol hatchet,sword --instance FOREST --import); with --from-directory, from the asset directory instead',
|
|
355
|
+
)
|
|
356
|
+
.option(
|
|
357
|
+
'--from-directory',
|
|
358
|
+
'Source --import and --import-types from src/client/public/cyberia/assets/<type>/<item-id>/<direction>/<frame>.png',
|
|
359
|
+
)
|
|
360
|
+
.option(
|
|
361
|
+
'--minify',
|
|
362
|
+
'Refresh the minified atlas render the client downloads, for stored object layers (e.g. ol hatchet --minify, or ol --minify for all)',
|
|
363
|
+
)
|
|
364
|
+
.option(
|
|
365
|
+
'--instance <instance-code>',
|
|
366
|
+
'Limit --minify and --to-atlas-sprite-sheet to the object layers one instance runs on, or make --import restore item(s) from that instance backup under engine-private (e.g. ol hatchet --instance FOREST --import)',
|
|
367
|
+
)
|
|
368
|
+
.option(
|
|
369
|
+
'--normalize-stats',
|
|
370
|
+
'Clamp every stat of each object layer the action writes into the semantic bounds of its item type',
|
|
371
|
+
)
|
|
372
|
+
.option(
|
|
373
|
+
'--random-stats',
|
|
374
|
+
'Regenerate every stat of each object layer the action writes, with random signed modifiers',
|
|
375
|
+
)
|
|
376
|
+
.option(
|
|
377
|
+
'--min-stat <value>',
|
|
378
|
+
'Lowest value --random-stats or --normalize-stats may leave (default: -100)',
|
|
379
|
+
parseInt,
|
|
380
|
+
)
|
|
381
|
+
.option(
|
|
382
|
+
'--max-stat <value>',
|
|
383
|
+
'Highest value --random-stats or --normalize-stats may leave (default: 100)',
|
|
384
|
+
parseInt,
|
|
385
|
+
)
|
|
386
|
+
.option(
|
|
387
|
+
'--upscale <px-factor>',
|
|
388
|
+
`Pixels per cell of the human-resolution atlas render; on its own it rebuilds that render (default: ${DEFAULT_ATLAS_UPSCALE_FACTOR})`,
|
|
389
|
+
parseInt,
|
|
390
|
+
)
|
|
391
|
+
.option(
|
|
392
|
+
'--import-types [object-layer-type]',
|
|
393
|
+
'Batch import by object layer type from the asset directory, needs --from-directory (e.g. skin,floors or all)',
|
|
139
394
|
)
|
|
140
|
-
.option('--import-types [object-layer-type]', 'Batch import by object layer type e.g. skin,floors or all')
|
|
141
395
|
.option('--show-frame [direction-frame]', 'View object layer frame for given item-id e.g. 08_0 (default: 08_0)')
|
|
142
396
|
.option('--generate', 'Generate procedural object layers from semantic item-id (e.g. floor-desert)')
|
|
143
397
|
.option('--count <count>', 'Shape element count multiplier for --generate (default: 3)', parseFloat)
|
|
@@ -147,7 +401,6 @@ try {
|
|
|
147
401
|
.option('--density <density>', 'Density factor 0..1 for --generate (default: 0.5)', parseFloat)
|
|
148
402
|
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
149
403
|
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
150
|
-
.option('--storage-file-path <storage-file-path>', 'Storage file path override')
|
|
151
404
|
.option('--drop', 'Drop existing data before importing')
|
|
152
405
|
.option('--client-public', 'When used with --drop, also remove static asset folders for dropped items')
|
|
153
406
|
.option('--git-clean', 'When used with --drop, run underpost clean on the cyberia asset directory')
|
|
@@ -160,11 +413,18 @@ try {
|
|
|
160
413
|
* @param {string|undefined} itemId - Optional item ID argument.
|
|
161
414
|
* @param {Object} options - Command options parsed by Commander.
|
|
162
415
|
* @param {boolean} options.import - Import specific item-id(s) from the command argument (comma-separated).
|
|
416
|
+
* @param {boolean} options.fromDirectory - Source --import and --import-types from the asset directory.
|
|
417
|
+
* @param {boolean} options.minify - Refresh the minified atlas render of stored item(s).
|
|
418
|
+
* @param {string} options.instance - Instance code whose object layers --minify reprocesses.
|
|
419
|
+
* @param {boolean} options.normalizeStats - Clamp the stats of every object layer the action writes to its type's bounds.
|
|
420
|
+
* @param {boolean} options.randomStats - Regenerate the stats of every object layer the action writes.
|
|
421
|
+
* @param {number} [options.minStat] - Lowest value --random-stats may draw.
|
|
422
|
+
* @param {number} [options.maxStat] - Highest value --random-stats may draw.
|
|
423
|
+
* @param {number} options.upscale - Pixels per cell of the human-resolution atlas render.
|
|
163
424
|
* @param {boolean|string} options.importTypes - Object layer types to batch import (e.g., 'all', 'skin,floor') or `false`.
|
|
164
425
|
* @param {boolean|string} options.showFrame - Direction-frame string (e.g., '08_0') or `true` for default.
|
|
165
426
|
* @param {string} options.envPath - Path to the `.env` file.
|
|
166
427
|
* @param {string} options.mongoHost - MongoDB host override.
|
|
167
|
-
* @param {string} options.storageFilePath - Path to a storage filter JSON file.
|
|
168
428
|
* @param {boolean|string} options.toAtlasSpriteSheet - Atlas dimension or `true` for auto-calc.
|
|
169
429
|
* @param {boolean} options.showAtlasSpriteSheet - Whether to display the atlas sprite sheet.
|
|
170
430
|
* @param {boolean} options.drop - Whether to drop existing data before importing.
|
|
@@ -184,11 +444,16 @@ try {
|
|
|
184
444
|
itemId,
|
|
185
445
|
options = {
|
|
186
446
|
import: false,
|
|
447
|
+
fromDirectory: false,
|
|
448
|
+
minify: false,
|
|
449
|
+
instance: '',
|
|
450
|
+
upscale: DEFAULT_ATLAS_UPSCALE_FACTOR,
|
|
187
451
|
importTypes: false,
|
|
452
|
+
normalizeStats: false,
|
|
453
|
+
randomStats: false,
|
|
188
454
|
showFrame: '',
|
|
189
455
|
envPath: '',
|
|
190
456
|
mongoHost: '',
|
|
191
|
-
storageFilePath: '',
|
|
192
457
|
toAtlasSpriteSheet: '',
|
|
193
458
|
showAtlasSpriteSheet: false,
|
|
194
459
|
drop: false,
|
|
@@ -203,6 +468,12 @@ try {
|
|
|
203
468
|
density: 0.5,
|
|
204
469
|
},
|
|
205
470
|
) => {
|
|
471
|
+
const upscaleFactor = options.upscale ?? DEFAULT_ATLAS_UPSCALE_FACTOR;
|
|
472
|
+
if (!Number.isInteger(upscaleFactor) || upscaleFactor < 1) {
|
|
473
|
+
logger.error('--upscale takes a whole pixel factor of 1 or more');
|
|
474
|
+
process.exit(1);
|
|
475
|
+
}
|
|
476
|
+
|
|
206
477
|
if (!options.envPath) options.envPath = `./.env`;
|
|
207
478
|
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
208
479
|
|
|
@@ -238,8 +509,21 @@ try {
|
|
|
238
509
|
path,
|
|
239
510
|
});
|
|
240
511
|
|
|
512
|
+
// --instance reads the world the runtime reads, so its content collections load too.
|
|
513
|
+
const instanceApis = options.instance
|
|
514
|
+
? [
|
|
515
|
+
'cyberia-instance',
|
|
516
|
+
'cyberia-instance-conf',
|
|
517
|
+
'cyberia-map',
|
|
518
|
+
'cyberia-quest',
|
|
519
|
+
'cyberia-action',
|
|
520
|
+
'cyberia-skill',
|
|
521
|
+
'cyberia-entity-type-default',
|
|
522
|
+
]
|
|
523
|
+
: [];
|
|
524
|
+
|
|
241
525
|
await DataBaseProviderService.load({
|
|
242
|
-
apis: ['object-layer', 'object-layer-render-frames', 'atlas-sprite-sheet', 'file', 'ipfs'],
|
|
526
|
+
apis: ['object-layer', 'object-layer-render-frames', 'atlas-sprite-sheet', 'file', 'ipfs', ...instanceApis],
|
|
243
527
|
host,
|
|
244
528
|
path,
|
|
245
529
|
db,
|
|
@@ -256,12 +540,47 @@ try {
|
|
|
256
540
|
/** @type {import('mongoose').Model} */
|
|
257
541
|
const Ipfs = DataBaseProviderService.getModel('ipfs', { host, path });
|
|
258
542
|
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
543
|
+
// A model handle binds to one connection, and the health monitor replaces that
|
|
544
|
+
// connection when it drops. A batch that runs for minutes therefore resolves its
|
|
545
|
+
// model per item instead of holding the handle it started with.
|
|
546
|
+
const liveObjectLayer = () => DataBaseProviderService.getModel('object-layer', { host, path });
|
|
547
|
+
|
|
548
|
+
const rebuildAtlases = ObjectLayerEngine.selectAtlasRebuild(options);
|
|
549
|
+
|
|
550
|
+
/* Bounds fail here, before any write, rather than on the first item. */
|
|
551
|
+
const statPolicy = {
|
|
552
|
+
normalize: !!options.normalizeStats,
|
|
553
|
+
random: !!options.randomStats,
|
|
554
|
+
min: options.minStat,
|
|
555
|
+
max: options.maxStat,
|
|
556
|
+
};
|
|
557
|
+
if (statPolicyActive(statPolicy)) {
|
|
558
|
+
try {
|
|
559
|
+
resolveStatBounds('', statPolicy);
|
|
560
|
+
} catch (boundsError) {
|
|
561
|
+
logger.error(`--min-stat/--max-stat: ${boundsError.message}`);
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
} else if (statPolicy.min !== undefined || statPolicy.max !== undefined) {
|
|
565
|
+
logger.warn('--min-stat and --max-stat only bound --random-stats and --normalize-stats, ignored');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (options.instance && !options.minify && !rebuildAtlases && !options.import) {
|
|
569
|
+
logger.warn('--instance only narrows --minify and --to-atlas-sprite-sheet, or sources --import, ignored');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Idempotent repair, run only before a flow that writes: collapse
|
|
573
|
+
// duplicates and make the data.item.id index unique, so every later
|
|
574
|
+
// write lands on one document. A read-only subcommand stays free of
|
|
575
|
+
// side effects; findByItemId resolves the same canonical document.
|
|
576
|
+
if (
|
|
577
|
+
options.import ||
|
|
578
|
+
options.minify ||
|
|
579
|
+
options.importTypes ||
|
|
580
|
+
options.drop ||
|
|
581
|
+
options.generate ||
|
|
582
|
+
rebuildAtlases
|
|
583
|
+
) {
|
|
265
584
|
const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
|
|
266
585
|
if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
|
|
267
586
|
if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
|
|
@@ -283,9 +602,9 @@ try {
|
|
|
283
602
|
logger.info('Dropping ALL object layer data');
|
|
284
603
|
}
|
|
285
604
|
|
|
286
|
-
// Build query filter: targeted or all
|
|
605
|
+
// Build query filter: targeted or all. The atlas side of the drop is selected by
|
|
606
|
+
// the store, from the item keys and the links collected below.
|
|
287
607
|
const olFilter = isTargetedDrop ? { 'data.item.id': { $in: dropItemIds } } : {};
|
|
288
|
-
const atlasFilter = isTargetedDrop ? { 'metadata.itemKey': { $in: dropItemIds } } : {};
|
|
289
608
|
|
|
290
609
|
// Collect data before deletion
|
|
291
610
|
const olDocs = await ObjectLayer.find(olFilter, {
|
|
@@ -296,7 +615,6 @@ try {
|
|
|
296
615
|
objectLayerRenderFramesId: 1,
|
|
297
616
|
atlasSpriteSheetId: 1,
|
|
298
617
|
}).lean();
|
|
299
|
-
const atlasDocs = await AtlasSpriteSheet.find(atlasFilter, { fileId: 1, cid: 1 }).lean();
|
|
300
618
|
|
|
301
619
|
const cidsToUnpin = new Set();
|
|
302
620
|
const itemIdsToClean = new Set();
|
|
@@ -312,34 +630,36 @@ try {
|
|
|
312
630
|
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
313
631
|
}
|
|
314
632
|
|
|
315
|
-
const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
|
|
316
|
-
for (const atlas of atlasDocs) {
|
|
317
|
-
if (atlas.cid) cidsToUnpin.add(atlas.cid);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
633
|
const olCount = olDocs.length;
|
|
321
|
-
const atlasCount = atlasDocs.length;
|
|
322
634
|
|
|
323
635
|
// Delete targeted documents
|
|
324
636
|
if (isTargetedDrop) {
|
|
325
637
|
const olIds = olDocs.map((d) => d._id);
|
|
326
638
|
if (olIds.length > 0) await ObjectLayer.deleteMany({ _id: { $in: olIds } });
|
|
327
639
|
if (renderFrameIds.length > 0) await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
|
|
328
|
-
if (atlasIds.length > 0) await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
329
640
|
} else {
|
|
330
641
|
await ObjectLayer.deleteMany();
|
|
331
642
|
await ObjectLayerRenderFrames.deleteMany();
|
|
332
|
-
await AtlasSpriteSheet.deleteMany();
|
|
333
643
|
}
|
|
334
644
|
|
|
335
645
|
const rfCount = renderFrameIds.length;
|
|
336
646
|
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
647
|
+
// The atlas owns two renders and the store is what knows that: a drop naming
|
|
648
|
+
// `fileId` alone left every minified render unreachable in the File collection.
|
|
649
|
+
// Both selectors are passed, so an atlas linked by the object layer and one
|
|
650
|
+
// matching the item key are the same drop.
|
|
651
|
+
const purged = await AtlasSpriteSheetStore.purge({
|
|
652
|
+
itemKeys: isTargetedDrop ? dropItemIds : [],
|
|
653
|
+
atlasIds: isTargetedDrop ? atlasIds : [],
|
|
654
|
+
all: !isTargetedDrop,
|
|
655
|
+
options: { host, path },
|
|
656
|
+
});
|
|
657
|
+
for (const cid of purged.cids) cidsToUnpin.add(cid);
|
|
658
|
+
const atlasCount = purged.atlases;
|
|
659
|
+
// Renders an earlier drop left behind are unreachable by definition, so this
|
|
660
|
+
// drop takes them too instead of letting them accumulate.
|
|
661
|
+
const fileCount =
|
|
662
|
+
purged.files + (await AtlasSpriteSheetStore.pruneOrphanRenders({ options: { host, path } }));
|
|
343
663
|
|
|
344
664
|
// Delete IPFS pin registry records for all collected CIDs
|
|
345
665
|
if (cidsToUnpin.size > 0) {
|
|
@@ -366,7 +686,7 @@ try {
|
|
|
366
686
|
`IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemIdsToClean.size} MFS paths removed`,
|
|
367
687
|
);
|
|
368
688
|
if (options.gitClean) {
|
|
369
|
-
shellExec(`cd src/client/public/cyberia &&
|
|
689
|
+
shellExec(`cd src/client/public/cyberia && ${cli()} run clean .`);
|
|
370
690
|
logger.info('Asset directory cleaned');
|
|
371
691
|
}
|
|
372
692
|
|
|
@@ -392,374 +712,166 @@ try {
|
|
|
392
712
|
}
|
|
393
713
|
}
|
|
394
714
|
|
|
395
|
-
|
|
396
|
-
|
|
715
|
+
// ── Handle --minify (stored item-id(s)) ──────────────────────────
|
|
716
|
+
// Refreshes only the minified atlas render, the one the client runtime
|
|
717
|
+
// downloads. It reads its item ids from the collection, so it never
|
|
718
|
+
// creates an object layer. With --random-stats every stored document
|
|
719
|
+
// in scope is rewritten, whether or not its render can be refreshed.
|
|
720
|
+
if (options.minify) {
|
|
721
|
+
const selectedItemIds = await selectScopedItemIds({
|
|
722
|
+
ObjectLayer,
|
|
723
|
+
itemId,
|
|
724
|
+
instance: options.instance,
|
|
725
|
+
host,
|
|
726
|
+
path,
|
|
727
|
+
action: '--minify',
|
|
728
|
+
});
|
|
397
729
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
730
|
+
logger.info(`Minify refresh for ${selectedItemIds.length} stored item(s)`);
|
|
731
|
+
const tally = { updated: 0, unchanged: 0, missing: 0, stale: 0, failed: [] };
|
|
732
|
+
|
|
733
|
+
// Isolated per item, for the same reason the atlas rebuild is.
|
|
734
|
+
for (const currentItemId of selectedItemIds) {
|
|
735
|
+
try {
|
|
736
|
+
const objectLayer = await liveObjectLayer()
|
|
737
|
+
.findByItemId(currentItemId)
|
|
738
|
+
.populate('objectLayerRenderFramesId');
|
|
739
|
+
if (objectLayer && applyStatPolicy(objectLayer, statPolicy)) {
|
|
740
|
+
await objectLayer.save();
|
|
741
|
+
await ObjectLayerEngine.computeAndSaveFinalSha256({ objectLayer, options: { host, path } });
|
|
742
|
+
}
|
|
743
|
+
if (!objectLayer?.objectLayerRenderFramesId) {
|
|
744
|
+
logger.warn(`No render frames stored for '${currentItemId}', render skipped`);
|
|
745
|
+
tally.missing++;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const { status } = await AtlasSpriteSheetStore.syncMinifyRender({
|
|
750
|
+
itemKey: currentItemId,
|
|
751
|
+
objectLayerRenderFrames: objectLayer.objectLayerRenderFramesId,
|
|
752
|
+
options: { host, path },
|
|
753
|
+
});
|
|
754
|
+
tally[status]++;
|
|
755
|
+
|
|
756
|
+
if (status === 'missing') logger.warn(`No atlas stored for '${currentItemId}'; generate it first`);
|
|
757
|
+
else if (status === 'stale')
|
|
758
|
+
logger.warn(`Render frames of '${currentItemId}' moved the atlas layout; regenerate the atlas`);
|
|
759
|
+
else logger.info(`Minified render ${status} for '${currentItemId}'`);
|
|
760
|
+
|
|
761
|
+
const still = await AtlasSpriteSheetStore.syncIdlePreview({
|
|
762
|
+
itemKey: currentItemId,
|
|
763
|
+
options: { host, path },
|
|
764
|
+
});
|
|
765
|
+
if (still.status !== 'missing') logger.info(`Idle preview ${still.status} for '${currentItemId}'`);
|
|
766
|
+
} catch (minifyError) {
|
|
767
|
+
logger.error(`Minify failed for '${currentItemId}': ${minifyError.message}`);
|
|
768
|
+
tally.failed.push(currentItemId);
|
|
769
|
+
}
|
|
403
770
|
}
|
|
404
771
|
|
|
405
|
-
|
|
406
|
-
.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
772
|
+
logger.info(
|
|
773
|
+
`Minify done: ${tally.updated} updated, ${tally.unchanged} unchanged, ` +
|
|
774
|
+
`${tally.stale} stale, ${tally.missing} without an atlas, ${tally.failed.length} failed`,
|
|
775
|
+
);
|
|
776
|
+
if (tally.failed.length > 0) {
|
|
777
|
+
logger.warn(`Rerun for the failed item(s): ${tally.failed.join(',')}`);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
410
780
|
|
|
781
|
+
// ── Handle --import --instance: restore item(s) from the instance backup ──
|
|
782
|
+
// The backup under engine-private is the authority for the item id: every document it
|
|
783
|
+
// holds for the item replaces the database's, rather than regenerating from the asset
|
|
784
|
+
// directory. The item never touches the stat policy — the backup already states its stats.
|
|
785
|
+
if (options.import && options.instance) {
|
|
786
|
+
const itemIds = parseItemIds(itemId);
|
|
787
|
+
if (itemIds.length === 0) {
|
|
788
|
+
logger.error(
|
|
789
|
+
'item-id is required for --import --instance (e.g. ol hatchet,sword --instance FOREST --import)',
|
|
790
|
+
);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
const backupDir = `./engine-private/cyberia-instances/${options.instance}`;
|
|
794
|
+
if (!fs.existsSync(backupDir)) {
|
|
795
|
+
logger.error(`No instance backup at ${backupDir}`);
|
|
796
|
+
process.exit(1);
|
|
797
|
+
}
|
|
798
|
+
logger.info(`Restoring ${itemIds.length} item(s) from instance backup '${options.instance}'`);
|
|
799
|
+
let restored = 0;
|
|
411
800
|
for (const currentItemId of itemIds) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
801
|
+
try {
|
|
802
|
+
const summary = await restoreObjectLayerBackup({
|
|
803
|
+
backupDir,
|
|
804
|
+
itemId: currentItemId,
|
|
805
|
+
options: { host, path },
|
|
806
|
+
});
|
|
807
|
+
logger.info(`Restored '${currentItemId}' from backup`, summary);
|
|
808
|
+
restored++;
|
|
809
|
+
} catch (restoreError) {
|
|
810
|
+
logger.error(`Restore failed for '${currentItemId}': ${restoreError.message}`);
|
|
422
811
|
}
|
|
812
|
+
}
|
|
813
|
+
logger.info(`Instance restore done: ${restored}/${itemIds.length} item(s)`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (options.import && !options.instance === !options.fromDirectory) {
|
|
817
|
+
logger.error(
|
|
818
|
+
'--import takes exactly one source: --instance <code> for a backup, or --from-directory for the asset tree',
|
|
819
|
+
);
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
if (options.importTypes && !options.fromDirectory) {
|
|
823
|
+
logger.error('--import-types reads the asset tree and needs --from-directory');
|
|
824
|
+
process.exit(1);
|
|
825
|
+
}
|
|
423
826
|
|
|
424
|
-
|
|
827
|
+
// ── Handle --import --from-directory (specific item-id(s)) ────────
|
|
828
|
+
if (options.import && options.fromDirectory) {
|
|
829
|
+
const itemIds = parseItemIds(itemId);
|
|
830
|
+
if (itemIds.length === 0) {
|
|
831
|
+
logger.error(
|
|
832
|
+
'item-id is required for --import --from-directory (comma-separated item IDs, e.g. ol hatchet,sword --from-directory --import)',
|
|
833
|
+
);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
logger.info(`Importing specific item(s) from the asset directory: ${itemIds.join(', ')}`);
|
|
837
|
+
|
|
838
|
+
for (const currentItemId of itemIds) {
|
|
839
|
+
const found = findAssetFolder(currentItemId);
|
|
840
|
+
if (!found) {
|
|
425
841
|
logger.error(
|
|
426
842
|
`Item-id '${currentItemId}' not found in any asset type directory (${Object.keys(itemTypes).join(', ')})`,
|
|
427
843
|
);
|
|
428
844
|
continue;
|
|
429
845
|
}
|
|
430
|
-
|
|
431
|
-
logger.info(`Found item '${currentItemId}' in type '${foundType}' at ${foundFolder}`);
|
|
846
|
+
logger.info(`Found item '${currentItemId}' in type '${found.type}' at ${found.folder}`);
|
|
432
847
|
|
|
433
848
|
const { objectLayerRenderFramesData, objectLayerData } =
|
|
434
849
|
await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
|
|
435
|
-
folder:
|
|
436
|
-
objectLayerType:
|
|
850
|
+
folder: found.folder,
|
|
851
|
+
objectLayerType: found.type,
|
|
437
852
|
objectLayerId: currentItemId,
|
|
438
853
|
});
|
|
854
|
+
applyStatPolicy(objectLayerData, statPolicy);
|
|
439
855
|
|
|
440
856
|
// Write processed frames back to disk so WebP matches atlas
|
|
441
|
-
const srcBasePath = './src/client/public/cyberia/';
|
|
442
|
-
const publicBasePath = `./public/${host}${path}`;
|
|
443
857
|
await ObjectLayerEngine.writeStaticFrameAssets({
|
|
444
|
-
basePaths: [
|
|
445
|
-
itemType:
|
|
858
|
+
basePaths: ['./src/client/public/cyberia/', `./public/${host}${path}`],
|
|
859
|
+
itemType: found.type,
|
|
446
860
|
itemId: currentItemId,
|
|
447
861
|
objectLayerRenderFramesData,
|
|
448
862
|
objectLayerData,
|
|
449
|
-
cellPixelDim:
|
|
863
|
+
cellPixelDim: upscaleFactor,
|
|
450
864
|
});
|
|
451
865
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// 1. Prepare staging data entirely in memory (no DB writes yet)
|
|
461
|
-
const stagingData = JSON.parse(JSON.stringify(objectLayerData.data));
|
|
462
|
-
if (!stagingData.render) stagingData.render = {};
|
|
463
|
-
stagingData.render.cid = '';
|
|
464
|
-
stagingData.render.metadataCid = '';
|
|
465
|
-
|
|
466
|
-
// 2. Generate atlas, pin to IPFS, compute SHA-256 — all in memory
|
|
467
|
-
let cutoverReady = false;
|
|
468
|
-
let stagingFileDoc = null;
|
|
469
|
-
let stagingAtlasDoc = null;
|
|
470
|
-
let stagingCid = '';
|
|
471
|
-
try {
|
|
472
|
-
const itemKey = currentItemId;
|
|
473
|
-
|
|
474
|
-
// Generate atlas from in-memory render frames data (plain object, no DB doc needed)
|
|
475
|
-
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
476
|
-
objectLayerRenderFramesData,
|
|
477
|
-
itemKey,
|
|
478
|
-
20,
|
|
479
|
-
);
|
|
480
|
-
|
|
481
|
-
stagingFileDoc = await new File({
|
|
482
|
-
name: `${itemKey}-atlas.png`,
|
|
483
|
-
data: buffer,
|
|
484
|
-
size: buffer.length,
|
|
485
|
-
mimetype: 'image/png',
|
|
486
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
487
|
-
}).save();
|
|
488
|
-
|
|
489
|
-
let importItemCid = '';
|
|
490
|
-
let importItemMetadataCid = '';
|
|
491
|
-
try {
|
|
492
|
-
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
493
|
-
buffer,
|
|
494
|
-
`${itemKey}_atlas_sprite_sheet.png`,
|
|
495
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
496
|
-
);
|
|
497
|
-
if (ipfsResult) {
|
|
498
|
-
importItemCid = ipfsResult.cid;
|
|
499
|
-
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importItemCid}`);
|
|
500
|
-
try {
|
|
501
|
-
await createPinRecord({
|
|
502
|
-
cid: importItemCid,
|
|
503
|
-
resourceType: 'atlas-sprite-sheet',
|
|
504
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
505
|
-
options: { host, path },
|
|
506
|
-
});
|
|
507
|
-
} catch (prErr) {
|
|
508
|
-
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
} catch (ipfsError) {
|
|
512
|
-
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
517
|
-
metadata,
|
|
518
|
-
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
519
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
520
|
-
);
|
|
521
|
-
if (metadataIpfsResult) {
|
|
522
|
-
importItemMetadataCid = metadataIpfsResult.cid;
|
|
523
|
-
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importItemMetadataCid}`);
|
|
524
|
-
try {
|
|
525
|
-
await createPinRecord({
|
|
526
|
-
cid: importItemMetadataCid,
|
|
527
|
-
resourceType: 'atlas-metadata',
|
|
528
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
529
|
-
options: { host, path },
|
|
530
|
-
});
|
|
531
|
-
} catch (prErr) {
|
|
532
|
-
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
} catch (ipfsError) {
|
|
536
|
-
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// Persist atlas doc (or update existing one for this itemKey)
|
|
540
|
-
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
541
|
-
if (stagingAtlasDoc) {
|
|
542
|
-
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
543
|
-
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
544
|
-
stagingAtlasDoc.cid = importItemCid;
|
|
545
|
-
stagingAtlasDoc.metadata = metadata;
|
|
546
|
-
await stagingAtlasDoc.save();
|
|
547
|
-
} else {
|
|
548
|
-
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
549
|
-
fileId: stagingFileDoc._id,
|
|
550
|
-
cid: importItemCid,
|
|
551
|
-
metadata,
|
|
552
|
-
}).save();
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// Finalize staging data in memory with render CIDs
|
|
556
|
-
stagingData.render.cid = importItemCid;
|
|
557
|
-
stagingData.render.metadataCid = importItemMetadataCid;
|
|
558
|
-
|
|
559
|
-
// Pin data JSON to IPFS
|
|
560
|
-
try {
|
|
561
|
-
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
562
|
-
stagingData,
|
|
563
|
-
`${itemKey}_data.json`,
|
|
564
|
-
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
565
|
-
);
|
|
566
|
-
if (ipfsDataResult) {
|
|
567
|
-
stagingCid = ipfsDataResult.cid;
|
|
568
|
-
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
569
|
-
try {
|
|
570
|
-
await createPinRecord({
|
|
571
|
-
cid: stagingCid,
|
|
572
|
-
resourceType: 'object-layer-data',
|
|
573
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
574
|
-
options: { host, path },
|
|
575
|
-
});
|
|
576
|
-
} catch (prErr) {
|
|
577
|
-
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
} catch (ipfsError) {
|
|
581
|
-
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
cutoverReady = true;
|
|
585
|
-
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for cut-over`);
|
|
586
|
-
} catch (atlasError) {
|
|
587
|
-
logger.error(`[staging] Failed for ${currentItemId}, live document untouched:`, atlasError);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// 3. Atomic cut-over: create new RenderFrames, swap live ObjectLayer in a single update
|
|
591
|
-
if (cutoverReady) {
|
|
592
|
-
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
593
|
-
|
|
594
|
-
// Create the new RenderFrames doc (only now touches DB)
|
|
595
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
596
|
-
|
|
597
|
-
// Single atomic upsert on the natural key; empty staged CIDs keep their stored value
|
|
598
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
599
|
-
data: stagingData,
|
|
600
|
-
cid: stagingCid,
|
|
601
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
602
|
-
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
// Clean up old render frames
|
|
606
|
-
if (oldRenderFramesId) {
|
|
607
|
-
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
|
|
611
|
-
} else {
|
|
612
|
-
// Rollback: only File/AtlasSpriteSheet were written, clean those up
|
|
613
|
-
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
614
|
-
logger.warn(`[cut-over] Staging rolled back for ${currentItemId}, live document preserved`);
|
|
615
|
-
objectLayer = await ObjectLayer.findByItemId(currentItemId);
|
|
616
|
-
}
|
|
617
|
-
} else {
|
|
618
|
-
// ── New item: stage everything before creating (same cut-over pattern) ──
|
|
619
|
-
logger.info(`ObjectLayer '${currentItemId}' is new, staging creation...`);
|
|
620
|
-
|
|
621
|
-
const itemKey = currentItemId;
|
|
622
|
-
const stagingData = JSON.parse(JSON.stringify(objectLayerData.data));
|
|
623
|
-
if (!stagingData.render) stagingData.render = {};
|
|
624
|
-
stagingData.render.cid = '';
|
|
625
|
-
stagingData.render.metadataCid = '';
|
|
626
|
-
|
|
627
|
-
let cutoverReady = false;
|
|
628
|
-
let stagingFileDoc = null;
|
|
629
|
-
let stagingAtlasDoc = null;
|
|
630
|
-
let stagingCid = '';
|
|
631
|
-
try {
|
|
632
|
-
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
633
|
-
objectLayerRenderFramesData,
|
|
634
|
-
itemKey,
|
|
635
|
-
20,
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
stagingFileDoc = await new File({
|
|
639
|
-
name: `${itemKey}-atlas.png`,
|
|
640
|
-
data: buffer,
|
|
641
|
-
size: buffer.length,
|
|
642
|
-
mimetype: 'image/png',
|
|
643
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
644
|
-
}).save();
|
|
645
|
-
|
|
646
|
-
let importItemCid = '';
|
|
647
|
-
let importItemMetadataCid = '';
|
|
648
|
-
try {
|
|
649
|
-
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
650
|
-
buffer,
|
|
651
|
-
`${itemKey}_atlas_sprite_sheet.png`,
|
|
652
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
653
|
-
);
|
|
654
|
-
if (ipfsResult) {
|
|
655
|
-
importItemCid = ipfsResult.cid;
|
|
656
|
-
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importItemCid}`);
|
|
657
|
-
try {
|
|
658
|
-
await createPinRecord({
|
|
659
|
-
cid: importItemCid,
|
|
660
|
-
resourceType: 'atlas-sprite-sheet',
|
|
661
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
662
|
-
options: { host, path },
|
|
663
|
-
});
|
|
664
|
-
} catch (prErr) {
|
|
665
|
-
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
} catch (ipfsError) {
|
|
669
|
-
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
674
|
-
metadata,
|
|
675
|
-
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
676
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
677
|
-
);
|
|
678
|
-
if (metadataIpfsResult) {
|
|
679
|
-
importItemMetadataCid = metadataIpfsResult.cid;
|
|
680
|
-
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importItemMetadataCid}`);
|
|
681
|
-
try {
|
|
682
|
-
await createPinRecord({
|
|
683
|
-
cid: importItemMetadataCid,
|
|
684
|
-
resourceType: 'atlas-metadata',
|
|
685
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
686
|
-
options: { host, path },
|
|
687
|
-
});
|
|
688
|
-
} catch (prErr) {
|
|
689
|
-
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
} catch (ipfsError) {
|
|
693
|
-
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
697
|
-
if (stagingAtlasDoc) {
|
|
698
|
-
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
699
|
-
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
700
|
-
stagingAtlasDoc.cid = importItemCid;
|
|
701
|
-
stagingAtlasDoc.metadata = metadata;
|
|
702
|
-
await stagingAtlasDoc.save();
|
|
703
|
-
} else {
|
|
704
|
-
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
705
|
-
fileId: stagingFileDoc._id,
|
|
706
|
-
cid: importItemCid,
|
|
707
|
-
metadata,
|
|
708
|
-
}).save();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
stagingData.render.cid = importItemCid;
|
|
712
|
-
stagingData.render.metadataCid = importItemMetadataCid;
|
|
713
|
-
|
|
714
|
-
try {
|
|
715
|
-
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
716
|
-
stagingData,
|
|
717
|
-
`${itemKey}_data.json`,
|
|
718
|
-
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
719
|
-
);
|
|
720
|
-
if (ipfsDataResult) {
|
|
721
|
-
stagingCid = ipfsDataResult.cid;
|
|
722
|
-
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
723
|
-
try {
|
|
724
|
-
await createPinRecord({
|
|
725
|
-
cid: stagingCid,
|
|
726
|
-
resourceType: 'object-layer-data',
|
|
727
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
728
|
-
options: { host, path },
|
|
729
|
-
});
|
|
730
|
-
} catch (prErr) {
|
|
731
|
-
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
} catch (ipfsError) {
|
|
735
|
-
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
cutoverReady = true;
|
|
739
|
-
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for creation`);
|
|
740
|
-
} catch (atlasError) {
|
|
741
|
-
logger.error(`[staging] Failed for ${currentItemId}, no document created:`, atlasError);
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
if (cutoverReady) {
|
|
745
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
746
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
747
|
-
data: stagingData,
|
|
748
|
-
cid: stagingCid,
|
|
749
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
750
|
-
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
751
|
-
});
|
|
752
|
-
logger.info(`[cut-over] New ObjectLayer ${objectLayer._id} created with all CIDs populated`);
|
|
753
|
-
} else {
|
|
754
|
-
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
755
|
-
logger.warn(`[cut-over] Staging failed for ${currentItemId}, no ObjectLayer created`);
|
|
756
|
-
continue;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
866
|
+
const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
|
|
867
|
+
ObjectLayer,
|
|
868
|
+
ObjectLayerRenderFrames,
|
|
869
|
+
objectLayerRenderFramesData,
|
|
870
|
+
objectLayerData,
|
|
871
|
+
persistOptions: { upscaleFactor, options: { host, path } },
|
|
872
|
+
});
|
|
759
873
|
|
|
760
|
-
|
|
761
|
-
const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
|
|
762
|
-
console.log(finalObjectLayer.toObject());
|
|
874
|
+
console.log((await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId')).toObject());
|
|
763
875
|
}
|
|
764
876
|
}
|
|
765
877
|
|
|
@@ -794,12 +906,6 @@ try {
|
|
|
794
906
|
await pngDirectoryIteratorByObjectLayerType(
|
|
795
907
|
argItemType,
|
|
796
908
|
async ({ path: framePath, objectLayerType, objectLayerId, direction, frame }) => {
|
|
797
|
-
if (
|
|
798
|
-
storage &&
|
|
799
|
-
!storage[`src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/08/0.png`]
|
|
800
|
-
)
|
|
801
|
-
return;
|
|
802
|
-
|
|
803
909
|
// Skip items that already exist in the database (bulk import only)
|
|
804
910
|
if (isImportAll && existingItemIds.has(objectLayerId)) return;
|
|
805
911
|
|
|
@@ -814,6 +920,7 @@ try {
|
|
|
814
920
|
objectLayerType,
|
|
815
921
|
objectLayerId,
|
|
816
922
|
});
|
|
923
|
+
applyStatPolicy(objectLayerData, statPolicy);
|
|
817
924
|
|
|
818
925
|
// Write processed frames back to disk so WebP matches atlas
|
|
819
926
|
const srcBasePath = './src/client/public/cyberia/';
|
|
@@ -824,14 +931,10 @@ try {
|
|
|
824
931
|
itemId: objectLayerId,
|
|
825
932
|
objectLayerRenderFramesData,
|
|
826
933
|
objectLayerData,
|
|
827
|
-
cellPixelDim:
|
|
934
|
+
cellPixelDim: upscaleFactor,
|
|
828
935
|
});
|
|
829
936
|
|
|
830
|
-
objectLayers[objectLayerId] = {
|
|
831
|
-
...objectLayerData,
|
|
832
|
-
objectLayerRenderFramesData,
|
|
833
|
-
_processed: true,
|
|
834
|
-
};
|
|
937
|
+
objectLayers[objectLayerId] = { ...objectLayerData, objectLayerRenderFramesData };
|
|
835
938
|
}
|
|
836
939
|
},
|
|
837
940
|
);
|
|
@@ -840,369 +943,17 @@ try {
|
|
|
840
943
|
for (const objectLayerId of Object.keys(objectLayers)) {
|
|
841
944
|
const entry = objectLayers[objectLayerId];
|
|
842
945
|
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
if (existingOL) {
|
|
853
|
-
// ── Cut-over consistency: stage everything in memory before touching the live document ──
|
|
854
|
-
logger.info(`ObjectLayer '${objectLayerId}' already exists (${existingOL._id}), staging update...`);
|
|
855
|
-
|
|
856
|
-
// 1. Prepare staging data entirely in memory (no DB writes yet)
|
|
857
|
-
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
858
|
-
if (!stagingData.render) stagingData.render = {};
|
|
859
|
-
stagingData.render.cid = '';
|
|
860
|
-
stagingData.render.metadataCid = '';
|
|
861
|
-
|
|
862
|
-
// 2. Generate atlas, pin to IPFS, compute SHA-256 — all in memory
|
|
863
|
-
let cutoverReady = false;
|
|
864
|
-
let stagingFileDoc = null;
|
|
865
|
-
let stagingAtlasDoc = null;
|
|
866
|
-
let stagingCid = '';
|
|
867
|
-
try {
|
|
868
|
-
const itemKey = objectLayerId;
|
|
869
|
-
|
|
870
|
-
// Generate atlas from in-memory render frames data (plain object, no DB doc needed)
|
|
871
|
-
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
872
|
-
entry.objectLayerRenderFramesData,
|
|
873
|
-
itemKey,
|
|
874
|
-
20,
|
|
875
|
-
);
|
|
876
|
-
|
|
877
|
-
stagingFileDoc = await new File({
|
|
878
|
-
name: `${itemKey}-atlas.png`,
|
|
879
|
-
data: buffer,
|
|
880
|
-
size: buffer.length,
|
|
881
|
-
mimetype: 'image/png',
|
|
882
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
883
|
-
}).save();
|
|
884
|
-
|
|
885
|
-
let importAtlasCid = '';
|
|
886
|
-
let importAtlasMetadataCid = '';
|
|
887
|
-
try {
|
|
888
|
-
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
889
|
-
buffer,
|
|
890
|
-
`${itemKey}_atlas_sprite_sheet.png`,
|
|
891
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
892
|
-
);
|
|
893
|
-
if (ipfsResult) {
|
|
894
|
-
importAtlasCid = ipfsResult.cid;
|
|
895
|
-
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importAtlasCid}`);
|
|
896
|
-
try {
|
|
897
|
-
await createPinRecord({
|
|
898
|
-
cid: importAtlasCid,
|
|
899
|
-
resourceType: 'atlas-sprite-sheet',
|
|
900
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
901
|
-
options: { host, path },
|
|
902
|
-
});
|
|
903
|
-
} catch (prErr) {
|
|
904
|
-
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
} catch (ipfsError) {
|
|
908
|
-
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
try {
|
|
912
|
-
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
913
|
-
metadata,
|
|
914
|
-
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
915
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
916
|
-
);
|
|
917
|
-
if (metadataIpfsResult) {
|
|
918
|
-
importAtlasMetadataCid = metadataIpfsResult.cid;
|
|
919
|
-
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importAtlasMetadataCid}`);
|
|
920
|
-
try {
|
|
921
|
-
await createPinRecord({
|
|
922
|
-
cid: importAtlasMetadataCid,
|
|
923
|
-
resourceType: 'atlas-metadata',
|
|
924
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
925
|
-
options: { host, path },
|
|
926
|
-
});
|
|
927
|
-
} catch (prErr) {
|
|
928
|
-
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
} catch (ipfsError) {
|
|
932
|
-
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
936
|
-
if (stagingAtlasDoc) {
|
|
937
|
-
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
938
|
-
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
939
|
-
stagingAtlasDoc.cid = importAtlasCid;
|
|
940
|
-
stagingAtlasDoc.metadata = metadata;
|
|
941
|
-
await stagingAtlasDoc.save();
|
|
942
|
-
} else {
|
|
943
|
-
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
944
|
-
fileId: stagingFileDoc._id,
|
|
945
|
-
cid: importAtlasCid,
|
|
946
|
-
metadata,
|
|
947
|
-
}).save();
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
// Finalize staging data in memory with render CIDs
|
|
951
|
-
stagingData.render.cid = importAtlasCid;
|
|
952
|
-
stagingData.render.metadataCid = importAtlasMetadataCid;
|
|
953
|
-
|
|
954
|
-
// Pin data JSON to IPFS
|
|
955
|
-
try {
|
|
956
|
-
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
957
|
-
stagingData,
|
|
958
|
-
`${itemKey}_data.json`,
|
|
959
|
-
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
960
|
-
);
|
|
961
|
-
if (ipfsDataResult) {
|
|
962
|
-
stagingCid = ipfsDataResult.cid;
|
|
963
|
-
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
964
|
-
try {
|
|
965
|
-
await createPinRecord({
|
|
966
|
-
cid: stagingCid,
|
|
967
|
-
resourceType: 'object-layer-data',
|
|
968
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
969
|
-
options: { host, path },
|
|
970
|
-
});
|
|
971
|
-
} catch (prErr) {
|
|
972
|
-
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
} catch (ipfsError) {
|
|
976
|
-
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
cutoverReady = true;
|
|
980
|
-
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for cut-over`);
|
|
981
|
-
} catch (atlasError) {
|
|
982
|
-
logger.error(`[staging] Failed for ${objectLayerId}, live document untouched:`, atlasError);
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
// 3. Atomic cut-over: create new RenderFrames, swap live ObjectLayer in a single update
|
|
986
|
-
if (cutoverReady) {
|
|
987
|
-
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
988
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
989
|
-
|
|
990
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
991
|
-
data: stagingData,
|
|
992
|
-
cid: stagingCid,
|
|
993
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
994
|
-
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
if (oldRenderFramesId) {
|
|
998
|
-
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
999
|
-
}
|
|
1000
|
-
logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically`);
|
|
1001
|
-
} else {
|
|
1002
|
-
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
1003
|
-
logger.warn(`[cut-over] Staging rolled back for ${objectLayerId}, live document preserved`);
|
|
1004
|
-
objectLayer = await ObjectLayer.findByItemId(objectLayerId);
|
|
1005
|
-
}
|
|
1006
|
-
} else {
|
|
1007
|
-
// ── New item: stage everything before creating (same cut-over pattern) ──
|
|
1008
|
-
logger.info(`ObjectLayer '${objectLayerId}' is new, staging creation...`);
|
|
1009
|
-
|
|
1010
|
-
const itemKey = objectLayerId;
|
|
1011
|
-
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
1012
|
-
if (!stagingData.render) stagingData.render = {};
|
|
1013
|
-
stagingData.render.cid = '';
|
|
1014
|
-
stagingData.render.metadataCid = '';
|
|
1015
|
-
|
|
1016
|
-
let cutoverReady = false;
|
|
1017
|
-
let stagingFileDoc = null;
|
|
1018
|
-
let stagingAtlasDoc = null;
|
|
1019
|
-
let stagingCid = '';
|
|
1020
|
-
try {
|
|
1021
|
-
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
1022
|
-
entry.objectLayerRenderFramesData,
|
|
1023
|
-
itemKey,
|
|
1024
|
-
20,
|
|
1025
|
-
);
|
|
1026
|
-
|
|
1027
|
-
stagingFileDoc = await new File({
|
|
1028
|
-
name: `${itemKey}-atlas.png`,
|
|
1029
|
-
data: buffer,
|
|
1030
|
-
size: buffer.length,
|
|
1031
|
-
mimetype: 'image/png',
|
|
1032
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
1033
|
-
}).save();
|
|
1034
|
-
|
|
1035
|
-
let importAtlasCid = '';
|
|
1036
|
-
let importAtlasMetadataCid = '';
|
|
1037
|
-
try {
|
|
1038
|
-
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
1039
|
-
buffer,
|
|
1040
|
-
`${itemKey}_atlas_sprite_sheet.png`,
|
|
1041
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1042
|
-
);
|
|
1043
|
-
if (ipfsResult) {
|
|
1044
|
-
importAtlasCid = ipfsResult.cid;
|
|
1045
|
-
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importAtlasCid}`);
|
|
1046
|
-
try {
|
|
1047
|
-
await createPinRecord({
|
|
1048
|
-
cid: importAtlasCid,
|
|
1049
|
-
resourceType: 'atlas-sprite-sheet',
|
|
1050
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1051
|
-
options: { host, path },
|
|
1052
|
-
});
|
|
1053
|
-
} catch (prErr) {
|
|
1054
|
-
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
} catch (ipfsError) {
|
|
1058
|
-
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
try {
|
|
1062
|
-
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
1063
|
-
metadata,
|
|
1064
|
-
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1065
|
-
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1066
|
-
);
|
|
1067
|
-
if (metadataIpfsResult) {
|
|
1068
|
-
importAtlasMetadataCid = metadataIpfsResult.cid;
|
|
1069
|
-
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importAtlasMetadataCid}`);
|
|
1070
|
-
try {
|
|
1071
|
-
await createPinRecord({
|
|
1072
|
-
cid: importAtlasMetadataCid,
|
|
1073
|
-
resourceType: 'atlas-metadata',
|
|
1074
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1075
|
-
options: { host, path },
|
|
1076
|
-
});
|
|
1077
|
-
} catch (prErr) {
|
|
1078
|
-
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
} catch (ipfsError) {
|
|
1082
|
-
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
1086
|
-
if (stagingAtlasDoc) {
|
|
1087
|
-
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
1088
|
-
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
1089
|
-
stagingAtlasDoc.cid = importAtlasCid;
|
|
1090
|
-
stagingAtlasDoc.metadata = metadata;
|
|
1091
|
-
await stagingAtlasDoc.save();
|
|
1092
|
-
} else {
|
|
1093
|
-
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
1094
|
-
fileId: stagingFileDoc._id,
|
|
1095
|
-
cid: importAtlasCid,
|
|
1096
|
-
metadata,
|
|
1097
|
-
}).save();
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
stagingData.render.cid = importAtlasCid;
|
|
1101
|
-
stagingData.render.metadataCid = importAtlasMetadataCid;
|
|
1102
|
-
|
|
1103
|
-
try {
|
|
1104
|
-
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
1105
|
-
stagingData,
|
|
1106
|
-
`${itemKey}_data.json`,
|
|
1107
|
-
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
1108
|
-
);
|
|
1109
|
-
if (ipfsDataResult) {
|
|
1110
|
-
stagingCid = ipfsDataResult.cid;
|
|
1111
|
-
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
1112
|
-
try {
|
|
1113
|
-
await createPinRecord({
|
|
1114
|
-
cid: stagingCid,
|
|
1115
|
-
resourceType: 'object-layer-data',
|
|
1116
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
1117
|
-
options: { host, path },
|
|
1118
|
-
});
|
|
1119
|
-
} catch (prErr) {
|
|
1120
|
-
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
} catch (ipfsError) {
|
|
1124
|
-
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
cutoverReady = true;
|
|
1128
|
-
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for creation`);
|
|
1129
|
-
} catch (atlasError) {
|
|
1130
|
-
logger.error(`[staging] Failed for ${objectLayerId}, no document created:`, atlasError);
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
if (cutoverReady) {
|
|
1134
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1135
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
1136
|
-
data: stagingData,
|
|
1137
|
-
cid: stagingCid,
|
|
1138
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1139
|
-
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
1140
|
-
});
|
|
1141
|
-
logger.info(`[cut-over] New ObjectLayer ${objectLayer._id} created with all CIDs populated`);
|
|
1142
|
-
} else {
|
|
1143
|
-
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
1144
|
-
logger.warn(`[cut-over] Staging failed for ${objectLayerId}, no ObjectLayer created`);
|
|
1145
|
-
continue;
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
// Reload final state to include CID and render updates
|
|
1150
|
-
const finalObjectLayer = await ObjectLayer.findById((objectLayer._id || objectLayer).toString()).populate(
|
|
1151
|
-
'objectLayerRenderFramesId',
|
|
1152
|
-
);
|
|
1153
|
-
console.log(finalObjectLayer.toObject());
|
|
1154
|
-
} else {
|
|
1155
|
-
// --import all: skip items that already exist in the database
|
|
1156
|
-
if (existingItemIds.has(objectLayerId)) continue;
|
|
1157
|
-
|
|
1158
|
-
// --import all: create documents without atlas generation
|
|
1159
|
-
const existingOL = await ObjectLayer.findByItemId(objectLayerId);
|
|
1160
|
-
let objectLayer;
|
|
1161
|
-
|
|
1162
|
-
if (existingOL) {
|
|
1163
|
-
logger.info(
|
|
1164
|
-
`ObjectLayer '${objectLayerId}' already exists (${existingOL._id}), staging update (atlas skipped)...`,
|
|
1165
|
-
);
|
|
1166
|
-
|
|
1167
|
-
// ── In-memory staging (no atlas) ──────────────────────
|
|
1168
|
-
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
1169
|
-
if (!stagingData.render) stagingData.render = {};
|
|
1170
|
-
stagingData.render.cid = '';
|
|
1171
|
-
stagingData.render.metadataCid = '';
|
|
1172
|
-
|
|
1173
|
-
// Atomic cut-over: create new RenderFrames, swap live doc, delete old
|
|
1174
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1175
|
-
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
1176
|
-
|
|
1177
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
1178
|
-
data: stagingData,
|
|
1179
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1180
|
-
});
|
|
1181
|
-
|
|
1182
|
-
if (oldRenderFramesId) {
|
|
1183
|
-
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
logger.info(`[cut-over] Live document ${objectLayer._id} updated atomically (atlas skipped)`);
|
|
1187
|
-
} else {
|
|
1188
|
-
// New item (no atlas for bulk import)
|
|
1189
|
-
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
1190
|
-
if (!stagingData.render) stagingData.render = {};
|
|
1191
|
-
stagingData.render.cid = '';
|
|
1192
|
-
stagingData.render.metadataCid = '';
|
|
1193
|
-
|
|
1194
|
-
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1195
|
-
objectLayer = await ObjectLayer.upsertByItemId({
|
|
1196
|
-
data: stagingData,
|
|
1197
|
-
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
946
|
+
// A bulk import of every type skips atlas generation; `--to-atlas-sprite-sheet`
|
|
947
|
+
// or a targeted `--import` builds the atlas for an item that needs one.
|
|
948
|
+
const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
|
|
949
|
+
ObjectLayer,
|
|
950
|
+
ObjectLayerRenderFrames,
|
|
951
|
+
objectLayerRenderFramesData: entry.objectLayerRenderFramesData,
|
|
952
|
+
objectLayerData: { data: entry.data },
|
|
953
|
+
persistOptions: { generateAtlas: !isImportAll, upscaleFactor, options: { host, path } },
|
|
954
|
+
});
|
|
1200
955
|
|
|
1201
|
-
|
|
1202
|
-
`ObjectLayer ${existingOL ? 'updated' : 'created'} (atlas skipped for bulk import): ${objectLayerId}`,
|
|
1203
|
-
);
|
|
1204
|
-
console.log(objectLayer.toObject ? objectLayer.toObject() : objectLayer);
|
|
1205
|
-
}
|
|
956
|
+
console.log(objectLayer.toObject());
|
|
1206
957
|
}
|
|
1207
958
|
}
|
|
1208
959
|
|
|
@@ -1222,20 +973,16 @@ try {
|
|
|
1222
973
|
|
|
1223
974
|
logger.info(`Showing frame for item: ${itemId}, direction: ${direction}, frame: ${frameIndexNum}`);
|
|
1224
975
|
|
|
1225
|
-
// Find ObjectLayer by item-id
|
|
1226
976
|
const objectLayer = await ObjectLayer.findByItemId(itemId).populate('objectLayerRenderFramesId');
|
|
1227
|
-
|
|
1228
977
|
if (!objectLayer) {
|
|
1229
978
|
logger.error(`ObjectLayer not found for item-id: ${itemId}`);
|
|
1230
979
|
process.exit(1);
|
|
1231
980
|
}
|
|
1232
|
-
|
|
1233
981
|
if (!objectLayer.objectLayerRenderFramesId) {
|
|
1234
982
|
logger.error(`ObjectLayerRenderFrames not found for item: ${itemId}`);
|
|
1235
983
|
process.exit(1);
|
|
1236
984
|
}
|
|
1237
985
|
|
|
1238
|
-
// Get the keyframe direction name from the numerical direction code
|
|
1239
986
|
const objectLayerFrameDirections = getKeyframeDirectionsByCode(direction);
|
|
1240
987
|
if (objectLayerFrameDirections.length === 0) {
|
|
1241
988
|
logger.error(`Invalid direction code: ${direction}. Valid codes: 08, 18, 02, 12, 04, 14, 06, 16`);
|
|
@@ -1259,15 +1006,14 @@ try {
|
|
|
1259
1006
|
process.exit(1);
|
|
1260
1007
|
}
|
|
1261
1008
|
|
|
1262
|
-
const
|
|
1263
|
-
const outputPath = `./${itemKey}_${showFrameInput}.png`;
|
|
1009
|
+
const outputPath = `./${objectLayer.data.item.id}_${showFrameInput}.png`;
|
|
1264
1010
|
|
|
1265
1011
|
await buildImgFromTile({
|
|
1266
1012
|
tile: {
|
|
1267
1013
|
map_color: objectLayer.objectLayerRenderFramesId.colors,
|
|
1268
1014
|
frame_matrix: frames[frameIndexNum],
|
|
1269
1015
|
},
|
|
1270
|
-
cellPixelDim:
|
|
1016
|
+
cellPixelDim: upscaleFactor,
|
|
1271
1017
|
opacityFilter: (x, y, color) => 255,
|
|
1272
1018
|
imagePath: outputPath,
|
|
1273
1019
|
});
|
|
@@ -1277,158 +1023,90 @@ try {
|
|
|
1277
1023
|
}
|
|
1278
1024
|
|
|
1279
1025
|
// ── Handle --to-atlas-sprite-sheet ───────────────────────────────
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1026
|
+
// Rebuilds both atlas renders. The scope is the same selection --minify
|
|
1027
|
+
// uses: the given item-id(s), one instance, or the whole collection.
|
|
1028
|
+
// A bare --upscale asks for the same rebuild, since the factor only
|
|
1029
|
+
// takes effect on the human-resolution render.
|
|
1030
|
+
if (rebuildAtlases) {
|
|
1283
1031
|
/** @type {number|null} */
|
|
1284
|
-
const maxAtlasDim =
|
|
1032
|
+
const maxAtlasDim =
|
|
1033
|
+
options.toAtlasSpriteSheet === true || options.toAtlasSpriteSheet === undefined
|
|
1034
|
+
? null
|
|
1035
|
+
: parseInt(options.toAtlasSpriteSheet) || null;
|
|
1285
1036
|
|
|
1286
|
-
if (
|
|
1287
|
-
logger.error('item-id is required for --to-atlas-sprite-sheet');
|
|
1288
|
-
process.exit(1);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
if (maxAtlasDim) {
|
|
1292
|
-
/** @type {string} */
|
|
1037
|
+
if (maxAtlasDim !== null) {
|
|
1293
1038
|
const sizeRecommendation =
|
|
1294
1039
|
maxAtlasDim < 2048
|
|
1295
1040
|
? ' (Warning: May be too small for all frames)'
|
|
1296
1041
|
: maxAtlasDim > 4096
|
|
1297
1042
|
? ' (Large size: ensure GPU compatibility)'
|
|
1298
1043
|
: ' (Recommended size)';
|
|
1299
|
-
|
|
1300
|
-
logger.info(
|
|
1301
|
-
`Generating atlas sprite sheet for item: ${itemId} with max dimension: ${maxAtlasDim}x${maxAtlasDim}${sizeRecommendation}`,
|
|
1302
|
-
);
|
|
1303
|
-
} else {
|
|
1304
|
-
logger.info(
|
|
1305
|
-
`Generating atlas sprite sheet for item: ${itemId} with auto-calculated dimensions (based on frame count)`,
|
|
1306
|
-
);
|
|
1044
|
+
logger.info(`Max atlas dimension: ${maxAtlasDim}x${maxAtlasDim}${sizeRecommendation}`);
|
|
1307
1045
|
}
|
|
1308
1046
|
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1047
|
+
const selectedItemIds = await selectScopedItemIds({
|
|
1048
|
+
ObjectLayer,
|
|
1049
|
+
itemId,
|
|
1050
|
+
instance: options.instance,
|
|
1051
|
+
host,
|
|
1052
|
+
path,
|
|
1053
|
+
action: '--to-atlas-sprite-sheet',
|
|
1054
|
+
});
|
|
1316
1055
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
// Generate atlas sprite sheet
|
|
1321
|
-
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
1322
|
-
objectLayer.objectLayerRenderFramesId,
|
|
1323
|
-
itemKey,
|
|
1324
|
-
20, // cellPixelDim
|
|
1325
|
-
maxAtlasDim,
|
|
1326
|
-
);
|
|
1056
|
+
logger.info(`Atlas rebuild for ${selectedItemIds.length} stored item(s) at ${upscaleFactor}px per cell`);
|
|
1057
|
+
const tally = { rebuilt: 0, skipped: 0, failed: [] };
|
|
1327
1058
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1059
|
+
// One item at a time, isolated: a long batch runs through IPFS and can
|
|
1060
|
+
// meet a dropped database connection, and a rerun of a failed item is a
|
|
1061
|
+
// no-op for every item that already succeeded.
|
|
1062
|
+
for (const currentItemId of selectedItemIds) {
|
|
1063
|
+
try {
|
|
1064
|
+
const objectLayer = await liveObjectLayer()
|
|
1065
|
+
.findByItemId(currentItemId)
|
|
1066
|
+
.populate('objectLayerRenderFramesId');
|
|
1067
|
+
if (!objectLayer?.objectLayerRenderFramesId) {
|
|
1068
|
+
logger.warn(`No render frames stored for '${currentItemId}', skipped`);
|
|
1069
|
+
tally.skipped++;
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1333
1072
|
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}).save();
|
|
1073
|
+
const { atlasDoc, metadata, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
|
|
1074
|
+
itemKey: currentItemId,
|
|
1075
|
+
objectLayerRenderFrames: objectLayer.objectLayerRenderFramesId,
|
|
1076
|
+
upscaleFactor,
|
|
1077
|
+
maxAtlasDim,
|
|
1078
|
+
options: { host, path },
|
|
1079
|
+
});
|
|
1342
1080
|
|
|
1343
|
-
|
|
1081
|
+
const frameCount = Object.values(metadata.frames).reduce((sum, frames) => sum + frames.length, 0);
|
|
1082
|
+
logger.info(
|
|
1083
|
+
`Atlas for '${currentItemId}': ${metadata.atlasWidth}x${metadata.atlasHeight} cells, ` +
|
|
1084
|
+
`${frameCount} frames packed`,
|
|
1085
|
+
);
|
|
1344
1086
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
logger.
|
|
1357
|
-
|
|
1358
|
-
await createPinRecord({
|
|
1359
|
-
cid: toAtlasCid,
|
|
1360
|
-
resourceType: 'atlas-sprite-sheet',
|
|
1361
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1362
|
-
});
|
|
1363
|
-
} catch (e) {
|
|
1364
|
-
logger.warn('Failed to create pin record for atlas sprite sheet:', e.message);
|
|
1365
|
-
}
|
|
1087
|
+
objectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
1088
|
+
if (!objectLayer.data.render) objectLayer.data.render = {};
|
|
1089
|
+
objectLayer.data.render.cid = atlasCid;
|
|
1090
|
+
objectLayer.data.render.metadataCid = atlasMetadataCid;
|
|
1091
|
+
objectLayer.markModified('data.render');
|
|
1092
|
+
applyStatPolicy(objectLayer, statPolicy);
|
|
1093
|
+
await objectLayer.save();
|
|
1094
|
+
|
|
1095
|
+
await ObjectLayerEngine.computeAndSaveFinalSha256({ objectLayer, options: { host, path } });
|
|
1096
|
+
tally.rebuilt++;
|
|
1097
|
+
} catch (rebuildError) {
|
|
1098
|
+
logger.error(`Atlas rebuild failed for '${currentItemId}': ${rebuildError.message}`);
|
|
1099
|
+
tally.failed.push(currentItemId);
|
|
1366
1100
|
}
|
|
1367
|
-
} catch (ipfsError) {
|
|
1368
|
-
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
1369
1101
|
}
|
|
1370
1102
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
);
|
|
1378
|
-
if (metadataIpfsResult) {
|
|
1379
|
-
toAtlasMetadataCid = metadataIpfsResult.cid;
|
|
1380
|
-
logger.info(`Atlas metadata pinned to IPFS – CID: ${toAtlasMetadataCid}`);
|
|
1381
|
-
try {
|
|
1382
|
-
await createPinRecord({
|
|
1383
|
-
cid: toAtlasMetadataCid,
|
|
1384
|
-
resourceType: 'atlas-metadata',
|
|
1385
|
-
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1386
|
-
});
|
|
1387
|
-
} catch (e) {
|
|
1388
|
-
logger.warn('Failed to create pin record for atlas metadata:', e.message);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
} catch (ipfsError) {
|
|
1392
|
-
logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
// Check if atlas sprite sheet already exists
|
|
1396
|
-
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
1397
|
-
|
|
1398
|
-
if (atlasDoc) {
|
|
1399
|
-
// Update existing – remove old File to prevent orphans
|
|
1400
|
-
if (atlasDoc.fileId) await File.findByIdAndDelete(atlasDoc.fileId);
|
|
1401
|
-
atlasDoc.fileId = fileDoc._id;
|
|
1402
|
-
atlasDoc.cid = toAtlasCid;
|
|
1403
|
-
atlasDoc.metadata = metadata;
|
|
1404
|
-
await atlasDoc.save();
|
|
1405
|
-
logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1406
|
-
} else {
|
|
1407
|
-
// Create new
|
|
1408
|
-
atlasDoc = await new AtlasSpriteSheet({
|
|
1409
|
-
fileId: fileDoc._id,
|
|
1410
|
-
cid: toAtlasCid,
|
|
1411
|
-
metadata,
|
|
1412
|
-
}).save();
|
|
1413
|
-
logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1103
|
+
logger.info(
|
|
1104
|
+
`Atlas rebuild done: ${tally.rebuilt} rebuilt, ${tally.skipped} without render frames, ` +
|
|
1105
|
+
`${tally.failed.length} failed`,
|
|
1106
|
+
);
|
|
1107
|
+
if (tally.failed.length > 0) {
|
|
1108
|
+
logger.warn(`Rerun for the failed item(s): ${tally.failed.join(',')}`);
|
|
1414
1109
|
}
|
|
1415
|
-
|
|
1416
|
-
// Update ObjectLayer with reference to atlas sprite sheet and render CIDs
|
|
1417
|
-
objectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
1418
|
-
if (!objectLayer.data.render) objectLayer.data.render = {};
|
|
1419
|
-
objectLayer.data.render.cid = toAtlasCid;
|
|
1420
|
-
objectLayer.data.render.metadataCid = toAtlasMetadataCid;
|
|
1421
|
-
objectLayer.markModified('data.render');
|
|
1422
|
-
await objectLayer.save();
|
|
1423
|
-
|
|
1424
|
-
// Compute final SHA-256 and pin object layer data JSON to IPFS
|
|
1425
|
-
await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
1426
|
-
objectLayer,
|
|
1427
|
-
ipfsClient: IpfsClient,
|
|
1428
|
-
createPinRecord,
|
|
1429
|
-
});
|
|
1430
|
-
|
|
1431
|
-
logger.info(`Atlas sprite sheet completed for item: ${itemKey}`);
|
|
1432
1110
|
}
|
|
1433
1111
|
|
|
1434
1112
|
// ── Handle --show-atlas-sprite-sheet ─────────────────────────────
|
|
@@ -1529,6 +1207,7 @@ try {
|
|
|
1529
1207
|
|
|
1530
1208
|
// Overwrite the item id in the generated data with the unique variant
|
|
1531
1209
|
multiFrameResult.objectLayerData.data.item.id = uniqueItemId;
|
|
1210
|
+
applyStatPolicy(multiFrameResult.objectLayerData, statPolicy);
|
|
1532
1211
|
|
|
1533
1212
|
logger.info(
|
|
1534
1213
|
`Generated ${multiFrameResult.frameCount} frame(s) with ${multiFrameResult.objectLayerRenderFramesData.colors.length} unique colors`,
|
|
@@ -1543,7 +1222,7 @@ try {
|
|
|
1543
1222
|
itemId: uniqueItemId,
|
|
1544
1223
|
objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
|
|
1545
1224
|
objectLayerData: multiFrameResult.objectLayerData,
|
|
1546
|
-
cellPixelDim:
|
|
1225
|
+
cellPixelDim: upscaleFactor,
|
|
1547
1226
|
});
|
|
1548
1227
|
|
|
1549
1228
|
logger.info(`Wrote ${writtenFiles.length} asset file(s):`);
|
|
@@ -1551,148 +1230,34 @@ try {
|
|
|
1551
1230
|
logger.info(` → ${f}`);
|
|
1552
1231
|
}
|
|
1553
1232
|
|
|
1554
|
-
// 3. Persist
|
|
1555
|
-
const { objectLayer } = await ObjectLayerEngine.
|
|
1233
|
+
// 3. Persist ObjectLayer, render frames and the atlas, with every CID staged first
|
|
1234
|
+
const { objectLayer } = await ObjectLayerEngine.persistObjectLayerDocuments({
|
|
1556
1235
|
ObjectLayer,
|
|
1557
1236
|
ObjectLayerRenderFrames,
|
|
1558
1237
|
objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
|
|
1559
1238
|
objectLayerData: multiFrameResult.objectLayerData,
|
|
1560
|
-
|
|
1561
|
-
generateAtlas: false,
|
|
1562
|
-
},
|
|
1239
|
+
persistOptions: { upscaleFactor, options: { host, path } },
|
|
1563
1240
|
});
|
|
1564
1241
|
|
|
1565
1242
|
logger.info(`ObjectLayer persisted to MongoDB: ${objectLayer._id} (item: ${objectLayer.data.item.id})`);
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1573
|
-
)
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
const fileDoc = await new File({
|
|
1583
|
-
name: `${atlasItemKey}-atlas.png`,
|
|
1584
|
-
data: buffer,
|
|
1585
|
-
size: buffer.length,
|
|
1586
|
-
mimetype: 'image/png',
|
|
1587
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
1588
|
-
}).save();
|
|
1589
|
-
|
|
1590
|
-
// Pin atlas PNG to IPFS + copy into MFS
|
|
1591
|
-
let atlasMetadataCid = '';
|
|
1592
|
-
try {
|
|
1593
|
-
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
1594
|
-
buffer,
|
|
1595
|
-
`${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1596
|
-
`/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1597
|
-
);
|
|
1598
|
-
if (ipfsResult) {
|
|
1599
|
-
atlasCid = ipfsResult.cid;
|
|
1600
|
-
logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
|
|
1601
|
-
try {
|
|
1602
|
-
await createPinRecord({
|
|
1603
|
-
cid: atlasCid,
|
|
1604
|
-
resourceType: 'atlas-sprite-sheet',
|
|
1605
|
-
mfsPath: `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1606
|
-
options: { host, path },
|
|
1607
|
-
});
|
|
1608
|
-
} catch (e) {
|
|
1609
|
-
logger.warn('Failed to create pin record for atlas sprite sheet:', e.message);
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
} catch (ipfsError) {
|
|
1613
|
-
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
// Pin atlas metadata JSON to IPFS (fast-json-stable-stringify)
|
|
1617
|
-
try {
|
|
1618
|
-
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
1619
|
-
metadata,
|
|
1620
|
-
`${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1621
|
-
`/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1622
|
-
);
|
|
1623
|
-
if (metadataIpfsResult) {
|
|
1624
|
-
atlasMetadataCid = metadataIpfsResult.cid;
|
|
1625
|
-
logger.info(`Atlas metadata pinned to IPFS – CID: ${atlasMetadataCid}`);
|
|
1626
|
-
try {
|
|
1627
|
-
await createPinRecord({
|
|
1628
|
-
cid: atlasMetadataCid,
|
|
1629
|
-
resourceType: 'atlas-metadata',
|
|
1630
|
-
mfsPath: `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1631
|
-
options: { host, path },
|
|
1632
|
-
});
|
|
1633
|
-
} catch (e) {
|
|
1634
|
-
logger.warn('Failed to create pin record for atlas metadata:', e.message);
|
|
1635
|
-
}
|
|
1243
|
+
logger.info(`Final SHA-256: ${objectLayer.sha256}`);
|
|
1244
|
+
if (objectLayer.cid) logger.info(`ObjectLayer data pinned to IPFS – CID: ${objectLayer.cid}`);
|
|
1245
|
+
|
|
1246
|
+
// 4. Mirror the human-resolution atlas PNG into both static asset directories
|
|
1247
|
+
if (objectLayer.atlasSpriteSheetId) {
|
|
1248
|
+
const atlasDoc = await AtlasSpriteSheet.findById(objectLayer.atlasSpriteSheetId);
|
|
1249
|
+
const atlasFile = atlasDoc ? await File.findById(atlasDoc.fileId) : null;
|
|
1250
|
+
if (atlasFile?.data) {
|
|
1251
|
+
for (const bp of [srcBasePath, publicBasePath]) {
|
|
1252
|
+
const atlasOutputDir = nodePath.join(bp, 'assets', descriptor.itemType, uniqueItemId);
|
|
1253
|
+
await fs.ensureDir(atlasOutputDir);
|
|
1254
|
+
const atlasOutputPath = nodePath.join(atlasOutputDir, `${uniqueItemId}-atlas.png`);
|
|
1255
|
+
await fs.writeFile(atlasOutputPath, atlasFile.data);
|
|
1256
|
+
logger.info(
|
|
1257
|
+
`Atlas sprite sheet written: ${atlasDoc.metadata.atlasWidth}x${atlasDoc.metadata.atlasHeight} cells → ${atlasOutputPath}`,
|
|
1258
|
+
);
|
|
1636
1259
|
}
|
|
1637
|
-
} catch (ipfsError) {
|
|
1638
|
-
logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
1639
1260
|
}
|
|
1640
|
-
|
|
1641
|
-
// Upsert AtlasSpriteSheet document (with CID)
|
|
1642
|
-
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': atlasItemKey });
|
|
1643
|
-
if (atlasDoc) {
|
|
1644
|
-
if (atlasDoc.fileId) await File.findByIdAndDelete(atlasDoc.fileId);
|
|
1645
|
-
atlasDoc.fileId = fileDoc._id;
|
|
1646
|
-
atlasDoc.cid = atlasCid;
|
|
1647
|
-
atlasDoc.metadata = metadata;
|
|
1648
|
-
await atlasDoc.save();
|
|
1649
|
-
logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1650
|
-
} else {
|
|
1651
|
-
atlasDoc = await new AtlasSpriteSheet({
|
|
1652
|
-
fileId: fileDoc._id,
|
|
1653
|
-
cid: atlasCid,
|
|
1654
|
-
metadata,
|
|
1655
|
-
}).save();
|
|
1656
|
-
logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
// Link atlas to ObjectLayer and set data.render.cid + data.render.metadataCid
|
|
1660
|
-
populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
1661
|
-
if (!populatedObjectLayer.data.render) populatedObjectLayer.data.render = {};
|
|
1662
|
-
populatedObjectLayer.data.render.cid = atlasCid;
|
|
1663
|
-
populatedObjectLayer.data.render.metadataCid = atlasMetadataCid;
|
|
1664
|
-
populatedObjectLayer.markModified('data.render');
|
|
1665
|
-
await populatedObjectLayer.save();
|
|
1666
|
-
|
|
1667
|
-
// Also write atlas PNG to both static asset directories
|
|
1668
|
-
for (const bp of [srcBasePath, publicBasePath]) {
|
|
1669
|
-
const atlasOutputDir = nodePath.join(bp, 'assets', descriptor.itemType, uniqueItemId);
|
|
1670
|
-
await fs.ensureDir(atlasOutputDir);
|
|
1671
|
-
const atlasOutputPath = nodePath.join(atlasOutputDir, `${atlasItemKey}-atlas.png`);
|
|
1672
|
-
await fs.writeFile(atlasOutputPath, buffer);
|
|
1673
|
-
logger.info(
|
|
1674
|
-
`Atlas sprite sheet generated: ${metadata.atlasWidth}x${metadata.atlasHeight} → ${atlasOutputPath}`,
|
|
1675
|
-
);
|
|
1676
|
-
}
|
|
1677
|
-
} catch (atlasError) {
|
|
1678
|
-
logger.error(`Failed to generate atlas for ${uniqueItemId}:`, atlasError);
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
// 5. Compute final SHA-256, pin OL data JSON to IPFS, create pin records
|
|
1682
|
-
try {
|
|
1683
|
-
const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
|
|
1684
|
-
const finalized = await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
1685
|
-
objectLayer: finalObjectLayer,
|
|
1686
|
-
ipfsClient: IpfsClient,
|
|
1687
|
-
createPinRecord,
|
|
1688
|
-
options: { host, path },
|
|
1689
|
-
});
|
|
1690
|
-
logger.info(`Final SHA-256: ${finalized.sha256}`);
|
|
1691
|
-
if (finalized.cid) {
|
|
1692
|
-
logger.info(`ObjectLayer data pinned to IPFS – CID: ${finalized.cid}`);
|
|
1693
|
-
}
|
|
1694
|
-
} catch (finalizeError) {
|
|
1695
|
-
logger.error('Failed to finalize SHA-256 / IPFS:', finalizeError);
|
|
1696
1261
|
}
|
|
1697
1262
|
|
|
1698
1263
|
logger.info(`✓ Generation complete for "${uniqueItemId}" (seed: ${genSeed}, frames: ${genFrameCount})`);
|
|
@@ -1721,9 +1286,25 @@ try {
|
|
|
1721
1286
|
'When used with --export or --import, only process cyberia-instance.json and cyberia-instance-conf.json',
|
|
1722
1287
|
)
|
|
1723
1288
|
.option('--drop', 'Drop all documents associated with the instance code before importing or as a standalone action')
|
|
1289
|
+
.option(
|
|
1290
|
+
'--export-current-fallbackworld',
|
|
1291
|
+
'Capture the in-memory procedural fallback world as instance [instance-code]: materialize it into MongoDB (maps, conf, actions, quests, missing content defaults) and then export it',
|
|
1292
|
+
)
|
|
1293
|
+
.option(
|
|
1294
|
+
'--keep-fallback-codes',
|
|
1295
|
+
'With --export-current-fallbackworld, keep the raw fallback-map-* / canonical action-quest codes instead of namespacing them under the instance code',
|
|
1296
|
+
)
|
|
1297
|
+
.option(
|
|
1298
|
+
'--fallback-url <url>',
|
|
1299
|
+
'With --export-current-fallbackworld, capture the world a running engine currently serves (e.g. http://localhost:4001) instead of regenerating it locally',
|
|
1300
|
+
)
|
|
1724
1301
|
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
1725
1302
|
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
1726
1303
|
.option('--dev', 'Force development environment')
|
|
1304
|
+
.option(
|
|
1305
|
+
'--sync-entities',
|
|
1306
|
+
'Point the instance conf at every entity-type default its maps place and every skill their items trigger, dropping what the world no longer carries',
|
|
1307
|
+
)
|
|
1727
1308
|
.option('--publish-build', 'Build instance backup directory with all related maps, entities and object layers')
|
|
1728
1309
|
.option('--publish-remove', 'Remove published instance from underpostnet/cyberia-instances repository')
|
|
1729
1310
|
.option('--publish', 'Publish instance in underpostnet/cyberia-instances repository')
|
|
@@ -1735,16 +1316,40 @@ try {
|
|
|
1735
1316
|
.description('Export/import a Cyberia instance with all related maps, entities and object layers')
|
|
1736
1317
|
.action(async (instanceCode, options = {}) => {
|
|
1737
1318
|
if (options.revert) {
|
|
1738
|
-
|
|
1739
|
-
shellExec(`cd /home/dd/
|
|
1740
|
-
shellExec(`cd /home/dd/engine/cyberia-
|
|
1319
|
+
Underpost.repo.declareSafeDirectory('/home/dd/cyberia-instances');
|
|
1320
|
+
shellExec(`cd /home/dd/cyberia-instances && ${cli()} cmt . reset && ${cli()} run clean .`);
|
|
1321
|
+
shellExec(`cd /home/dd/engine/cyberia-server && ${cli()} cmt . reset && ${cli()} run clean .`);
|
|
1322
|
+
shellExec(`cd /home/dd/engine/cyberia-client && ${cli()} cmt . reset && ${cli()} run clean .`);
|
|
1741
1323
|
return;
|
|
1742
1324
|
}
|
|
1325
|
+
if (options.exportCurrentFallbackworld) {
|
|
1326
|
+
// A capture writes one named instance: no default list, no import in the
|
|
1327
|
+
// same run, and never the reserved code the in-memory world itself uses.
|
|
1328
|
+
if (!instanceCode || instanceCode.includes(',')) {
|
|
1329
|
+
logger.error('--export-current-fallbackworld requires a single [instance-code] to capture the world under');
|
|
1330
|
+
process.exit(1);
|
|
1331
|
+
}
|
|
1332
|
+
if (instanceCode === 'fallback') {
|
|
1333
|
+
logger.error('"fallback" is reserved for the in-memory world — capture it under a different instance code');
|
|
1334
|
+
process.exit(1);
|
|
1335
|
+
}
|
|
1336
|
+
if (options.import !== undefined) {
|
|
1337
|
+
logger.error('--export-current-fallbackworld cannot be combined with --import');
|
|
1338
|
+
process.exit(1);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1743
1342
|
if (!instanceCode) {
|
|
1744
|
-
instanceCode = 'amethyst-strata-expansion,FOREST';
|
|
1343
|
+
instanceCode = 'amethyst-strata-expansion,FOREST,TEST';
|
|
1745
1344
|
logger.warn(`No instance code provided, defaulting to: ${instanceCode}`);
|
|
1746
1345
|
}
|
|
1747
1346
|
|
|
1347
|
+
// An explicitly named env must exist: falling through to the ambient `./.env` resolves the
|
|
1348
|
+
// instance against whatever deployment was loaded last, which is not the one the caller named.
|
|
1349
|
+
if (options.envPath && !fs.existsSync(options.envPath)) {
|
|
1350
|
+
logger.error(`Env file not found: ${options.envPath}`);
|
|
1351
|
+
process.exit(1);
|
|
1352
|
+
}
|
|
1748
1353
|
if (!options.envPath) options.envPath = `./.env`;
|
|
1749
1354
|
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
1750
1355
|
|
|
@@ -1766,15 +1371,13 @@ try {
|
|
|
1766
1371
|
}
|
|
1767
1372
|
|
|
1768
1373
|
if (options.publish || options.publishBuild || options.publishRemove) {
|
|
1374
|
+
// The instances checkout is cloned by the deploy user and driven by root during a deploy,
|
|
1375
|
+
// and git refuses to touch a tree owned by someone else until it is declared safe.
|
|
1376
|
+
Underpost.repo.declareSafeDirectory('/home/dd/cyberia-instances');
|
|
1769
1377
|
if (options.publishBuild) {
|
|
1770
|
-
if (!fs.existsSync('/home/dd/cyberia-instances'))
|
|
1771
|
-
shellExec(
|
|
1772
|
-
}
|
|
1773
|
-
shellExec(`underpost run clean /home/dd/cyberia-instances`);
|
|
1774
|
-
shellExec(`cd /home/dd/cyberia-instances && underpost pull . underpostnet/cyberia-instances`, {
|
|
1775
|
-
silentOnError: true,
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1378
|
+
if (!fs.existsSync('/home/dd/cyberia-instances'))
|
|
1379
|
+
shellExec(`cd /home/dd && ${cli()} clone underpostnet/cyberia-instances`);
|
|
1380
|
+
else shellExec(`cd /home/dd/cyberia-instances && ${cli()} cmt --switch-repo underpostnet/cyberia-instances`);
|
|
1778
1381
|
|
|
1779
1382
|
fs.mkdirpSync(`/home/dd/cyberia-instances/conf/dd-cyberia`);
|
|
1780
1383
|
fs.copyFileSync(
|
|
@@ -1798,40 +1401,24 @@ try {
|
|
|
1798
1401
|
`/home/dd/cyberia-instances/conf/dd-cyberia/conf.volume.json`,
|
|
1799
1402
|
);
|
|
1800
1403
|
{
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
);
|
|
1806
|
-
const originPackageJson = JSON.parse(
|
|
1807
|
-
fs.readFileSync(`./engine-private/conf/dd-cyberia/package.json`, 'utf-8'),
|
|
1808
|
-
);
|
|
1809
|
-
const scriptsOrigin = originPackageJson.scripts;
|
|
1810
|
-
const scriptsTarget = JSON.parse(fs.readFileSync(`./package.json`, 'utf-8')).scripts;
|
|
1811
|
-
originPackageJson.name = 'cyberia';
|
|
1812
|
-
originPackageJson.bin = {
|
|
1813
|
-
cyberia: 'bin/index.js',
|
|
1814
|
-
};
|
|
1815
|
-
originPackageJson.keywords = catalog.keywords;
|
|
1816
|
-
originPackageJson.description = catalog.description;
|
|
1404
|
+
// The published manifest is the deploy's, under the product's own identity — one
|
|
1405
|
+
// builder for every generated package.json in the project, so the instances repo
|
|
1406
|
+
// cannot drift from what the deploy and the product CLI declare.
|
|
1407
|
+
const deployPackagePath = deployPackagePathFactory('dd-cyberia');
|
|
1817
1408
|
fs.writeFileSync(
|
|
1818
1409
|
`/home/dd/cyberia-instances/conf/dd-cyberia/package.json`,
|
|
1819
|
-
JSON.stringify(
|
|
1820
|
-
{
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
dependencies: {
|
|
1828
|
-
...originPackageJson.dependencies,
|
|
1829
|
-
...CyberiaDependencies,
|
|
1830
|
-
},
|
|
1831
|
-
},
|
|
1410
|
+
`${JSON.stringify(
|
|
1411
|
+
buildDeployPackageJson({
|
|
1412
|
+
deployId: 'dd-cyberia',
|
|
1413
|
+
enginePackageJson: JSON.parse(fs.readFileSync(`./package.json`, 'utf-8')),
|
|
1414
|
+
catalog: await loadDeployCatalog('dd-cyberia'),
|
|
1415
|
+
currentPackageJson: JSON.parse(fs.readFileSync(deployPackagePath, 'utf-8')),
|
|
1416
|
+
productIdentity: true,
|
|
1417
|
+
}),
|
|
1832
1418
|
null,
|
|
1833
|
-
|
|
1834
|
-
)
|
|
1419
|
+
DEPLOY_MANIFEST_INDENT,
|
|
1420
|
+
)}\n`,
|
|
1421
|
+
'utf8',
|
|
1835
1422
|
);
|
|
1836
1423
|
}
|
|
1837
1424
|
fs.copyFileSync(
|
|
@@ -1848,7 +1435,11 @@ try {
|
|
|
1848
1435
|
);
|
|
1849
1436
|
|
|
1850
1437
|
fs.mkdirpSync(`/home/dd/cyberia-instances/deployments`);
|
|
1851
|
-
|
|
1438
|
+
// The staged CLI package is a local image-build artifact, not a deployment manifest —
|
|
1439
|
+
// it must never be published into the instances repository.
|
|
1440
|
+
fs.copySync(`./src/runtime/engine-cyberia`, `/home/dd/cyberia-instances/deployments/engine-cyberia`, {
|
|
1441
|
+
filter: (src) => nodePath.basename(src) !== STAGED_CLI_PACKAGE,
|
|
1442
|
+
});
|
|
1852
1443
|
fs.copySync(
|
|
1853
1444
|
`./manifests/deployment/dd-cyberia-development/.`,
|
|
1854
1445
|
`/home/dd/cyberia-instances/deployments/engine-cyberia/.`,
|
|
@@ -1863,35 +1454,21 @@ try {
|
|
|
1863
1454
|
`./engine-private/conf/dd-cyberia/instances/mmo-server/build/development/.`,
|
|
1864
1455
|
`/home/dd/cyberia-instances/deployments/cyberia-server/.`,
|
|
1865
1456
|
);
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
logger.info(`Copying asset: ${assetPath} → ${targetPath}`);
|
|
1875
|
-
fs.copySync(`./${assetPath}`, targetPath);
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
// Copy default-items asset folders (src/client/public/cyberia/assets/<type>/<id>/ → public/cyberia/assets/<type>/<id>/)
|
|
1879
|
-
for (const entry of DefaultCyberiaItems) {
|
|
1880
|
-
const { id, type } = entry.item;
|
|
1881
|
-
const srcDir = `src/client/public/cyberia/assets/${type}/${id}`;
|
|
1882
|
-
const targetDir = `/home/dd/cyberia-instances/public/cyberia/assets/${type}/${id}`;
|
|
1883
|
-
if (fs.existsSync(srcDir)) {
|
|
1884
|
-
fs.mkdirpSync(nodePath.dirname(targetDir));
|
|
1885
|
-
logger.info(`Copying default-item asset: ${srcDir} → ${targetDir}`);
|
|
1886
|
-
fs.copySync(srcDir, targetDir);
|
|
1887
|
-
} else {
|
|
1888
|
-
logger.warn(`Default-item asset directory not found, skipping: ${srcDir}`);
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1457
|
+
const folders = ['ui-icons', 'cursor', 'fonts', 'icons', 'splash', 'templates'];
|
|
1458
|
+
for (const folder of folders)
|
|
1459
|
+
fs.copySync(
|
|
1460
|
+
`./src/client/public/cyberia/assets/${folder}`,
|
|
1461
|
+
`/home/dd/cyberia-instances/public/cyberia/assets/${folder}`,
|
|
1462
|
+
);
|
|
1463
|
+
// fs.removeSync(`/home/dd/cyberia-instances/public/cyberia`);
|
|
1464
|
+
// fs.mkdirpSync(`/home/dd/cyberia-instances/public/cyberia`);
|
|
1892
1465
|
fs.mkdirpSync(`/home/dd/cyberia-instances/instances`);
|
|
1893
1466
|
fs.mkdirpSync(`/home/dd/cyberia-instances/sagas`);
|
|
1894
1467
|
for (const _instanceCode of instanceCode.split(',')) {
|
|
1468
|
+
if (fs.existsSync(`/home/dd/cyberia-instances/instances/${_instanceCode}`))
|
|
1469
|
+
shellExec(`rm -rf /home/dd/cyberia-instances/instances/${_instanceCode}`);
|
|
1470
|
+
if (fs.existsSync(`/home/dd/cyberia-instances/sagas/${_instanceCode}.json`))
|
|
1471
|
+
shellExec(`rm -rf /home/dd/cyberia-instances/sagas/${_instanceCode}.json`);
|
|
1895
1472
|
fs.copySync(
|
|
1896
1473
|
`./engine-private/cyberia-instances/${_instanceCode}`,
|
|
1897
1474
|
`/home/dd/cyberia-instances/instances/${_instanceCode}`,
|
|
@@ -1908,6 +1485,19 @@ try {
|
|
|
1908
1485
|
'/home/dd/cyberia-instances/conf/dd-cyberia/conf.instances.json',
|
|
1909
1486
|
);
|
|
1910
1487
|
|
|
1488
|
+
if (!fs.existsSync('/home/dd/cyberia-instances/manifests'))
|
|
1489
|
+
fs.mkdirSync('/home/dd/cyberia-instances/manifests');
|
|
1490
|
+
fs.copySync('./cyberia-server/manifests', '/home/dd/cyberia-instances/manifests', { overwrite: true });
|
|
1491
|
+
fs.copySync('./cyberia-client/manifests', '/home/dd/cyberia-instances/manifests', { overwrite: true });
|
|
1492
|
+
if (!fs.existsSync('/home/dd/cyberia-instances/manifests/deployments/dd-cyberia-development'))
|
|
1493
|
+
fs.mkdirSync('/home/dd/cyberia-instances/manifests/deployments/dd-cyberia-development', {
|
|
1494
|
+
recursive: true,
|
|
1495
|
+
});
|
|
1496
|
+
fs.copySync(
|
|
1497
|
+
'./manifests/deployment/dd-cyberia-development',
|
|
1498
|
+
'/home/dd/cyberia-instances/manifests/deployments/dd-cyberia-development',
|
|
1499
|
+
{ overwrite: true },
|
|
1500
|
+
);
|
|
1911
1501
|
const fromN = parseInt(options.fromNCommit) > 0 ? parseInt(options.fromNCommit) : 1;
|
|
1912
1502
|
const publishMessage =
|
|
1913
1503
|
shellExec(`node bin cmt --changelog-msg --from-n-commit ${fromN} --changelog-no-hash`, {
|
|
@@ -1945,7 +1535,7 @@ try {
|
|
|
1945
1535
|
shellExec(`rm -rf /home/dd/cyberia-instances/sagas/${instanceCode}.json`);
|
|
1946
1536
|
return;
|
|
1947
1537
|
}
|
|
1948
|
-
shellExec(`cd /home/dd/cyberia-instances &&
|
|
1538
|
+
shellExec(`cd /home/dd/cyberia-instances && ${cli()} push . underpostnet/cyberia-instances`);
|
|
1949
1539
|
return;
|
|
1950
1540
|
}
|
|
1951
1541
|
|
|
@@ -1972,6 +1562,8 @@ try {
|
|
|
1972
1562
|
'cyberia-skill',
|
|
1973
1563
|
'cyberia-entity-type-default',
|
|
1974
1564
|
'cyberia-saga',
|
|
1565
|
+
'cyberia-audio',
|
|
1566
|
+
'cyberia-map-audio-conf',
|
|
1975
1567
|
'object-layer',
|
|
1976
1568
|
'object-layer-render-frames',
|
|
1977
1569
|
'atlas-sprite-sheet',
|
|
@@ -1992,6 +1584,8 @@ try {
|
|
|
1992
1584
|
const CyberiaSkill = DataBaseProviderService.getModel('cyberia-skill', { host, path });
|
|
1993
1585
|
const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('cyberia-entity-type-default', { host, path });
|
|
1994
1586
|
const CyberiaSaga = DataBaseProviderService.getModel('cyberia-saga', { host, path });
|
|
1587
|
+
const CyberiaAudio = DataBaseProviderService.getModel('cyberia-audio', { host, path });
|
|
1588
|
+
const CyberiaMapAudioConf = DataBaseProviderService.getModel('cyberia-map-audio-conf', { host, path });
|
|
1995
1589
|
const ObjectLayer = DataBaseProviderService.getModel('object-layer', { host, path });
|
|
1996
1590
|
const ObjectLayerRenderFrames = DataBaseProviderService.getModel('object-layer-render-frames', { host, path });
|
|
1997
1591
|
const AtlasSpriteSheet = DataBaseProviderService.getModel('atlas-sprite-sheet', { host, path });
|
|
@@ -2012,8 +1606,8 @@ try {
|
|
|
2012
1606
|
atlasMetadata: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
2013
1607
|
});
|
|
2014
1608
|
|
|
2015
|
-
//
|
|
2016
|
-
//
|
|
1609
|
+
// A canonical pin carries one `mfsPath`. `mfsPaths` (plural) is read too,
|
|
1610
|
+
// because a backup that consolidated shared CIDs writes that field.
|
|
2017
1611
|
const collectMfsPaths = (doc = {}) => {
|
|
2018
1612
|
const paths = new Set();
|
|
2019
1613
|
if (doc.mfsPath) paths.add(doc.mfsPath);
|
|
@@ -2069,10 +1663,9 @@ try {
|
|
|
2069
1663
|
...(entry.mfsPath ? { mfsPath: entry.mfsPath } : {}),
|
|
2070
1664
|
}));
|
|
2071
1665
|
|
|
2072
|
-
// Bring the
|
|
2073
|
-
// duplicate mfsPath rows
|
|
2074
|
-
//
|
|
2075
|
-
// partial unique index on mfsPath is built. Idempotent and safe to re-run.
|
|
1666
|
+
// Bring the Ipfs collection in line with the mfsPath-unique model: collapse
|
|
1667
|
+
// duplicate mfsPath rows, keeping the most recent, then sync indexes so the
|
|
1668
|
+
// partial unique index on mfsPath is built. Safe to re-run.
|
|
2076
1669
|
const reconcileIpfsRegistryIndexes = async () => {
|
|
2077
1670
|
let removedDuplicates = 0;
|
|
2078
1671
|
const duplicateGroups = await Ipfs.aggregate([
|
|
@@ -2124,6 +1717,111 @@ try {
|
|
|
2124
1717
|
}
|
|
2125
1718
|
};
|
|
2126
1719
|
|
|
1720
|
+
// ── CAPTURE CURRENT FALLBACK WORLD ──────────────────────────────
|
|
1721
|
+
//
|
|
1722
|
+
// The procedural fallback world lives only in engine memory. A capture
|
|
1723
|
+
// writes it to MongoDB under [instance-code], with the content
|
|
1724
|
+
// collections the fallback path serves from code defaults, so the export
|
|
1725
|
+
// below can back it up and `--import` can restore it.
|
|
1726
|
+
if (options.exportCurrentFallbackworld) {
|
|
1727
|
+
const { generateFallbackWorld } = await import('../src/api/cyberia-instance/cyberia-fallback-world.js');
|
|
1728
|
+
const { captureFallbackWorld } = await import('../src/api/cyberia-instance/cyberia-fallback-capture.js');
|
|
1729
|
+
|
|
1730
|
+
let world;
|
|
1731
|
+
if (options.fallbackUrl) {
|
|
1732
|
+
// Staged fallback default items live only in the serving engine
|
|
1733
|
+
// process, so a faithful capture of a live world must read it back
|
|
1734
|
+
// over REST instead of regenerating it here.
|
|
1735
|
+
const base = options.fallbackUrl.replace(/\/+$/, '');
|
|
1736
|
+
const worldUrl = base.includes('/fallback-world') ? base : `${base}/api/cyberia-instance/fallback-world`;
|
|
1737
|
+
logger.info('Fetching live fallback world', { url: worldUrl });
|
|
1738
|
+
const response = await fetch(worldUrl);
|
|
1739
|
+
if (!response.ok) {
|
|
1740
|
+
logger.error(`Fallback world fetch failed: ${response.status} ${response.statusText}`, { url: worldUrl });
|
|
1741
|
+
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
1742
|
+
process.exit(1);
|
|
1743
|
+
}
|
|
1744
|
+
const payload = await response.json();
|
|
1745
|
+
world = payload?.data ?? payload;
|
|
1746
|
+
} else {
|
|
1747
|
+
world = generateFallbackWorld();
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
if (!world?.instance || !Array.isArray(world.maps) || world.maps.length === 0) {
|
|
1751
|
+
logger.error('Fallback world payload has no maps — nothing to capture');
|
|
1752
|
+
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
1753
|
+
process.exit(1);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
const capture = await captureFallbackWorld({
|
|
1757
|
+
models: {
|
|
1758
|
+
CyberiaInstance,
|
|
1759
|
+
CyberiaInstanceConf,
|
|
1760
|
+
CyberiaMap,
|
|
1761
|
+
CyberiaAction,
|
|
1762
|
+
CyberiaQuest,
|
|
1763
|
+
CyberiaSkill,
|
|
1764
|
+
CyberiaEntityTypeDefault,
|
|
1765
|
+
CyberiaDialogue,
|
|
1766
|
+
CyberiaMapAudioConf,
|
|
1767
|
+
CyberiaAudio,
|
|
1768
|
+
ObjectLayer,
|
|
1769
|
+
},
|
|
1770
|
+
world,
|
|
1771
|
+
instanceCode,
|
|
1772
|
+
keepFallbackCodes: !!options.keepFallbackCodes,
|
|
1773
|
+
});
|
|
1774
|
+
|
|
1775
|
+
// Sprites are the one thing a capture cannot synthesise: without their
|
|
1776
|
+
// ObjectLayer documents the backup would export atlas-less items and
|
|
1777
|
+
// the restored world would render solid-colour rectangles.
|
|
1778
|
+
if (capture.missingObjectLayerItemIds.length > 0) {
|
|
1779
|
+
logger.error(
|
|
1780
|
+
`Capture aborted: ${capture.missingObjectLayerItemIds.length} referenced item id(s) have no ObjectLayer in MongoDB:`,
|
|
1781
|
+
capture.missingObjectLayerItemIds.join(', '),
|
|
1782
|
+
`— run \`node bin/cyberia ol ${capture.missingObjectLayerItemIds.join(',')} --from-directory --import\` (or ` +
|
|
1783
|
+
'`node bin/cyberia run-workflow import-default-items`) first.',
|
|
1784
|
+
);
|
|
1785
|
+
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
logger.info('Captured fallback world into MongoDB', {
|
|
1790
|
+
code: instanceCode,
|
|
1791
|
+
mapCodes: capture.plan.instance.cyberiaMapCodes,
|
|
1792
|
+
actions: capture.plan.actions.length,
|
|
1793
|
+
quests: capture.plan.quests.length,
|
|
1794
|
+
audioConfs: capture.audio.audioConfs,
|
|
1795
|
+
objectLayerItemIds: capture.plan.itemIds.length,
|
|
1796
|
+
});
|
|
1797
|
+
|
|
1798
|
+
// The capture is only half the command — fall through to the export so
|
|
1799
|
+
// it lands in ./engine-private/cyberia-instances/<instance-code>.
|
|
1800
|
+
if (options.export === undefined) options.export = true;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// ── SYNC ENTITY-TYPE DEFAULTS ───────────────────────────────────
|
|
1804
|
+
if (options.syncEntities) {
|
|
1805
|
+
const result = await CyberiaEntityTypeDefaultService.syncInstance({ instanceCode }, { host, path });
|
|
1806
|
+
logger.info(`sync-entities ${instanceCode}`, {
|
|
1807
|
+
maps: result.mapCodes.length,
|
|
1808
|
+
references: result.entityDefaults.length,
|
|
1809
|
+
...(result.linked.length > 0 ? { linked: result.linked } : {}),
|
|
1810
|
+
...(result.dropped.length > 0 ? { dropped: result.dropped } : {}),
|
|
1811
|
+
...(result.skipped.length > 0 ? { claimedByAnotherInstance: result.skipped } : {}),
|
|
1812
|
+
...(result.skills.length > 0 ? { skills: result.skills } : {}),
|
|
1813
|
+
...(result.entitiesUpdated.length > 0
|
|
1814
|
+
? { entitiesUpdated: result.entitiesUpdated.map(({ mapCode, entities }) => `${mapCode}:${entities}`) }
|
|
1815
|
+
: {}),
|
|
1816
|
+
...(result.conflicts.length > 0 ? { notLinkedSameBuildAlreadyReferenced: result.conflicts } : {}),
|
|
1817
|
+
...(result.duplicates.length > 0 ? { duplicateReferencesDeleteOne: result.duplicates } : {}),
|
|
1818
|
+
});
|
|
1819
|
+
if (options.export === undefined && !options.import && !options.drop) {
|
|
1820
|
+
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
2127
1825
|
// ── EXPORT ──────────────────────────────────────────────────────
|
|
2128
1826
|
if (options.export !== undefined) {
|
|
2129
1827
|
const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
@@ -2139,6 +1837,25 @@ try {
|
|
|
2139
1837
|
: `./engine-private/cyberia-instances/${instanceCode}`;
|
|
2140
1838
|
|
|
2141
1839
|
fs.ensureDirSync(backupDir);
|
|
1840
|
+
// The export is a projection of what this instance references, never an archive of
|
|
1841
|
+
// everything it once did: a document that drops out of the world has to leave the
|
|
1842
|
+
// directory with it, or the next import brings it back from the dead. Emptied up front,
|
|
1843
|
+
// so a collection that exports nothing this run ends up empty rather than stale. The
|
|
1844
|
+
// asset directories — files, ipfs, render-frames, atlas sheets — are content-addressed
|
|
1845
|
+
// and expensive to refetch, so they keep what they have.
|
|
1846
|
+
for (const collection of [
|
|
1847
|
+
'maps',
|
|
1848
|
+
'cyberia-map-audio-confs',
|
|
1849
|
+
'cyberia-audio',
|
|
1850
|
+
'cyberia-quests',
|
|
1851
|
+
'cyberia-actions',
|
|
1852
|
+
'cyberia-skills',
|
|
1853
|
+
'cyberia-entity-type-defaults',
|
|
1854
|
+
'cyberia-dialogues',
|
|
1855
|
+
'object-layers',
|
|
1856
|
+
]) {
|
|
1857
|
+
fs.emptyDirSync(`${backupDir}/${collection}`);
|
|
1858
|
+
}
|
|
2142
1859
|
logger.info('Exporting instance', { code: instanceCode, backupDir });
|
|
2143
1860
|
|
|
2144
1861
|
// Helper: export a File document to the files/ directory
|
|
@@ -2168,6 +1885,11 @@ try {
|
|
|
2168
1885
|
// 1b. Export linked CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
|
|
2169
1886
|
// If no conf doc exists yet (instance created before auto-upsert logic), create one using
|
|
2170
1887
|
// schema defaults — identical to the behaviour in CyberiaInstanceService.post().
|
|
1888
|
+
//
|
|
1889
|
+
// Compact first: a reference whose entity-type default is gone cannot be exported, and
|
|
1890
|
+
// writing it into the backup would carry the dangling id into every world restored from
|
|
1891
|
+
// it. Dropping it here repairs the live conf and the backup in one step.
|
|
1892
|
+
await CyberiaEntityTypeDefaultService.compactInstanceRefs({ host, path }, { instanceCode });
|
|
2171
1893
|
let instanceConf =
|
|
2172
1894
|
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
2173
1895
|
(instance.conf ? await CyberiaInstanceConf.findById(instance.conf).lean() : null);
|
|
@@ -2185,10 +1907,10 @@ try {
|
|
|
2185
1907
|
instanceConf = created?.toObject ? created.toObject() : created;
|
|
2186
1908
|
}
|
|
2187
1909
|
if (instanceConf) {
|
|
2188
|
-
// `.lean()` skips
|
|
2189
|
-
//
|
|
2190
|
-
//
|
|
2191
|
-
instanceConf =
|
|
1910
|
+
// `.lean()` skips the schema defaults and a document written before a bound tightened
|
|
1911
|
+
// may carry a value the schema now rejects; the backup is made whole and valid here so
|
|
1912
|
+
// it imports back as-is.
|
|
1913
|
+
({ conf: instanceConf } = await CyberiaInstanceConfService.coerceToSchema(instanceConf, CyberiaInstanceConf));
|
|
2192
1914
|
fs.writeJsonSync(`${backupDir}/cyberia-instance-conf.json`, instanceConf, { spaces: 2 });
|
|
2193
1915
|
logger.info('Exported CyberiaInstanceConf', { instanceCode });
|
|
2194
1916
|
} else {
|
|
@@ -2225,6 +1947,50 @@ try {
|
|
|
2225
1947
|
}
|
|
2226
1948
|
logger.info(`Exported ${maps.length} CyberiaMap document(s)`, { codes: maps.map((m) => m.code) });
|
|
2227
1949
|
|
|
1950
|
+
// 3a. Export the audio configuration of those maps, and the assets it binds.
|
|
1951
|
+
// A CyberiaMapAudioConf belongs to one map, so it travels with the instance. A
|
|
1952
|
+
// CyberiaAudio is global and shared by every map that binds its code, so it travels
|
|
1953
|
+
// as a copy: the import upserts it by code and leaves other instances' bindings alone.
|
|
1954
|
+
// The WAV rides along as an ordinary File document, keeping its _id so `fileId` still
|
|
1955
|
+
// resolves after a restore.
|
|
1956
|
+
const audioConfs = await CyberiaMapAudioConf.find({ mapCode: { $in: [...mapCodes] } }).lean();
|
|
1957
|
+
if (audioConfs.length > 0) {
|
|
1958
|
+
fs.ensureDirSync(`${backupDir}/cyberia-map-audio-confs`);
|
|
1959
|
+
const audioCodes = new Set();
|
|
1960
|
+
for (const conf of audioConfs) {
|
|
1961
|
+
fs.writeJsonSync(`${backupDir}/cyberia-map-audio-confs/${encodeURIComponent(conf.mapCode)}.json`, conf, {
|
|
1962
|
+
spaces: 2,
|
|
1963
|
+
});
|
|
1964
|
+
if (conf.defaultMusic) audioCodes.add(conf.defaultMusic);
|
|
1965
|
+
for (const event of conf.events || []) if (event.audioCode) audioCodes.add(event.audioCode);
|
|
1966
|
+
}
|
|
1967
|
+
logger.info(`Exported ${audioConfs.length} CyberiaMapAudioConf document(s)`, {
|
|
1968
|
+
mapCodes: audioConfs.map((c) => c.mapCode),
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
const audioAssets = await CyberiaAudio.find({ code: { $in: [...audioCodes] } }).lean();
|
|
1972
|
+
if (audioAssets.length > 0) {
|
|
1973
|
+
fs.ensureDirSync(`${backupDir}/cyberia-audio`);
|
|
1974
|
+
for (const asset of audioAssets) {
|
|
1975
|
+
fs.writeJsonSync(`${backupDir}/cyberia-audio/${encodeURIComponent(asset.code)}.json`, asset, {
|
|
1976
|
+
spaces: 2,
|
|
1977
|
+
});
|
|
1978
|
+
if (asset.fileId) await exportFileDoc(asset.fileId, `audio-${asset.code}`);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
const missingAudioCodes = [...audioCodes].filter((code) => !audioAssets.some((asset) => asset.code === code));
|
|
1982
|
+
logger.info(`Exported ${audioAssets.length} CyberiaAudio document(s)`, {
|
|
1983
|
+
codes: audioAssets.map((a) => a.code),
|
|
1984
|
+
});
|
|
1985
|
+
if (missingAudioCodes.length > 0) {
|
|
1986
|
+
logger.warn(
|
|
1987
|
+
'Audio bindings reference codes with no imported CyberiaAudio document — ' +
|
|
1988
|
+
'run `node bin/cyberia audio --import` before restoring this backup',
|
|
1989
|
+
{ codes: missingAudioCodes },
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
|
|
2228
1994
|
// 3b. Export quests + actions bound to THIS instance's maps (sourceMapCode
|
|
2229
1995
|
// in the instance's map codes) — only the content tied to this instance.
|
|
2230
1996
|
// Dialogue codes the actions reference are collected so they travel too.
|
|
@@ -2255,103 +2021,51 @@ try {
|
|
|
2255
2021
|
logger.info(`Exported ${actions.length} CyberiaAction document(s)`, { codes: actions.map((a) => a.code) });
|
|
2256
2022
|
}
|
|
2257
2023
|
|
|
2258
|
-
// 4.
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
if (id) objectLayerItemIds.add(id);
|
|
2272
|
-
}
|
|
2273
|
-
|
|
2274
|
-
const contentItemIds = new Set(objectLayerItemIds);
|
|
2275
|
-
|
|
2276
|
-
// 4c. Add all itemIds referenced by CyberiaInstanceConf (entityDefaults + skillConfig).
|
|
2277
|
-
// This ensures liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers and
|
|
2278
|
-
// skill trigger items are included even if no map entity currently uses them.
|
|
2279
|
-
if (instanceConf) {
|
|
2280
|
-
for (const ed of instanceConf.entityDefaults || []) {
|
|
2281
|
-
for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2282
|
-
for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2283
|
-
for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2284
|
-
for (const slot of ed.defaultObjectLayers || []) {
|
|
2285
|
-
if (slot.itemId) objectLayerItemIds.add(slot.itemId);
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
for (const sc of instanceConf.skillConfig || []) {
|
|
2289
|
-
if (sc.triggerItemId) objectLayerItemIds.add(sc.triggerItemId);
|
|
2290
|
-
for (const skill of sc.skills || []) {
|
|
2291
|
-
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$')) {
|
|
2292
|
-
objectLayerItemIds.add(skill.summonedEntityItemId);
|
|
2293
|
-
}
|
|
2294
|
-
}
|
|
2024
|
+
// 4. Export the entity-type defaults this instance's conf references, by _id.
|
|
2025
|
+
// Membership is a reference, not a resemblance: matching on item ids used to pull in
|
|
2026
|
+
// every document that happened to share a skin, so two instances built on the same
|
|
2027
|
+
// art exported each other's wiring and overwrote it on the way back in.
|
|
2028
|
+
const referencedIds = (instanceConf?.entityDefaults || []).map((id) => String(id?._id ?? id));
|
|
2029
|
+
// Every reference resolves: the conf was compacted before it was read.
|
|
2030
|
+
const entityDefaults = referencedIds.length
|
|
2031
|
+
? await CyberiaEntityTypeDefault.find({ _id: { $in: referencedIds } }).lean()
|
|
2032
|
+
: [];
|
|
2033
|
+
if (entityDefaults.length > 0) {
|
|
2034
|
+
fs.ensureDirSync(`${backupDir}/cyberia-entity-type-defaults`);
|
|
2035
|
+
for (const ed of entityDefaults) {
|
|
2036
|
+
fs.writeJsonSync(`${backupDir}/cyberia-entity-type-defaults/${ed._id}.json`, ed, { spaces: 2 });
|
|
2295
2037
|
}
|
|
2038
|
+
logger.info(`Exported ${entityDefaults.length} CyberiaEntityTypeDefault document(s)`, {
|
|
2039
|
+
entityTypes: entityDefaults.map((ed) => ed.entityType),
|
|
2040
|
+
});
|
|
2296
2041
|
}
|
|
2297
2042
|
|
|
2298
|
-
//
|
|
2299
|
-
//
|
|
2300
|
-
//
|
|
2301
|
-
//
|
|
2302
|
-
|
|
2303
|
-
|
|
2043
|
+
// 4b. Everything this instance names: what its maps place, what its entity-type defaults
|
|
2044
|
+
// wire, and what its vendor / assembler / quest catalogs trade. Every one of these
|
|
2045
|
+
// draws an icon somewhere, so the atlases travel with the backup even when no map
|
|
2046
|
+
// entity wears them. One rule, shared with the boot payload and the editor's sync.
|
|
2047
|
+
const objectLayerItemIds = collectInstanceItemIds({ maps, entityDefaults, actions, quests });
|
|
2048
|
+
|
|
2049
|
+
// 4c. Export the skills this instance runs: the collection owns the definitions, and one
|
|
2050
|
+
// belongs here when its trigger item is an id the instance names — which is how a
|
|
2051
|
+
// trigger only a quest objective or a vendor's shelf mentions still travels. Their
|
|
2052
|
+
// summoned entities join the OL set, so this runs before the dialogue + OL queries.
|
|
2053
|
+
{
|
|
2054
|
+
const skills = selectInstanceSkills(await CyberiaSkill.find({}).lean(), objectLayerItemIds);
|
|
2304
2055
|
if (skills.length > 0) {
|
|
2305
2056
|
fs.ensureDirSync(`${backupDir}/cyberia-skills`);
|
|
2306
2057
|
for (const skill of skills) {
|
|
2307
2058
|
fs.writeJsonSync(`${backupDir}/cyberia-skills/${encodeURIComponent(skill.triggerItemId)}.json`, skill, {
|
|
2308
2059
|
spaces: 2,
|
|
2309
2060
|
});
|
|
2310
|
-
for (const def of skill.skills || []) {
|
|
2311
|
-
if (def.summonedEntityItemId && !def.summonedEntityItemId.startsWith('$')) {
|
|
2312
|
-
objectLayerItemIds.add(def.summonedEntityItemId);
|
|
2313
|
-
}
|
|
2314
|
-
}
|
|
2315
2061
|
}
|
|
2062
|
+
for (const summoned of collectSummonedItemIds(skills)) objectLayerItemIds.add(summoned);
|
|
2316
2063
|
logger.info(`Exported ${skills.length} CyberiaSkill document(s)`, {
|
|
2317
2064
|
triggerItemIds: skills.map((sk) => sk.triggerItemId),
|
|
2318
2065
|
});
|
|
2319
2066
|
}
|
|
2320
2067
|
}
|
|
2321
2068
|
|
|
2322
|
-
// 4d-bis. Export entity-type defaults whose item ids belong to this
|
|
2323
|
-
// instance's real content (own model: CyberiaEntityTypeDefault). A
|
|
2324
|
-
// default is related when any of its live/dead/drop ids or default
|
|
2325
|
-
// object-layer ids appears in contentItemIds — map/instance content
|
|
2326
|
-
// only, NOT the canonical conf defaults every instance shares (which
|
|
2327
|
-
// would spuriously drag in the global seed entity-type-defaults).
|
|
2328
|
-
// Matched ids are folded back into objectLayerItemIds so the related
|
|
2329
|
-
// atlases + dialogues export too.
|
|
2330
|
-
if (contentItemIds.size > 0) {
|
|
2331
|
-
const idsForMatch = [...contentItemIds];
|
|
2332
|
-
const entityDefaults = await CyberiaEntityTypeDefault.find({
|
|
2333
|
-
$or: [
|
|
2334
|
-
{ liveItemIds: { $in: idsForMatch } },
|
|
2335
|
-
{ deadItemIds: { $in: idsForMatch } },
|
|
2336
|
-
{ dropItemIds: { $in: idsForMatch } },
|
|
2337
|
-
{ 'defaultObjectLayers.itemId': { $in: idsForMatch } },
|
|
2338
|
-
],
|
|
2339
|
-
}).lean();
|
|
2340
|
-
if (entityDefaults.length > 0) {
|
|
2341
|
-
fs.ensureDirSync(`${backupDir}/cyberia-entity-type-defaults`);
|
|
2342
|
-
for (const ed of entityDefaults) {
|
|
2343
|
-
fs.writeJsonSync(`${backupDir}/cyberia-entity-type-defaults/${ed._id}.json`, ed, { spaces: 2 });
|
|
2344
|
-
for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2345
|
-
for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2346
|
-
for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2347
|
-
for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) objectLayerItemIds.add(slot.itemId);
|
|
2348
|
-
}
|
|
2349
|
-
logger.info(`Exported ${entityDefaults.length} CyberiaEntityTypeDefault document(s)`, {
|
|
2350
|
-
entityTypes: entityDefaults.map((ed) => ed.entityType),
|
|
2351
|
-
});
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
2069
|
// 4e. Export sagas related to this instance. A saga is considered related
|
|
2356
2070
|
// when its code matches the instance code (direct namespace match), or
|
|
2357
2071
|
// when its mapCodes or itemIds overlap with the instance's data.
|
|
@@ -2359,22 +2073,7 @@ try {
|
|
|
2359
2073
|
// conf-default, and skill-summoned item IDs — giving the broadest possible
|
|
2360
2074
|
// match surface for saga discovery.
|
|
2361
2075
|
const sagaCodeMatch = instanceCode ? await CyberiaSaga.find({ code: instanceCode }).lean() : [];
|
|
2362
|
-
|
|
2363
|
-
const sagaMapOverlap = true
|
|
2364
|
-
? []
|
|
2365
|
-
: mapCodes.size > 0
|
|
2366
|
-
? await CyberiaSaga.find({ mapCodes: { $in: [...mapCodes] } }).lean()
|
|
2367
|
-
: [];
|
|
2368
|
-
const sagaItemOverlap = true
|
|
2369
|
-
? []
|
|
2370
|
-
: objectLayerItemIds.size > 0
|
|
2371
|
-
? await CyberiaSaga.find({ itemIds: { $in: [...objectLayerItemIds] } }).lean()
|
|
2372
|
-
: [];
|
|
2373
|
-
const allSagas = [
|
|
2374
|
-
...new Map(
|
|
2375
|
-
[...sagaCodeMatch, ...sagaMapOverlap, ...sagaItemOverlap].map((s) => [s._id.toString(), s]),
|
|
2376
|
-
).values(),
|
|
2377
|
-
];
|
|
2076
|
+
const allSagas = [...new Map(sagaCodeMatch.map((s) => [s._id.toString(), s])).values()];
|
|
2378
2077
|
if (allSagas.length > 0) {
|
|
2379
2078
|
fs.ensureDirSync(`${backupDir}/cyberia-sagas`);
|
|
2380
2079
|
for (const saga of allSagas) {
|
|
@@ -2486,8 +2185,9 @@ try {
|
|
|
2486
2185
|
if (atlas) {
|
|
2487
2186
|
const atlasExport = newInstance(atlas);
|
|
2488
2187
|
objectLayerExport.atlasSpriteSheetId = atlas._id;
|
|
2489
|
-
|
|
2490
|
-
|
|
2188
|
+
// Every render travels with the atlas, so a restore leaves no reference dangling.
|
|
2189
|
+
for (const field of ATLAS_FILE_FIELDS) {
|
|
2190
|
+
if (atlas[field]) await exportFileDoc(atlas[field], atlasBackupFileKey(field, itemKey));
|
|
2491
2191
|
}
|
|
2492
2192
|
|
|
2493
2193
|
const atlasFile = atlas.fileId ? await File.findById(atlas.fileId).lean() : null;
|
|
@@ -2683,15 +2383,15 @@ try {
|
|
|
2683
2383
|
|
|
2684
2384
|
logger.info('Importing instance', { code: instanceCode, backupDir });
|
|
2685
2385
|
|
|
2686
|
-
//
|
|
2687
|
-
//
|
|
2386
|
+
// A restore writes object layers, so collapse duplicates and make the
|
|
2387
|
+
// data.item.id index unique first.
|
|
2688
2388
|
const { removedIds, indexUpgraded } = await ObjectLayer.ensureUniqueItemIdIndex();
|
|
2689
2389
|
if (removedIds.length > 0) logger.warn(`Removed ${removedIds.length} duplicate ObjectLayer document(s)`);
|
|
2690
2390
|
if (indexUpgraded) logger.info('Upgraded data.item.id index to unique');
|
|
2691
2391
|
|
|
2692
|
-
// Item ids
|
|
2693
|
-
//
|
|
2694
|
-
//
|
|
2392
|
+
// Item ids of this instance, from the imported object layers and the
|
|
2393
|
+
// instance doc. They backfill skills from DefaultSkillConfig when the
|
|
2394
|
+
// backup carries none.
|
|
2695
2395
|
const importedItemIds = new Set();
|
|
2696
2396
|
|
|
2697
2397
|
// 0. Drop existing documents if --drop is set
|
|
@@ -2712,30 +2412,17 @@ try {
|
|
|
2712
2412
|
// Query other instances/maps for shared thumbnail exclusion
|
|
2713
2413
|
const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
|
|
2714
2414
|
|
|
2715
|
-
// Add
|
|
2716
|
-
for (const entry of existingInstance.itemIds || []) {
|
|
2717
|
-
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
2718
|
-
if (id) dropOlItemIds.add(id);
|
|
2719
|
-
}
|
|
2720
|
-
|
|
2721
|
-
// Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
|
|
2415
|
+
// Add the item ids the conf's referenced entity-type defaults name.
|
|
2722
2416
|
const existingConf =
|
|
2723
2417
|
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
2724
2418
|
(existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
|
|
2725
2419
|
if (existingConf) {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
}
|
|
2732
|
-
for (const sc of existingConf.skillConfig || []) {
|
|
2733
|
-
if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
|
|
2734
|
-
for (const skill of sc.skills || []) {
|
|
2735
|
-
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
|
|
2736
|
-
dropOlItemIds.add(skill.summonedEntityItemId);
|
|
2737
|
-
}
|
|
2738
|
-
}
|
|
2420
|
+
// The conf names its entity-type defaults by _id; read the documents to reach their items.
|
|
2421
|
+
const referencedIds = (existingConf.entityDefaults || []).map((id) => String(id?._id ?? id));
|
|
2422
|
+
const referenced = referencedIds.length
|
|
2423
|
+
? await CyberiaEntityTypeDefault.find({ _id: { $in: referencedIds } }).lean()
|
|
2424
|
+
: [];
|
|
2425
|
+
for (const itemId of collectInstanceItemIds({ entityDefaults: referenced })) dropOlItemIds.add(itemId);
|
|
2739
2426
|
}
|
|
2740
2427
|
|
|
2741
2428
|
const otherMaps = await CyberiaMap.find(
|
|
@@ -2759,6 +2446,12 @@ try {
|
|
|
2759
2446
|
const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
|
|
2760
2447
|
logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
|
|
2761
2448
|
|
|
2449
|
+
// A map's audio configuration belongs to that map. The assets it bound do not:
|
|
2450
|
+
// they are global and shared, so they stay.
|
|
2451
|
+
const audioConfResult = await CyberiaMapAudioConf.deleteMany({ mapCode: { $in: [...dropMapCodes] } });
|
|
2452
|
+
if (audioConfResult.deletedCount > 0)
|
|
2453
|
+
logger.info(`Dropped ${audioConfResult.deletedCount} CyberiaMapAudioConf document(s)`);
|
|
2454
|
+
|
|
2762
2455
|
// Quests + actions are bound to maps by sourceMapCode, so they drop
|
|
2763
2456
|
// with this instance's maps — only the content tied to this instance.
|
|
2764
2457
|
const questResult = await CyberiaQuest.deleteMany({ sourceMapCode: { $in: [...dropMapCodes] } });
|
|
@@ -2791,6 +2484,15 @@ try {
|
|
|
2791
2484
|
}
|
|
2792
2485
|
}
|
|
2793
2486
|
|
|
2487
|
+
// A conf stores no skills: the ones this instance ran are the ones its own items trigger.
|
|
2488
|
+
// What those summon is drawn by this instance alone, so it joins the drop surface and is
|
|
2489
|
+
// then protected by the same shared-with-another-map check as everything else.
|
|
2490
|
+
for (const itemId of collectSummonedItemIds(
|
|
2491
|
+
selectInstanceSkills(await CyberiaSkill.find({}).lean(), dropOlItemIds),
|
|
2492
|
+
)) {
|
|
2493
|
+
dropOlItemIds.add(itemId);
|
|
2494
|
+
}
|
|
2495
|
+
|
|
2794
2496
|
// Exclude OL item IDs referenced by maps outside this instance
|
|
2795
2497
|
const sharedOlItemIds = new Set();
|
|
2796
2498
|
for (const m of otherMaps) {
|
|
@@ -2850,19 +2552,17 @@ try {
|
|
|
2850
2552
|
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
2851
2553
|
}
|
|
2852
2554
|
|
|
2853
|
-
// Delete AtlasSpriteSheet +
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
}
|
|
2864
|
-
const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
2865
|
-
logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
|
|
2555
|
+
// Delete AtlasSpriteSheet + every File render it owns, through the store that
|
|
2556
|
+
// knows how many renders that is.
|
|
2557
|
+
if (atlasIds.length > 0 || itemKeysToClean.size > 0) {
|
|
2558
|
+
const purged = await AtlasSpriteSheetStore.purge({
|
|
2559
|
+
itemKeys: [...itemKeysToClean],
|
|
2560
|
+
atlasIds,
|
|
2561
|
+
options: { host, path },
|
|
2562
|
+
});
|
|
2563
|
+
for (const cid of purged.cids) cidsToUnpin.add(cid);
|
|
2564
|
+
if (purged.files > 0) logger.info(`Dropped ${purged.files} File document(s) (atlas)`);
|
|
2565
|
+
if (purged.atlases > 0) logger.info(`Dropped ${purged.atlases} AtlasSpriteSheet document(s)`);
|
|
2866
2566
|
}
|
|
2867
2567
|
|
|
2868
2568
|
// Delete RenderFrames
|
|
@@ -2919,13 +2619,16 @@ try {
|
|
|
2919
2619
|
const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
|
|
2920
2620
|
let importedConf = null;
|
|
2921
2621
|
if (fs.existsSync(confImportPath)) {
|
|
2922
|
-
//
|
|
2923
|
-
//
|
|
2924
|
-
const confData =
|
|
2622
|
+
// Made whole and valid before the live conf is touched, so a backup the schema
|
|
2623
|
+
// rejects resets to defaults rather than leaving the instance with no conf.
|
|
2624
|
+
const { conf: confData } = await CyberiaInstanceConfService.coerceToSchema(
|
|
2625
|
+
await adoptEntityTypeDefaultRefs(fs.readJsonSync(confImportPath), backupDir, CyberiaEntityTypeDefault),
|
|
2626
|
+
CyberiaInstanceConf,
|
|
2627
|
+
);
|
|
2925
2628
|
if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
|
|
2926
2629
|
await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
|
|
2927
|
-
//
|
|
2928
|
-
//
|
|
2630
|
+
// Bump updatedAt so the world version changes and the server
|
|
2631
|
+
// re-applies the config without a restart.
|
|
2929
2632
|
confData.updatedAt = new Date();
|
|
2930
2633
|
importedConf = await CyberiaInstanceConf.create(confData);
|
|
2931
2634
|
logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
|
|
@@ -2933,11 +2636,10 @@ try {
|
|
|
2933
2636
|
logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
|
|
2934
2637
|
}
|
|
2935
2638
|
|
|
2936
|
-
//
|
|
2937
|
-
//
|
|
2938
|
-
//
|
|
2939
|
-
//
|
|
2940
|
-
// server's version hash changes and ReloadWorld re-applies the config.
|
|
2639
|
+
// --conf must not recreate the CyberiaInstance: that would overwrite
|
|
2640
|
+
// cyberiaMapCodes, portals and itemIds from a possibly stale backup.
|
|
2641
|
+
// Update the conf ref and bump updatedAt, so the world version changes
|
|
2642
|
+
// and the server re-applies the config.
|
|
2941
2643
|
if (importedConf) {
|
|
2942
2644
|
const result = await CyberiaInstance.updateOne(
|
|
2943
2645
|
{ code: instanceCode },
|
|
@@ -3001,6 +2703,7 @@ try {
|
|
|
3001
2703
|
|
|
3002
2704
|
// 3. Import AtlasSpriteSheet
|
|
3003
2705
|
const atlasDir = `${backupDir}/atlas-sprite-sheets`;
|
|
2706
|
+
const atlasesWithoutMinify = new Set();
|
|
3004
2707
|
if (fs.existsSync(atlasDir)) {
|
|
3005
2708
|
const atlasFiles = fs.readdirSync(atlasDir).filter((f) => f.endsWith('.json'));
|
|
3006
2709
|
let atlasCount = 0;
|
|
@@ -3014,6 +2717,22 @@ try {
|
|
|
3014
2717
|
atlasCount++;
|
|
3015
2718
|
}
|
|
3016
2719
|
logger.info(`Imported ${atlasCount} AtlasSpriteSheet document(s)`);
|
|
2720
|
+
// A backup from before the still existed restores without one; cut each from its render.
|
|
2721
|
+
// An atlas restored without its minified render is rebuilt whole once its object
|
|
2722
|
+
// layer is in place, below.
|
|
2723
|
+
let stillCount = 0;
|
|
2724
|
+
for (const f of atlasFiles) {
|
|
2725
|
+
const atlasData = fs.readJsonSync(`${atlasDir}/${f}`);
|
|
2726
|
+
const itemKey = atlasData.metadata?.itemKey;
|
|
2727
|
+
if (!itemKey) continue;
|
|
2728
|
+
if (!atlasData.minifyFileId) atlasesWithoutMinify.add(itemKey);
|
|
2729
|
+
const { status } = await AtlasSpriteSheetStore.syncIdlePreview({ itemKey, options: { host, path } });
|
|
2730
|
+
if (status === 'updated') stillCount++;
|
|
2731
|
+
}
|
|
2732
|
+
if (stillCount) logger.info(`Filled ${stillCount} idle preview still(s)`);
|
|
2733
|
+
// The replaced atlases took their renders out of reach; the imported ones are
|
|
2734
|
+
// already stored, so what no atlas points at now is exactly the leftover.
|
|
2735
|
+
await AtlasSpriteSheetStore.pruneOrphanRenders({ options: { host, path } });
|
|
3017
2736
|
}
|
|
3018
2737
|
|
|
3019
2738
|
// 4. Import object layers
|
|
@@ -3043,6 +2762,36 @@ try {
|
|
|
3043
2762
|
logger.info(`Imported ${olCount} ObjectLayer document(s)`);
|
|
3044
2763
|
}
|
|
3045
2764
|
|
|
2765
|
+
// 4a. An atlas the backup restored without its minified render cannot be served: the
|
|
2766
|
+
// client runtime pairs the blob with the metadata, so a render cut from the frames
|
|
2767
|
+
// alone would be refused wherever the stored layout moved. Regenerate the whole
|
|
2768
|
+
// atlas from the frames the backup carries, and relink the object layer to it the
|
|
2769
|
+
// way `ol --to-atlas-sprite-sheet` does.
|
|
2770
|
+
for (const itemKey of atlasesWithoutMinify) {
|
|
2771
|
+
try {
|
|
2772
|
+
const objectLayer = await ObjectLayer.findByItemId(itemKey).populate('objectLayerRenderFramesId');
|
|
2773
|
+
if (!objectLayer?.objectLayerRenderFramesId) {
|
|
2774
|
+
logger.warn(`Backup atlas '${itemKey}' has no minified render and no render frames to rebuild it from`);
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
|
|
2778
|
+
itemKey,
|
|
2779
|
+
objectLayerRenderFrames: objectLayer.objectLayerRenderFramesId,
|
|
2780
|
+
options: { host, path },
|
|
2781
|
+
});
|
|
2782
|
+
objectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
2783
|
+
if (!objectLayer.data.render) objectLayer.data.render = {};
|
|
2784
|
+
objectLayer.data.render.cid = atlasCid;
|
|
2785
|
+
objectLayer.data.render.metadataCid = atlasMetadataCid;
|
|
2786
|
+
objectLayer.markModified('data.render');
|
|
2787
|
+
await objectLayer.save();
|
|
2788
|
+
await ObjectLayerEngine.computeAndSaveFinalSha256({ objectLayer, options: { host, path } });
|
|
2789
|
+
logger.info(`Rebuilt the atlas of '${itemKey}': the backup carried no minified render`);
|
|
2790
|
+
} catch (rebuildError) {
|
|
2791
|
+
logger.error(`Atlas rebuild failed for '${itemKey}': ${rebuildError.message}`);
|
|
2792
|
+
}
|
|
2793
|
+
}
|
|
2794
|
+
|
|
3046
2795
|
// 4b. Regenerate static frame PNGs from imported render-frames + object-layer documents.
|
|
3047
2796
|
// Mirrors the writeStaticFrameAssets call in `ol --import` so src/client/public/cyberia
|
|
3048
2797
|
// and the public/<host><path> deployment dir are populated even when the cyberia
|
|
@@ -3081,7 +2830,7 @@ try {
|
|
|
3081
2830
|
itemId,
|
|
3082
2831
|
objectLayerRenderFramesData,
|
|
3083
2832
|
objectLayerData: olData,
|
|
3084
|
-
cellPixelDim:
|
|
2833
|
+
cellPixelDim: DEFAULT_ATLAS_UPSCALE_FACTOR,
|
|
3085
2834
|
});
|
|
3086
2835
|
staticWriteCount += written.length;
|
|
3087
2836
|
} catch (err) {
|
|
@@ -3106,12 +2855,69 @@ try {
|
|
|
3106
2855
|
logger.info(`Imported ${mapCount} CyberiaMap document(s)`);
|
|
3107
2856
|
}
|
|
3108
2857
|
|
|
2858
|
+
// 5a. Import audio assets, then the map bindings that reference them by code. Assets are
|
|
2859
|
+
// upserted by code (they are global and may already be present from another import);
|
|
2860
|
+
// their File documents were restored above with their original _id, so `fileId` still
|
|
2861
|
+
// resolves. A binding whose asset is absent is kept: the code is the reference, and
|
|
2862
|
+
// importing the asset later makes it play.
|
|
2863
|
+
const audioAssetsDir = `${backupDir}/cyberia-audio`;
|
|
2864
|
+
if (fs.existsSync(audioAssetsDir)) {
|
|
2865
|
+
const assetFiles = fs.readdirSync(audioAssetsDir).filter((f) => f.endsWith('.json'));
|
|
2866
|
+
let assetCount = 0;
|
|
2867
|
+
let replacedFiles = 0;
|
|
2868
|
+
for (const file of assetFiles) {
|
|
2869
|
+
const assetData = fs.readJsonSync(`${audioAssetsDir}/${file}`);
|
|
2870
|
+
if (!assetData.code) {
|
|
2871
|
+
logger.warn(`Skipping CyberiaAudio backup without code: ${file}`);
|
|
2872
|
+
continue;
|
|
2873
|
+
}
|
|
2874
|
+
// The asset this restore supersedes may hold different bytes under a different
|
|
2875
|
+
// fileId. Replacing the document without dropping that blob leaves it referenced by
|
|
2876
|
+
// nothing — an orphan `db clean-fs` would later have to sweep.
|
|
2877
|
+
const superseded = await CyberiaAudio.find({
|
|
2878
|
+
$or: [{ code: assetData.code }, ...(assetData._id ? [{ _id: assetData._id }] : [])],
|
|
2879
|
+
}).lean();
|
|
2880
|
+
await CyberiaAudio.deleteOne({ code: assetData.code });
|
|
2881
|
+
if (assetData._id) await CyberiaAudio.deleteOne({ _id: assetData._id });
|
|
2882
|
+
await CyberiaAudio.create(assetData);
|
|
2883
|
+
for (const old of superseded) {
|
|
2884
|
+
if (!old.fileId || String(old.fileId) === String(assetData.fileId)) continue;
|
|
2885
|
+
if (await File.findByIdAndDelete(old.fileId)) replacedFiles++;
|
|
2886
|
+
}
|
|
2887
|
+
assetCount++;
|
|
2888
|
+
}
|
|
2889
|
+
logger.info(`Imported ${assetCount} CyberiaAudio document(s)`, {
|
|
2890
|
+
...(replacedFiles > 0 ? { replacedFiles } : {}),
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
const audioConfsDir = `${backupDir}/cyberia-map-audio-confs`;
|
|
2895
|
+
if (fs.existsSync(audioConfsDir)) {
|
|
2896
|
+
const confFiles = fs.readdirSync(audioConfsDir).filter((f) => f.endsWith('.json'));
|
|
2897
|
+
let audioConfCount = 0;
|
|
2898
|
+
for (const file of confFiles) {
|
|
2899
|
+
const confData = fs.readJsonSync(`${audioConfsDir}/${file}`);
|
|
2900
|
+
if (!confData.mapCode) {
|
|
2901
|
+
logger.warn(`Skipping CyberiaMapAudioConf backup without mapCode: ${file}`);
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
await CyberiaMapAudioConf.deleteOne({ mapCode: confData.mapCode });
|
|
2905
|
+
if (confData._id) await CyberiaMapAudioConf.deleteOne({ _id: confData._id });
|
|
2906
|
+
await CyberiaMapAudioConf.create(confData);
|
|
2907
|
+
audioConfCount++;
|
|
2908
|
+
}
|
|
2909
|
+
logger.info(`Imported ${audioConfCount} CyberiaMapAudioConf document(s)`);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
3109
2912
|
// 6. Import CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
|
|
3110
2913
|
const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
|
|
3111
2914
|
if (fs.existsSync(confImportPath)) {
|
|
3112
|
-
//
|
|
3113
|
-
//
|
|
3114
|
-
const confData =
|
|
2915
|
+
// Made whole and valid before the live conf is touched, so a backup the schema
|
|
2916
|
+
// rejects resets to defaults rather than leaving the instance with no conf.
|
|
2917
|
+
const { conf: confData } = await CyberiaInstanceConfService.coerceToSchema(
|
|
2918
|
+
await adoptEntityTypeDefaultRefs(fs.readJsonSync(confImportPath), backupDir, CyberiaEntityTypeDefault),
|
|
2919
|
+
CyberiaInstanceConf,
|
|
2920
|
+
);
|
|
3115
2921
|
if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
|
|
3116
2922
|
await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
|
|
3117
2923
|
await CyberiaInstanceConf.create(confData);
|
|
@@ -3124,13 +2930,6 @@ try {
|
|
|
3124
2930
|
const instancePath = `${backupDir}/cyberia-instance.json`;
|
|
3125
2931
|
if (fs.existsSync(instancePath)) {
|
|
3126
2932
|
const instanceData = fs.readJsonSync(instancePath);
|
|
3127
|
-
// Heal legacy shapes against the current model. itemIds migrated from a
|
|
3128
|
-
// flat string[] to [{ id, defaultPlayerInventory }] — a raw old backup
|
|
3129
|
-
// would fail Mongoose embedded-cast validation, so normalize it here.
|
|
3130
|
-
instanceData.itemIds = (instanceData.itemIds || [])
|
|
3131
|
-
.map((entry) => (typeof entry === 'string' ? { id: entry, defaultPlayerInventory: false } : entry))
|
|
3132
|
-
.filter((entry) => entry && entry.id);
|
|
3133
|
-
for (const entry of instanceData.itemIds) importedItemIds.add(entry.id);
|
|
3134
2933
|
await CyberiaInstance.deleteOne({ code: instanceCode });
|
|
3135
2934
|
await CyberiaInstance.deleteOne({ _id: instanceData._id });
|
|
3136
2935
|
await CyberiaInstance.create(instanceData);
|
|
@@ -3225,29 +3024,32 @@ try {
|
|
|
3225
3024
|
logger.info(`Imported ${skillCount} CyberiaSkill document(s)`);
|
|
3226
3025
|
}
|
|
3227
3026
|
|
|
3228
|
-
// 8d-bis. Import CyberiaEntityTypeDefault documents (
|
|
3229
|
-
//
|
|
3027
|
+
// 8d-bis. Import CyberiaEntityTypeDefault documents by _id (preserveUUID), which is how
|
|
3028
|
+
// the conf's references keep resolving after a restore. Only the documents this
|
|
3029
|
+
// backup carries are touched: overwriting by an (entityType, liveItemIds) "natural
|
|
3030
|
+
// key" clobbered another instance's document whenever two worlds shared a skin.
|
|
3230
3031
|
const entityDefaultsDir = `${backupDir}/cyberia-entity-type-defaults`;
|
|
3231
3032
|
if (fs.existsSync(entityDefaultsDir)) {
|
|
3232
3033
|
const entityDefaultFiles = fs.readdirSync(entityDefaultsDir).filter((f) => f.endsWith('.json'));
|
|
3233
3034
|
let entityDefaultCount = 0;
|
|
3234
3035
|
for (const file of entityDefaultFiles) {
|
|
3235
3036
|
const edData = fs.readJsonSync(`${entityDefaultsDir}/${file}`);
|
|
3236
|
-
if (!edData.entityType) {
|
|
3237
|
-
logger.warn(`Skipping CyberiaEntityTypeDefault backup without entityType: ${file}`);
|
|
3037
|
+
if (!edData.entityType || !edData._id) {
|
|
3038
|
+
logger.warn(`Skipping CyberiaEntityTypeDefault backup without entityType or _id: ${file}`);
|
|
3238
3039
|
continue;
|
|
3239
3040
|
}
|
|
3240
|
-
await CyberiaEntityTypeDefault.
|
|
3241
|
-
entityType: edData.entityType,
|
|
3242
|
-
liveItemIds: edData.liveItemIds || [],
|
|
3243
|
-
});
|
|
3244
|
-
if (edData._id) await CyberiaEntityTypeDefault.deleteOne({ _id: edData._id });
|
|
3041
|
+
await CyberiaEntityTypeDefault.deleteOne({ _id: edData._id });
|
|
3245
3042
|
await CyberiaEntityTypeDefault.create(edData);
|
|
3246
3043
|
entityDefaultCount++;
|
|
3247
3044
|
}
|
|
3248
3045
|
logger.info(`Imported ${entityDefaultCount} CyberiaEntityTypeDefault document(s)`);
|
|
3249
3046
|
}
|
|
3250
3047
|
|
|
3048
|
+
// A conf can reference a default this backup does not carry — an older backup, or one
|
|
3049
|
+
// exported before the reference existed. Restoring that reference would recreate the
|
|
3050
|
+
// orphan the export just removed, so the restored conf is compacted too.
|
|
3051
|
+
await CyberiaEntityTypeDefaultService.compactInstanceRefs({ host, path }, { instanceCode });
|
|
3052
|
+
|
|
3251
3053
|
// 8e. Backfill missing skills from the canonical DefaultSkillConfig. Old
|
|
3252
3054
|
// backups predate the CyberiaSkill model and ship no skills/ dir, so
|
|
3253
3055
|
// any instance item that has a canonical skill (e.g. atlas_pistol_mk2,
|
|
@@ -3564,30 +3366,17 @@ try {
|
|
|
3564
3366
|
// Query other instances for shared thumbnail exclusion
|
|
3565
3367
|
const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
|
|
3566
3368
|
|
|
3567
|
-
// Add
|
|
3568
|
-
for (const entry of existingInstance.itemIds || []) {
|
|
3569
|
-
const id = typeof entry === 'string' ? entry : entry?.id;
|
|
3570
|
-
if (id) dropOlItemIds.add(id);
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
|
-
// Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
|
|
3369
|
+
// Add the item ids the conf's referenced entity-type defaults name.
|
|
3574
3370
|
const existingConf =
|
|
3575
3371
|
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
3576
3372
|
(existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
|
|
3577
3373
|
if (existingConf) {
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
}
|
|
3584
|
-
for (const sc of existingConf.skillConfig || []) {
|
|
3585
|
-
if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
|
|
3586
|
-
for (const skill of sc.skills || []) {
|
|
3587
|
-
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
|
|
3588
|
-
dropOlItemIds.add(skill.summonedEntityItemId);
|
|
3589
|
-
}
|
|
3590
|
-
}
|
|
3374
|
+
// The conf names its entity-type defaults by _id; read the documents to reach their items.
|
|
3375
|
+
const referencedIds = (existingConf.entityDefaults || []).map((id) => String(id?._id ?? id));
|
|
3376
|
+
const referenced = referencedIds.length
|
|
3377
|
+
? await CyberiaEntityTypeDefault.find({ _id: { $in: referencedIds } }).lean()
|
|
3378
|
+
: [];
|
|
3379
|
+
for (const itemId of collectInstanceItemIds({ entityDefaults: referenced })) dropOlItemIds.add(itemId);
|
|
3591
3380
|
}
|
|
3592
3381
|
|
|
3593
3382
|
const otherMaps = await CyberiaMap.find(
|
|
@@ -3608,6 +3397,20 @@ try {
|
|
|
3608
3397
|
}
|
|
3609
3398
|
const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
|
|
3610
3399
|
logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
|
|
3400
|
+
|
|
3401
|
+
// A map's audio configuration belongs to that map; the shared assets it bound do not.
|
|
3402
|
+
const audioConfResult = await CyberiaMapAudioConf.deleteMany({ mapCode: { $in: [...dropMapCodes] } });
|
|
3403
|
+
if (audioConfResult.deletedCount > 0)
|
|
3404
|
+
logger.info(`Dropped ${audioConfResult.deletedCount} CyberiaMapAudioConf document(s)`);
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
// A conf stores no skills: the ones this instance ran are the ones its own items trigger.
|
|
3408
|
+
// What those summon is drawn by this instance alone, so it joins the drop surface and is
|
|
3409
|
+
// then protected by the same shared-with-another-map check as everything else.
|
|
3410
|
+
for (const itemId of collectSummonedItemIds(
|
|
3411
|
+
selectInstanceSkills(await CyberiaSkill.find({}).lean(), dropOlItemIds),
|
|
3412
|
+
)) {
|
|
3413
|
+
dropOlItemIds.add(itemId);
|
|
3611
3414
|
}
|
|
3612
3415
|
|
|
3613
3416
|
// Exclude OL item IDs referenced by maps outside this instance
|
|
@@ -3664,18 +3467,15 @@ try {
|
|
|
3664
3467
|
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
3665
3468
|
}
|
|
3666
3469
|
|
|
3667
|
-
if (atlasIds.length > 0) {
|
|
3668
|
-
const
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
}
|
|
3677
|
-
const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
3678
|
-
logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
|
|
3470
|
+
if (atlasIds.length > 0 || itemKeysToClean.size > 0) {
|
|
3471
|
+
const purged = await AtlasSpriteSheetStore.purge({
|
|
3472
|
+
itemKeys: [...itemKeysToClean],
|
|
3473
|
+
atlasIds,
|
|
3474
|
+
options: { host, path },
|
|
3475
|
+
});
|
|
3476
|
+
for (const cid of purged.cids) cidsToUnpin.add(cid);
|
|
3477
|
+
if (purged.files > 0) logger.info(`Dropped ${purged.files} File document(s) (atlas)`);
|
|
3478
|
+
if (purged.atlases > 0) logger.info(`Dropped ${purged.atlases} AtlasSpriteSheet document(s)`);
|
|
3679
3479
|
}
|
|
3680
3480
|
|
|
3681
3481
|
if (renderFrameIds.length > 0) {
|
|
@@ -3827,6 +3627,130 @@ try {
|
|
|
3827
3627
|
}
|
|
3828
3628
|
});
|
|
3829
3629
|
|
|
3630
|
+
const runAudioCommand = async (audioCode, options = {}) => {
|
|
3631
|
+
const assignments = {
|
|
3632
|
+
...(options.setDefaultMusic === undefined ? {} : { defaultMusic: options.setDefaultMusic }),
|
|
3633
|
+
events: options.setEvent ?? [],
|
|
3634
|
+
};
|
|
3635
|
+
const configuring = assignments.defaultMusic !== undefined || assignments.events.length > 0;
|
|
3636
|
+
|
|
3637
|
+
if (configuring && !options.map) {
|
|
3638
|
+
logger.error('--map <map-code> is required to apply --set-default-music/--set-event');
|
|
3639
|
+
process.exit(1);
|
|
3640
|
+
}
|
|
3641
|
+
if (!options.import && !options.map) {
|
|
3642
|
+
logger.error('Nothing to do: pass --import, or --map <map-code> to read or configure a map');
|
|
3643
|
+
process.exit(1);
|
|
3644
|
+
}
|
|
3645
|
+
|
|
3646
|
+
if (options.envPath && !fs.existsSync(options.envPath)) {
|
|
3647
|
+
logger.error(`Env file not found: ${options.envPath}`);
|
|
3648
|
+
process.exit(1);
|
|
3649
|
+
}
|
|
3650
|
+
const envPath =
|
|
3651
|
+
options.envPath || `./engine-private/conf/dd-cyberia/.env.${options.dev ? 'development' : 'production'}`;
|
|
3652
|
+
if (fs.existsSync(envPath)) dotenv.config({ path: envPath, override: true });
|
|
3653
|
+
|
|
3654
|
+
const deployId = process.env.DEFAULT_DEPLOY_ID;
|
|
3655
|
+
const host = process.env.DEFAULT_DEPLOY_HOST;
|
|
3656
|
+
const path = process.env.DEFAULT_DEPLOY_PATH;
|
|
3657
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
3658
|
+
if (!fs.existsSync(confServerPath)) {
|
|
3659
|
+
logger.error(`Server config not found: ${confServerPath}. Ensure DEFAULT_DEPLOY_ID is set.`);
|
|
3660
|
+
process.exit(1);
|
|
3661
|
+
}
|
|
3662
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
3663
|
+
const { db } = confServer[host][path];
|
|
3664
|
+
db.host = options.mongoHost
|
|
3665
|
+
? options.mongoHost
|
|
3666
|
+
: options.dev
|
|
3667
|
+
? db.host
|
|
3668
|
+
: db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
|
|
3669
|
+
|
|
3670
|
+
logger.info('env', { env: envPath, deployId, host, path });
|
|
3671
|
+
|
|
3672
|
+
await DataBaseProviderService.load({
|
|
3673
|
+
apis: ['cyberia-audio', 'cyberia-instance', 'cyberia-map', 'cyberia-map-audio-conf', 'file'],
|
|
3674
|
+
host,
|
|
3675
|
+
path,
|
|
3676
|
+
db,
|
|
3677
|
+
});
|
|
3678
|
+
|
|
3679
|
+
try {
|
|
3680
|
+
if (options.seedWorld) {
|
|
3681
|
+
// An instance names its own maps, in its own order; without one the fallback world is the
|
|
3682
|
+
// world being scored. Either way the bank and the rotation are the same.
|
|
3683
|
+
const result = options.instance
|
|
3684
|
+
? await seedInstanceAudio(
|
|
3685
|
+
{ instanceCode: options.instance, recordsPath: options.recordsPath },
|
|
3686
|
+
{ host, path },
|
|
3687
|
+
)
|
|
3688
|
+
: await seedFallbackAudio({ recordsPath: options.recordsPath }, { host, path });
|
|
3689
|
+
logger.info(
|
|
3690
|
+
`seed-audio${options.instance ? ` --instance ${options.instance}` : ''}: ` +
|
|
3691
|
+
`${result.assets.length} assets, ${result.maps.length} maps`,
|
|
3692
|
+
);
|
|
3693
|
+
} else if (options.import) {
|
|
3694
|
+
const recordsPath = options.recordsPath || DEFAULT_AUDIO_RECORDS_PATH;
|
|
3695
|
+
const codes = audioCode
|
|
3696
|
+
? audioCode
|
|
3697
|
+
.split(',')
|
|
3698
|
+
.map((code) => code.trim())
|
|
3699
|
+
.filter(Boolean)
|
|
3700
|
+
: null;
|
|
3701
|
+
const imported = await CyberiaAudioService.importRecords({ recordsPath, codes }, { host, path });
|
|
3702
|
+
logger.info(`audio --import: imported ${imported.length} asset(s) from ${recordsPath}`);
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
if (options.map) {
|
|
3706
|
+
/** @type {import('mongoose').Model} */
|
|
3707
|
+
const CyberiaMap = DataBaseProviderService.getModel('cyberia-map', { host, path });
|
|
3708
|
+
if (!(await CyberiaMap.exists({ code: options.map }))) {
|
|
3709
|
+
throw new Error(`cyberia-map not found for code="${options.map}"`);
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
if (configuring) {
|
|
3713
|
+
const conf = await CyberiaMapAudioConfService.assign(
|
|
3714
|
+
{ mapCode: options.map, ...assignments },
|
|
3715
|
+
{ host, path },
|
|
3716
|
+
);
|
|
3717
|
+
logger.info(`audio --map: updated audio configuration for "${options.map}"`, {
|
|
3718
|
+
defaultMusic: conf.defaultMusic || null,
|
|
3719
|
+
events: conf.events.map(({ logicEventId, audioCode: code }) => `${logicEventId}:${code}`),
|
|
3720
|
+
});
|
|
3721
|
+
} else {
|
|
3722
|
+
const conf = await CyberiaMapAudioConfService.getByMapCode(options.map, { host, path });
|
|
3723
|
+
if (!conf) logger.warn(`No audio configuration for map "${options.map}"`);
|
|
3724
|
+
else console.log(JSON.stringify(conf, null, 2));
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
} finally {
|
|
3728
|
+
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
3729
|
+
}
|
|
3730
|
+
};
|
|
3731
|
+
|
|
3732
|
+
// ── audio: import cyberia-audio assets and configure per-map audio ──
|
|
3733
|
+
program
|
|
3734
|
+
.command('audio [audio-code]')
|
|
3735
|
+
.option('--import', 'Import <name>.wav + <name>.json pairs into MongoDB (all pairs when no id is given)')
|
|
3736
|
+
.option(
|
|
3737
|
+
'--records-path <records-path>',
|
|
3738
|
+
`Records directory to import from (default: ${DEFAULT_AUDIO_RECORDS_PATH})`,
|
|
3739
|
+
)
|
|
3740
|
+
.option('--map <map-code>', 'Target cyberia-map code to read or configure')
|
|
3741
|
+
.option('--set-default-music <audio-code>', 'Set the default background music of --map')
|
|
3742
|
+
.option(
|
|
3743
|
+
'--set-event <logic-event-id:audio-code>',
|
|
3744
|
+
'Bind an audio asset to a logic event e.g. combat:combat or shoot:shoot, repeatable',
|
|
3745
|
+
eventAudioBindingFactory('--set-event'),
|
|
3746
|
+
[],
|
|
3747
|
+
)
|
|
3748
|
+
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
3749
|
+
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
3750
|
+
.option('--dev', 'Force development environment')
|
|
3751
|
+
.description('Import cyberia-audio assets into MongoDB and configure cyberia-map audio')
|
|
3752
|
+
.action(runAudioCommand);
|
|
3753
|
+
|
|
3830
3754
|
// ── generate-saga: Top-Down PCG guided by LLMs (Semantic Reverse-Engineering) ──
|
|
3831
3755
|
program
|
|
3832
3756
|
.command('generate-saga')
|
|
@@ -3836,7 +3760,7 @@ try {
|
|
|
3836
3760
|
)
|
|
3837
3761
|
.option('--import <file>', 'Load a previously generated payload file (the shape --out writes) into the database')
|
|
3838
3762
|
.option('--model <model>', 'Gemini model id (default: gemma-4-26b-a4b-it)')
|
|
3839
|
-
.option('--timeout <ms>', 'Per-request timeout in ms (default:
|
|
3763
|
+
.option('--timeout <ms>', 'Per-request timeout in ms (default: 10000)', (v) => parseInt(v, 10))
|
|
3840
3764
|
.option('--thinking-level <level>', 'Gemini thinking level: low | medium | high (default: high)')
|
|
3841
3765
|
.option(
|
|
3842
3766
|
'--lore-path <path>',
|
|
@@ -4792,6 +4716,26 @@ try {
|
|
|
4792
4716
|
|
|
4793
4717
|
const runner = program.command('run-workflow').description('Run a Cyberia script from the "scripts" directory');
|
|
4794
4718
|
|
|
4719
|
+
runner
|
|
4720
|
+
.command('seed-audio')
|
|
4721
|
+
.option('--records-path <path>', 'Recorded WAV and manifest directory', DEFAULT_AUDIO_RECORDS_PATH)
|
|
4722
|
+
.option('--records-only', 'Record the WAV and manifest pairs without touching the database')
|
|
4723
|
+
.option(
|
|
4724
|
+
'--instance <instance-code>',
|
|
4725
|
+
"Configure that instance's maps instead of the fallback world's, in its own cyberiaMapCodes order",
|
|
4726
|
+
)
|
|
4727
|
+
.option('--env-path <path>', 'Engine environment file')
|
|
4728
|
+
.option('--mongo-host <host>', 'Mongo host override')
|
|
4729
|
+
.option('--dev', 'Use the development environment')
|
|
4730
|
+
.description("Record audio, upsert generic files and audio metadata, and configure a world's maps")
|
|
4731
|
+
.action(async (options) => {
|
|
4732
|
+
// Recording writes only `records/`: the client bundles no audio and fetches every asset
|
|
4733
|
+
// from engine-cyberia by code, so seeding the database is what makes a recording reachable.
|
|
4734
|
+
await prepareFallbackAudio({ recordsPath: options.recordsPath ?? DEFAULT_AUDIO_RECORDS_PATH });
|
|
4735
|
+
if (!options.recordsOnly) await runAudioCommand(undefined, { ...options, import: true, seedWorld: true });
|
|
4736
|
+
logger.info('seed-audio complete');
|
|
4737
|
+
});
|
|
4738
|
+
|
|
4795
4739
|
runner
|
|
4796
4740
|
.command('import-default-items')
|
|
4797
4741
|
.option('--dev', 'Force development environment (loads .env.development for IPFS localhost, etc.)')
|
|
@@ -4830,21 +4774,83 @@ try {
|
|
|
4830
4774
|
shellExec(
|
|
4831
4775
|
`node bin/cyberia generate-saga --import engine-private/cyberia-sagas/${sagaCode}.json${devFlag}${mongoHostFlag}`,
|
|
4832
4776
|
);
|
|
4833
|
-
shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}${mongoHostFlag}`);
|
|
4834
|
-
shellExec(`node bin/cyberia run-workflow seed-skills${devFlag}${mongoHostFlag}`);
|
|
4835
|
-
shellExec(`node bin/cyberia run-workflow seed-entities${devFlag}${mongoHostFlag}`);
|
|
4836
|
-
shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}${mongoHostFlag}`);
|
|
4837
|
-
shellExec(`node bin/cyberia run-workflow seed-actions-quests${devFlag}${mongoHostFlag}`);
|
|
4838
|
-
shellExec(`node bin/cyberia client-hints ${instanceHintsCode} --seed-defaults${devFlag}${mongoHostFlag}`);
|
|
4777
|
+
// shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}${mongoHostFlag}`);
|
|
4778
|
+
// shellExec(`node bin/cyberia run-workflow seed-skills${devFlag}${mongoHostFlag}`);
|
|
4779
|
+
// shellExec(`node bin/cyberia run-workflow seed-entities${devFlag}${mongoHostFlag}`);
|
|
4780
|
+
// shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}${mongoHostFlag}`);
|
|
4781
|
+
// shellExec(`node bin/cyberia run-workflow seed-actions-quests${devFlag}${mongoHostFlag}`);
|
|
4782
|
+
// shellExec(`node bin/cyberia client-hints ${instanceHintsCode} --seed-defaults${devFlag}${mongoHostFlag}`);
|
|
4839
4783
|
shellExec(`node bin/cyberia instance ${sagaCode} --import${devFlag}`);
|
|
4840
4784
|
shellExec(`node bin/cyberia instance FOREST --import${devFlag}`);
|
|
4785
|
+
shellExec(`node bin/cyberia instance TEST --import${devFlag}`);
|
|
4786
|
+
});
|
|
4787
|
+
|
|
4788
|
+
runner
|
|
4789
|
+
.command('stage-cli')
|
|
4790
|
+
.option('--output-path <output-path>', "Build context to stage the package in (default: '.')")
|
|
4791
|
+
.description('Packs this engine checkout as underpost-cli.tgz for a runtime image build context')
|
|
4792
|
+
.action((options) => stageCliPackage(options.outputPath || '.'));
|
|
4793
|
+
|
|
4794
|
+
// Every file mirrored between this engine and a product checkout, engine path first. One
|
|
4795
|
+
// table for both directions, so a pair cannot be synced one way and forgotten the other.
|
|
4796
|
+
const cyberiaSrcSyncPairs = [
|
|
4797
|
+
['./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md'],
|
|
4798
|
+
['./src/runtime/cyberia-server/Dockerfile', './cyberia-server/Dockerfile'],
|
|
4799
|
+
['./src/runtime/cyberia-server/Dockerfile.dev', './cyberia-server/Dockerfile.dev'],
|
|
4800
|
+
['./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md'],
|
|
4801
|
+
['./src/runtime/cyberia-client/Dockerfile', './cyberia-client/Dockerfile'],
|
|
4802
|
+
['./src/runtime/cyberia-client/Dockerfile.dev', './cyberia-client/Dockerfile.dev'],
|
|
4803
|
+
];
|
|
4804
|
+
|
|
4805
|
+
runner
|
|
4806
|
+
.command('sync-src')
|
|
4807
|
+
.option('--from-repo', 'Copy from the product checkouts into this engine instead of out to them')
|
|
4808
|
+
.option('--dry-run', 'Report what would be copied without writing it')
|
|
4809
|
+
.description(
|
|
4810
|
+
'Mirrors the cyberia product READMEs and runtime Dockerfiles between this engine and the product checkouts',
|
|
4811
|
+
)
|
|
4812
|
+
.action((options) => {
|
|
4813
|
+
const fromRepo = options.fromRepo === true;
|
|
4814
|
+
const dryRun = options.dryRun === true;
|
|
4815
|
+
const copied = [];
|
|
4816
|
+
const missing = [];
|
|
4817
|
+
|
|
4818
|
+
for (const [enginePath, repoPath] of cyberiaSrcSyncPairs) {
|
|
4819
|
+
const [source, target] = fromRepo ? [repoPath, enginePath] : [enginePath, repoPath];
|
|
4820
|
+
// A product checkout `setup-workspace` has not cloned yet must not leave the run
|
|
4821
|
+
// half applied, so a missing source is reported rather than thrown on.
|
|
4822
|
+
if (!fs.existsSync(source)) {
|
|
4823
|
+
missing.push(source);
|
|
4824
|
+
continue;
|
|
4825
|
+
}
|
|
4826
|
+
if (!dryRun) fs.copySync(source, target);
|
|
4827
|
+
copied.push(`${source} -> ${target}`);
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
logger.info(`Cyberia sources synced ${fromRepo ? 'from' : 'to'} the product checkouts`, {
|
|
4831
|
+
copied,
|
|
4832
|
+
missing,
|
|
4833
|
+
dryRun,
|
|
4834
|
+
});
|
|
4841
4835
|
});
|
|
4842
4836
|
|
|
4843
|
-
runner.command('
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
fs.
|
|
4847
|
-
fs.
|
|
4837
|
+
runner.command('setup-workspace').action(() => {
|
|
4838
|
+
shellExec(`node bin fs src/client/public/cyberia --tracked --pull --deploy-id dd-cyberia`);
|
|
4839
|
+
shellExec(`node bin/deploy.js cyberia`);
|
|
4840
|
+
if (!fs.existsSync('./cyberia-server')) shellExec(`${cli()} clone underpostnet/cyberia-server`);
|
|
4841
|
+
if (!fs.existsSync('./cyberia-client')) shellExec(`${cli()} clone underpostnet/cyberia-client`);
|
|
4842
|
+
});
|
|
4843
|
+
|
|
4844
|
+
runner.command('e2e-build').action(() => {
|
|
4845
|
+
shellExec(`node bin run build-cluster-deployment-manifests`);
|
|
4846
|
+
shellExec(`node bin/cyberia run-workflow build-manifest`);
|
|
4847
|
+
shellExec(`node bin/cyberia run-workflow publish --dry-run`);
|
|
4848
|
+
shellExec(`npm run security`);
|
|
4849
|
+
shellExec(`sudo rm -rf ./conf.dd*.js`);
|
|
4850
|
+
});
|
|
4851
|
+
|
|
4852
|
+
runner.command('cluster').action(() => {
|
|
4853
|
+
shellExec(`node bin run cluster --runtime-image express --deploy-id dd-cyberia --instance-id mmo-server --dev`);
|
|
4848
4854
|
});
|
|
4849
4855
|
|
|
4850
4856
|
runner
|
|
@@ -4941,7 +4947,7 @@ try {
|
|
|
4941
4947
|
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
4942
4948
|
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
4943
4949
|
.option('--dev', 'Force development environment')
|
|
4944
|
-
.description('Drop all Cyberia collections and remove File documents
|
|
4950
|
+
.description('Drop all Cyberia collections and remove the File documents they reference')
|
|
4945
4951
|
.action(async (options = {}) => {
|
|
4946
4952
|
if (!options.envPath) options.envPath = `./.env`;
|
|
4947
4953
|
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
@@ -4984,30 +4990,37 @@ try {
|
|
|
4984
4990
|
'cyberia-entity-type-default',
|
|
4985
4991
|
'cyberia-client-hints',
|
|
4986
4992
|
'cyberia-saga',
|
|
4993
|
+
'cyberia-audio',
|
|
4994
|
+
'cyberia-map-audio-conf',
|
|
4987
4995
|
];
|
|
4988
4996
|
|
|
4997
|
+
// Every File _id a Cyberia document owns: instance and map thumbnails and previews, and the
|
|
4998
|
+
// recorded WAV each audio asset points at. Read from the registry that maps a model to its
|
|
4999
|
+
// File fields, so a reference added there is dropped here without editing this command, and
|
|
5000
|
+
// read before anything is dropped, so no backing File survives the collection that held it.
|
|
5001
|
+
const fileReferences = cyberiaCollections
|
|
5002
|
+
.map((api) => ({ api, fields: fileRefFields(api) }))
|
|
5003
|
+
.filter(({ fields }) => fields.length > 0);
|
|
5004
|
+
|
|
4989
5005
|
await DataBaseProviderService.load({ apis: [...cyberiaCollections, 'file'], host, path, db });
|
|
4990
5006
|
|
|
4991
5007
|
const File = DataBaseProviderService.getModel('file', { host, path });
|
|
4992
5008
|
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
const thumbnailFileIds = new Set();
|
|
4996
|
-
for (const api of ['cyberia-instance', 'cyberia-map']) {
|
|
5009
|
+
const fileIds = new Set();
|
|
5010
|
+
for (const { api, fields } of fileReferences) {
|
|
4997
5011
|
const Model = DataBaseProviderService.getModel(api, { host, path });
|
|
4998
5012
|
const docs = await Model.find(
|
|
4999
|
-
{ $or:
|
|
5000
|
-
|
|
5013
|
+
{ $or: fields.map((field) => ({ [field]: { $ne: null } })) },
|
|
5014
|
+
Object.fromEntries(fields.map((field) => [field, 1])),
|
|
5001
5015
|
).lean();
|
|
5002
5016
|
for (const doc of docs) {
|
|
5003
|
-
if (doc
|
|
5004
|
-
if (doc.preview) thumbnailFileIds.add(doc.preview.toString());
|
|
5017
|
+
for (const field of fields) if (doc[field]) fileIds.add(doc[field].toString());
|
|
5005
5018
|
}
|
|
5006
5019
|
}
|
|
5007
5020
|
|
|
5008
|
-
if (
|
|
5009
|
-
const result = await File.deleteMany({ _id: { $in: [...
|
|
5010
|
-
logger.info(`Removed ${result.deletedCount}
|
|
5021
|
+
if (fileIds.size > 0) {
|
|
5022
|
+
const result = await File.deleteMany({ _id: { $in: [...fileIds] } });
|
|
5023
|
+
logger.info(`Removed ${result.deletedCount} referenced File document(s)`);
|
|
5011
5024
|
}
|
|
5012
5025
|
|
|
5013
5026
|
for (const api of cyberiaCollections) {
|
|
@@ -5029,6 +5042,44 @@ try {
|
|
|
5029
5042
|
shellExec(`gh workflow run ${id}.cd.yml -R underpostnet/${id} -f job=deploy`);
|
|
5030
5043
|
});
|
|
5031
5044
|
|
|
5045
|
+
runner.command('cp-assets').action(() => {
|
|
5046
|
+
for (const assetPath of Object.keys(
|
|
5047
|
+
JSON.parse(fs.readFileSync(`./engine-private/conf/dd-cyberia/storage.engine-cyberia.json`, 'utf-8')),
|
|
5048
|
+
)) {
|
|
5049
|
+
const relativePath = assetPath.replace(/^src\/client\/public\/cyberia\//, '');
|
|
5050
|
+
const targetPath = `/home/dd/cyberia-instances/public/cyberia/${relativePath}`;
|
|
5051
|
+
fs.mkdirpSync(nodePath.dirname(targetPath));
|
|
5052
|
+
logger.info(`Copying asset: ${assetPath} → ${targetPath}`);
|
|
5053
|
+
fs.copySync(`./${assetPath}`, targetPath);
|
|
5054
|
+
}
|
|
5055
|
+
|
|
5056
|
+
// Copy default-items asset folders (src/client/public/cyberia/assets/<type>/<id>/ → public/cyberia/assets/<type>/<id>/)
|
|
5057
|
+
for (const entry of DefaultCyberiaItems) {
|
|
5058
|
+
const { id, type } = entry.item;
|
|
5059
|
+
const srcDir = `src/client/public/cyberia/assets/${type}/${id}`;
|
|
5060
|
+
const targetDir = `/home/dd/cyberia-instances/public/cyberia/assets/${type}/${id}`;
|
|
5061
|
+
if (fs.existsSync(srcDir)) {
|
|
5062
|
+
fs.mkdirpSync(nodePath.dirname(targetDir));
|
|
5063
|
+
logger.info(`Copying default-item asset: ${srcDir} → ${targetDir}`);
|
|
5064
|
+
fs.copySync(srcDir, targetDir);
|
|
5065
|
+
} else {
|
|
5066
|
+
logger.warn(`Default-item asset directory not found, skipping: ${srcDir}`);
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
});
|
|
5070
|
+
|
|
5071
|
+
runner
|
|
5072
|
+
.command('sync-cluster')
|
|
5073
|
+
.option('--build')
|
|
5074
|
+
.action((options) => {
|
|
5075
|
+
shellExec(`node bin/build dd-cyberia --update-private`);
|
|
5076
|
+
shellExec(`node bin/build dd-core --update-private`);
|
|
5077
|
+
if (options.build) return;
|
|
5078
|
+
shellExec(
|
|
5079
|
+
`node bin wireguard --sync --repo-engine underpostnet/engine-test-cyberia --repo-engine-private underpostnet/engine-private`,
|
|
5080
|
+
);
|
|
5081
|
+
});
|
|
5082
|
+
|
|
5032
5083
|
runner
|
|
5033
5084
|
.command('test')
|
|
5034
5085
|
.option('--n-con <connections>', 'Number of concurrent WebSocket connections')
|
|
@@ -5039,7 +5090,7 @@ try {
|
|
|
5039
5090
|
CYBERIA_LOAD_CONNECTIONS=${options.nCon ?? 40} \
|
|
5040
5091
|
CYBERIA_LOAD_TAP_FREQUENCY=${options.tapFreq ?? 5} \
|
|
5041
5092
|
CYBERIA_LOAD_DURATION_MS=${options.duration ?? 1000 * 60 * 30} \
|
|
5042
|
-
|
|
5093
|
+
node bin test cyberia --grep 'Cyberia load'`);
|
|
5043
5094
|
});
|
|
5044
5095
|
|
|
5045
5096
|
runner
|
|
@@ -5049,46 +5100,49 @@ c8 mocha test/cyberia-load.test.js`);
|
|
|
5049
5100
|
// no funca
|
|
5050
5101
|
if (options.loadTar) {
|
|
5051
5102
|
for (const imageId of dockerImageIds)
|
|
5052
|
-
if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.3.
|
|
5103
|
+
if (imageId === id || id === '.') shellExec(`docker load -i ./${imageId}-dev_v3.3.76.tar`);
|
|
5053
5104
|
return;
|
|
5054
5105
|
}
|
|
5055
5106
|
switch (id) {
|
|
5056
5107
|
case 'engine-cyberia':
|
|
5108
|
+
stageCliPackage(`./src/runtime/engine-cyberia`);
|
|
5057
5109
|
shellExec(`
|
|
5058
5110
|
node bin/build dd-cyberia --conf
|
|
5059
5111
|
node bin/build dd-cyberia --update-private
|
|
5060
5112
|
node bin image --path src/runtime/engine-cyberia \
|
|
5061
5113
|
--docker-compose --pull-base --build \
|
|
5062
5114
|
--dockerfile-name Dockerfile.dev \
|
|
5063
|
-
--image-name engine-cyberia-dev:v3.3.
|
|
5115
|
+
--image-name engine-cyberia-dev:v3.3.76 \
|
|
5064
5116
|
--image-out-path .
|
|
5065
5117
|
`);
|
|
5066
5118
|
break;
|
|
5067
5119
|
|
|
5068
5120
|
case 'cyberia-server':
|
|
5121
|
+
stageCliPackage(`./cyberia-server`);
|
|
5069
5122
|
shellExec(`
|
|
5070
5123
|
cp -f src/runtime/cyberia-server/Dockerfile.dev cyberia-server/Dockerfile.dev
|
|
5071
5124
|
node bin image --path cyberia-server \
|
|
5072
5125
|
--docker-compose --pull-base --build \
|
|
5073
5126
|
--dockerfile-name Dockerfile.dev \
|
|
5074
|
-
--image-name cyberia-server-dev:v3.3.
|
|
5127
|
+
--image-name cyberia-server-dev:v3.3.76 \
|
|
5075
5128
|
--image-out-path .
|
|
5076
5129
|
`);
|
|
5077
5130
|
break;
|
|
5078
5131
|
case 'cyberia-client':
|
|
5132
|
+
stageCliPackage(`./cyberia-client`);
|
|
5079
5133
|
shellExec(`
|
|
5080
5134
|
cp -f src/runtime/cyberia-client/Dockerfile.dev cyberia-client/Dockerfile.dev
|
|
5081
5135
|
node bin image --path cyberia-client \
|
|
5082
5136
|
--docker-compose --pull-base --build \
|
|
5083
5137
|
--dockerfile-name Dockerfile.dev \
|
|
5084
|
-
--image-name cyberia-client-dev:v3.3.
|
|
5138
|
+
--image-name cyberia-client-dev:v3.3.76 \
|
|
5085
5139
|
--image-out-path .
|
|
5086
5140
|
`);
|
|
5087
5141
|
break;
|
|
5088
5142
|
}
|
|
5089
5143
|
});
|
|
5090
5144
|
|
|
5091
|
-
for (const [cmd, action] of Object.entries(
|
|
5145
|
+
for (const [cmd, action] of Object.entries(cyberiaCatalog.packageScripts))
|
|
5092
5146
|
runner.command(cmd).action(() => {
|
|
5093
5147
|
if (cmd === 'docker:up' || cmd === 'docker:up:build' || cmd === 'docker:restart') {
|
|
5094
5148
|
const { aliases, changed } = installCyberiaDockerHostAliases();
|
|
@@ -5247,9 +5301,9 @@ node bin image --path cyberia-client \
|
|
|
5247
5301
|
|
|
5248
5302
|
const CyberiaSkill = DataBaseProviderService.getModel('cyberia-skill', { host, path });
|
|
5249
5303
|
|
|
5250
|
-
// Upsert each skill record keyed by triggerItemId — full record (logic
|
|
5251
|
-
//
|
|
5252
|
-
//
|
|
5304
|
+
// Upsert each skill record keyed by triggerItemId — the full record (logic event keys +
|
|
5305
|
+
// expanded skills metadata). The collection is deployment-wide; an instance runs the
|
|
5306
|
+
// subset its own content triggers, decided at export and boot, never stored.
|
|
5253
5307
|
let upserted = 0;
|
|
5254
5308
|
for (const sk of DefaultSkillConfig) {
|
|
5255
5309
|
await CyberiaSkill.findOneAndUpdate(
|
|
@@ -5272,6 +5326,10 @@ node bin image --path cyberia-client \
|
|
|
5272
5326
|
.command('seed-entities')
|
|
5273
5327
|
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
5274
5328
|
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
5329
|
+
.option(
|
|
5330
|
+
'--instance <instance-code>',
|
|
5331
|
+
"Point that instance's conf at the seeded documents, replacing whatever it referenced",
|
|
5332
|
+
)
|
|
5275
5333
|
.option('--dev', 'Force development environment')
|
|
5276
5334
|
.description('Upsert ENTITY_TYPE_DEFAULTS into the cyberia-entity-type-default collection (idempotent)')
|
|
5277
5335
|
.action(async (options) => {
|
|
@@ -5303,7 +5361,12 @@ node bin image --path cyberia-client \
|
|
|
5303
5361
|
|
|
5304
5362
|
logger.info('seed-entities', { deployId, host, path });
|
|
5305
5363
|
|
|
5306
|
-
await DataBaseProviderService.load({
|
|
5364
|
+
await DataBaseProviderService.load({
|
|
5365
|
+
apis: ['cyberia-entity-type-default', 'cyberia-instance-conf'],
|
|
5366
|
+
host,
|
|
5367
|
+
path,
|
|
5368
|
+
db,
|
|
5369
|
+
});
|
|
5307
5370
|
|
|
5308
5371
|
const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('cyberia-entity-type-default', { host, path });
|
|
5309
5372
|
|
|
@@ -5322,8 +5385,9 @@ node bin image --path cyberia-client \
|
|
|
5322
5385
|
// (across entity types, or within one type at different specificity). Every
|
|
5323
5386
|
// entry is upserted, idempotently, by its exact (entityType, liveItemIds) key.
|
|
5324
5387
|
let upserted = 0;
|
|
5388
|
+
const seededIds = [];
|
|
5325
5389
|
for (const ed of ENTITY_TYPE_DEFAULTS) {
|
|
5326
|
-
await CyberiaEntityTypeDefault.findOneAndUpdate(
|
|
5390
|
+
const doc = await CyberiaEntityTypeDefault.findOneAndUpdate(
|
|
5327
5391
|
{ entityType: ed.entityType, liveItemIds: ed.liveItemIds || [] },
|
|
5328
5392
|
{
|
|
5329
5393
|
$set: {
|
|
@@ -5331,12 +5395,14 @@ node bin image --path cyberia-client \
|
|
|
5331
5395
|
liveItemIds: ed.liveItemIds || [],
|
|
5332
5396
|
deadItemIds: ed.deadItemIds || [],
|
|
5333
5397
|
dropItemIds: ed.dropItemIds || [],
|
|
5334
|
-
|
|
5398
|
+
inventoryItemsIds: ed.inventoryItemsIds || [],
|
|
5399
|
+
overrideItemsIdsState: ed.overrideItemsIdsState || [],
|
|
5335
5400
|
behavior: ed.behavior || '',
|
|
5336
5401
|
},
|
|
5337
5402
|
},
|
|
5338
|
-
{ upsert: true },
|
|
5403
|
+
{ upsert: true, returnDocument: 'after' },
|
|
5339
5404
|
);
|
|
5405
|
+
if (doc?._id) seededIds.push(doc._id);
|
|
5340
5406
|
upserted++;
|
|
5341
5407
|
}
|
|
5342
5408
|
|
|
@@ -5345,6 +5411,23 @@ node bin image --path cyberia-client \
|
|
|
5345
5411
|
ENTITY_TYPE_DEFAULTS.map((e) => `${e.entityType} → [${(e.liveItemIds || []).join(', ')}]`),
|
|
5346
5412
|
);
|
|
5347
5413
|
|
|
5414
|
+
// A seeded document reaches a world only when that world's conf names it. Binding here is
|
|
5415
|
+
// what makes an edited row take effect, and it states the whole reference set so re-running
|
|
5416
|
+
// converges instead of accumulating.
|
|
5417
|
+
if (options.instance) {
|
|
5418
|
+
const CyberiaInstanceConf = DataBaseProviderService.getModel('cyberia-instance-conf', { host, path });
|
|
5419
|
+
const conf = await CyberiaInstanceConf.findOneAndUpdate(
|
|
5420
|
+
{ instanceCode: options.instance },
|
|
5421
|
+
{ $set: { entityDefaults: seededIds, updatedAt: new Date() } },
|
|
5422
|
+
{ returnDocument: 'after' },
|
|
5423
|
+
);
|
|
5424
|
+
if (!conf) {
|
|
5425
|
+
logger.error(`cyberia-instance-conf not found for instanceCode="${options.instance}"`);
|
|
5426
|
+
process.exit(1);
|
|
5427
|
+
}
|
|
5428
|
+
logger.info(`seed-entities --instance ${options.instance}: ${seededIds.length} reference(s) bound`);
|
|
5429
|
+
}
|
|
5430
|
+
|
|
5348
5431
|
await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
|
|
5349
5432
|
});
|
|
5350
5433
|
|
|
@@ -5368,26 +5451,6 @@ node bin image --path cyberia-client \
|
|
|
5368
5451
|
'skin-vivid',
|
|
5369
5452
|
'skin-natural',
|
|
5370
5453
|
'skin-shaved',
|
|
5371
|
-
// 'resource-desert-petal',
|
|
5372
|
-
// 'resource-desert-stone',
|
|
5373
|
-
// 'resource-desert-polygon',
|
|
5374
|
-
// 'resource-desert-thread',
|
|
5375
|
-
// 'resource-grass-petal',
|
|
5376
|
-
// 'resource-grass-stone',
|
|
5377
|
-
// 'resource-grass-polygon',
|
|
5378
|
-
// 'resource-grass-thread',
|
|
5379
|
-
// 'resource-water-petal',
|
|
5380
|
-
// 'resource-water-stone',
|
|
5381
|
-
// 'resource-water-polygon',
|
|
5382
|
-
// 'resource-water-thread',
|
|
5383
|
-
// 'resource-stone-petal',
|
|
5384
|
-
// 'resource-stone-stone',
|
|
5385
|
-
// 'resource-stone-polygon',
|
|
5386
|
-
// 'resource-stone-thread',
|
|
5387
|
-
// 'resource-lava-petal',
|
|
5388
|
-
// 'resource-lava-stone',
|
|
5389
|
-
// 'resource-lava-polygon',
|
|
5390
|
-
// 'resource-lava-thread',
|
|
5391
5454
|
];
|
|
5392
5455
|
|
|
5393
5456
|
const baseSeed = options.seed || 'example';
|
|
@@ -5412,10 +5475,10 @@ node bin image --path cyberia-client \
|
|
|
5412
5475
|
// Instance id → project root. Single source of truth for the workloads this
|
|
5413
5476
|
// workflow builds: both the k8s manifests and the status page artifacts each
|
|
5414
5477
|
// project ships are resolved from this list plus conf.instances.json.
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
];
|
|
5478
|
+
// The template instances this deploy builds artifacts for. Where each one publishes is not
|
|
5479
|
+
// listed here: instanceProjectPathFactory reads it off the conf entry, the same rule
|
|
5480
|
+
// instance-build-manifest applies, so the two can never name different checkouts.
|
|
5481
|
+
const CYBERIA_INSTANCE_IDS = ['mmo-client', 'mmo-server'];
|
|
5419
5482
|
const CYBERIA_CONF_INSTANCES_PATH = './engine-private/conf/dd-cyberia/conf.instances.json';
|
|
5420
5483
|
const CYBERIA_CONF_SSR_PATH = './engine-private/conf/dd-cyberia/conf.ssr.json';
|
|
5421
5484
|
// Copy shared by the server and the client for a given status code. A status
|
|
@@ -5508,10 +5571,9 @@ node bin image --path cyberia-client \
|
|
|
5508
5571
|
const nodeFlag = options.nodeName ? ` --node-name ${options.nodeName}` : '';
|
|
5509
5572
|
|
|
5510
5573
|
// ── Dynamically resolve instance codes from conf.instances.json ──────
|
|
5511
|
-
//
|
|
5512
|
-
//
|
|
5513
|
-
//
|
|
5514
|
-
// provisions every variant's backup dir and saga at build time.
|
|
5574
|
+
// Collect the multiInstance variant codes of every game-server runtime
|
|
5575
|
+
// instance. They set the INSTANCE_CODES label in Dockerfile.dev, so the
|
|
5576
|
+
// dev image provisions each variant's backup dir and saga at build time.
|
|
5515
5577
|
//
|
|
5516
5578
|
// Only codes that have an on-disk instance backup directory are
|
|
5517
5579
|
// included. The saga file is optional — the Dockerfile's for loop
|
|
@@ -5573,14 +5635,12 @@ node bin image --path cyberia-client \
|
|
|
5573
5635
|
}
|
|
5574
5636
|
|
|
5575
5637
|
// ── Update catalog-cyberia.js privateConfPaths ───────────────────────
|
|
5576
|
-
// The array block
|
|
5577
|
-
// markers
|
|
5578
|
-
//
|
|
5579
|
-
//
|
|
5580
|
-
//
|
|
5581
|
-
//
|
|
5582
|
-
// (which uses `instances/` + `sagas/`). Only emit paths that exist on disk
|
|
5583
|
-
// so the sync never hits ENOENT on a variant without local content.
|
|
5638
|
+
// The privateConfPaths array block sits between the /** INSTANCE_CODES */
|
|
5639
|
+
// markers. Replace its content with the resolved per-code paths.
|
|
5640
|
+
// syncPrivateConf copies each entry from `./engine-private/<path>`, so the
|
|
5641
|
+
// paths follow the local engine-private layout (`cyberia-instances/<code>`,
|
|
5642
|
+
// `cyberia-sagas/<code>.json`). Emit only paths that exist on disk, so the
|
|
5643
|
+
// sync never hits ENOENT for a variant without local content.
|
|
5584
5644
|
const catalogPath = './src/projects/cyberia/catalog-cyberia.js';
|
|
5585
5645
|
try {
|
|
5586
5646
|
const catalogContent = fs.readFileSync(catalogPath, 'utf8');
|
|
@@ -5613,11 +5673,11 @@ node bin image --path cyberia-client \
|
|
|
5613
5673
|
// embed each document declared under an instance's `customStatusPages`,
|
|
5614
5674
|
// so the artifact has to exist at its `hostPath` by manifest time.
|
|
5615
5675
|
const statusPagesBuilt = [];
|
|
5616
|
-
for (const
|
|
5676
|
+
for (const id of CYBERIA_INSTANCE_IDS) {
|
|
5617
5677
|
const instance = confInstancesEntries.find((entry) => entry.id === id);
|
|
5618
5678
|
for (const page of instance?.customStatusPages || []) {
|
|
5619
5679
|
if (!page?.status || !page?.hostPath) continue;
|
|
5620
|
-
const outputPath = nodePath.normalize(`${
|
|
5680
|
+
const outputPath = nodePath.normalize(`${instanceProjectPathFactory(instance)}/${page.hostPath}`);
|
|
5621
5681
|
if (buildCyberiaStatusPage({ status: page.status, outputPath, dev: isDev }))
|
|
5622
5682
|
statusPagesBuilt.push({ instance: id, status: page.status, outputPath });
|
|
5623
5683
|
}
|
|
@@ -5630,24 +5690,36 @@ node bin image --path cyberia-client \
|
|
|
5630
5690
|
// ── Build dev manifests (always --kind --dev) ────────────────────────
|
|
5631
5691
|
{
|
|
5632
5692
|
const flags = `--kind --dev${nodeFlag}`;
|
|
5633
|
-
for (const
|
|
5634
|
-
shellExec(`node bin run instance-build-manifest
|
|
5693
|
+
for (const id of CYBERIA_INSTANCE_IDS)
|
|
5694
|
+
shellExec(`node bin run instance-build-manifest --deploy-id dd-cyberia --instance-id ${id} ${flags}`);
|
|
5635
5695
|
}
|
|
5636
5696
|
// ── Build prod manifests (--kubeadm, no --dev) ───────────────────────
|
|
5637
5697
|
if (!isDev) {
|
|
5638
5698
|
const flags = `--kubeadm${nodeFlag}`;
|
|
5639
|
-
for (const
|
|
5640
|
-
shellExec(`node bin run instance-build-manifest
|
|
5699
|
+
for (const id of CYBERIA_INSTANCE_IDS)
|
|
5700
|
+
shellExec(`node bin run instance-build-manifest --deploy-id dd-cyberia --instance-id ${id} ${flags}`);
|
|
5641
5701
|
}
|
|
5642
5702
|
|
|
5643
5703
|
// Copy canonical doc sources into the generated project READMEs.
|
|
5644
5704
|
// Edit the canonical sources; never hand-edit these generated outputs.
|
|
5645
|
-
|
|
5646
|
-
|
|
5705
|
+
// The mirrored deploy tree is generated, and rebuilt from scratch so a file
|
|
5706
|
+
// renamed upstream cannot linger. It keeps the engine layout, the
|
|
5707
|
+
// <deploy-id> directory beside lib/, because every deploy script sources
|
|
5708
|
+
// `$SCRIPT_DIR/../lib/logging.sh`.
|
|
5709
|
+
for (const project of ['cyberia-client', 'cyberia-server']) {
|
|
5710
|
+
const scripts = `./deploy/${project}`;
|
|
5711
|
+
// A tree assembled before these scripts were packaged does not carry them; mirroring is
|
|
5712
|
+
// what publishes them, so say so and continue rather than failing the whole manifest build.
|
|
5713
|
+
if (!fs.existsSync(scripts)) {
|
|
5714
|
+
logger.warn(`[build-manifest] No deploy scripts to mirror for ${project}`, { path: scripts });
|
|
5715
|
+
continue;
|
|
5716
|
+
}
|
|
5717
|
+
fs.removeSync(`./${project}/deploy`);
|
|
5718
|
+
fs.copySync('./deploy/lib', `./${project}/deploy/lib`);
|
|
5719
|
+
fs.copySync(scripts, `./${project}/deploy/${project}`);
|
|
5720
|
+
}
|
|
5647
5721
|
fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
|
|
5648
|
-
fs.copySync('./deploy/cyberia-client', './cyberia-client/deploy');
|
|
5649
5722
|
fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md');
|
|
5650
|
-
fs.copySync('./deploy/cyberia-server', './cyberia-server/deploy');
|
|
5651
5723
|
fs.copyFileSync(
|
|
5652
5724
|
'./.github/workflows/cyberia-client.cd.yml',
|
|
5653
5725
|
'./cyberia-client/.github/workflows/cyberia-client.cd.yml',
|
|
@@ -5657,7 +5729,14 @@ node bin image --path cyberia-client \
|
|
|
5657
5729
|
'./cyberia-server/.github/workflows/cyberia-server.cd.yml',
|
|
5658
5730
|
);
|
|
5659
5731
|
shellExec('cp -a ./engine-private/conf/dd-cyberia/docker-compose/cyberia/. ./src/runtime/engine-cyberia/');
|
|
5660
|
-
|
|
5732
|
+
// Scope the publish to the deployment this workflow builds: read
|
|
5733
|
+
// dd-cyberia's own environment, not the working-tree `./.env`.
|
|
5734
|
+
shellExec(
|
|
5735
|
+
`node bin/cyberia.js instance --publish-build --env-path ${deployEnvFilePath(
|
|
5736
|
+
'dd-cyberia',
|
|
5737
|
+
isDev ? 'development' : 'production',
|
|
5738
|
+
)}`,
|
|
5739
|
+
);
|
|
5661
5740
|
logger.info(`run-workflow build-manifest complete (${isDev ? 'dev' : 'prod'})`);
|
|
5662
5741
|
});
|
|
5663
5742
|
|
|
@@ -5668,6 +5747,7 @@ node bin image --path cyberia-client \
|
|
|
5668
5747
|
if (options.dryRun) {
|
|
5669
5748
|
shellExec('node bin cmt --log --unpush cyberia-server');
|
|
5670
5749
|
shellExec('node bin cmt --log --unpush cyberia-client');
|
|
5750
|
+
shellExec('node bin cmt --log --unpush cyberia-audio');
|
|
5671
5751
|
shellExec('node bin cmt --log --unpush');
|
|
5672
5752
|
shellExec('node bin cmt --log --unpush ../cyberia-instances');
|
|
5673
5753
|
} else {
|
|
@@ -5680,6 +5760,9 @@ node bin image --path cyberia-client \
|
|
|
5680
5760
|
shellExec('node bin push cyberia-client underpostnet/cyberia-client', {
|
|
5681
5761
|
silentOnError: true,
|
|
5682
5762
|
});
|
|
5763
|
+
shellExec('node bin push cyberia-audio underpostnet/cyberia-audio', {
|
|
5764
|
+
silentOnError: true,
|
|
5765
|
+
});
|
|
5683
5766
|
shellExec('node bin run template-deploy', {
|
|
5684
5767
|
silentOnError: true,
|
|
5685
5768
|
});
|
|
@@ -5778,16 +5861,24 @@ node bin image --path cyberia-client \
|
|
|
5778
5861
|
|
|
5779
5862
|
await program.parseAsync();
|
|
5780
5863
|
} catch (error) {
|
|
5781
|
-
//
|
|
5782
|
-
// error
|
|
5783
|
-
//
|
|
5784
|
-
// exit so GitHub Actions / CI parents observe the failure. Without this
|
|
5785
|
-
// guard, a genuine build failure was being silently rerouted into the
|
|
5786
|
-
// underpost CLI and then masked behind a misleading "unknown command"
|
|
5787
|
-
// line.
|
|
5864
|
+
// Reroute only on the passthrough sentinel. Every other error — a non-zero
|
|
5865
|
+
// subprocess, a CLI parse error, a missing module — must exit non-zero, so a
|
|
5866
|
+
// CI parent sees the failure.
|
|
5788
5867
|
if (error && error.message === 'Trigger underpost passthrough') {
|
|
5789
|
-
|
|
5790
|
-
|
|
5868
|
+
// A redundant CLI name can only appear before the command; everything from the command
|
|
5869
|
+
// onward is an argument. Filtering the whole of argv removed those too, so an option whose
|
|
5870
|
+
// value happens to be `underpost` — a storage id, a public asset path — lost it, and the
|
|
5871
|
+
// parse failed on a missing argument rather than on anything the caller wrote.
|
|
5872
|
+
const commandIndex = process.argv.findIndex(
|
|
5873
|
+
(token, index) => index >= 2 && underpostProgram.commands.some((command) => command._name === token),
|
|
5874
|
+
);
|
|
5875
|
+
if (commandIndex > 2)
|
|
5876
|
+
process.argv = [
|
|
5877
|
+
...process.argv.slice(0, 2),
|
|
5878
|
+
...process.argv.slice(2, commandIndex).filter((token) => token !== 'underpost'),
|
|
5879
|
+
...process.argv.slice(commandIndex),
|
|
5880
|
+
];
|
|
5881
|
+
if (!process.argv.includes('--plain')) logger.info('Rerouting to underpost cli...');
|
|
5791
5882
|
try {
|
|
5792
5883
|
await underpostProgram.parseAsync();
|
|
5793
5884
|
} catch (err) {
|