cyberia 3.1.3 → 3.2.9
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/.env.example +0 -2
- package/.github/workflows/engine-cyberia.cd.yml +10 -8
- package/.github/workflows/engine-cyberia.ci.yml +12 -29
- package/.github/workflows/ghpkg.ci.yml +4 -4
- package/.github/workflows/npmpkg.ci.yml +28 -11
- package/.github/workflows/publish.ci.yml +21 -2
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
- package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
- package/.github/workflows/release.cd.yml +14 -10
- package/CHANGELOG.md +783 -1
- package/CLI-HELP.md +95 -18
- package/Dockerfile +0 -2
- package/README.md +290 -220
- package/bin/build.js +24 -7
- package/bin/cyberia.js +2838 -252
- package/bin/deploy.js +747 -125
- package/bin/file.js +9 -0
- package/bin/index.js +2838 -252
- package/bin/vs.js +1 -1
- package/conf.js +99 -65
- package/deployment.yaml +18 -164
- package/hardhat/hardhat.config.js +13 -13
- package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
- package/hardhat/package-lock.json +2559 -5864
- package/hardhat/package.json +14 -23
- package/hardhat/scripts/deployObjectLayerToken.js +1 -1
- package/hardhat/test/ObjectLayerToken.js +4 -2
- package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
- package/hardhat/types/ethers-contracts/common.ts +92 -0
- package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
- package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
- package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
- package/hardhat/types/ethers-contracts/index.ts +6 -0
- package/jsconfig.json +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +6 -5
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +6 -5
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +18 -164
- package/manifests/deployment/dd-cyberia-development/proxy.yaml +7 -79
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
- package/manifests/deployment/dd-test-development/deployment.yaml +112 -28
- package/manifests/deployment/dd-test-development/proxy.yaml +46 -1
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/nodemon.json +1 -1
- package/package.json +39 -24
- package/proxy.yaml +7 -79
- package/scripts/k3s-node-setup.sh +2 -2
- package/scripts/nat-iptables.sh +103 -18
- package/scripts/rhel-grpc-setup.sh +56 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +58 -14
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +23 -14
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +148 -20
- package/src/api/core/core.controller.js +10 -10
- package/src/api/core/core.service.js +10 -10
- package/src/api/crypto/crypto.controller.js +8 -8
- package/src/api/crypto/crypto.service.js +8 -8
- package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
- package/src/api/cyberia-action/cyberia-action.model.js +87 -0
- package/src/api/cyberia-action/cyberia-action.router.js +27 -0
- package/src/api/cyberia-action/cyberia-action.service.js +42 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
- package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
- package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
- package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
- package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +178 -0
- package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
- package/src/api/cyberia-instance/cyberia-instance.model.js +87 -0
- package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
- package/src/api/cyberia-instance/cyberia-instance.service.js +156 -0
- package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
- package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +574 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +231 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
- package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
- package/src/api/cyberia-map/cyberia-map.model.js +30 -0
- package/src/api/cyberia-map/cyberia-map.router.js +40 -0
- package/src/api/cyberia-map/cyberia-map.service.js +74 -0
- package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
- package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
- package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
- package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
- package/src/api/default/default.controller.js +10 -10
- package/src/api/default/default.service.js +10 -10
- package/src/api/document/document.controller.js +12 -12
- package/src/api/document/document.model.js +10 -16
- package/src/api/file/file.controller.js +8 -8
- package/src/api/file/file.model.js +10 -10
- package/src/api/file/file.ref.json +18 -0
- package/src/api/file/file.service.js +36 -36
- package/src/api/instance/instance.controller.js +10 -10
- package/src/api/instance/instance.model.js +4 -10
- package/src/api/instance/instance.service.js +10 -10
- package/src/api/ipfs/ipfs.controller.js +15 -36
- package/src/api/ipfs/ipfs.model.js +47 -47
- package/src/api/ipfs/ipfs.router.js +8 -13
- package/src/api/ipfs/ipfs.service.js +67 -129
- package/src/api/object-layer/object-layer.controller.js +12 -12
- package/src/api/object-layer/object-layer.model.js +4 -17
- package/src/api/object-layer/object-layer.router.js +30 -0
- package/src/api/object-layer/object-layer.service.js +126 -43
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
- package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
- package/src/api/test/test.controller.js +8 -8
- package/src/api/test/test.service.js +8 -8
- package/src/api/user/guest.service.js +99 -0
- package/src/api/user/user.controller.js +6 -6
- package/src/api/user/user.model.js +8 -13
- package/src/api/user/user.service.js +11 -27
- package/src/cli/cluster.js +68 -21
- package/src/cli/db.js +753 -825
- package/src/cli/deploy.js +215 -125
- package/src/cli/env.js +29 -0
- package/src/cli/fs.js +82 -8
- package/src/cli/image.js +43 -1
- package/src/cli/index.js +74 -3
- package/src/cli/kubectl.js +211 -0
- package/src/cli/release.js +340 -0
- package/src/cli/repository.js +475 -74
- package/src/cli/run.js +582 -43
- package/src/cli/secrets.js +73 -0
- package/src/cli/ssh.js +1 -1
- package/src/cli/static.js +43 -115
- package/src/cli/test.js +3 -3
- package/src/client/Cryptokoyn.index.js +18 -22
- package/src/client/CyberiaPortal.index.js +19 -24
- package/src/client/Default.index.js +21 -34
- package/src/client/Itemledger.index.js +20 -27
- package/src/client/Underpost.index.js +19 -24
- package/src/client/components/core/404.js +4 -4
- package/src/client/components/core/500.js +4 -4
- package/src/client/components/core/Account.js +73 -60
- package/src/client/components/core/AgGrid.js +23 -33
- package/src/client/components/core/Alert.js +12 -13
- package/src/client/components/core/AppStore.js +69 -0
- package/src/client/components/core/Auth.js +35 -37
- package/src/client/components/core/Badge.js +7 -13
- package/src/client/components/core/BtnIcon.js +15 -17
- package/src/client/components/core/CalendarCore.js +43 -64
- package/src/client/components/core/Chat.js +13 -15
- package/src/client/components/core/ClientEvents.js +87 -0
- package/src/client/components/core/ColorPaletteElement.js +309 -0
- package/src/client/components/core/Content.js +17 -14
- package/src/client/components/core/Css.js +15 -71
- package/src/client/components/core/CssCore.js +12 -16
- package/src/client/components/core/D3Chart.js +4 -4
- package/src/client/components/core/Docs.js +64 -91
- package/src/client/components/core/DropDown.js +194 -96
- package/src/client/components/core/EventBus.js +92 -0
- package/src/client/components/core/EventsUI.js +14 -17
- package/src/client/components/core/FileExplorer.js +96 -228
- package/src/client/components/core/FullScreen.js +47 -75
- package/src/client/components/core/Input.js +24 -69
- package/src/client/components/core/Keyboard.js +26 -19
- package/src/client/components/core/KeyboardAvoidance.js +145 -0
- package/src/client/components/core/LoadingAnimation.js +25 -31
- package/src/client/components/core/LogIn.js +43 -43
- package/src/client/components/core/LogOut.js +25 -16
- package/src/client/components/core/Modal.js +462 -179
- package/src/client/components/core/NotificationManager.js +14 -18
- package/src/client/components/core/Panel.js +54 -51
- package/src/client/components/core/PanelForm.js +44 -144
- package/src/client/components/core/Polyhedron.js +110 -214
- package/src/client/components/core/PublicProfile.js +39 -32
- package/src/client/components/core/Recover.js +48 -44
- package/src/client/components/core/Responsive.js +88 -32
- package/src/client/components/core/RichText.js +9 -18
- package/src/client/components/core/Router.js +24 -3
- package/src/client/components/core/SearchBox.js +37 -37
- package/src/client/components/core/SignUp.js +39 -30
- package/src/client/components/core/SocketIo.js +112 -30
- package/src/client/components/core/SocketIoHandler.js +75 -0
- package/src/client/components/core/Stream.js +143 -95
- package/src/client/components/core/ToggleSwitch.js +8 -20
- package/src/client/components/core/ToolTip.js +5 -17
- package/src/client/components/core/Translate.js +56 -59
- package/src/client/components/core/Validator.js +26 -16
- package/src/client/components/core/Wallet.js +15 -26
- package/src/client/components/core/Webhook.js +40 -7
- package/src/client/components/core/Worker.js +163 -27
- package/src/client/components/core/windowGetDimensions.js +7 -7
- package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +59 -59
- package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
- package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
- package/src/client/components/cryptokoyn/LogInCryptokoyn.js +9 -7
- package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +8 -6
- package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
- package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
- package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
- package/src/client/components/cyberia/InstanceEngineCyberia.js +781 -0
- package/src/client/components/cyberia/MapEngineCyberia.js +1836 -2
- package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +1220 -99
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +252 -316
- package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +136 -103
- package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
- package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +462 -32
- package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
- package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +9 -7
- package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +8 -6
- package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
- package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
- package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -4
- package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +91 -91
- package/src/client/components/default/AppStoreDefault.js +5 -0
- package/src/client/components/default/CssDefault.js +12 -12
- package/src/client/components/default/LogInDefault.js +9 -7
- package/src/client/components/default/LogOutDefault.js +8 -6
- package/src/client/components/default/RouterDefault.js +47 -0
- package/src/client/components/default/SettingsDefault.js +4 -4
- package/src/client/components/default/SignUpDefault.js +6 -4
- package/src/client/components/default/SocketIoDefault.js +3 -51
- package/src/client/components/default/TranslateDefault.js +3 -3
- package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +59 -59
- package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
- package/src/client/components/itemledger/CssItemledger.js +15 -15
- package/src/client/components/itemledger/LogInItemledger.js +9 -7
- package/src/client/components/itemledger/LogOutItemledger.js +8 -6
- package/src/client/components/itemledger/RouterItemledger.js +38 -0
- package/src/client/components/itemledger/SettingsItemledger.js +4 -4
- package/src/client/components/itemledger/SignUpItemledger.js +6 -4
- package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
- package/src/client/components/itemledger/TranslateItemledger.js +3 -3
- package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +92 -92
- package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
- package/src/client/components/underpost/CssUnderpost.js +14 -14
- package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
- package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
- package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
- package/src/client/components/underpost/LogInUnderpost.js +9 -7
- package/src/client/components/underpost/LogOutUnderpost.js +8 -6
- package/src/client/components/underpost/RouterUnderpost.js +45 -0
- package/src/client/components/underpost/SettingsUnderpost.js +4 -4
- package/src/client/components/underpost/SignUpUnderpost.js +6 -4
- package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
- package/src/client/components/underpost/TranslateUnderpost.js +4 -4
- package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
- package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
- package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
- package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
- package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
- package/src/client/services/core/core.service.js +35 -55
- package/src/client/services/crypto/crypto.service.js +8 -13
- package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
- package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
- package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
- package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
- package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +116 -0
- package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
- package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
- package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
- package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
- package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
- package/src/client/services/default/default.management.js +159 -267
- package/src/client/services/default/default.service.js +10 -16
- package/src/client/services/document/document.service.js +14 -19
- package/src/client/services/file/file.service.js +8 -13
- package/src/client/services/instance/instance.management.js +6 -6
- package/src/client/services/instance/instance.service.js +10 -15
- package/src/client/services/ipfs/ipfs.service.js +14 -40
- package/src/client/services/object-layer/object-layer.management.js +14 -14
- package/src/client/services/object-layer/object-layer.service.js +39 -24
- package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
- package/src/client/services/test/test.service.js +8 -13
- package/src/client/services/user/guest.service.js +86 -0
- package/src/client/services/user/user.management.js +6 -6
- package/src/client/services/user/user.service.js +14 -20
- package/src/client/ssr/body/404.js +3 -3
- package/src/client/ssr/body/500.js +3 -3
- package/src/client/ssr/body/CacheControl.js +5 -2
- package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
- package/src/client/ssr/head/PwaItemledger.js +197 -60
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
- package/src/client/ssr/offline/Maintenance.js +12 -11
- package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
- package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/pages/Test.js +2 -2
- package/src/client/sw/core.sw.js +212 -0
- package/src/grpc/cyberia/grpc-server.js +642 -0
- package/src/index.js +24 -1
- package/src/runtime/cyberia-client/Dockerfile +80 -0
- package/src/runtime/cyberia-server/Dockerfile +37 -0
- package/src/runtime/express/Dockerfile +5 -1
- package/src/runtime/express/Express.js +18 -1
- package/src/runtime/lampp/Dockerfile +17 -5
- package/src/runtime/lampp/Lampp.js +27 -4
- package/src/runtime/wp/Dockerfile +62 -0
- package/src/runtime/wp/Wp.js +639 -0
- package/src/server/atlas-sprite-sheet-generator.js +4 -2
- package/src/server/auth.js +24 -1
- package/src/server/backup.js +37 -9
- package/src/server/client-build-docs.js +52 -46
- package/src/server/client-build.js +356 -82
- package/src/server/client-formatted.js +140 -57
- package/src/server/conf.js +29 -13
- package/src/server/cron.js +25 -23
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +24 -1
- package/src/server/ipfs-client.js +253 -89
- package/src/server/object-layer.js +150 -114
- package/src/server/peer.js +8 -0
- package/src/server/process.js +13 -27
- package/src/server/runtime.js +25 -1
- package/src/server/semantic-layer-generator-floor.js +319 -0
- package/src/server/semantic-layer-generator-resource.js +259 -0
- package/src/server/semantic-layer-generator-skin.js +1164 -0
- package/src/server/semantic-layer-generator.js +211 -542
- package/src/server/shape-generator.js +108 -0
- package/src/server/start.js +19 -5
- package/src/server/valkey.js +141 -235
- package/src/ws/IoInterface.js +1 -10
- package/src/ws/IoServer.js +14 -33
- package/src/ws/core/channels/core.ws.chat.js +65 -20
- package/src/ws/core/channels/core.ws.mailer.js +113 -32
- package/src/ws/core/channels/core.ws.stream.js +90 -31
- package/src/ws/core/core.ws.connection.js +12 -33
- package/src/ws/core/core.ws.emit.js +10 -26
- package/src/ws/core/core.ws.server.js +25 -58
- package/src/ws/default/channels/default.ws.main.js +53 -12
- package/src/ws/default/default.ws.connection.js +26 -13
- package/src/ws/default/default.ws.server.js +30 -12
- package/tsconfig.docs.json +15 -0
- package/typedoc.dd-cyberia.json +29 -0
- package/typedoc.json +29 -0
- package/WHITE-PAPER.md +0 -1540
- package/hardhat/README.md +0 -531
- package/hardhat/WHITE-PAPER.md +0 -1540
- package/jsdoc.dd-cyberia.json +0 -59
- package/jsdoc.json +0 -59
- package/src/api/object-layer/README.md +0 -347
- package/src/client/components/core/ColorPalette.js +0 -5267
- package/src/client/components/core/JoyStick.js +0 -80
- package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
- package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
- package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
- package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
- package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -58
- package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
- package/src/client/components/default/ElementsDefault.js +0 -38
- package/src/client/components/default/RoutesDefault.js +0 -49
- package/src/client/components/itemledger/CommonItemledger.js +0 -29
- package/src/client/components/itemledger/ElementsItemledger.js +0 -38
- package/src/client/components/itemledger/RoutesItemledger.js +0 -40
- package/src/client/components/underpost/CommonUnderpost.js +0 -29
- package/src/client/components/underpost/ElementsUnderpost.js +0 -38
- package/src/client/components/underpost/RoutesUnderpost.js +0 -47
- package/src/client/sw/default.sw.js +0 -127
- package/src/client/sw/template.sw.js +0 -84
- package/src/ws/core/management/core.ws.chat.js +0 -8
- package/src/ws/core/management/core.ws.mailer.js +0 -16
- package/src/ws/core/management/core.ws.stream.js +0 -8
- package/src/ws/default/management/default.ws.main.js +0 -8
package/bin/cyberia.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import dotenv from 'dotenv';
|
|
17
17
|
import { Command } from 'commander';
|
|
18
18
|
import fs from 'fs-extra';
|
|
19
|
+
import stringify from 'fast-json-stable-stringify';
|
|
19
20
|
import { shellExec } from '../src/server/process.js';
|
|
20
21
|
import { loggerFactory } from '../src/server/logger.js';
|
|
21
22
|
import { generateBesuManifests, deployBesu, removeBesu } from '../src/server/besu-genesis-generator.js';
|
|
@@ -27,21 +28,24 @@ import {
|
|
|
27
28
|
pngDirectoryIteratorByObjectLayerType,
|
|
28
29
|
getKeyFramesDirectionsFromNumberFolderDirection,
|
|
29
30
|
buildImgFromTile,
|
|
30
|
-
itemTypes,
|
|
31
31
|
} from '../src/server/object-layer.js';
|
|
32
32
|
import { AtlasSpriteSheetGenerator } from '../src/server/atlas-sprite-sheet-generator.js';
|
|
33
|
-
import {
|
|
34
|
-
generateFrame,
|
|
35
|
-
generateMultiFrame,
|
|
36
|
-
lookupSemantic,
|
|
37
|
-
semanticRegistry,
|
|
38
|
-
} from '../src/server/semantic-layer-generator.js';
|
|
33
|
+
import { generateMultiFrame, lookupSemantic, semanticRegistry } from '../src/server/semantic-layer-generator.js';
|
|
39
34
|
import { IpfsClient } from '../src/server/ipfs-client.js';
|
|
40
35
|
import { createPinRecord } from '../src/api/ipfs/ipfs.service.js';
|
|
41
36
|
import { program as underpostProgram } from '../src/cli/index.js';
|
|
42
37
|
import crypto from 'crypto';
|
|
43
38
|
import nodePath from 'path';
|
|
44
39
|
import Underpost from '../src/index.js';
|
|
40
|
+
import { newInstance } from '../src/client/components/core/CommonJs.js';
|
|
41
|
+
import {
|
|
42
|
+
ITEM_TYPES as itemTypes,
|
|
43
|
+
DefaultCyberiaItems,
|
|
44
|
+
DefaultSkillConfig,
|
|
45
|
+
DefaultCyberiaDialogues,
|
|
46
|
+
DefaultCyberiaActions,
|
|
47
|
+
DefaultCyberiaQuests,
|
|
48
|
+
} from '../src/client/components/cyberia-portal/CommonCyberiaPortal.js';
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Connect to the project MongoDB instance using the standard env / conf layout.
|
|
@@ -103,7 +107,11 @@ try {
|
|
|
103
107
|
'Convert object layers to atlas sprite sheets, specify dimension (default: auto-calculated based on frame count)',
|
|
104
108
|
)
|
|
105
109
|
.option('--show-atlas-sprite-sheet', 'Show consolidated atlas sprite sheet PNG for given item-id')
|
|
106
|
-
.option(
|
|
110
|
+
.option(
|
|
111
|
+
'--import',
|
|
112
|
+
'Import specific item-id(s) passed as comma-separated command argument (e.g. ol hatchet,sword --import)',
|
|
113
|
+
)
|
|
114
|
+
.option('--import-types [object-layer-type]', 'Batch import by object layer type e.g. skin,floors or all')
|
|
107
115
|
.option('--show-frame [direction-frame]', 'View object layer frame for given item-id e.g. 08_0 (default: 08_0)')
|
|
108
116
|
.option('--generate', 'Generate procedural object layers from semantic item-id (e.g. floor-desert)')
|
|
109
117
|
.option('--count <count>', 'Shape element count multiplier for --generate (default: 3)', parseFloat)
|
|
@@ -115,6 +123,9 @@ try {
|
|
|
115
123
|
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
116
124
|
.option('--storage-file-path <storage-file-path>', 'Storage file path override')
|
|
117
125
|
.option('--drop', 'Drop existing data before importing')
|
|
126
|
+
.option('--client-public', 'When used with --drop, also remove static asset folders for dropped items')
|
|
127
|
+
.option('--git-clean', 'When used with --drop, run underpost clean on the cyberia asset directory')
|
|
128
|
+
.option('--dev', 'Force development environment (loads .env.development for IPFS localhost, etc.)')
|
|
118
129
|
.action(
|
|
119
130
|
/**
|
|
120
131
|
* Main action handler for the `ol` command.
|
|
@@ -122,7 +133,8 @@ try {
|
|
|
122
133
|
*
|
|
123
134
|
* @param {string|undefined} itemId - Optional item ID argument.
|
|
124
135
|
* @param {Object} options - Command options parsed by Commander.
|
|
125
|
-
* @param {boolean
|
|
136
|
+
* @param {boolean} options.import - Import specific item-id(s) from the command argument (comma-separated).
|
|
137
|
+
* @param {boolean|string} options.importTypes - Object layer types to batch import (e.g., 'all', 'skin,floor') or `false`.
|
|
126
138
|
* @param {boolean|string} options.showFrame - Direction-frame string (e.g., '08_0') or `true` for default.
|
|
127
139
|
* @param {string} options.envPath - Path to the `.env` file.
|
|
128
140
|
* @param {string} options.mongoHost - MongoDB host override.
|
|
@@ -130,6 +142,9 @@ try {
|
|
|
130
142
|
* @param {boolean|string} options.toAtlasSpriteSheet - Atlas dimension or `true` for auto-calc.
|
|
131
143
|
* @param {boolean} options.showAtlasSpriteSheet - Whether to display the atlas sprite sheet.
|
|
132
144
|
* @param {boolean} options.drop - Whether to drop existing data before importing.
|
|
145
|
+
* @param {boolean} options.clientPublic - Also remove static asset folders when dropping.
|
|
146
|
+
* @param {boolean} options.gitClean - Run underpost clean on the cyberia asset directory when dropping.
|
|
147
|
+
* @param {boolean} options.dev - Force development environment.
|
|
133
148
|
* @param {boolean} options.generate - Whether to run procedural generation for the item-id.
|
|
134
149
|
* @param {number} options.count - Shape element count multiplier for generation.
|
|
135
150
|
* @param {string} options.seed - Deterministic seed string for generation.
|
|
@@ -143,12 +158,17 @@ try {
|
|
|
143
158
|
itemId,
|
|
144
159
|
options = {
|
|
145
160
|
import: false,
|
|
161
|
+
importTypes: false,
|
|
146
162
|
showFrame: '',
|
|
147
163
|
envPath: '',
|
|
148
164
|
mongoHost: '',
|
|
149
165
|
storageFilePath: '',
|
|
150
166
|
toAtlasSpriteSheet: '',
|
|
151
167
|
showAtlasSpriteSheet: false,
|
|
168
|
+
drop: false,
|
|
169
|
+
clientPublic: false,
|
|
170
|
+
gitClean: false,
|
|
171
|
+
dev: false,
|
|
152
172
|
generate: false,
|
|
153
173
|
count: 3,
|
|
154
174
|
seed: '',
|
|
@@ -160,6 +180,14 @@ try {
|
|
|
160
180
|
if (!options.envPath) options.envPath = `./.env`;
|
|
161
181
|
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
162
182
|
|
|
183
|
+
// --dev: force development environment (IPFS localhost, etc.)
|
|
184
|
+
if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
|
|
185
|
+
const deployDevEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
|
|
186
|
+
if (fs.existsSync(deployDevEnvPath)) {
|
|
187
|
+
dotenv.config({ path: deployDevEnvPath, override: true });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
163
191
|
/** @type {string} */
|
|
164
192
|
const deployId = process.env.DEFAULT_DEPLOY_ID;
|
|
165
193
|
/** @type {string} */
|
|
@@ -171,7 +199,11 @@ try {
|
|
|
171
199
|
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
172
200
|
const { db } = confServer[host][path];
|
|
173
201
|
|
|
174
|
-
db.host = options.mongoHost
|
|
202
|
+
db.host = options.mongoHost
|
|
203
|
+
? options.mongoHost
|
|
204
|
+
: options.dev
|
|
205
|
+
? db.host
|
|
206
|
+
: db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
|
|
175
207
|
|
|
176
208
|
logger.info('env', {
|
|
177
209
|
env: options.envPath,
|
|
@@ -197,23 +229,519 @@ try {
|
|
|
197
229
|
const AtlasSpriteSheet = DataBaseProvider.instance[`${host}${path}`].mongoose.models.AtlasSpriteSheet;
|
|
198
230
|
/** @type {import('mongoose').Model} */
|
|
199
231
|
const File = DataBaseProvider.instance[`${host}${path}`].mongoose.models.File;
|
|
232
|
+
/** @type {import('mongoose').Model} */
|
|
233
|
+
const Ipfs = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Ipfs;
|
|
200
234
|
|
|
201
235
|
if (options.drop) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
236
|
+
// Parse comma-separated item IDs for targeted drop; if none provided, drop everything
|
|
237
|
+
const dropItemIds = itemId
|
|
238
|
+
? itemId
|
|
239
|
+
.split(',')
|
|
240
|
+
.map((id) => id.trim())
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
: null;
|
|
243
|
+
const isTargetedDrop = dropItemIds && dropItemIds.length > 0;
|
|
244
|
+
|
|
245
|
+
if (isTargetedDrop) {
|
|
246
|
+
logger.info(`Targeted drop for item(s): ${dropItemIds.join(', ')}`);
|
|
247
|
+
} else {
|
|
248
|
+
logger.info('Dropping ALL object layer data');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build query filter: targeted or all
|
|
252
|
+
const olFilter = isTargetedDrop ? { 'data.item.id': { $in: dropItemIds } } : {};
|
|
253
|
+
const atlasFilter = isTargetedDrop ? { 'metadata.itemKey': { $in: dropItemIds } } : {};
|
|
254
|
+
|
|
255
|
+
// Collect data before deletion
|
|
256
|
+
const olDocs = await ObjectLayer.find(olFilter, {
|
|
257
|
+
cid: 1,
|
|
258
|
+
'data.item.id': 1,
|
|
259
|
+
'data.item.type': 1,
|
|
260
|
+
'data.render': 1,
|
|
261
|
+
objectLayerRenderFramesId: 1,
|
|
262
|
+
atlasSpriteSheetId: 1,
|
|
263
|
+
}).lean();
|
|
264
|
+
const atlasDocs = await AtlasSpriteSheet.find(atlasFilter, { fileId: 1, cid: 1 }).lean();
|
|
265
|
+
|
|
266
|
+
const cidsToUnpin = new Set();
|
|
267
|
+
const itemIdsToClean = new Set();
|
|
268
|
+
const renderFrameIds = [];
|
|
269
|
+
const atlasIds = [];
|
|
270
|
+
|
|
271
|
+
for (const doc of olDocs) {
|
|
272
|
+
if (doc.cid) cidsToUnpin.add(doc.cid);
|
|
273
|
+
if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
|
|
274
|
+
if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
|
|
275
|
+
if (doc.data?.item?.id) itemIdsToClean.add(doc.data.item.id);
|
|
276
|
+
if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
|
|
277
|
+
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
|
|
281
|
+
for (const atlas of atlasDocs) {
|
|
282
|
+
if (atlas.cid) cidsToUnpin.add(atlas.cid);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const olCount = olDocs.length;
|
|
286
|
+
const atlasCount = atlasDocs.length;
|
|
287
|
+
|
|
288
|
+
// Delete targeted documents
|
|
289
|
+
if (isTargetedDrop) {
|
|
290
|
+
const olIds = olDocs.map((d) => d._id);
|
|
291
|
+
if (olIds.length > 0) await ObjectLayer.deleteMany({ _id: { $in: olIds } });
|
|
292
|
+
if (renderFrameIds.length > 0) await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
|
|
293
|
+
if (atlasIds.length > 0) await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
294
|
+
} else {
|
|
295
|
+
await ObjectLayer.deleteMany();
|
|
296
|
+
await ObjectLayerRenderFrames.deleteMany();
|
|
297
|
+
await AtlasSpriteSheet.deleteMany();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const rfCount = renderFrameIds.length;
|
|
301
|
+
|
|
302
|
+
// Remove only the File documents that were referenced by atlas sprite sheets
|
|
303
|
+
let fileCount = 0;
|
|
304
|
+
if (atlasFileIds.length > 0) {
|
|
305
|
+
const result = await File.deleteMany({ _id: { $in: atlasFileIds } });
|
|
306
|
+
fileCount = result.deletedCount || 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Delete IPFS pin registry records for all collected CIDs
|
|
310
|
+
if (cidsToUnpin.size > 0) {
|
|
311
|
+
const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
|
|
312
|
+
logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Unpin CIDs from IPFS Cluster + Kubo and remove MFS directories
|
|
316
|
+
let unpinCount = 0;
|
|
317
|
+
let mfsCount = 0;
|
|
318
|
+
for (const cid of cidsToUnpin) {
|
|
319
|
+
const ok = await IpfsClient.unpinCid(cid);
|
|
320
|
+
if (ok) unpinCount++;
|
|
321
|
+
}
|
|
322
|
+
for (const itemKey of itemIdsToClean) {
|
|
323
|
+
const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
|
|
324
|
+
if (ok) mfsCount++;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
logger.info(
|
|
328
|
+
`Dropped: ${olCount} ObjectLayer, ${rfCount} RenderFrames, ${atlasCount} AtlasSpriteSheet, ${fileCount} File (atlas)`,
|
|
329
|
+
);
|
|
330
|
+
logger.info(
|
|
331
|
+
`IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemIdsToClean.size} MFS paths removed`,
|
|
332
|
+
);
|
|
333
|
+
if (options.gitClean) {
|
|
334
|
+
shellExec(`cd src/client/public/cyberia && underpost run clean .`);
|
|
335
|
+
logger.info('Asset directory cleaned');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --client-public: remove static asset folders for dropped items
|
|
339
|
+
if (options.clientPublic) {
|
|
340
|
+
const srcBase = './src/client/public/cyberia/assets';
|
|
341
|
+
const publicBase = `./public/${host}${path}/assets`;
|
|
342
|
+
let removedCount = 0;
|
|
343
|
+
for (const doc of olDocs) {
|
|
344
|
+
const docItemId = doc.data?.item?.id;
|
|
345
|
+
const docItemType = doc.data?.item?.type;
|
|
346
|
+
if (!docItemId || !docItemType) continue;
|
|
347
|
+
for (const base of [srcBase, publicBase]) {
|
|
348
|
+
const folder = `${base}/${docItemType}/${docItemId}`;
|
|
349
|
+
if (fs.existsSync(folder)) {
|
|
350
|
+
fs.removeSync(folder);
|
|
351
|
+
removedCount++;
|
|
352
|
+
logger.info(`Removed static folder: ${folder}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
logger.info(`Static asset cleanup: ${removedCount} folder(s) removed`);
|
|
357
|
+
}
|
|
205
358
|
}
|
|
206
359
|
|
|
207
360
|
/** @type {Object|null} */
|
|
208
361
|
const storage = options.storageFilePath ? JSON.parse(fs.readFileSync(options.storageFilePath, 'utf8')) : null;
|
|
209
362
|
|
|
210
|
-
// ── Handle --import
|
|
363
|
+
// ── Handle --import (specific item-id(s)) ─────────────────────
|
|
211
364
|
if (options.import) {
|
|
365
|
+
if (!itemId) {
|
|
366
|
+
logger.error('item-id is required for --import (comma-separated item IDs, e.g. ol hatchet,sword --import)');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const itemIds = itemId
|
|
371
|
+
.split(',')
|
|
372
|
+
.map((id) => id.trim())
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
logger.info(`Importing specific item(s): ${itemIds.join(', ')}`);
|
|
375
|
+
|
|
376
|
+
for (const currentItemId of itemIds) {
|
|
377
|
+
// Search across all asset type directories to find which type contains this item-id
|
|
378
|
+
let foundType = null;
|
|
379
|
+
let foundFolder = null;
|
|
380
|
+
for (const type of Object.keys(itemTypes)) {
|
|
381
|
+
const candidateFolder = `./src/client/public/cyberia/assets/${type}/${currentItemId}`;
|
|
382
|
+
if (fs.existsSync(candidateFolder) && fs.statSync(candidateFolder).isDirectory()) {
|
|
383
|
+
foundType = type;
|
|
384
|
+
foundFolder = candidateFolder;
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!foundType) {
|
|
390
|
+
logger.error(
|
|
391
|
+
`Item-id '${currentItemId}' not found in any asset type directory (${Object.keys(itemTypes).join(', ')})`,
|
|
392
|
+
);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
logger.info(`Found item '${currentItemId}' in type '${foundType}' at ${foundFolder}`);
|
|
397
|
+
|
|
398
|
+
const { objectLayerRenderFramesData, objectLayerData } =
|
|
399
|
+
await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
|
|
400
|
+
folder: foundFolder,
|
|
401
|
+
objectLayerType: foundType,
|
|
402
|
+
objectLayerId: currentItemId,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Write processed frames back to disk so WebP matches atlas
|
|
406
|
+
const srcBasePath = './src/client/public/cyberia/';
|
|
407
|
+
const publicBasePath = `./public/${host}${path}`;
|
|
408
|
+
await ObjectLayerEngine.writeStaticFrameAssets({
|
|
409
|
+
basePaths: [srcBasePath, publicBasePath],
|
|
410
|
+
itemType: foundType,
|
|
411
|
+
itemId: currentItemId,
|
|
412
|
+
objectLayerRenderFramesData,
|
|
413
|
+
objectLayerData,
|
|
414
|
+
cellPixelDim: 20,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Check if an ObjectLayer with the same item.id already exists (upsert by item ID)
|
|
418
|
+
const existingOL = await ObjectLayer.findOne({ 'data.item.id': currentItemId });
|
|
419
|
+
let objectLayer;
|
|
420
|
+
|
|
421
|
+
if (existingOL) {
|
|
422
|
+
// ── Cut-over consistency: stage everything in memory before touching the live document ──
|
|
423
|
+
logger.info(`ObjectLayer '${currentItemId}' already exists (${existingOL._id}), staging update...`);
|
|
424
|
+
|
|
425
|
+
// 1. Prepare staging data entirely in memory (no DB writes yet)
|
|
426
|
+
const stagingData = JSON.parse(JSON.stringify(objectLayerData.data));
|
|
427
|
+
if (!stagingData.render) stagingData.render = {};
|
|
428
|
+
stagingData.render.cid = '';
|
|
429
|
+
stagingData.render.metadataCid = '';
|
|
430
|
+
|
|
431
|
+
// 2. Generate atlas, pin to IPFS, compute SHA-256 — all in memory
|
|
432
|
+
let cutoverReady = false;
|
|
433
|
+
let stagingFileDoc = null;
|
|
434
|
+
let stagingAtlasDoc = null;
|
|
435
|
+
let stagingCid = '';
|
|
436
|
+
let stagingSha256 = '';
|
|
437
|
+
try {
|
|
438
|
+
const itemKey = currentItemId;
|
|
439
|
+
|
|
440
|
+
// Generate atlas from in-memory render frames data (plain object, no DB doc needed)
|
|
441
|
+
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
442
|
+
objectLayerRenderFramesData,
|
|
443
|
+
itemKey,
|
|
444
|
+
20,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
stagingFileDoc = await new File({
|
|
448
|
+
name: `${itemKey}-atlas.png`,
|
|
449
|
+
data: buffer,
|
|
450
|
+
size: buffer.length,
|
|
451
|
+
mimetype: 'image/png',
|
|
452
|
+
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
453
|
+
}).save();
|
|
454
|
+
|
|
455
|
+
let importItemCid = '';
|
|
456
|
+
let importItemMetadataCid = '';
|
|
457
|
+
try {
|
|
458
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
459
|
+
buffer,
|
|
460
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
461
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
462
|
+
);
|
|
463
|
+
if (ipfsResult) {
|
|
464
|
+
importItemCid = ipfsResult.cid;
|
|
465
|
+
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importItemCid}`);
|
|
466
|
+
try {
|
|
467
|
+
await createPinRecord({
|
|
468
|
+
cid: importItemCid,
|
|
469
|
+
resourceType: 'atlas-sprite-sheet',
|
|
470
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
471
|
+
options: { host, path },
|
|
472
|
+
});
|
|
473
|
+
} catch (prErr) {
|
|
474
|
+
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
} catch (ipfsError) {
|
|
478
|
+
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
try {
|
|
482
|
+
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
483
|
+
metadata,
|
|
484
|
+
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
485
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
486
|
+
);
|
|
487
|
+
if (metadataIpfsResult) {
|
|
488
|
+
importItemMetadataCid = metadataIpfsResult.cid;
|
|
489
|
+
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importItemMetadataCid}`);
|
|
490
|
+
try {
|
|
491
|
+
await createPinRecord({
|
|
492
|
+
cid: importItemMetadataCid,
|
|
493
|
+
resourceType: 'atlas-metadata',
|
|
494
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
495
|
+
options: { host, path },
|
|
496
|
+
});
|
|
497
|
+
} catch (prErr) {
|
|
498
|
+
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (ipfsError) {
|
|
502
|
+
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Persist atlas doc (or update existing one for this itemKey)
|
|
506
|
+
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
507
|
+
if (stagingAtlasDoc) {
|
|
508
|
+
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
509
|
+
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
510
|
+
stagingAtlasDoc.cid = importItemCid;
|
|
511
|
+
stagingAtlasDoc.metadata = metadata;
|
|
512
|
+
await stagingAtlasDoc.save();
|
|
513
|
+
} else {
|
|
514
|
+
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
515
|
+
fileId: stagingFileDoc._id,
|
|
516
|
+
cid: importItemCid,
|
|
517
|
+
metadata,
|
|
518
|
+
}).save();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Finalize staging data in memory with render CIDs
|
|
522
|
+
stagingData.render.cid = importItemCid;
|
|
523
|
+
stagingData.render.metadataCid = importItemMetadataCid;
|
|
524
|
+
|
|
525
|
+
// Pin data JSON to IPFS (compute final SHA-256 in memory)
|
|
526
|
+
stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
527
|
+
try {
|
|
528
|
+
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
529
|
+
stagingData,
|
|
530
|
+
`${itemKey}_data.json`,
|
|
531
|
+
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
532
|
+
);
|
|
533
|
+
if (ipfsDataResult) {
|
|
534
|
+
stagingCid = ipfsDataResult.cid;
|
|
535
|
+
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
536
|
+
try {
|
|
537
|
+
await createPinRecord({
|
|
538
|
+
cid: stagingCid,
|
|
539
|
+
resourceType: 'object-layer-data',
|
|
540
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
541
|
+
options: { host, path },
|
|
542
|
+
});
|
|
543
|
+
} catch (prErr) {
|
|
544
|
+
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
} catch (ipfsError) {
|
|
548
|
+
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
cutoverReady = true;
|
|
552
|
+
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for cut-over`);
|
|
553
|
+
} catch (atlasError) {
|
|
554
|
+
logger.error(`[staging] Failed for ${currentItemId}, live document untouched:`, atlasError);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 3. Atomic cut-over: create new RenderFrames, swap live ObjectLayer in a single update
|
|
558
|
+
if (cutoverReady) {
|
|
559
|
+
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
560
|
+
|
|
561
|
+
// Create the new RenderFrames doc (only now touches DB)
|
|
562
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
563
|
+
|
|
564
|
+
// Single atomic update of the live document
|
|
565
|
+
await ObjectLayer.findByIdAndUpdate(existingOL._id, {
|
|
566
|
+
data: stagingData,
|
|
567
|
+
sha256: stagingSha256,
|
|
568
|
+
cid: stagingCid,
|
|
569
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
570
|
+
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// Clean up old render frames
|
|
574
|
+
if (oldRenderFramesId) {
|
|
575
|
+
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
|
|
579
|
+
} else {
|
|
580
|
+
// Rollback: only File/AtlasSpriteSheet were written, clean those up
|
|
581
|
+
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
582
|
+
logger.warn(`[cut-over] Staging rolled back for ${currentItemId}, live document preserved`);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
objectLayer = await ObjectLayer.findById(existingOL._id);
|
|
586
|
+
} else {
|
|
587
|
+
// ── New item: stage everything before creating (same cut-over pattern) ──
|
|
588
|
+
logger.info(`ObjectLayer '${currentItemId}' is new, staging creation...`);
|
|
589
|
+
|
|
590
|
+
const itemKey = currentItemId;
|
|
591
|
+
const stagingData = JSON.parse(JSON.stringify(objectLayerData.data));
|
|
592
|
+
if (!stagingData.render) stagingData.render = {};
|
|
593
|
+
stagingData.render.cid = '';
|
|
594
|
+
stagingData.render.metadataCid = '';
|
|
595
|
+
|
|
596
|
+
let cutoverReady = false;
|
|
597
|
+
let stagingFileDoc = null;
|
|
598
|
+
let stagingAtlasDoc = null;
|
|
599
|
+
let stagingCid = '';
|
|
600
|
+
let stagingSha256 = '';
|
|
601
|
+
try {
|
|
602
|
+
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
603
|
+
objectLayerRenderFramesData,
|
|
604
|
+
itemKey,
|
|
605
|
+
20,
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
stagingFileDoc = await new File({
|
|
609
|
+
name: `${itemKey}-atlas.png`,
|
|
610
|
+
data: buffer,
|
|
611
|
+
size: buffer.length,
|
|
612
|
+
mimetype: 'image/png',
|
|
613
|
+
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
614
|
+
}).save();
|
|
615
|
+
|
|
616
|
+
let importItemCid = '';
|
|
617
|
+
let importItemMetadataCid = '';
|
|
618
|
+
try {
|
|
619
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
620
|
+
buffer,
|
|
621
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
622
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
623
|
+
);
|
|
624
|
+
if (ipfsResult) {
|
|
625
|
+
importItemCid = ipfsResult.cid;
|
|
626
|
+
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importItemCid}`);
|
|
627
|
+
try {
|
|
628
|
+
await createPinRecord({
|
|
629
|
+
cid: importItemCid,
|
|
630
|
+
resourceType: 'atlas-sprite-sheet',
|
|
631
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
632
|
+
options: { host, path },
|
|
633
|
+
});
|
|
634
|
+
} catch (prErr) {
|
|
635
|
+
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch (ipfsError) {
|
|
639
|
+
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
644
|
+
metadata,
|
|
645
|
+
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
646
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
647
|
+
);
|
|
648
|
+
if (metadataIpfsResult) {
|
|
649
|
+
importItemMetadataCid = metadataIpfsResult.cid;
|
|
650
|
+
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importItemMetadataCid}`);
|
|
651
|
+
try {
|
|
652
|
+
await createPinRecord({
|
|
653
|
+
cid: importItemMetadataCid,
|
|
654
|
+
resourceType: 'atlas-metadata',
|
|
655
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
656
|
+
options: { host, path },
|
|
657
|
+
});
|
|
658
|
+
} catch (prErr) {
|
|
659
|
+
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
} catch (ipfsError) {
|
|
663
|
+
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
667
|
+
if (stagingAtlasDoc) {
|
|
668
|
+
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
669
|
+
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
670
|
+
stagingAtlasDoc.cid = importItemCid;
|
|
671
|
+
stagingAtlasDoc.metadata = metadata;
|
|
672
|
+
await stagingAtlasDoc.save();
|
|
673
|
+
} else {
|
|
674
|
+
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
675
|
+
fileId: stagingFileDoc._id,
|
|
676
|
+
cid: importItemCid,
|
|
677
|
+
metadata,
|
|
678
|
+
}).save();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
stagingData.render.cid = importItemCid;
|
|
682
|
+
stagingData.render.metadataCid = importItemMetadataCid;
|
|
683
|
+
|
|
684
|
+
stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
685
|
+
try {
|
|
686
|
+
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
687
|
+
stagingData,
|
|
688
|
+
`${itemKey}_data.json`,
|
|
689
|
+
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
690
|
+
);
|
|
691
|
+
if (ipfsDataResult) {
|
|
692
|
+
stagingCid = ipfsDataResult.cid;
|
|
693
|
+
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
694
|
+
try {
|
|
695
|
+
await createPinRecord({
|
|
696
|
+
cid: stagingCid,
|
|
697
|
+
resourceType: 'object-layer-data',
|
|
698
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
699
|
+
options: { host, path },
|
|
700
|
+
});
|
|
701
|
+
} catch (prErr) {
|
|
702
|
+
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} catch (ipfsError) {
|
|
706
|
+
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
cutoverReady = true;
|
|
710
|
+
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for creation`);
|
|
711
|
+
} catch (atlasError) {
|
|
712
|
+
logger.error(`[staging] Failed for ${currentItemId}, no document created:`, atlasError);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (cutoverReady) {
|
|
716
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
|
|
717
|
+
objectLayer = await ObjectLayer.create({
|
|
718
|
+
data: stagingData,
|
|
719
|
+
sha256: stagingSha256,
|
|
720
|
+
cid: stagingCid,
|
|
721
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
722
|
+
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
723
|
+
});
|
|
724
|
+
logger.info(`[cut-over] New ObjectLayer ${objectLayer._id} created with all CIDs populated`);
|
|
725
|
+
} else {
|
|
726
|
+
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
727
|
+
logger.warn(`[cut-over] Staging failed for ${currentItemId}, no ObjectLayer created`);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Reload final state to include CID and render updates
|
|
733
|
+
const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
|
|
734
|
+
console.log(finalObjectLayer.toObject());
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// ── Handle --import-types (batch by type) ────────────────────────
|
|
739
|
+
if (options.importTypes) {
|
|
212
740
|
/** @type {boolean} */
|
|
213
|
-
const isImportAll = options.
|
|
741
|
+
const isImportAll = options.importTypes === 'all';
|
|
214
742
|
|
|
215
743
|
/** @type {string[]} */
|
|
216
|
-
const argItemTypes = isImportAll ? Object.keys(itemTypes) : options.
|
|
744
|
+
const argItemTypes = isImportAll ? Object.keys(itemTypes) : options.importTypes.split(',');
|
|
217
745
|
|
|
218
746
|
/**
|
|
219
747
|
* Accumulated object layer data keyed by objectLayerId.
|
|
@@ -221,6 +749,19 @@ try {
|
|
|
221
749
|
*/
|
|
222
750
|
const objectLayers = {};
|
|
223
751
|
|
|
752
|
+
// When importing all types, pre-fetch existing item IDs so we can skip them entirely
|
|
753
|
+
/** @type {Set<string>} */
|
|
754
|
+
const existingItemIds = new Set();
|
|
755
|
+
if (isImportAll) {
|
|
756
|
+
const existingDocs = await ObjectLayer.find({}, { 'data.item.id': 1 }).lean();
|
|
757
|
+
for (const doc of existingDocs) {
|
|
758
|
+
if (doc.data?.item?.id) existingItemIds.add(doc.data.item.id);
|
|
759
|
+
}
|
|
760
|
+
if (existingItemIds.size > 0) {
|
|
761
|
+
logger.info(`Skipping ${existingItemIds.size} existing item(s): ${[...existingItemIds].join(', ')}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
224
765
|
for (const argItemType of argItemTypes) {
|
|
225
766
|
await pngDirectoryIteratorByObjectLayerType(
|
|
226
767
|
argItemType,
|
|
@@ -231,6 +772,9 @@ try {
|
|
|
231
772
|
)
|
|
232
773
|
return;
|
|
233
774
|
|
|
775
|
+
// Skip items that already exist in the database (bulk import only)
|
|
776
|
+
if (isImportAll && existingItemIds.has(objectLayerId)) return;
|
|
777
|
+
|
|
234
778
|
console.log(framePath, { objectLayerType, objectLayerId, direction, frame });
|
|
235
779
|
|
|
236
780
|
// On first encounter of an objectLayerId, build its data from the asset directory
|
|
@@ -243,6 +787,18 @@ try {
|
|
|
243
787
|
objectLayerId,
|
|
244
788
|
});
|
|
245
789
|
|
|
790
|
+
// Write processed frames back to disk so WebP matches atlas
|
|
791
|
+
const srcBasePath = './src/client/public/cyberia/';
|
|
792
|
+
const publicBasePath = `./public/${host}${path}`;
|
|
793
|
+
await ObjectLayerEngine.writeStaticFrameAssets({
|
|
794
|
+
basePaths: [srcBasePath, publicBasePath],
|
|
795
|
+
itemType: objectLayerType,
|
|
796
|
+
itemId: objectLayerId,
|
|
797
|
+
objectLayerRenderFramesData,
|
|
798
|
+
objectLayerData,
|
|
799
|
+
cellPixelDim: 20,
|
|
800
|
+
});
|
|
801
|
+
|
|
246
802
|
objectLayers[objectLayerId] = {
|
|
247
803
|
...objectLayerData,
|
|
248
804
|
objectLayerRenderFramesData,
|
|
@@ -261,116 +817,375 @@ try {
|
|
|
261
817
|
const shouldGenerateAtlas = !isImportAll;
|
|
262
818
|
|
|
263
819
|
if (shouldGenerateAtlas) {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
820
|
+
// Check if an ObjectLayer with the same item.id already exists (upsert by item ID)
|
|
821
|
+
const existingOL = await ObjectLayer.findOne({ 'data.item.id': objectLayerId });
|
|
822
|
+
let objectLayer;
|
|
823
|
+
|
|
824
|
+
if (existingOL) {
|
|
825
|
+
// ── Cut-over consistency: stage everything in memory before touching the live document ──
|
|
826
|
+
logger.info(`ObjectLayer '${objectLayerId}' already exists (${existingOL._id}), staging update...`);
|
|
827
|
+
|
|
828
|
+
// 1. Prepare staging data entirely in memory (no DB writes yet)
|
|
829
|
+
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
830
|
+
if (!stagingData.render) stagingData.render = {};
|
|
831
|
+
stagingData.render.cid = '';
|
|
832
|
+
stagingData.render.metadataCid = '';
|
|
833
|
+
|
|
834
|
+
// 2. Generate atlas, pin to IPFS, compute SHA-256 — all in memory
|
|
835
|
+
let cutoverReady = false;
|
|
836
|
+
let stagingFileDoc = null;
|
|
837
|
+
let stagingAtlasDoc = null;
|
|
838
|
+
let stagingCid = '';
|
|
839
|
+
let stagingSha256 = '';
|
|
840
|
+
try {
|
|
841
|
+
const itemKey = objectLayerId;
|
|
276
842
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
843
|
+
// Generate atlas from in-memory render frames data (plain object, no DB doc needed)
|
|
844
|
+
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
845
|
+
entry.objectLayerRenderFramesData,
|
|
846
|
+
itemKey,
|
|
847
|
+
20,
|
|
848
|
+
);
|
|
283
849
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
850
|
+
stagingFileDoc = await new File({
|
|
851
|
+
name: `${itemKey}-atlas.png`,
|
|
852
|
+
data: buffer,
|
|
853
|
+
size: buffer.length,
|
|
854
|
+
mimetype: 'image/png',
|
|
855
|
+
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
856
|
+
}).save();
|
|
289
857
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
858
|
+
let importAtlasCid = '';
|
|
859
|
+
let importAtlasMetadataCid = '';
|
|
860
|
+
try {
|
|
861
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
862
|
+
buffer,
|
|
863
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
864
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
865
|
+
);
|
|
866
|
+
if (ipfsResult) {
|
|
867
|
+
importAtlasCid = ipfsResult.cid;
|
|
868
|
+
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importAtlasCid}`);
|
|
869
|
+
try {
|
|
870
|
+
await createPinRecord({
|
|
871
|
+
cid: importAtlasCid,
|
|
872
|
+
resourceType: 'atlas-sprite-sheet',
|
|
873
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
874
|
+
options: { host, path },
|
|
875
|
+
});
|
|
876
|
+
} catch (prErr) {
|
|
877
|
+
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
} catch (ipfsError) {
|
|
881
|
+
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
882
|
+
}
|
|
297
883
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
884
|
+
try {
|
|
885
|
+
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
886
|
+
metadata,
|
|
887
|
+
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
888
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
889
|
+
);
|
|
890
|
+
if (metadataIpfsResult) {
|
|
891
|
+
importAtlasMetadataCid = metadataIpfsResult.cid;
|
|
892
|
+
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importAtlasMetadataCid}`);
|
|
893
|
+
try {
|
|
894
|
+
await createPinRecord({
|
|
895
|
+
cid: importAtlasMetadataCid,
|
|
896
|
+
resourceType: 'atlas-metadata',
|
|
897
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
898
|
+
options: { host, path },
|
|
899
|
+
});
|
|
900
|
+
} catch (prErr) {
|
|
901
|
+
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
} catch (ipfsError) {
|
|
905
|
+
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
310
906
|
}
|
|
311
|
-
|
|
312
|
-
|
|
907
|
+
|
|
908
|
+
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
909
|
+
if (stagingAtlasDoc) {
|
|
910
|
+
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
911
|
+
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
912
|
+
stagingAtlasDoc.cid = importAtlasCid;
|
|
913
|
+
stagingAtlasDoc.metadata = metadata;
|
|
914
|
+
await stagingAtlasDoc.save();
|
|
915
|
+
} else {
|
|
916
|
+
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
917
|
+
fileId: stagingFileDoc._id,
|
|
918
|
+
cid: importAtlasCid,
|
|
919
|
+
metadata,
|
|
920
|
+
}).save();
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Finalize staging data in memory with render CIDs
|
|
924
|
+
stagingData.render.cid = importAtlasCid;
|
|
925
|
+
stagingData.render.metadataCid = importAtlasMetadataCid;
|
|
926
|
+
|
|
927
|
+
// Pin data JSON to IPFS (compute final SHA-256 in memory)
|
|
928
|
+
stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
929
|
+
try {
|
|
930
|
+
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
931
|
+
stagingData,
|
|
932
|
+
`${itemKey}_data.json`,
|
|
933
|
+
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
934
|
+
);
|
|
935
|
+
if (ipfsDataResult) {
|
|
936
|
+
stagingCid = ipfsDataResult.cid;
|
|
937
|
+
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
938
|
+
try {
|
|
939
|
+
await createPinRecord({
|
|
940
|
+
cid: stagingCid,
|
|
941
|
+
resourceType: 'object-layer-data',
|
|
942
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
943
|
+
options: { host, path },
|
|
944
|
+
});
|
|
945
|
+
} catch (prErr) {
|
|
946
|
+
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch (ipfsError) {
|
|
950
|
+
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
cutoverReady = true;
|
|
954
|
+
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for cut-over`);
|
|
955
|
+
} catch (atlasError) {
|
|
956
|
+
logger.error(`[staging] Failed for ${objectLayerId}, live document untouched:`, atlasError);
|
|
313
957
|
}
|
|
314
958
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
959
|
+
// 3. Atomic cut-over: create new RenderFrames, swap live ObjectLayer in a single update
|
|
960
|
+
if (cutoverReady) {
|
|
961
|
+
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
962
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
963
|
+
|
|
964
|
+
await ObjectLayer.findByIdAndUpdate(existingOL._id, {
|
|
965
|
+
data: stagingData,
|
|
966
|
+
sha256: stagingSha256,
|
|
967
|
+
cid: stagingCid,
|
|
968
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
969
|
+
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
if (oldRenderFramesId) {
|
|
973
|
+
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
325
974
|
}
|
|
326
|
-
|
|
327
|
-
|
|
975
|
+
logger.info(`[cut-over] Live document ${existingOL._id} updated atomically`);
|
|
976
|
+
} else {
|
|
977
|
+
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
978
|
+
logger.warn(`[cut-over] Staging rolled back for ${objectLayerId}, live document preserved`);
|
|
328
979
|
}
|
|
329
980
|
|
|
330
|
-
|
|
981
|
+
objectLayer = await ObjectLayer.findById(existingOL._id);
|
|
982
|
+
} else {
|
|
983
|
+
// ── New item: stage everything before creating (same cut-over pattern) ──
|
|
984
|
+
logger.info(`ObjectLayer '${objectLayerId}' is new, staging creation...`);
|
|
985
|
+
|
|
986
|
+
const itemKey = objectLayerId;
|
|
987
|
+
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
988
|
+
if (!stagingData.render) stagingData.render = {};
|
|
989
|
+
stagingData.render.cid = '';
|
|
990
|
+
stagingData.render.metadataCid = '';
|
|
991
|
+
|
|
992
|
+
let cutoverReady = false;
|
|
993
|
+
let stagingFileDoc = null;
|
|
994
|
+
let stagingAtlasDoc = null;
|
|
995
|
+
let stagingCid = '';
|
|
996
|
+
let stagingSha256 = '';
|
|
997
|
+
try {
|
|
998
|
+
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
999
|
+
entry.objectLayerRenderFramesData,
|
|
1000
|
+
itemKey,
|
|
1001
|
+
20,
|
|
1002
|
+
);
|
|
331
1003
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
} else {
|
|
339
|
-
atlasDoc = await new AtlasSpriteSheet({
|
|
340
|
-
fileId: fileDoc._id,
|
|
341
|
-
cid: importAtlasCid,
|
|
342
|
-
metadata,
|
|
1004
|
+
stagingFileDoc = await new File({
|
|
1005
|
+
name: `${itemKey}-atlas.png`,
|
|
1006
|
+
data: buffer,
|
|
1007
|
+
size: buffer.length,
|
|
1008
|
+
mimetype: 'image/png',
|
|
1009
|
+
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
343
1010
|
}).save();
|
|
344
|
-
logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
345
|
-
}
|
|
346
1011
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
1012
|
+
let importAtlasCid = '';
|
|
1013
|
+
let importAtlasMetadataCid = '';
|
|
1014
|
+
try {
|
|
1015
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
1016
|
+
buffer,
|
|
1017
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
1018
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1019
|
+
);
|
|
1020
|
+
if (ipfsResult) {
|
|
1021
|
+
importAtlasCid = ipfsResult.cid;
|
|
1022
|
+
logger.info(`[staging] Atlas pinned to IPFS – CID: ${importAtlasCid}`);
|
|
1023
|
+
try {
|
|
1024
|
+
await createPinRecord({
|
|
1025
|
+
cid: importAtlasCid,
|
|
1026
|
+
resourceType: 'atlas-sprite-sheet',
|
|
1027
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1028
|
+
options: { host, path },
|
|
1029
|
+
});
|
|
1030
|
+
} catch (prErr) {
|
|
1031
|
+
logger.warn('[staging] Failed to create atlas pin record:', prErr.message);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
} catch (ipfsError) {
|
|
1035
|
+
logger.warn('[staging] Failed to add atlas to IPFS:', ipfsError.message);
|
|
1036
|
+
}
|
|
353
1037
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
1038
|
+
try {
|
|
1039
|
+
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
1040
|
+
metadata,
|
|
1041
|
+
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1042
|
+
`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1043
|
+
);
|
|
1044
|
+
if (metadataIpfsResult) {
|
|
1045
|
+
importAtlasMetadataCid = metadataIpfsResult.cid;
|
|
1046
|
+
logger.info(`[staging] Atlas metadata pinned to IPFS – CID: ${importAtlasMetadataCid}`);
|
|
1047
|
+
try {
|
|
1048
|
+
await createPinRecord({
|
|
1049
|
+
cid: importAtlasMetadataCid,
|
|
1050
|
+
resourceType: 'atlas-metadata',
|
|
1051
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1052
|
+
options: { host, path },
|
|
1053
|
+
});
|
|
1054
|
+
} catch (prErr) {
|
|
1055
|
+
logger.warn('[staging] Failed to create atlas-metadata pin record:', prErr.message);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
} catch (ipfsError) {
|
|
1059
|
+
logger.warn('[staging] Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
stagingAtlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
1063
|
+
if (stagingAtlasDoc) {
|
|
1064
|
+
if (stagingAtlasDoc.fileId) await File.findByIdAndDelete(stagingAtlasDoc.fileId);
|
|
1065
|
+
stagingAtlasDoc.fileId = stagingFileDoc._id;
|
|
1066
|
+
stagingAtlasDoc.cid = importAtlasCid;
|
|
1067
|
+
stagingAtlasDoc.metadata = metadata;
|
|
1068
|
+
await stagingAtlasDoc.save();
|
|
1069
|
+
} else {
|
|
1070
|
+
stagingAtlasDoc = await new AtlasSpriteSheet({
|
|
1071
|
+
fileId: stagingFileDoc._id,
|
|
1072
|
+
cid: importAtlasCid,
|
|
1073
|
+
metadata,
|
|
1074
|
+
}).save();
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
stagingData.render.cid = importAtlasCid;
|
|
1078
|
+
stagingData.render.metadataCid = importAtlasMetadataCid;
|
|
1079
|
+
|
|
1080
|
+
stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
1081
|
+
try {
|
|
1082
|
+
const ipfsDataResult = await IpfsClient.addJsonToIpfs(
|
|
1083
|
+
stagingData,
|
|
1084
|
+
`${itemKey}_data.json`,
|
|
1085
|
+
`/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
1086
|
+
);
|
|
1087
|
+
if (ipfsDataResult) {
|
|
1088
|
+
stagingCid = ipfsDataResult.cid;
|
|
1089
|
+
logger.info(`[staging] Data JSON pinned to IPFS – CID: ${stagingCid}`);
|
|
1090
|
+
try {
|
|
1091
|
+
await createPinRecord({
|
|
1092
|
+
cid: stagingCid,
|
|
1093
|
+
resourceType: 'object-layer-data',
|
|
1094
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
1095
|
+
options: { host, path },
|
|
1096
|
+
});
|
|
1097
|
+
} catch (prErr) {
|
|
1098
|
+
logger.warn('[staging] Failed to create data pin record:', prErr.message);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
} catch (ipfsError) {
|
|
1102
|
+
logger.warn('[staging] Failed to pin data JSON to IPFS:', ipfsError.message);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
cutoverReady = true;
|
|
1106
|
+
logger.info(`[staging] Item '${itemKey}' fully staged in memory, ready for creation`);
|
|
1107
|
+
} catch (atlasError) {
|
|
1108
|
+
logger.error(`[staging] Failed for ${objectLayerId}, no document created:`, atlasError);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
if (cutoverReady) {
|
|
1112
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1113
|
+
objectLayer = await ObjectLayer.create({
|
|
1114
|
+
data: stagingData,
|
|
1115
|
+
sha256: stagingSha256,
|
|
1116
|
+
cid: stagingCid,
|
|
1117
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1118
|
+
atlasSpriteSheetId: stagingAtlasDoc._id,
|
|
1119
|
+
});
|
|
1120
|
+
logger.info(`[cut-over] New ObjectLayer ${objectLayer._id} created with all CIDs populated`);
|
|
1121
|
+
} else {
|
|
1122
|
+
if (stagingFileDoc) await File.findByIdAndDelete(stagingFileDoc._id);
|
|
1123
|
+
logger.warn(`[cut-over] Staging failed for ${objectLayerId}, no ObjectLayer created`);
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
357
1126
|
}
|
|
358
1127
|
|
|
359
|
-
|
|
1128
|
+
// Reload final state to include CID and render updates
|
|
1129
|
+
const finalObjectLayer = await ObjectLayer.findById((objectLayer._id || objectLayer).toString()).populate(
|
|
1130
|
+
'objectLayerRenderFramesId',
|
|
1131
|
+
);
|
|
1132
|
+
console.log(finalObjectLayer.toObject());
|
|
360
1133
|
} else {
|
|
1134
|
+
// --import all: skip items that already exist in the database
|
|
1135
|
+
if (existingItemIds.has(objectLayerId)) continue;
|
|
1136
|
+
|
|
361
1137
|
// --import all: create documents without atlas generation
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
1138
|
+
const existingOL = await ObjectLayer.findOne({ 'data.item.id': objectLayerId });
|
|
1139
|
+
let objectLayer;
|
|
1140
|
+
|
|
1141
|
+
if (existingOL) {
|
|
1142
|
+
logger.info(
|
|
1143
|
+
`ObjectLayer '${objectLayerId}' already exists (${existingOL._id}), staging update (atlas skipped)...`,
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
// ── In-memory staging (no atlas) ──────────────────────
|
|
1147
|
+
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
1148
|
+
if (!stagingData.render) stagingData.render = {};
|
|
1149
|
+
stagingData.render.cid = '';
|
|
1150
|
+
stagingData.render.metadataCid = '';
|
|
1151
|
+
const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
1152
|
+
|
|
1153
|
+
// Atomic cut-over: create new RenderFrames, swap live doc, delete old
|
|
1154
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1155
|
+
const oldRenderFramesId = existingOL.objectLayerRenderFramesId;
|
|
1156
|
+
|
|
1157
|
+
await ObjectLayer.findByIdAndUpdate(existingOL._id, {
|
|
1158
|
+
data: stagingData,
|
|
1159
|
+
sha256: stagingSha256,
|
|
1160
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
if (oldRenderFramesId) {
|
|
1164
|
+
await ObjectLayerRenderFrames.findByIdAndDelete(oldRenderFramesId);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
objectLayer = await ObjectLayer.findById(existingOL._id);
|
|
1168
|
+
logger.info(`[cut-over] Live document ${existingOL._id} updated atomically (atlas skipped)`);
|
|
1169
|
+
} else {
|
|
1170
|
+
// New item: create with sha256 populated (no atlas for bulk import)
|
|
1171
|
+
const stagingData = JSON.parse(JSON.stringify(entry.data));
|
|
1172
|
+
if (!stagingData.render) stagingData.render = {};
|
|
1173
|
+
stagingData.render.cid = '';
|
|
1174
|
+
stagingData.render.metadataCid = '';
|
|
1175
|
+
const stagingSha256 = ObjectLayerEngine.computeSha256(stagingData);
|
|
1176
|
+
|
|
1177
|
+
const newRenderFrames = await ObjectLayerRenderFrames.create(entry.objectLayerRenderFramesData);
|
|
1178
|
+
objectLayer = await ObjectLayer.create({
|
|
1179
|
+
data: stagingData,
|
|
1180
|
+
sha256: stagingSha256,
|
|
1181
|
+
objectLayerRenderFramesId: newRenderFrames._id,
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
371
1184
|
|
|
372
|
-
logger.info(
|
|
373
|
-
|
|
1185
|
+
logger.info(
|
|
1186
|
+
`ObjectLayer ${existingOL ? 'updated' : 'created'} (atlas skipped for bulk import): ${objectLayerId}`,
|
|
1187
|
+
);
|
|
1188
|
+
console.log(objectLayer.toObject ? objectLayer.toObject() : objectLayer);
|
|
374
1189
|
}
|
|
375
1190
|
}
|
|
376
1191
|
}
|
|
@@ -527,6 +1342,15 @@ try {
|
|
|
527
1342
|
if (ipfsResult) {
|
|
528
1343
|
toAtlasCid = ipfsResult.cid;
|
|
529
1344
|
logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${toAtlasCid}`);
|
|
1345
|
+
try {
|
|
1346
|
+
await createPinRecord({
|
|
1347
|
+
cid: toAtlasCid,
|
|
1348
|
+
resourceType: 'atlas-sprite-sheet',
|
|
1349
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1350
|
+
});
|
|
1351
|
+
} catch (e) {
|
|
1352
|
+
logger.warn('Failed to create pin record for atlas sprite sheet:', e.message);
|
|
1353
|
+
}
|
|
530
1354
|
}
|
|
531
1355
|
} catch (ipfsError) {
|
|
532
1356
|
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
@@ -542,6 +1366,15 @@ try {
|
|
|
542
1366
|
if (metadataIpfsResult) {
|
|
543
1367
|
toAtlasMetadataCid = metadataIpfsResult.cid;
|
|
544
1368
|
logger.info(`Atlas metadata pinned to IPFS – CID: ${toAtlasMetadataCid}`);
|
|
1369
|
+
try {
|
|
1370
|
+
await createPinRecord({
|
|
1371
|
+
cid: toAtlasMetadataCid,
|
|
1372
|
+
resourceType: 'atlas-metadata',
|
|
1373
|
+
mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1374
|
+
});
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
logger.warn('Failed to create pin record for atlas metadata:', e.message);
|
|
1377
|
+
}
|
|
545
1378
|
}
|
|
546
1379
|
} catch (ipfsError) {
|
|
547
1380
|
logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
@@ -551,7 +1384,8 @@ try {
|
|
|
551
1384
|
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
|
|
552
1385
|
|
|
553
1386
|
if (atlasDoc) {
|
|
554
|
-
// Update existing
|
|
1387
|
+
// Update existing – remove old File to prevent orphans
|
|
1388
|
+
if (atlasDoc.fileId) await File.findByIdAndDelete(atlasDoc.fileId);
|
|
555
1389
|
atlasDoc.fileId = fileDoc._id;
|
|
556
1390
|
atlasDoc.cid = toAtlasCid;
|
|
557
1391
|
atlasDoc.metadata = metadata;
|
|
@@ -575,6 +1409,13 @@ try {
|
|
|
575
1409
|
objectLayer.markModified('data.render');
|
|
576
1410
|
await objectLayer.save();
|
|
577
1411
|
|
|
1412
|
+
// Compute final SHA-256 and pin object layer data JSON to IPFS
|
|
1413
|
+
await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
1414
|
+
objectLayer,
|
|
1415
|
+
ipfsClient: IpfsClient,
|
|
1416
|
+
createPinRecord,
|
|
1417
|
+
});
|
|
1418
|
+
|
|
578
1419
|
logger.info(`Atlas sprite sheet completed for item: ${itemKey}`);
|
|
579
1420
|
}
|
|
580
1421
|
|
|
@@ -674,169 +1515,1704 @@ try {
|
|
|
674
1515
|
density: genDensity,
|
|
675
1516
|
});
|
|
676
1517
|
|
|
677
|
-
// Overwrite the item id in the generated data with the unique variant
|
|
678
|
-
multiFrameResult.objectLayerData.data.item.id = uniqueItemId;
|
|
1518
|
+
// Overwrite the item id in the generated data with the unique variant
|
|
1519
|
+
multiFrameResult.objectLayerData.data.item.id = uniqueItemId;
|
|
1520
|
+
|
|
1521
|
+
logger.info(
|
|
1522
|
+
`Generated ${multiFrameResult.frameCount} frame(s) with ${multiFrameResult.objectLayerRenderFramesData.colors.length} unique colors`,
|
|
1523
|
+
);
|
|
1524
|
+
|
|
1525
|
+
// 2. Write static asset PNGs to both source and public directories
|
|
1526
|
+
const srcBasePath = './src/client/public/cyberia/';
|
|
1527
|
+
const publicBasePath = `./public/${host}${path}`;
|
|
1528
|
+
const writtenFiles = await ObjectLayerEngine.writeStaticFrameAssets({
|
|
1529
|
+
basePaths: [srcBasePath, publicBasePath],
|
|
1530
|
+
itemType: descriptor.itemType,
|
|
1531
|
+
itemId: uniqueItemId,
|
|
1532
|
+
objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
|
|
1533
|
+
objectLayerData: multiFrameResult.objectLayerData,
|
|
1534
|
+
cellPixelDim: 20,
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
logger.info(`Wrote ${writtenFiles.length} asset file(s):`);
|
|
1538
|
+
for (const f of writtenFiles) {
|
|
1539
|
+
logger.info(` → ${f}`);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// 3. Persist to MongoDB (ObjectLayerRenderFrames + ObjectLayer)
|
|
1543
|
+
const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
|
|
1544
|
+
ObjectLayer,
|
|
1545
|
+
ObjectLayerRenderFrames,
|
|
1546
|
+
objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
|
|
1547
|
+
objectLayerData: multiFrameResult.objectLayerData,
|
|
1548
|
+
createOptions: {
|
|
1549
|
+
generateAtlas: false,
|
|
1550
|
+
},
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
logger.info(`ObjectLayer persisted to MongoDB: ${objectLayer._id} (item: ${objectLayer.data.item.id})`);
|
|
1554
|
+
|
|
1555
|
+
// 4. Generate atlas sprite sheet + pin to IPFS
|
|
1556
|
+
let atlasCid = '';
|
|
1557
|
+
try {
|
|
1558
|
+
const atlasItemKey = objectLayer.data.item.id;
|
|
1559
|
+
const populatedObjectLayer = await ObjectLayer.findById(objectLayer._id).populate(
|
|
1560
|
+
'objectLayerRenderFramesId',
|
|
1561
|
+
);
|
|
1562
|
+
|
|
1563
|
+
const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
|
|
1564
|
+
populatedObjectLayer.objectLayerRenderFramesId,
|
|
1565
|
+
atlasItemKey,
|
|
1566
|
+
20,
|
|
1567
|
+
);
|
|
1568
|
+
|
|
1569
|
+
// Save atlas file to File collection
|
|
1570
|
+
const fileDoc = await new File({
|
|
1571
|
+
name: `${atlasItemKey}-atlas.png`,
|
|
1572
|
+
data: buffer,
|
|
1573
|
+
size: buffer.length,
|
|
1574
|
+
mimetype: 'image/png',
|
|
1575
|
+
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
1576
|
+
}).save();
|
|
1577
|
+
|
|
1578
|
+
// Pin atlas PNG to IPFS + copy into MFS
|
|
1579
|
+
let atlasMetadataCid = '';
|
|
1580
|
+
try {
|
|
1581
|
+
const ipfsResult = await IpfsClient.addBufferToIpfs(
|
|
1582
|
+
buffer,
|
|
1583
|
+
`${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1584
|
+
`/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1585
|
+
);
|
|
1586
|
+
if (ipfsResult) {
|
|
1587
|
+
atlasCid = ipfsResult.cid;
|
|
1588
|
+
logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
|
|
1589
|
+
try {
|
|
1590
|
+
await createPinRecord({
|
|
1591
|
+
cid: atlasCid,
|
|
1592
|
+
resourceType: 'atlas-sprite-sheet',
|
|
1593
|
+
mfsPath: `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
|
|
1594
|
+
options: { host, path },
|
|
1595
|
+
});
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
logger.warn('Failed to create pin record for atlas sprite sheet:', e.message);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
} catch (ipfsError) {
|
|
1601
|
+
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
// Pin atlas metadata JSON to IPFS (fast-json-stable-stringify)
|
|
1605
|
+
try {
|
|
1606
|
+
const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
|
|
1607
|
+
metadata,
|
|
1608
|
+
`${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1609
|
+
`/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1610
|
+
);
|
|
1611
|
+
if (metadataIpfsResult) {
|
|
1612
|
+
atlasMetadataCid = metadataIpfsResult.cid;
|
|
1613
|
+
logger.info(`Atlas metadata pinned to IPFS – CID: ${atlasMetadataCid}`);
|
|
1614
|
+
try {
|
|
1615
|
+
await createPinRecord({
|
|
1616
|
+
cid: atlasMetadataCid,
|
|
1617
|
+
resourceType: 'atlas-metadata',
|
|
1618
|
+
mfsPath: `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1619
|
+
options: { host, path },
|
|
1620
|
+
});
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
logger.warn('Failed to create pin record for atlas metadata:', e.message);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
} catch (ipfsError) {
|
|
1626
|
+
logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// Upsert AtlasSpriteSheet document (with CID)
|
|
1630
|
+
let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': atlasItemKey });
|
|
1631
|
+
if (atlasDoc) {
|
|
1632
|
+
if (atlasDoc.fileId) await File.findByIdAndDelete(atlasDoc.fileId);
|
|
1633
|
+
atlasDoc.fileId = fileDoc._id;
|
|
1634
|
+
atlasDoc.cid = atlasCid;
|
|
1635
|
+
atlasDoc.metadata = metadata;
|
|
1636
|
+
await atlasDoc.save();
|
|
1637
|
+
logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1638
|
+
} else {
|
|
1639
|
+
atlasDoc = await new AtlasSpriteSheet({
|
|
1640
|
+
fileId: fileDoc._id,
|
|
1641
|
+
cid: atlasCid,
|
|
1642
|
+
metadata,
|
|
1643
|
+
}).save();
|
|
1644
|
+
logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
// Link atlas to ObjectLayer and set data.render.cid + data.render.metadataCid
|
|
1648
|
+
populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
|
|
1649
|
+
if (!populatedObjectLayer.data.render) populatedObjectLayer.data.render = {};
|
|
1650
|
+
populatedObjectLayer.data.render.cid = atlasCid;
|
|
1651
|
+
populatedObjectLayer.data.render.metadataCid = atlasMetadataCid;
|
|
1652
|
+
populatedObjectLayer.markModified('data.render');
|
|
1653
|
+
await populatedObjectLayer.save();
|
|
1654
|
+
|
|
1655
|
+
// Also write atlas PNG to both static asset directories
|
|
1656
|
+
for (const bp of [srcBasePath, publicBasePath]) {
|
|
1657
|
+
const atlasOutputDir = nodePath.join(bp, 'assets', descriptor.itemType, uniqueItemId);
|
|
1658
|
+
await fs.ensureDir(atlasOutputDir);
|
|
1659
|
+
const atlasOutputPath = nodePath.join(atlasOutputDir, `${atlasItemKey}-atlas.png`);
|
|
1660
|
+
await fs.writeFile(atlasOutputPath, buffer);
|
|
1661
|
+
logger.info(
|
|
1662
|
+
`Atlas sprite sheet generated: ${metadata.atlasWidth}x${metadata.atlasHeight} → ${atlasOutputPath}`,
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
} catch (atlasError) {
|
|
1666
|
+
logger.error(`Failed to generate atlas for ${uniqueItemId}:`, atlasError);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// 5. Compute final SHA-256, pin OL data JSON to IPFS, create pin records
|
|
1670
|
+
try {
|
|
1671
|
+
const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
|
|
1672
|
+
const finalized = await ObjectLayerEngine.computeAndSaveFinalSha256({
|
|
1673
|
+
objectLayer: finalObjectLayer,
|
|
1674
|
+
ipfsClient: IpfsClient,
|
|
1675
|
+
createPinRecord,
|
|
1676
|
+
options: { host, path },
|
|
1677
|
+
});
|
|
1678
|
+
logger.info(`Final SHA-256: ${finalized.sha256}`);
|
|
1679
|
+
if (finalized.cid) {
|
|
1680
|
+
logger.info(`ObjectLayer data pinned to IPFS – CID: ${finalized.cid}`);
|
|
1681
|
+
}
|
|
1682
|
+
} catch (finalizeError) {
|
|
1683
|
+
logger.error('Failed to finalize SHA-256 / IPFS:', finalizeError);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
logger.info(`✓ Generation complete for "${uniqueItemId}" (seed: ${genSeed}, frames: ${genFrameCount})`);
|
|
1687
|
+
|
|
1688
|
+
// Log per-layer summary
|
|
1689
|
+
if (multiFrameResult.frames.length > 0) {
|
|
1690
|
+
const firstFrame = multiFrameResult.frames[0];
|
|
1691
|
+
for (const layer of firstFrame.layers) {
|
|
1692
|
+
logger.info(` Layer "${layer.layerKey}" (${layer.layerId}): ${layer.keys.length} element(s)`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1698
|
+
},
|
|
1699
|
+
)
|
|
1700
|
+
.description('Object layer management');
|
|
1701
|
+
|
|
1702
|
+
// ── instance: Cyberia instance backup / restore ─────────────────────────
|
|
1703
|
+
program
|
|
1704
|
+
.command('instance [instance-code]')
|
|
1705
|
+
.option('--export [path]', 'Export instance and related documents to a backup directory')
|
|
1706
|
+
.option('--import [path]', 'Import instance and related documents from a backup directory (preserveUUID, upsert)')
|
|
1707
|
+
.option(
|
|
1708
|
+
'--conf',
|
|
1709
|
+
'When used with --export or --import, only process cyberia-instance.json and cyberia-instance-conf.json',
|
|
1710
|
+
)
|
|
1711
|
+
.option('--drop', 'Drop all documents associated with the instance code before importing or as a standalone action')
|
|
1712
|
+
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
1713
|
+
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
1714
|
+
.option('--dev', 'Force development environment')
|
|
1715
|
+
.description('Export/import a Cyberia instance with all related maps, entities and object layers')
|
|
1716
|
+
.action(async (instanceCode, options = {}) => {
|
|
1717
|
+
if (!instanceCode) {
|
|
1718
|
+
logger.error('instance-code argument is required');
|
|
1719
|
+
process.exit(1);
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
if (!options.envPath) options.envPath = `./.env`;
|
|
1723
|
+
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
1724
|
+
|
|
1725
|
+
if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
|
|
1726
|
+
const deployDevEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
|
|
1727
|
+
if (fs.existsSync(deployDevEnvPath)) {
|
|
1728
|
+
dotenv.config({ path: deployDevEnvPath, override: true });
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const deployId = process.env.DEFAULT_DEPLOY_ID;
|
|
1733
|
+
const host = process.env.DEFAULT_DEPLOY_HOST;
|
|
1734
|
+
const path = process.env.DEFAULT_DEPLOY_PATH;
|
|
1735
|
+
|
|
1736
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
1737
|
+
if (!fs.existsSync(confServerPath)) {
|
|
1738
|
+
logger.error(`Server config not found: ${confServerPath}`);
|
|
1739
|
+
process.exit(1);
|
|
1740
|
+
}
|
|
1741
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
1742
|
+
const { db } = confServer[host][path];
|
|
1743
|
+
|
|
1744
|
+
db.host = options.mongoHost
|
|
1745
|
+
? options.mongoHost
|
|
1746
|
+
: options.dev
|
|
1747
|
+
? db.host
|
|
1748
|
+
: db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
|
|
1749
|
+
|
|
1750
|
+
logger.info('instance env', { env: options.envPath, deployId, host, path, db });
|
|
1751
|
+
|
|
1752
|
+
await DataBaseProvider.load({
|
|
1753
|
+
apis: [
|
|
1754
|
+
'cyberia-instance',
|
|
1755
|
+
'cyberia-instance-conf',
|
|
1756
|
+
'cyberia-dialogue',
|
|
1757
|
+
'cyberia-map',
|
|
1758
|
+
'cyberia-entity',
|
|
1759
|
+
'object-layer',
|
|
1760
|
+
'object-layer-render-frames',
|
|
1761
|
+
'atlas-sprite-sheet',
|
|
1762
|
+
'file',
|
|
1763
|
+
'ipfs',
|
|
1764
|
+
],
|
|
1765
|
+
host,
|
|
1766
|
+
path,
|
|
1767
|
+
db,
|
|
1768
|
+
});
|
|
1769
|
+
|
|
1770
|
+
const dbModels = DataBaseProvider.instance[`${host}${path}`].mongoose.models;
|
|
1771
|
+
const CyberiaInstance = dbModels.CyberiaInstance;
|
|
1772
|
+
const CyberiaInstanceConf = dbModels.CyberiaInstanceConf;
|
|
1773
|
+
const CyberiaDialogue = dbModels.CyberiaDialogue;
|
|
1774
|
+
const CyberiaMap = dbModels.CyberiaMap;
|
|
1775
|
+
const ObjectLayer = dbModels.ObjectLayer;
|
|
1776
|
+
const ObjectLayerRenderFrames = dbModels.ObjectLayerRenderFrames;
|
|
1777
|
+
const AtlasSpriteSheet = dbModels.AtlasSpriteSheet;
|
|
1778
|
+
const File = dbModels.File;
|
|
1779
|
+
const Ipfs = dbModels.Ipfs;
|
|
1780
|
+
|
|
1781
|
+
const toBuffer = (value) => {
|
|
1782
|
+
if (!value) return null;
|
|
1783
|
+
if (Buffer.isBuffer(value)) return value;
|
|
1784
|
+
if (value.type === 'Buffer' && Array.isArray(value.data)) return Buffer.from(value.data);
|
|
1785
|
+
if (value.buffer) return Buffer.from(value.buffer);
|
|
1786
|
+
return Buffer.from(value);
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const getCanonicalIpfsPaths = (itemKey) => ({
|
|
1790
|
+
objectLayerData: `/object-layer/${itemKey}/${itemKey}_data.json`,
|
|
1791
|
+
atlasSpriteSheet: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
|
|
1792
|
+
atlasMetadata: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
const collectMfsPaths = (doc = {}) => {
|
|
1796
|
+
const paths = new Set();
|
|
1797
|
+
if (doc.mfsPath) paths.add(doc.mfsPath);
|
|
1798
|
+
for (const p of doc.mfsPaths || []) {
|
|
1799
|
+
if (p) paths.add(p);
|
|
1800
|
+
}
|
|
1801
|
+
return [...paths];
|
|
1802
|
+
};
|
|
1803
|
+
|
|
1804
|
+
const inferResourceType = (doc = {}) => {
|
|
1805
|
+
if (doc.resourceType) return doc.resourceType;
|
|
1806
|
+
for (const path of collectMfsPaths(doc)) {
|
|
1807
|
+
if (path.endsWith('_atlas_sprite_sheet.png')) return 'atlas-sprite-sheet';
|
|
1808
|
+
if (path.endsWith('_atlas_sprite_sheet_metadata.json')) return 'atlas-metadata';
|
|
1809
|
+
if (path.endsWith('_data.json')) return 'object-layer-data';
|
|
1810
|
+
}
|
|
1811
|
+
return null;
|
|
1812
|
+
};
|
|
1813
|
+
|
|
1814
|
+
const findInstanceRelatedIpfsDoc = (ipfsDocs, { linkedCid, resourceType, mfsPath }) =>
|
|
1815
|
+
ipfsDocs.find(
|
|
1816
|
+
(doc) =>
|
|
1817
|
+
inferResourceType(doc) === resourceType &&
|
|
1818
|
+
linkedCid &&
|
|
1819
|
+
doc.cid === linkedCid &&
|
|
1820
|
+
collectMfsPaths(doc).includes(mfsPath),
|
|
1821
|
+
) ||
|
|
1822
|
+
ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && linkedCid && doc.cid === linkedCid) ||
|
|
1823
|
+
ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && collectMfsPaths(doc).includes(mfsPath)) ||
|
|
1824
|
+
null;
|
|
1825
|
+
|
|
1826
|
+
const upsertCanonicalPinEntry = (pinMap, { cid, resourceType, mfsPath = '' }) => {
|
|
1827
|
+
if (!cid || !resourceType) return;
|
|
1828
|
+
const key = `${resourceType}:${cid}`;
|
|
1829
|
+
const nextPath = mfsPath || '';
|
|
1830
|
+
if (!pinMap.has(key)) {
|
|
1831
|
+
pinMap.set(key, {
|
|
1832
|
+
cid,
|
|
1833
|
+
resourceType,
|
|
1834
|
+
mfsPath: nextPath,
|
|
1835
|
+
mfsPaths: nextPath ? [nextPath] : [],
|
|
1836
|
+
});
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const existing = pinMap.get(key);
|
|
1841
|
+
if (nextPath && !existing.mfsPaths.includes(nextPath)) {
|
|
1842
|
+
existing.mfsPaths.push(nextPath);
|
|
1843
|
+
}
|
|
1844
|
+
if (!existing.mfsPath && nextPath) {
|
|
1845
|
+
existing.mfsPath = nextPath;
|
|
1846
|
+
}
|
|
1847
|
+
};
|
|
1848
|
+
|
|
1849
|
+
const serialiseCanonicalPins = (pinMap) =>
|
|
1850
|
+
[...pinMap.values()].map((entry) => ({
|
|
1851
|
+
cid: entry.cid,
|
|
1852
|
+
resourceType: entry.resourceType,
|
|
1853
|
+
...(entry.mfsPath ? { mfsPath: entry.mfsPath } : {}),
|
|
1854
|
+
...(entry.mfsPaths.length ? { mfsPaths: entry.mfsPaths } : {}),
|
|
1855
|
+
}));
|
|
1856
|
+
|
|
1857
|
+
const getDefaultDialoguesByItemId = (itemIds = []) => {
|
|
1858
|
+
const requestedItemIds = new Set(itemIds.filter(Boolean));
|
|
1859
|
+
const defaultsByItemId = new Map();
|
|
1860
|
+
|
|
1861
|
+
for (const dialogue of DefaultCyberiaDialogues) {
|
|
1862
|
+
// Match by code prefix: "default-<itemId>" covers the common case;
|
|
1863
|
+
// callers may also pass a full code directly.
|
|
1864
|
+
const matchingIds = [...requestedItemIds].filter(
|
|
1865
|
+
(id) => dialogue.code === `default-${id}` || dialogue.code === id,
|
|
1866
|
+
);
|
|
1867
|
+
if (!matchingIds.length) continue;
|
|
1868
|
+
for (const id of matchingIds) {
|
|
1869
|
+
if (!defaultsByItemId.has(id)) defaultsByItemId.set(id, []);
|
|
1870
|
+
defaultsByItemId.get(id).push({
|
|
1871
|
+
code: dialogue.code,
|
|
1872
|
+
order: dialogue.order ?? 0,
|
|
1873
|
+
speaker: dialogue.speaker ?? '',
|
|
1874
|
+
text: dialogue.text,
|
|
1875
|
+
mood: dialogue.mood ?? 'neutral',
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
for (const dialogues of defaultsByItemId.values()) {
|
|
1881
|
+
dialogues.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
return defaultsByItemId;
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1887
|
+
const rewriteImportedCidReferences = async ({ oldCid, newCid, resourceType }) => {
|
|
1888
|
+
if (!oldCid || !newCid || oldCid === newCid) return;
|
|
1889
|
+
|
|
1890
|
+
if (resourceType === 'object-layer-data') {
|
|
1891
|
+
await ObjectLayer.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
if (resourceType === 'atlas-sprite-sheet') {
|
|
1896
|
+
await AtlasSpriteSheet.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
|
|
1897
|
+
await ObjectLayer.updateMany({ 'data.render.cid': oldCid }, { $set: { 'data.render.cid': newCid } });
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (resourceType === 'atlas-metadata') {
|
|
1902
|
+
await ObjectLayer.updateMany(
|
|
1903
|
+
{ 'data.render.metadataCid': oldCid },
|
|
1904
|
+
{ $set: { 'data.render.metadataCid': newCid } },
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
// ── EXPORT ──────────────────────────────────────────────────────
|
|
1910
|
+
if (options.export !== undefined) {
|
|
1911
|
+
const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
1912
|
+
if (!instance) {
|
|
1913
|
+
logger.error(`CyberiaInstance with code "${instanceCode}" not found`);
|
|
1914
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1915
|
+
process.exit(1);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
const backupDir =
|
|
1919
|
+
typeof options.export === 'string' && options.export
|
|
1920
|
+
? options.export
|
|
1921
|
+
: `./engine-private/cyberia-instances/${instanceCode}`;
|
|
1922
|
+
|
|
1923
|
+
fs.ensureDirSync(backupDir);
|
|
1924
|
+
logger.info('Exporting instance', { code: instanceCode, backupDir });
|
|
1925
|
+
|
|
1926
|
+
// Helper: export a File document to the files/ directory
|
|
1927
|
+
const exportFileDoc = async (fileId, fileKey) => {
|
|
1928
|
+
if (!fileId) return;
|
|
1929
|
+
const file = await File.findById(fileId).lean();
|
|
1930
|
+
if (!file) return;
|
|
1931
|
+
fs.ensureDirSync(`${backupDir}/files`);
|
|
1932
|
+
const fileExport = { ...file };
|
|
1933
|
+
// Handle both Node.js Buffer and BSON Binary types from .lean()
|
|
1934
|
+
if (fileExport.data) {
|
|
1935
|
+
const buf = Buffer.isBuffer(fileExport.data)
|
|
1936
|
+
? fileExport.data
|
|
1937
|
+
: Buffer.from(fileExport.data.buffer || fileExport.data);
|
|
1938
|
+
fileExport.data = { $base64: buf.toString('base64') };
|
|
1939
|
+
}
|
|
1940
|
+
fs.writeJsonSync(`${backupDir}/files/${fileKey}.json`, fileExport, { spaces: 2 });
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
// 1. Save instance document + thumbnail
|
|
1944
|
+
fs.writeJsonSync(`${backupDir}/cyberia-instance.json`, instance, { spaces: 2 });
|
|
1945
|
+
if (!options.conf && instance.thumbnail) {
|
|
1946
|
+
await exportFileDoc(instance.thumbnail, `thumb-instance-${instanceCode}`);
|
|
1947
|
+
}
|
|
1948
|
+
logger.info('Exported CyberiaInstance', { code: instanceCode });
|
|
1949
|
+
|
|
1950
|
+
// 1b. Export linked CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
|
|
1951
|
+
// If no conf doc exists yet (instance created before auto-upsert logic), create one using
|
|
1952
|
+
// schema defaults — identical to the behaviour in CyberiaInstanceService.post().
|
|
1953
|
+
let instanceConf =
|
|
1954
|
+
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
1955
|
+
(instance.conf ? await CyberiaInstanceConf.findById(instance.conf).lean() : null);
|
|
1956
|
+
if (!instanceConf) {
|
|
1957
|
+
logger.info('No CyberiaInstanceConf found — creating default', { instanceCode });
|
|
1958
|
+
const created = await CyberiaInstanceConf.findOneAndUpdate(
|
|
1959
|
+
{ instanceCode },
|
|
1960
|
+
{ $setOnInsert: { instanceCode } },
|
|
1961
|
+
{ upsert: true, returnDocument: 'after' },
|
|
1962
|
+
);
|
|
1963
|
+
// Back-fill the instance.conf ref if it was missing
|
|
1964
|
+
if (created && !instance.conf) {
|
|
1965
|
+
await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: created._id });
|
|
1966
|
+
}
|
|
1967
|
+
instanceConf = created?.toObject ? created.toObject() : created;
|
|
1968
|
+
}
|
|
1969
|
+
if (instanceConf) {
|
|
1970
|
+
fs.writeJsonSync(`${backupDir}/cyberia-instance-conf.json`, instanceConf, { spaces: 2 });
|
|
1971
|
+
logger.info('Exported CyberiaInstanceConf', { instanceCode });
|
|
1972
|
+
} else {
|
|
1973
|
+
logger.warn('Could not create or find CyberiaInstanceConf', { instanceCode });
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (options.conf) {
|
|
1977
|
+
logger.info('Instance export completed in --conf mode', {
|
|
1978
|
+
backupDir,
|
|
1979
|
+
exportedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
|
|
1980
|
+
});
|
|
1981
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
// 2. Collect all map codes (instance maps + portal targets)
|
|
1986
|
+
const mapCodes = new Set(instance.cyberiaMapCodes || []);
|
|
1987
|
+
for (const portal of instance.portals || []) {
|
|
1988
|
+
if (portal.sourceMapCode) mapCodes.add(portal.sourceMapCode);
|
|
1989
|
+
if (portal.targetMapCode) mapCodes.add(portal.targetMapCode);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// 3. Export maps + thumbnails
|
|
1993
|
+
const maps = await CyberiaMap.find({ code: { $in: [...mapCodes] } }).lean();
|
|
1994
|
+
fs.ensureDirSync(`${backupDir}/maps`);
|
|
1995
|
+
for (const map of maps) {
|
|
1996
|
+
fs.writeJsonSync(`${backupDir}/maps/${map.code}.json`, map, { spaces: 2 });
|
|
1997
|
+
if (map.thumbnail) {
|
|
1998
|
+
await exportFileDoc(map.thumbnail, `thumb-map-${map.code}`);
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
logger.info(`Exported ${maps.length} CyberiaMap document(s)`, { codes: maps.map((m) => m.code) });
|
|
2002
|
+
|
|
2003
|
+
// 4. Collect all objectLayerItemIds from map entities
|
|
2004
|
+
const objectLayerItemIds = new Set();
|
|
2005
|
+
for (const map of maps) {
|
|
2006
|
+
for (const entity of map.entities || []) {
|
|
2007
|
+
for (const itemId of entity.objectLayerItemIds || []) {
|
|
2008
|
+
objectLayerItemIds.add(itemId);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// 4b. Add instance-level itemIds
|
|
2014
|
+
for (const id of instance.itemIds || []) {
|
|
2015
|
+
if (id) objectLayerItemIds.add(id);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// 4c. Add all itemIds referenced by CyberiaInstanceConf (entityDefaults + skillConfig).
|
|
2019
|
+
// This ensures liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers and
|
|
2020
|
+
// skill trigger items are included even if no map entity currently uses them.
|
|
2021
|
+
if (instanceConf) {
|
|
2022
|
+
for (const ed of instanceConf.entityDefaults || []) {
|
|
2023
|
+
for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2024
|
+
for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2025
|
+
for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
|
|
2026
|
+
for (const slot of ed.defaultObjectLayers || []) {
|
|
2027
|
+
if (slot.itemId) objectLayerItemIds.add(slot.itemId);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
for (const sc of instanceConf.skillConfig || []) {
|
|
2031
|
+
if (sc.triggerItemId) objectLayerItemIds.add(sc.triggerItemId);
|
|
2032
|
+
for (const skill of sc.skills || []) {
|
|
2033
|
+
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$')) {
|
|
2034
|
+
objectLayerItemIds.add(skill.summonedEntityItemId);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// 4d. Export dialogues for all relevant object-layer items. Codes follow the pattern
|
|
2041
|
+
// "default-<itemId>". If an item has no dialogue docs yet but ships with
|
|
2042
|
+
// DefaultCyberiaDialogues, seed those defaults into Mongo first.
|
|
2043
|
+
if (objectLayerItemIds.size > 0) {
|
|
2044
|
+
const requestedItemIds = [...objectLayerItemIds];
|
|
2045
|
+
const requestedCodes = requestedItemIds.map((id) => `default-${id}`);
|
|
2046
|
+
const defaultDialoguesByItemId = getDefaultDialoguesByItemId(requestedItemIds);
|
|
2047
|
+
const existingDialogueDocs = await CyberiaDialogue.find({
|
|
2048
|
+
code: { $in: requestedCodes },
|
|
2049
|
+
})
|
|
2050
|
+
.sort({ code: 1, order: 1 })
|
|
2051
|
+
.lean();
|
|
2052
|
+
|
|
2053
|
+
const existingDialogueCodes = new Set(existingDialogueDocs.map((dialogue) => dialogue.code).filter(Boolean));
|
|
2054
|
+
let seededDialogueCount = 0;
|
|
2055
|
+
|
|
2056
|
+
for (const [itemId, dialogues] of defaultDialoguesByItemId.entries()) {
|
|
2057
|
+
const firstCode = dialogues[0]?.code;
|
|
2058
|
+
if (firstCode && existingDialogueCodes.has(firstCode)) continue;
|
|
2059
|
+
|
|
2060
|
+
for (const dialogue of dialogues) {
|
|
2061
|
+
await CyberiaDialogue.findOneAndUpdate(
|
|
2062
|
+
{ code: dialogue.code, order: dialogue.order },
|
|
2063
|
+
{
|
|
2064
|
+
$set: {
|
|
2065
|
+
speaker: dialogue.speaker,
|
|
2066
|
+
text: dialogue.text,
|
|
2067
|
+
mood: dialogue.mood,
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
{ upsert: true },
|
|
2071
|
+
);
|
|
2072
|
+
seededDialogueCount++;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
const dialogueDocs = await CyberiaDialogue.find({ code: { $in: requestedCodes } })
|
|
2077
|
+
.sort({ code: 1, order: 1 })
|
|
2078
|
+
.lean();
|
|
2079
|
+
|
|
2080
|
+
if (seededDialogueCount > 0) {
|
|
2081
|
+
logger.info(`Seeded ${seededDialogueCount} CyberiaDialogue default record(s) for export`);
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
if (dialogueDocs.length > 0) {
|
|
2085
|
+
fs.ensureDirSync(`${backupDir}/cyberia-dialogues`);
|
|
2086
|
+
const dialoguesByCode = new Map();
|
|
2087
|
+
|
|
2088
|
+
for (const dialogue of dialogueDocs) {
|
|
2089
|
+
if (!dialoguesByCode.has(dialogue.code)) {
|
|
2090
|
+
dialoguesByCode.set(dialogue.code, []);
|
|
2091
|
+
}
|
|
2092
|
+
dialoguesByCode.get(dialogue.code).push(dialogue);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
for (const [code, dialogues] of dialoguesByCode.entries()) {
|
|
2096
|
+
fs.writeJsonSync(`${backupDir}/cyberia-dialogues/${encodeURIComponent(code)}.json`, dialogues, {
|
|
2097
|
+
spaces: 2,
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
logger.info(`Exported ${dialogueDocs.length} CyberiaDialogue document(s)`, {
|
|
2102
|
+
codes: [...dialoguesByCode.keys()],
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// 5. Export object layers with related render frames, atlas, files, and IPFS records
|
|
2108
|
+
if (objectLayerItemIds.size > 0) {
|
|
2109
|
+
const objectLayers = await ObjectLayer.find({
|
|
2110
|
+
'data.item.id': { $in: [...objectLayerItemIds] },
|
|
2111
|
+
}).lean();
|
|
2112
|
+
|
|
2113
|
+
fs.ensureDirSync(`${backupDir}/object-layers`);
|
|
2114
|
+
fs.ensureDirSync(`${backupDir}/render-frames`);
|
|
2115
|
+
fs.ensureDirSync(`${backupDir}/atlas-sprite-sheets`);
|
|
2116
|
+
fs.ensureDirSync(`${backupDir}/ipfs`);
|
|
2117
|
+
fs.ensureDirSync(`${backupDir}/ipfs/content`);
|
|
2118
|
+
|
|
2119
|
+
const canonicalPins = new Map();
|
|
2120
|
+
const expectedObjectLayerIpfsRefs = [];
|
|
2121
|
+
const ipfsPayloadFailures = [];
|
|
2122
|
+
let ipfsPayloadExportCount = 0;
|
|
2123
|
+
let ipfsPayloadAliasCount = 0;
|
|
2124
|
+
|
|
2125
|
+
const writeBackupPayload = (cid, payloadBuffer) => {
|
|
2126
|
+
if (!cid) return false;
|
|
2127
|
+
const payloadPath = `${backupDir}/ipfs/content/${cid}.bin`;
|
|
2128
|
+
if (fs.existsSync(payloadPath)) return false;
|
|
2129
|
+
fs.writeFileSync(payloadPath, payloadBuffer);
|
|
2130
|
+
ipfsPayloadExportCount++;
|
|
2131
|
+
return true;
|
|
2132
|
+
};
|
|
2133
|
+
|
|
2134
|
+
const writeBackupPayloadAlias = ({ canonicalCid, linkedCid, payloadBuffer }) => {
|
|
2135
|
+
if (!linkedCid || linkedCid === canonicalCid) return;
|
|
2136
|
+
if (writeBackupPayload(linkedCid, payloadBuffer)) {
|
|
2137
|
+
ipfsPayloadAliasCount++;
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
|
|
2141
|
+
const exportCanonicalPayload = async ({ payloadBuffer, resourceType, mfsPath, filename, itemKey }) => {
|
|
2142
|
+
const hashResult = await IpfsClient.hashBufferForIpfs(payloadBuffer, filename);
|
|
2143
|
+
if (!hashResult?.cid) {
|
|
2144
|
+
ipfsPayloadFailures.push({ itemKey, resourceType, mfsPath, reason: 'Failed to hash payload via Kubo' });
|
|
2145
|
+
return null;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
writeBackupPayload(hashResult.cid, payloadBuffer);
|
|
2149
|
+
return hashResult.cid;
|
|
2150
|
+
};
|
|
2151
|
+
|
|
2152
|
+
for (const ol of objectLayers) {
|
|
2153
|
+
const itemKey = ol.data?.item?.id || ol._id.toString();
|
|
2154
|
+
const itemPaths = getCanonicalIpfsPaths(itemKey);
|
|
2155
|
+
const objectLayerExport = newInstance(ol);
|
|
2156
|
+
|
|
2157
|
+
if (!objectLayerExport.data.render) {
|
|
2158
|
+
objectLayerExport.data.render = {};
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// Export ObjectLayerRenderFrames
|
|
2162
|
+
if (ol.objectLayerRenderFramesId) {
|
|
2163
|
+
const rf = await ObjectLayerRenderFrames.findById(ol.objectLayerRenderFramesId).lean();
|
|
2164
|
+
if (rf) {
|
|
2165
|
+
fs.writeJsonSync(`${backupDir}/render-frames/${itemKey}.json`, rf, { spaces: 2 });
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// Export AtlasSpriteSheet + its File using canonical payload bytes from the DB state.
|
|
2170
|
+
if (ol.atlasSpriteSheetId) {
|
|
2171
|
+
const atlas = await AtlasSpriteSheet.findById(ol.atlasSpriteSheetId).lean();
|
|
2172
|
+
if (!atlas) {
|
|
2173
|
+
ipfsPayloadFailures.push({
|
|
2174
|
+
itemKey,
|
|
2175
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2176
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2177
|
+
reason: 'AtlasSpriteSheet document not found in MongoDB',
|
|
2178
|
+
});
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
2181
|
+
|
|
2182
|
+
const atlasExport = newInstance(atlas);
|
|
2183
|
+
if (atlas.fileId) {
|
|
2184
|
+
await exportFileDoc(atlas.fileId, `atlas-${itemKey}`);
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
const atlasFile = atlas.fileId ? await File.findById(atlas.fileId).lean() : null;
|
|
2188
|
+
const atlasBuffer = toBuffer(atlasFile?.data);
|
|
2189
|
+
if (!atlasBuffer) {
|
|
2190
|
+
ipfsPayloadFailures.push({
|
|
2191
|
+
itemKey,
|
|
2192
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2193
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2194
|
+
reason: 'Atlas File payload not found in MongoDB',
|
|
2195
|
+
});
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
const atlasCid = await exportCanonicalPayload({
|
|
2200
|
+
payloadBuffer: atlasBuffer,
|
|
2201
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2202
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2203
|
+
filename: `${itemKey}_atlas_sprite_sheet.png`,
|
|
2204
|
+
itemKey,
|
|
2205
|
+
});
|
|
2206
|
+
if (!atlasCid) continue;
|
|
2207
|
+
|
|
2208
|
+
const linkedAtlasCid = atlas.cid || ol.data?.render?.cid || atlasCid;
|
|
2209
|
+
writeBackupPayloadAlias({
|
|
2210
|
+
canonicalCid: atlasCid,
|
|
2211
|
+
linkedCid: linkedAtlasCid,
|
|
2212
|
+
payloadBuffer: atlasBuffer,
|
|
2213
|
+
});
|
|
2214
|
+
expectedObjectLayerIpfsRefs.push({
|
|
2215
|
+
itemKey,
|
|
2216
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2217
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2218
|
+
linkedCid: linkedAtlasCid,
|
|
2219
|
+
fallbackCid: atlasCid,
|
|
2220
|
+
});
|
|
2221
|
+
|
|
2222
|
+
const atlasMetadataBuffer = Buffer.from(stringify(atlasExport.metadata || {}), 'utf-8');
|
|
2223
|
+
const atlasMetadataCid = await exportCanonicalPayload({
|
|
2224
|
+
payloadBuffer: atlasMetadataBuffer,
|
|
2225
|
+
resourceType: 'atlas-metadata',
|
|
2226
|
+
mfsPath: itemPaths.atlasMetadata,
|
|
2227
|
+
filename: `${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
2228
|
+
itemKey,
|
|
2229
|
+
});
|
|
2230
|
+
if (!atlasMetadataCid) continue;
|
|
2231
|
+
|
|
2232
|
+
const linkedAtlasMetadataCid = ol.data?.render?.metadataCid || atlasMetadataCid;
|
|
2233
|
+
writeBackupPayloadAlias({
|
|
2234
|
+
canonicalCid: atlasMetadataCid,
|
|
2235
|
+
linkedCid: linkedAtlasMetadataCid,
|
|
2236
|
+
payloadBuffer: atlasMetadataBuffer,
|
|
2237
|
+
});
|
|
2238
|
+
expectedObjectLayerIpfsRefs.push({
|
|
2239
|
+
itemKey,
|
|
2240
|
+
resourceType: 'atlas-metadata',
|
|
2241
|
+
mfsPath: itemPaths.atlasMetadata,
|
|
2242
|
+
linkedCid: linkedAtlasMetadataCid,
|
|
2243
|
+
fallbackCid: atlasMetadataCid,
|
|
2244
|
+
});
|
|
2245
|
+
|
|
2246
|
+
atlasExport.cid = atlasCid;
|
|
2247
|
+
objectLayerExport.data.render.cid = atlasCid;
|
|
2248
|
+
objectLayerExport.data.render.metadataCid = atlasMetadataCid;
|
|
2249
|
+
fs.writeJsonSync(`${backupDir}/atlas-sprite-sheets/${itemKey}.json`, atlasExport, { spaces: 2 });
|
|
2250
|
+
} else {
|
|
2251
|
+
if (objectLayerExport.data.render?.cid || objectLayerExport.data.render?.metadataCid) {
|
|
2252
|
+
ipfsPayloadFailures.push({
|
|
2253
|
+
itemKey,
|
|
2254
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2255
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2256
|
+
reason: 'ObjectLayer references atlas CIDs but no AtlasSpriteSheet document exists',
|
|
2257
|
+
});
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
delete objectLayerExport.data.render.cid;
|
|
2261
|
+
delete objectLayerExport.data.render.metadataCid;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const objectLayerBuffer = Buffer.from(stringify(objectLayerExport.data || {}), 'utf-8');
|
|
2265
|
+
const objectLayerCid = await exportCanonicalPayload({
|
|
2266
|
+
payloadBuffer: objectLayerBuffer,
|
|
2267
|
+
resourceType: 'object-layer-data',
|
|
2268
|
+
mfsPath: itemPaths.objectLayerData,
|
|
2269
|
+
filename: `${itemKey}_data.json`,
|
|
2270
|
+
itemKey,
|
|
2271
|
+
});
|
|
2272
|
+
if (!objectLayerCid) continue;
|
|
2273
|
+
|
|
2274
|
+
const linkedObjectLayerCid = ol.cid || objectLayerCid;
|
|
2275
|
+
writeBackupPayloadAlias({
|
|
2276
|
+
canonicalCid: objectLayerCid,
|
|
2277
|
+
linkedCid: linkedObjectLayerCid,
|
|
2278
|
+
payloadBuffer: objectLayerBuffer,
|
|
2279
|
+
});
|
|
2280
|
+
expectedObjectLayerIpfsRefs.push({
|
|
2281
|
+
itemKey,
|
|
2282
|
+
resourceType: 'object-layer-data',
|
|
2283
|
+
mfsPath: itemPaths.objectLayerData,
|
|
2284
|
+
linkedCid: linkedObjectLayerCid,
|
|
2285
|
+
fallbackCid: objectLayerCid,
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
objectLayerExport.cid = objectLayerCid;
|
|
2289
|
+
fs.writeJsonSync(`${backupDir}/object-layers/${itemKey}.json`, objectLayerExport, { spaces: 2 });
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
if (ipfsPayloadFailures.length > 0) {
|
|
2293
|
+
for (const failure of ipfsPayloadFailures) {
|
|
2294
|
+
logger.error('Canonical IPFS payload export failed', failure);
|
|
2295
|
+
}
|
|
2296
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
2297
|
+
process.exit(1);
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
const relatedPinPaths = [
|
|
2301
|
+
...new Set(expectedObjectLayerIpfsRefs.map((entry) => entry.mfsPath).filter(Boolean)),
|
|
2302
|
+
];
|
|
2303
|
+
const relatedPinCids = [
|
|
2304
|
+
...new Set(
|
|
2305
|
+
expectedObjectLayerIpfsRefs.flatMap((entry) => [entry.linkedCid, entry.fallbackCid]).filter(Boolean),
|
|
2306
|
+
),
|
|
2307
|
+
];
|
|
2308
|
+
const relatedIpfsDocs =
|
|
2309
|
+
relatedPinPaths.length > 0 || relatedPinCids.length > 0
|
|
2310
|
+
? await Ipfs.find({
|
|
2311
|
+
$or: [
|
|
2312
|
+
...(relatedPinPaths.length ? [{ mfsPath: { $in: relatedPinPaths } }] : []),
|
|
2313
|
+
...(relatedPinPaths.length ? [{ mfsPaths: { $in: relatedPinPaths } }] : []),
|
|
2314
|
+
...(relatedPinCids.length ? [{ cid: { $in: relatedPinCids } }] : []),
|
|
2315
|
+
],
|
|
2316
|
+
}).lean()
|
|
2317
|
+
: [];
|
|
2318
|
+
|
|
2319
|
+
let ipfsCollectionMatchCount = 0;
|
|
2320
|
+
let ipfsCollectionFallbackCount = 0;
|
|
2321
|
+
|
|
2322
|
+
for (const ref of expectedObjectLayerIpfsRefs) {
|
|
2323
|
+
const matchingDoc = findInstanceRelatedIpfsDoc(relatedIpfsDocs, ref);
|
|
2324
|
+
const exportCid = matchingDoc?.cid || ref.linkedCid || ref.fallbackCid;
|
|
2325
|
+
|
|
2326
|
+
if (!exportCid) {
|
|
2327
|
+
logger.warn('Skipping instance IPFS pin export because the ObjectLayer ref has no linked CID', {
|
|
2328
|
+
itemKey: ref.itemKey,
|
|
2329
|
+
resourceType: ref.resourceType,
|
|
2330
|
+
mfsPath: ref.mfsPath,
|
|
2331
|
+
});
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
upsertCanonicalPinEntry(canonicalPins, {
|
|
2336
|
+
cid: exportCid,
|
|
2337
|
+
resourceType: ref.resourceType,
|
|
2338
|
+
mfsPath: ref.mfsPath,
|
|
2339
|
+
});
|
|
2340
|
+
|
|
2341
|
+
if (matchingDoc) ipfsCollectionMatchCount++;
|
|
2342
|
+
else ipfsCollectionFallbackCount++;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
const sanitised = serialiseCanonicalPins(canonicalPins);
|
|
2346
|
+
fs.writeJsonSync(`${backupDir}/ipfs/pins.json`, sanitised, { spaces: 2 });
|
|
2347
|
+
logger.info(
|
|
2348
|
+
`Exported ${sanitised.length} instance-related Ipfs pin record(s) and ${ipfsPayloadExportCount} raw payload file(s)`,
|
|
2349
|
+
{
|
|
2350
|
+
matchedFromIpfsCollection: ipfsCollectionMatchCount,
|
|
2351
|
+
fallbackFromObjectLayerRefs: ipfsCollectionFallbackCount,
|
|
2352
|
+
rawPayloadAliases: ipfsPayloadAliasCount,
|
|
2353
|
+
},
|
|
2354
|
+
);
|
|
2355
|
+
|
|
2356
|
+
logger.info(`Exported ${objectLayers.length} ObjectLayer document(s)`, {
|
|
2357
|
+
itemIds: [...objectLayerItemIds],
|
|
2358
|
+
});
|
|
2359
|
+
} else {
|
|
2360
|
+
logger.info('No ObjectLayer references found in map entities');
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
logger.info('Instance export completed', { backupDir });
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
// ── IMPORT ──────────────────────────────────────────────────────
|
|
2367
|
+
if (options.import !== undefined) {
|
|
2368
|
+
const backupDir =
|
|
2369
|
+
typeof options.import === 'string' && options.import
|
|
2370
|
+
? options.import
|
|
2371
|
+
: `./engine-private/cyberia-instances/${instanceCode}`;
|
|
2372
|
+
|
|
2373
|
+
if (!fs.existsSync(backupDir)) {
|
|
2374
|
+
logger.error(`Backup directory not found: ${backupDir}`);
|
|
2375
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
2376
|
+
process.exit(1);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
logger.info('Importing instance', { code: instanceCode, backupDir });
|
|
2380
|
+
|
|
2381
|
+
// 0. Drop existing documents if --drop is set
|
|
2382
|
+
if (options.drop && !options.conf) {
|
|
2383
|
+
const existingInstance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
2384
|
+
if (existingInstance) {
|
|
2385
|
+
const dropMapCodes = new Set(existingInstance.cyberiaMapCodes || []);
|
|
2386
|
+
for (const portal of existingInstance.portals || []) {
|
|
2387
|
+
if (portal.sourceMapCode) dropMapCodes.add(portal.sourceMapCode);
|
|
2388
|
+
if (portal.targetMapCode) dropMapCodes.add(portal.targetMapCode);
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
// Collect thumbnail File IDs to drop
|
|
2392
|
+
const thumbFileIds = [];
|
|
2393
|
+
if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
|
|
2394
|
+
const dropOlItemIds = new Set();
|
|
2395
|
+
|
|
2396
|
+
// Query other instances/maps for shared thumbnail exclusion
|
|
2397
|
+
const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
|
|
2398
|
+
|
|
2399
|
+
// Add instance-level itemIds (may not appear in any map entity)
|
|
2400
|
+
for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
|
|
2401
|
+
|
|
2402
|
+
// Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
|
|
2403
|
+
const existingConf =
|
|
2404
|
+
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
2405
|
+
(existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
|
|
2406
|
+
if (existingConf) {
|
|
2407
|
+
for (const ed of existingConf.entityDefaults || []) {
|
|
2408
|
+
for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
|
|
2409
|
+
for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
|
|
2410
|
+
for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
|
|
2411
|
+
for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
|
|
2412
|
+
}
|
|
2413
|
+
for (const sc of existingConf.skillConfig || []) {
|
|
2414
|
+
if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
|
|
2415
|
+
for (const skill of sc.skills || []) {
|
|
2416
|
+
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
|
|
2417
|
+
dropOlItemIds.add(skill.summonedEntityItemId);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
const otherMaps = await CyberiaMap.find(
|
|
2423
|
+
{ code: { $nin: [...dropMapCodes] } },
|
|
2424
|
+
{ 'entities.objectLayerItemIds': 1, thumbnail: 1 },
|
|
2425
|
+
).lean();
|
|
2426
|
+
|
|
2427
|
+
if (dropMapCodes.size > 0) {
|
|
2428
|
+
const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
|
|
2429
|
+
for (const map of dropMaps) {
|
|
2430
|
+
if (map.thumbnail) thumbFileIds.push(map.thumbnail);
|
|
2431
|
+
for (const entity of map.entities || []) {
|
|
2432
|
+
for (const itemId of entity.objectLayerItemIds || []) {
|
|
2433
|
+
dropOlItemIds.add(itemId);
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
|
|
2439
|
+
logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// Exclude OL item IDs referenced by maps outside this instance
|
|
2443
|
+
const sharedOlItemIds = new Set();
|
|
2444
|
+
for (const m of otherMaps) {
|
|
2445
|
+
for (const entity of m.entities || []) {
|
|
2446
|
+
for (const itemId of entity.objectLayerItemIds || []) {
|
|
2447
|
+
if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
|
|
2452
|
+
if (sharedOlItemIds.size > 0) {
|
|
2453
|
+
logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
// Exclude thumbnail File IDs referenced by other instances or maps
|
|
2457
|
+
const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
|
|
2458
|
+
const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
|
|
2459
|
+
const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
|
|
2460
|
+
for (let i = thumbFileIds.length - 1; i >= 0; i--) {
|
|
2461
|
+
if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
if (dropOlItemIds.size > 0) {
|
|
2465
|
+
const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
|
|
2466
|
+
const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
|
|
2467
|
+
logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
|
|
2468
|
+
const olDocs = await ObjectLayer.find(
|
|
2469
|
+
{ 'data.item.id': { $in: [...dropOlItemIds] } },
|
|
2470
|
+
{
|
|
2471
|
+
cid: 1,
|
|
2472
|
+
'data.item.id': 1,
|
|
2473
|
+
'data.render': 1,
|
|
2474
|
+
objectLayerRenderFramesId: 1,
|
|
2475
|
+
atlasSpriteSheetId: 1,
|
|
2476
|
+
},
|
|
2477
|
+
).lean();
|
|
2478
|
+
|
|
2479
|
+
const cidsToUnpin = new Set();
|
|
2480
|
+
const renderFrameIds = [];
|
|
2481
|
+
const atlasIds = [];
|
|
2482
|
+
const itemKeysToClean = new Set();
|
|
2483
|
+
|
|
2484
|
+
for (const doc of olDocs) {
|
|
2485
|
+
if (doc.cid) cidsToUnpin.add(doc.cid);
|
|
2486
|
+
if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
|
|
2487
|
+
if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
|
|
2488
|
+
if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
|
|
2489
|
+
if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
|
|
2490
|
+
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
// Delete AtlasSpriteSheet + referenced File docs
|
|
2494
|
+
if (atlasIds.length > 0) {
|
|
2495
|
+
const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
|
|
2496
|
+
const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
|
|
2497
|
+
for (const atlas of atlasDocs) {
|
|
2498
|
+
if (atlas.cid) cidsToUnpin.add(atlas.cid);
|
|
2499
|
+
}
|
|
2500
|
+
if (atlasFileIds.length > 0) {
|
|
2501
|
+
const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
|
|
2502
|
+
logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
|
|
2503
|
+
}
|
|
2504
|
+
const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
2505
|
+
logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// Delete RenderFrames
|
|
2509
|
+
if (renderFrameIds.length > 0) {
|
|
2510
|
+
const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
|
|
2511
|
+
logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// Delete IPFS pin records
|
|
2515
|
+
if (cidsToUnpin.size > 0) {
|
|
2516
|
+
const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
|
|
2517
|
+
logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
|
|
2518
|
+
}
|
|
2519
|
+
|
|
2520
|
+
// Unpin CIDs from IPFS Kubo + Cluster and remove MFS paths
|
|
2521
|
+
let unpinCount = 0;
|
|
2522
|
+
for (const cid of cidsToUnpin) {
|
|
2523
|
+
const ok = await IpfsClient.unpinCid(cid);
|
|
2524
|
+
if (ok) unpinCount++;
|
|
2525
|
+
}
|
|
2526
|
+
let mfsCount = 0;
|
|
2527
|
+
for (const itemKey of itemKeysToClean) {
|
|
2528
|
+
const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
|
|
2529
|
+
if (ok) mfsCount++;
|
|
2530
|
+
}
|
|
2531
|
+
logger.info(
|
|
2532
|
+
`IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
|
|
2533
|
+
);
|
|
2534
|
+
|
|
2535
|
+
const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
|
|
2536
|
+
logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// Drop thumbnail File documents (instance + maps), excluding shared ones
|
|
2540
|
+
if (thumbFileIds.length > 0) {
|
|
2541
|
+
const thumbResult = await File.deleteMany({ _id: { $in: thumbFileIds } });
|
|
2542
|
+
logger.info(`Dropped ${thumbResult.deletedCount} File document(s) (thumbnails)`);
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
await CyberiaInstance.deleteOne({ code: instanceCode });
|
|
2546
|
+
logger.info('Dropped CyberiaInstance', { code: instanceCode });
|
|
2547
|
+
await CyberiaInstanceConf.deleteOne({ instanceCode });
|
|
2548
|
+
logger.info('Dropped CyberiaInstanceConf', { instanceCode });
|
|
2549
|
+
} else {
|
|
2550
|
+
logger.info('No existing instance to drop', { code: instanceCode });
|
|
2551
|
+
}
|
|
2552
|
+
} else if (options.drop && options.conf) {
|
|
2553
|
+
logger.info(
|
|
2554
|
+
'Skipping full instance drop because --conf only imports cyberia-instance.json and cyberia-instance-conf.json',
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
if (options.conf) {
|
|
2559
|
+
const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
|
|
2560
|
+
let importedConf = null;
|
|
2561
|
+
if (fs.existsSync(confImportPath)) {
|
|
2562
|
+
const confData = fs.readJsonSync(confImportPath);
|
|
2563
|
+
if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
|
|
2564
|
+
await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
|
|
2565
|
+
// Always bump updatedAt so the Go server's version hash changes and
|
|
2566
|
+
// ReloadWorld re-applies the config without requiring a full restart.
|
|
2567
|
+
confData.updatedAt = new Date();
|
|
2568
|
+
importedConf = await CyberiaInstanceConf.create(confData);
|
|
2569
|
+
logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
|
|
2570
|
+
} else {
|
|
2571
|
+
logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// In --conf mode we must NOT delete + recreate the CyberiaInstance because
|
|
2575
|
+
// that would overwrite cyberiaMapCodes / portals / itemIds with whatever was
|
|
2576
|
+
// in the (possibly stale) backup, effectively removing the live maps and OLs
|
|
2577
|
+
// from the instance. Only update the conf ref and bump updatedAt so the Go
|
|
2578
|
+
// server's version hash changes and ReloadWorld re-applies the config.
|
|
2579
|
+
if (importedConf) {
|
|
2580
|
+
const result = await CyberiaInstance.updateOne(
|
|
2581
|
+
{ code: instanceCode },
|
|
2582
|
+
{ $set: { conf: importedConf._id, updatedAt: new Date() } },
|
|
2583
|
+
);
|
|
2584
|
+
if (result.matchedCount > 0) {
|
|
2585
|
+
logger.info('Updated CyberiaInstance conf ref', { code: instanceCode });
|
|
2586
|
+
} else {
|
|
2587
|
+
logger.warn(`CyberiaInstance not found in DB for code "${instanceCode}" — cannot update conf ref`);
|
|
2588
|
+
}
|
|
2589
|
+
} else {
|
|
2590
|
+
logger.warn(`Skipping CyberiaInstance conf ref update — no conf was imported`);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
logger.info('Instance import completed in --conf mode', {
|
|
2594
|
+
backupDir,
|
|
2595
|
+
importedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
|
|
2596
|
+
});
|
|
2597
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
2598
|
+
return;
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
// 1. Import File documents first (atlas PNG + thumbnail dependencies)
|
|
2602
|
+
const filesDir = `${backupDir}/files`;
|
|
2603
|
+
if (fs.existsSync(filesDir)) {
|
|
2604
|
+
const fileFiles = fs.readdirSync(filesDir).filter((f) => f.endsWith('.json'));
|
|
2605
|
+
let fileCount = 0;
|
|
2606
|
+
for (const f of fileFiles) {
|
|
2607
|
+
const fileData = fs.readJsonSync(`${filesDir}/${f}`);
|
|
2608
|
+
// Restore base64-encoded Buffer (handle both $base64 and { type: 'Buffer', data: [...] })
|
|
2609
|
+
if (fileData.data) {
|
|
2610
|
+
if (fileData.data.$base64) {
|
|
2611
|
+
fileData.data = Buffer.from(fileData.data.$base64, 'base64');
|
|
2612
|
+
} else if (fileData.data.type === 'Buffer' && Array.isArray(fileData.data.data)) {
|
|
2613
|
+
fileData.data = Buffer.from(fileData.data.data);
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
// preserveUUID: delete any existing doc with this _id then create with exact _id
|
|
2617
|
+
await File.deleteOne({ _id: fileData._id });
|
|
2618
|
+
await File.create(fileData);
|
|
2619
|
+
fileCount++;
|
|
2620
|
+
}
|
|
2621
|
+
logger.info(`Imported ${fileCount} File document(s)`);
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
// 2. Import ObjectLayerRenderFrames
|
|
2625
|
+
const rfDir = `${backupDir}/render-frames`;
|
|
2626
|
+
if (fs.existsSync(rfDir)) {
|
|
2627
|
+
const rfFiles = fs.readdirSync(rfDir).filter((f) => f.endsWith('.json'));
|
|
2628
|
+
let rfCount = 0;
|
|
2629
|
+
for (const f of rfFiles) {
|
|
2630
|
+
const rfData = fs.readJsonSync(`${rfDir}/${f}`);
|
|
2631
|
+
if (rfData._id) {
|
|
2632
|
+
await ObjectLayerRenderFrames.deleteOne({ _id: rfData._id });
|
|
2633
|
+
await ObjectLayerRenderFrames.create(rfData);
|
|
2634
|
+
rfCount++;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
logger.info(`Imported ${rfCount} ObjectLayerRenderFrames document(s)`);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// 3. Import AtlasSpriteSheet
|
|
2641
|
+
const atlasDir = `${backupDir}/atlas-sprite-sheets`;
|
|
2642
|
+
if (fs.existsSync(atlasDir)) {
|
|
2643
|
+
const atlasFiles = fs.readdirSync(atlasDir).filter((f) => f.endsWith('.json'));
|
|
2644
|
+
let atlasCount = 0;
|
|
2645
|
+
for (const f of atlasFiles) {
|
|
2646
|
+
const atlasData = fs.readJsonSync(`${atlasDir}/${f}`);
|
|
2647
|
+
await AtlasSpriteSheet.deleteOne({ _id: atlasData._id });
|
|
2648
|
+
if (atlasData.metadata?.itemKey) {
|
|
2649
|
+
await AtlasSpriteSheet.deleteOne({ 'metadata.itemKey': atlasData.metadata.itemKey });
|
|
2650
|
+
}
|
|
2651
|
+
await AtlasSpriteSheet.create(atlasData);
|
|
2652
|
+
atlasCount++;
|
|
2653
|
+
}
|
|
2654
|
+
logger.info(`Imported ${atlasCount} AtlasSpriteSheet document(s)`);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// 4. Import object layers
|
|
2658
|
+
const olDir = `${backupDir}/object-layers`;
|
|
2659
|
+
if (fs.existsSync(olDir)) {
|
|
2660
|
+
const olFiles = fs.readdirSync(olDir).filter((f) => f.endsWith('.json'));
|
|
2661
|
+
let olCount = 0;
|
|
2662
|
+
for (const file of olFiles) {
|
|
2663
|
+
const olData = fs.readJsonSync(`${olDir}/${file}`);
|
|
2664
|
+
await ObjectLayer.deleteOne({ _id: olData._id });
|
|
2665
|
+
if (olData.sha256) {
|
|
2666
|
+
await ObjectLayer.deleteOne({ sha256: olData.sha256 });
|
|
2667
|
+
}
|
|
2668
|
+
await ObjectLayer.create(olData);
|
|
2669
|
+
olCount++;
|
|
2670
|
+
}
|
|
2671
|
+
logger.info(`Imported ${olCount} ObjectLayer document(s)`);
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// 4b. Regenerate static frame PNGs from imported render-frames + object-layer documents.
|
|
2675
|
+
// Mirrors the writeStaticFrameAssets call in `ol --import` so src/client/public/cyberia
|
|
2676
|
+
// and the public/<host><path> deployment dir are populated even when the cyberia
|
|
2677
|
+
// asset directory was wiped (e.g. git clean / rm -rf).
|
|
2678
|
+
const rfImportDir = `${backupDir}/render-frames`;
|
|
2679
|
+
const olImportDir = `${backupDir}/object-layers`;
|
|
2680
|
+
if (fs.existsSync(rfImportDir) && fs.existsSync(olImportDir)) {
|
|
2681
|
+
const srcBasePath = './src/client/public/cyberia/';
|
|
2682
|
+
const publicBasePath = `./public/${host}${path}`;
|
|
2683
|
+
let staticWriteCount = 0;
|
|
2684
|
+
const rfFileList = fs.readdirSync(rfImportDir).filter((f) => f.endsWith('.json'));
|
|
2685
|
+
for (const rfFile of rfFileList) {
|
|
2686
|
+
const rfData = fs.readJsonSync(`${rfImportDir}/${rfFile}`);
|
|
2687
|
+
const itemId = nodePath.basename(rfFile, '.json');
|
|
2688
|
+
const olFile = `${olImportDir}/${itemId}.json`;
|
|
2689
|
+
if (!fs.existsSync(olFile)) {
|
|
2690
|
+
logger.warn(`Skipping static asset generation for '${itemId}' — no matching object-layer file`);
|
|
2691
|
+
continue;
|
|
2692
|
+
}
|
|
2693
|
+
const olData = fs.readJsonSync(olFile);
|
|
2694
|
+
const itemType = olData.data?.item?.type;
|
|
2695
|
+
if (!itemType) {
|
|
2696
|
+
logger.warn(`Skipping static asset generation for '${itemId}' — missing data.item.type`);
|
|
2697
|
+
continue;
|
|
2698
|
+
}
|
|
2699
|
+
// rfData matches the ObjectLayerRenderFrames schema: { frames, colors, frame_duration }
|
|
2700
|
+
const objectLayerRenderFramesData = {
|
|
2701
|
+
frames: rfData.frames || {},
|
|
2702
|
+
colors: rfData.colors || [],
|
|
2703
|
+
frame_duration: rfData.frame_duration ?? 100,
|
|
2704
|
+
};
|
|
2705
|
+
try {
|
|
2706
|
+
const written = await ObjectLayerEngine.writeStaticFrameAssets({
|
|
2707
|
+
basePaths: [srcBasePath, publicBasePath],
|
|
2708
|
+
itemType,
|
|
2709
|
+
itemId,
|
|
2710
|
+
objectLayerRenderFramesData,
|
|
2711
|
+
objectLayerData: olData,
|
|
2712
|
+
cellPixelDim: 20,
|
|
2713
|
+
});
|
|
2714
|
+
staticWriteCount += written.length;
|
|
2715
|
+
} catch (err) {
|
|
2716
|
+
logger.warn(`Failed to write static assets for '${itemId}': ${err.message}`);
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
logger.info(`Static frame PNGs written: ${staticWriteCount} file(s) across src/client/public and public/`);
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// 5. Import maps (preserveUUID: delete by code then create with exact _id)
|
|
2723
|
+
const mapsDir = `${backupDir}/maps`;
|
|
2724
|
+
if (fs.existsSync(mapsDir)) {
|
|
2725
|
+
const mapFiles = fs.readdirSync(mapsDir).filter((f) => f.endsWith('.json'));
|
|
2726
|
+
let mapCount = 0;
|
|
2727
|
+
for (const file of mapFiles) {
|
|
2728
|
+
const mapData = fs.readJsonSync(`${mapsDir}/${file}`);
|
|
2729
|
+
await CyberiaMap.deleteOne({ code: mapData.code });
|
|
2730
|
+
await CyberiaMap.deleteOne({ _id: mapData._id });
|
|
2731
|
+
await CyberiaMap.create(mapData);
|
|
2732
|
+
mapCount++;
|
|
2733
|
+
}
|
|
2734
|
+
logger.info(`Imported ${mapCount} CyberiaMap document(s)`);
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
// 6. Import CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
|
|
2738
|
+
const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
|
|
2739
|
+
if (fs.existsSync(confImportPath)) {
|
|
2740
|
+
const confData = fs.readJsonSync(confImportPath);
|
|
2741
|
+
if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
|
|
2742
|
+
await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
|
|
2743
|
+
await CyberiaInstanceConf.create(confData);
|
|
2744
|
+
logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
|
|
2745
|
+
} else {
|
|
2746
|
+
logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
// 7. Import instance (preserveUUID: delete by code then create with exact _id)
|
|
2750
|
+
const instancePath = `${backupDir}/cyberia-instance.json`;
|
|
2751
|
+
if (fs.existsSync(instancePath)) {
|
|
2752
|
+
const instanceData = fs.readJsonSync(instancePath);
|
|
2753
|
+
await CyberiaInstance.deleteOne({ code: instanceCode });
|
|
2754
|
+
await CyberiaInstance.deleteOne({ _id: instanceData._id });
|
|
2755
|
+
await CyberiaInstance.create(instanceData);
|
|
2756
|
+
logger.info('Imported CyberiaInstance', { code: instanceCode });
|
|
2757
|
+
} else {
|
|
2758
|
+
logger.warn(`Instance file not found: ${instancePath}`);
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
// 8. Import CyberiaDialogue documents
|
|
2762
|
+
const dialoguesDir = `${backupDir}/cyberia-dialogues`;
|
|
2763
|
+
if (fs.existsSync(dialoguesDir)) {
|
|
2764
|
+
const dialogueFiles = fs.readdirSync(dialoguesDir).filter((f) => f.endsWith('.json'));
|
|
2765
|
+
let dialogueCount = 0;
|
|
2766
|
+
|
|
2767
|
+
for (const file of dialogueFiles) {
|
|
2768
|
+
const rawDialogueData = fs.readJsonSync(`${dialoguesDir}/${file}`);
|
|
2769
|
+
const dialogues = Array.isArray(rawDialogueData) ? rawDialogueData : [rawDialogueData];
|
|
2770
|
+
const dialogueCodes = [...new Set(dialogues.map((dialogue) => dialogue.code).filter(Boolean))];
|
|
2771
|
+
if (dialogueCodes.length === 0) {
|
|
2772
|
+
logger.warn(`Skipping CyberiaDialogue backup without code: ${file}`);
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2776
|
+
await CyberiaDialogue.deleteMany({ code: { $in: dialogueCodes } });
|
|
2777
|
+
|
|
2778
|
+
const dialogueIds = dialogues.map((dialogue) => dialogue._id).filter(Boolean);
|
|
2779
|
+
if (dialogueIds.length > 0) {
|
|
2780
|
+
await CyberiaDialogue.deleteMany({ _id: { $in: dialogueIds } });
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
await CyberiaDialogue.create(dialogues);
|
|
2784
|
+
dialogueCount += dialogues.length;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
logger.info(`Imported ${dialogueCount} CyberiaDialogue document(s)`);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
// 9. Restore IPFS pin records and payloads
|
|
2791
|
+
const ipfsFile = `${backupDir}/ipfs/pins.json`;
|
|
2792
|
+
if (fs.existsSync(ipfsFile)) {
|
|
2793
|
+
const ipfsDocs = fs.readJsonSync(ipfsFile);
|
|
2794
|
+
const ipfsContentDir = `${backupDir}/ipfs/content`;
|
|
2795
|
+
let ipfsCount = 0;
|
|
2796
|
+
let ipfsSkipped = 0;
|
|
2797
|
+
|
|
2798
|
+
const backupPins = new Map();
|
|
2799
|
+
for (const doc of ipfsDocs) {
|
|
2800
|
+
const resourceType = inferResourceType(doc);
|
|
2801
|
+
if (!resourceType) {
|
|
2802
|
+
logger.warn(
|
|
2803
|
+
`Ipfs record is missing resourceType and cannot be inferred (cid: ${doc.cid}, mfsPath: ${doc.mfsPath ?? '(none)'}) — skipping`,
|
|
2804
|
+
);
|
|
2805
|
+
ipfsSkipped++;
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
const mfsPaths = collectMfsPaths(doc);
|
|
2810
|
+
if (mfsPaths.length === 0) {
|
|
2811
|
+
upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath: '' });
|
|
2812
|
+
} else {
|
|
2813
|
+
for (const mfsPath of mfsPaths) {
|
|
2814
|
+
upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath });
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
const backupPinEntries = serialiseCanonicalPins(backupPins);
|
|
2820
|
+
const backupCids = [...new Set(backupPinEntries.map((entry) => entry.cid).filter(Boolean))];
|
|
2821
|
+
if (backupCids.length > 0) {
|
|
2822
|
+
await Ipfs.deleteMany({ cid: { $in: backupCids } });
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
const restoreAdditionalMfsPaths = async (cid, mfsPaths, primaryPath) => {
|
|
2826
|
+
let restoredCount = 0;
|
|
2827
|
+
for (const mfsPath of mfsPaths) {
|
|
2828
|
+
if (!mfsPath || mfsPath === primaryPath) continue;
|
|
2829
|
+
const ok = await IpfsClient.restoreMfsPath(cid, mfsPath);
|
|
2830
|
+
if (ok) restoredCount++;
|
|
2831
|
+
}
|
|
2832
|
+
return restoredCount;
|
|
2833
|
+
};
|
|
2834
|
+
|
|
2835
|
+
const upsertImportedPin = async ({ cid, resourceType, mfsPath }) => {
|
|
2836
|
+
if (!cid || !resourceType) return;
|
|
2837
|
+
await Ipfs.deleteMany({ cid, resourceType });
|
|
2838
|
+
await createPinRecord({ cid, resourceType, mfsPath: mfsPath || '', options: { host, path } });
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2841
|
+
if (fs.existsSync(ipfsContentDir)) {
|
|
2842
|
+
let cidRewriteCount = 0;
|
|
2843
|
+
let extraMfsRestoreCount = 0;
|
|
2844
|
+
|
|
2845
|
+
for (const [index, doc] of backupPinEntries.entries()) {
|
|
2846
|
+
const mfsPaths = collectMfsPaths(doc);
|
|
2847
|
+
const primaryPath = mfsPaths[0] || '';
|
|
2848
|
+
const payloadPath = `${ipfsContentDir}/${doc.cid}.bin`;
|
|
2849
|
+
|
|
2850
|
+
logger.info('IPFS raw payload restore start', {
|
|
2851
|
+
index: index + 1,
|
|
2852
|
+
total: backupPinEntries.length,
|
|
2853
|
+
cid: doc.cid,
|
|
2854
|
+
resourceType: doc.resourceType,
|
|
2855
|
+
mfsPath: primaryPath || null,
|
|
2856
|
+
});
|
|
2857
|
+
|
|
2858
|
+
if (!fs.existsSync(payloadPath)) {
|
|
2859
|
+
logger.warn('IPFS raw payload file missing from backup', {
|
|
2860
|
+
cid: doc.cid,
|
|
2861
|
+
resourceType: doc.resourceType,
|
|
2862
|
+
mfsPath: primaryPath || null,
|
|
2863
|
+
});
|
|
2864
|
+
ipfsSkipped++;
|
|
2865
|
+
continue;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
const addResult = await IpfsClient.addToIpfs(
|
|
2869
|
+
fs.readFileSync(payloadPath),
|
|
2870
|
+
nodePath.basename(primaryPath || doc.cid),
|
|
2871
|
+
primaryPath || undefined,
|
|
2872
|
+
);
|
|
2873
|
+
|
|
2874
|
+
if (!addResult?.cid) {
|
|
2875
|
+
logger.warn('IPFS raw payload restore failed', {
|
|
2876
|
+
cid: doc.cid,
|
|
2877
|
+
resourceType: doc.resourceType,
|
|
2878
|
+
mfsPath: primaryPath || null,
|
|
2879
|
+
});
|
|
2880
|
+
ipfsSkipped++;
|
|
2881
|
+
continue;
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2884
|
+
const finalCid = addResult.cid;
|
|
2885
|
+
if (doc.cid !== finalCid) {
|
|
2886
|
+
await rewriteImportedCidReferences({
|
|
2887
|
+
oldCid: doc.cid,
|
|
2888
|
+
newCid: finalCid,
|
|
2889
|
+
resourceType: doc.resourceType,
|
|
2890
|
+
});
|
|
2891
|
+
cidRewriteCount++;
|
|
2892
|
+
logger.warn('IPFS raw payload CID mismatch during import; rewriting imported references', {
|
|
2893
|
+
oldCid: doc.cid,
|
|
2894
|
+
newCid: finalCid,
|
|
2895
|
+
resourceType: doc.resourceType,
|
|
2896
|
+
mfsPath: primaryPath || null,
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
679
2899
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
2900
|
+
extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
|
|
2901
|
+
await upsertImportedPin({ cid: finalCid, resourceType: doc.resourceType, mfsPath: primaryPath });
|
|
2902
|
+
ipfsCount++;
|
|
2903
|
+
}
|
|
683
2904
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
});
|
|
2905
|
+
logger.info(
|
|
2906
|
+
`Imported ${ipfsCount} Ipfs pin record(s) from exact backup payloads${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
|
|
2907
|
+
);
|
|
2908
|
+
logger.info(
|
|
2909
|
+
`IPFS raw payload restore: ${ipfsCount}/${backupPinEntries.length} record(s) restored, ${extraMfsRestoreCount} additional MFS path(s) restored${cidRewriteCount ? `, ${cidRewriteCount} CID rewrite(s)` : ''}`,
|
|
2910
|
+
);
|
|
2911
|
+
} else {
|
|
2912
|
+
logger.warn(
|
|
2913
|
+
'Backup has no raw IPFS payload files under ipfs/content/. Rebuilding a canonical IPFS layout from imported ObjectLayer, AtlasSpriteSheet, and File documents.',
|
|
2914
|
+
);
|
|
695
2915
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
2916
|
+
const importedItemIds = fs.existsSync(olDir)
|
|
2917
|
+
? fs
|
|
2918
|
+
.readdirSync(olDir)
|
|
2919
|
+
.filter((f) => f.endsWith('.json'))
|
|
2920
|
+
.map((f) => nodePath.basename(f, '.json'))
|
|
2921
|
+
: [];
|
|
2922
|
+
const importedObjectLayers = importedItemIds.length
|
|
2923
|
+
? await ObjectLayer.find({ 'data.item.id': { $in: importedItemIds } }).lean()
|
|
2924
|
+
: [];
|
|
2925
|
+
|
|
2926
|
+
let rebuiltObjectLayers = 0;
|
|
2927
|
+
|
|
2928
|
+
for (const [index, objectLayerDoc] of importedObjectLayers.entries()) {
|
|
2929
|
+
const itemKey = objectLayerDoc.data?.item?.id || objectLayerDoc._id.toString();
|
|
2930
|
+
const itemPaths = getCanonicalIpfsPaths(itemKey);
|
|
2931
|
+
const updatedData = newInstance(objectLayerDoc.data || {});
|
|
2932
|
+
if (!updatedData.render) updatedData.render = {};
|
|
2933
|
+
|
|
2934
|
+
logger.info('IPFS legacy canonical rebuild start', {
|
|
2935
|
+
index: index + 1,
|
|
2936
|
+
total: importedObjectLayers.length,
|
|
2937
|
+
itemKey,
|
|
2938
|
+
});
|
|
700
2939
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
ObjectLayer,
|
|
704
|
-
ObjectLayerRenderFrames,
|
|
705
|
-
objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
|
|
706
|
-
objectLayerData: multiFrameResult.objectLayerData,
|
|
707
|
-
createOptions: {
|
|
708
|
-
generateAtlas: false,
|
|
709
|
-
},
|
|
710
|
-
});
|
|
2940
|
+
let atlasCid = '';
|
|
2941
|
+
let atlasMetadataCid = '';
|
|
711
2942
|
|
|
712
|
-
|
|
2943
|
+
if (objectLayerDoc.atlasSpriteSheetId) {
|
|
2944
|
+
const atlasDoc = await AtlasSpriteSheet.findById(objectLayerDoc.atlasSpriteSheetId).lean();
|
|
2945
|
+
if (atlasDoc) {
|
|
2946
|
+
const atlasFile = atlasDoc.fileId ? await File.findById(atlasDoc.fileId).lean() : null;
|
|
2947
|
+
const atlasBuffer = toBuffer(atlasFile?.data);
|
|
2948
|
+
|
|
2949
|
+
if (atlasBuffer) {
|
|
2950
|
+
const atlasAddResult = await IpfsClient.addBufferToIpfs(
|
|
2951
|
+
atlasBuffer,
|
|
2952
|
+
`${itemKey}_atlas_sprite_sheet.png`,
|
|
2953
|
+
itemPaths.atlasSpriteSheet,
|
|
2954
|
+
);
|
|
2955
|
+
if (atlasAddResult?.cid) {
|
|
2956
|
+
atlasCid = atlasAddResult.cid;
|
|
2957
|
+
await AtlasSpriteSheet.updateOne({ _id: atlasDoc._id }, { $set: { cid: atlasCid } });
|
|
2958
|
+
await createPinRecord({
|
|
2959
|
+
cid: atlasCid,
|
|
2960
|
+
resourceType: 'atlas-sprite-sheet',
|
|
2961
|
+
mfsPath: itemPaths.atlasSpriteSheet,
|
|
2962
|
+
options: { host, path },
|
|
2963
|
+
});
|
|
2964
|
+
ipfsCount++;
|
|
2965
|
+
} else {
|
|
2966
|
+
logger.warn(`Failed to rebuild atlas sprite sheet payload for '${itemKey}'`);
|
|
2967
|
+
}
|
|
2968
|
+
} else if (atlasDoc.fileId) {
|
|
2969
|
+
logger.warn(`Atlas File payload missing for '${itemKey}'`);
|
|
2970
|
+
}
|
|
713
2971
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
2972
|
+
const atlasMetadataResult = await IpfsClient.addJsonToIpfs(
|
|
2973
|
+
atlasDoc.metadata || {},
|
|
2974
|
+
`${itemKey}_atlas_sprite_sheet_metadata.json`,
|
|
2975
|
+
itemPaths.atlasMetadata,
|
|
2976
|
+
);
|
|
2977
|
+
if (atlasMetadataResult?.cid) {
|
|
2978
|
+
atlasMetadataCid = atlasMetadataResult.cid;
|
|
2979
|
+
await createPinRecord({
|
|
2980
|
+
cid: atlasMetadataCid,
|
|
2981
|
+
resourceType: 'atlas-metadata',
|
|
2982
|
+
mfsPath: itemPaths.atlasMetadata,
|
|
2983
|
+
options: { host, path },
|
|
2984
|
+
});
|
|
2985
|
+
ipfsCount++;
|
|
2986
|
+
} else {
|
|
2987
|
+
logger.warn(`Failed to rebuild atlas metadata payload for '${itemKey}'`);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
721
2991
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
2992
|
+
if (atlasCid) {
|
|
2993
|
+
updatedData.render.cid = atlasCid;
|
|
2994
|
+
} else {
|
|
2995
|
+
delete updatedData.render.cid;
|
|
2996
|
+
}
|
|
2997
|
+
if (atlasMetadataCid) {
|
|
2998
|
+
updatedData.render.metadataCid = atlasMetadataCid;
|
|
2999
|
+
} else {
|
|
3000
|
+
delete updatedData.render.metadataCid;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
const objectLayerAddResult = await IpfsClient.addJsonToIpfs(
|
|
3004
|
+
updatedData,
|
|
3005
|
+
`${itemKey}_data.json`,
|
|
3006
|
+
itemPaths.objectLayerData,
|
|
3007
|
+
);
|
|
3008
|
+
if (objectLayerAddResult?.cid) {
|
|
3009
|
+
await ObjectLayer.updateOne(
|
|
3010
|
+
{ _id: objectLayerDoc._id },
|
|
3011
|
+
{
|
|
3012
|
+
$set: {
|
|
3013
|
+
cid: objectLayerAddResult.cid,
|
|
3014
|
+
data: updatedData,
|
|
3015
|
+
},
|
|
3016
|
+
},
|
|
3017
|
+
);
|
|
3018
|
+
await createPinRecord({
|
|
3019
|
+
cid: objectLayerAddResult.cid,
|
|
3020
|
+
resourceType: 'object-layer-data',
|
|
3021
|
+
mfsPath: itemPaths.objectLayerData,
|
|
3022
|
+
options: { host, path },
|
|
3023
|
+
});
|
|
3024
|
+
ipfsCount++;
|
|
3025
|
+
rebuiltObjectLayers++;
|
|
3026
|
+
} else {
|
|
3027
|
+
logger.warn(`Failed to rebuild object-layer-data payload for '${itemKey}'`);
|
|
3028
|
+
ipfsSkipped++;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
logger.info(
|
|
3033
|
+
`Legacy IPFS rebuild: ${rebuiltObjectLayers}/${importedObjectLayers.length} ObjectLayer payload(s) rebuilt, ${ipfsCount} canonical pin record(s) upserted${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
|
|
726
3034
|
);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
727
3037
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
name: `${atlasItemKey}-atlas.png`,
|
|
731
|
-
data: buffer,
|
|
732
|
-
size: buffer.length,
|
|
733
|
-
mimetype: 'image/png',
|
|
734
|
-
md5: crypto.createHash('md5').update(buffer).digest('hex'),
|
|
735
|
-
}).save();
|
|
3038
|
+
logger.info('Instance import completed', { backupDir });
|
|
3039
|
+
}
|
|
736
3040
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
3041
|
+
// ── DROP (standalone) ───────────────────────────────────────────
|
|
3042
|
+
if (options.drop && options.import === undefined) {
|
|
3043
|
+
const existingInstance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
3044
|
+
if (existingInstance) {
|
|
3045
|
+
const dropMapCodes = new Set(existingInstance.cyberiaMapCodes || []);
|
|
3046
|
+
for (const portal of existingInstance.portals || []) {
|
|
3047
|
+
if (portal.sourceMapCode) dropMapCodes.add(portal.sourceMapCode);
|
|
3048
|
+
if (portal.targetMapCode) dropMapCodes.add(portal.targetMapCode);
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3051
|
+
// Collect thumbnail File IDs to drop
|
|
3052
|
+
const thumbFileIds = [];
|
|
3053
|
+
if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
|
|
3054
|
+
const dropOlItemIds = new Set();
|
|
3055
|
+
|
|
3056
|
+
// Query other instances for shared thumbnail exclusion
|
|
3057
|
+
const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
|
|
3058
|
+
|
|
3059
|
+
// Add instance-level itemIds (may not appear in any map entity)
|
|
3060
|
+
for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
|
|
3061
|
+
|
|
3062
|
+
// Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
|
|
3063
|
+
const existingConf =
|
|
3064
|
+
(await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
|
|
3065
|
+
(existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
|
|
3066
|
+
if (existingConf) {
|
|
3067
|
+
for (const ed of existingConf.entityDefaults || []) {
|
|
3068
|
+
for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
|
|
3069
|
+
for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
|
|
3070
|
+
for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
|
|
3071
|
+
for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
|
|
3072
|
+
}
|
|
3073
|
+
for (const sc of existingConf.skillConfig || []) {
|
|
3074
|
+
if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
|
|
3075
|
+
for (const skill of sc.skills || []) {
|
|
3076
|
+
if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
|
|
3077
|
+
dropOlItemIds.add(skill.summonedEntityItemId);
|
|
748
3078
|
}
|
|
749
|
-
} catch (ipfsError) {
|
|
750
|
-
logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
|
|
751
3079
|
}
|
|
3080
|
+
}
|
|
752
3081
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
3082
|
+
const otherMaps = await CyberiaMap.find(
|
|
3083
|
+
{ code: { $nin: [...dropMapCodes] } },
|
|
3084
|
+
{ 'entities.objectLayerItemIds': 1, thumbnail: 1 },
|
|
3085
|
+
).lean();
|
|
3086
|
+
|
|
3087
|
+
if (dropMapCodes.size > 0) {
|
|
3088
|
+
const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
|
|
3089
|
+
for (const map of dropMaps) {
|
|
3090
|
+
if (map.thumbnail) thumbFileIds.push(map.thumbnail);
|
|
3091
|
+
for (const entity of map.entities || []) {
|
|
3092
|
+
for (const itemId of entity.objectLayerItemIds || []) {
|
|
3093
|
+
dropOlItemIds.add(itemId);
|
|
3094
|
+
}
|
|
763
3095
|
}
|
|
764
|
-
} catch (ipfsError) {
|
|
765
|
-
logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
|
|
766
3096
|
}
|
|
3097
|
+
const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
|
|
3098
|
+
logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
|
|
3099
|
+
}
|
|
767
3100
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
776
|
-
} else {
|
|
777
|
-
atlasDoc = await new AtlasSpriteSheet({
|
|
778
|
-
fileId: fileDoc._id,
|
|
779
|
-
cid: atlasCid,
|
|
780
|
-
metadata,
|
|
781
|
-
}).save();
|
|
782
|
-
logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
|
|
3101
|
+
// Exclude OL item IDs referenced by maps outside this instance
|
|
3102
|
+
const sharedOlItemIds = new Set();
|
|
3103
|
+
for (const m of otherMaps) {
|
|
3104
|
+
for (const entity of m.entities || []) {
|
|
3105
|
+
for (const itemId of entity.objectLayerItemIds || []) {
|
|
3106
|
+
if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
|
|
3107
|
+
}
|
|
783
3108
|
}
|
|
3109
|
+
}
|
|
3110
|
+
for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
|
|
3111
|
+
if (sharedOlItemIds.size > 0) {
|
|
3112
|
+
logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
|
|
3113
|
+
}
|
|
784
3114
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
3115
|
+
// Exclude thumbnail File IDs referenced by other instances or maps
|
|
3116
|
+
const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
|
|
3117
|
+
const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
|
|
3118
|
+
const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
|
|
3119
|
+
for (let i = thumbFileIds.length - 1; i >= 0; i--) {
|
|
3120
|
+
if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
|
|
3121
|
+
}
|
|
792
3122
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
3123
|
+
if (dropOlItemIds.size > 0) {
|
|
3124
|
+
const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
|
|
3125
|
+
const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
|
|
3126
|
+
logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
|
|
3127
|
+
|
|
3128
|
+
const olDocs = await ObjectLayer.find(
|
|
3129
|
+
{ 'data.item.id': { $in: [...dropOlItemIds] } },
|
|
3130
|
+
{
|
|
3131
|
+
cid: 1,
|
|
3132
|
+
'data.item.id': 1,
|
|
3133
|
+
'data.render': 1,
|
|
3134
|
+
objectLayerRenderFramesId: 1,
|
|
3135
|
+
atlasSpriteSheetId: 1,
|
|
3136
|
+
},
|
|
3137
|
+
).lean();
|
|
3138
|
+
|
|
3139
|
+
const cidsToUnpin = new Set();
|
|
3140
|
+
const renderFrameIds = [];
|
|
3141
|
+
const atlasIds = [];
|
|
3142
|
+
const itemKeysToClean = new Set();
|
|
3143
|
+
|
|
3144
|
+
for (const doc of olDocs) {
|
|
3145
|
+
if (doc.cid) cidsToUnpin.add(doc.cid);
|
|
3146
|
+
if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
|
|
3147
|
+
if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
|
|
3148
|
+
if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
|
|
3149
|
+
if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
|
|
3150
|
+
if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
|
|
802
3151
|
}
|
|
803
|
-
} catch (atlasError) {
|
|
804
|
-
logger.error(`Failed to generate atlas for ${uniqueItemId}:`, atlasError);
|
|
805
|
-
}
|
|
806
3152
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
logger.info(`ObjectLayer data pinned to IPFS – CID: ${finalized.cid}`);
|
|
3153
|
+
if (atlasIds.length > 0) {
|
|
3154
|
+
const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
|
|
3155
|
+
const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
|
|
3156
|
+
for (const atlas of atlasDocs) {
|
|
3157
|
+
if (atlas.cid) cidsToUnpin.add(atlas.cid);
|
|
3158
|
+
}
|
|
3159
|
+
if (atlasFileIds.length > 0) {
|
|
3160
|
+
const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
|
|
3161
|
+
logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
|
|
3162
|
+
}
|
|
3163
|
+
const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
|
|
3164
|
+
logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
|
|
820
3165
|
}
|
|
821
|
-
} catch (finalizeError) {
|
|
822
|
-
logger.error('Failed to finalize SHA-256 / IPFS:', finalizeError);
|
|
823
|
-
}
|
|
824
3166
|
|
|
825
|
-
|
|
3167
|
+
if (renderFrameIds.length > 0) {
|
|
3168
|
+
const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
|
|
3169
|
+
logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
|
|
3170
|
+
}
|
|
826
3171
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
3172
|
+
if (cidsToUnpin.size > 0) {
|
|
3173
|
+
const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
|
|
3174
|
+
logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
let unpinCount = 0;
|
|
3178
|
+
for (const cid of cidsToUnpin) {
|
|
3179
|
+
const ok = await IpfsClient.unpinCid(cid);
|
|
3180
|
+
if (ok) unpinCount++;
|
|
832
3181
|
}
|
|
3182
|
+
let mfsCount = 0;
|
|
3183
|
+
for (const itemKey of itemKeysToClean) {
|
|
3184
|
+
const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
|
|
3185
|
+
if (ok) mfsCount++;
|
|
3186
|
+
}
|
|
3187
|
+
logger.info(
|
|
3188
|
+
`IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
|
|
3189
|
+
);
|
|
3190
|
+
|
|
3191
|
+
const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
|
|
3192
|
+
logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
|
|
833
3193
|
}
|
|
3194
|
+
|
|
3195
|
+
// Drop thumbnail File documents (instance + maps), excluding shared ones
|
|
3196
|
+
if (thumbFileIds.length > 0) {
|
|
3197
|
+
const thumbResult = await File.deleteMany({ _id: { $in: thumbFileIds } });
|
|
3198
|
+
logger.info(`Dropped ${thumbResult.deletedCount} File document(s) (thumbnails)`);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
await CyberiaInstance.deleteOne({ code: instanceCode });
|
|
3202
|
+
logger.info('Dropped CyberiaInstance', { code: instanceCode });
|
|
3203
|
+
await CyberiaInstanceConf.deleteOne({ instanceCode });
|
|
3204
|
+
logger.info('Dropped CyberiaInstanceConf', { instanceCode });
|
|
3205
|
+
} else {
|
|
3206
|
+
logger.info('No existing instance to drop', { code: instanceCode });
|
|
834
3207
|
}
|
|
3208
|
+
}
|
|
835
3209
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
3210
|
+
if (options.export === undefined && options.import === undefined && !options.drop) {
|
|
3211
|
+
logger.error('Specify --export, --import, or --drop flag');
|
|
3212
|
+
}
|
|
3213
|
+
|
|
3214
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
3215
|
+
});
|
|
840
3216
|
|
|
841
3217
|
// ── chain: Hyperledger Besu / ERC-1155 lifecycle commands ────────────────
|
|
842
3218
|
const chain = program.command('chain').description('Hyperledger Besu chain & ERC-1155 ObjectLayerToken lifecycle');
|
|
@@ -1054,7 +3430,7 @@ try {
|
|
|
1054
3430
|
// Use a Hardhat script via inline JS to call registerObjectLayer
|
|
1055
3431
|
const registerScript = `
|
|
1056
3432
|
import hre from 'hardhat';
|
|
1057
|
-
const { ethers } = hre;
|
|
3433
|
+
const { ethers } = await hre.network.connect();
|
|
1058
3434
|
async function main() {
|
|
1059
3435
|
const [deployer] = await ethers.getSigners();
|
|
1060
3436
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
@@ -1105,7 +3481,7 @@ try {
|
|
|
1105
3481
|
|
|
1106
3482
|
const mintScript = `
|
|
1107
3483
|
import hre from 'hardhat';
|
|
1108
|
-
const { ethers } = hre;
|
|
3484
|
+
const { ethers } = await hre.network.connect();
|
|
1109
3485
|
async function main() {
|
|
1110
3486
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
1111
3487
|
const tx = await token.mint('${options.to}', ${options.tokenId}, ${options.amount}, '0x');
|
|
@@ -1142,7 +3518,7 @@ try {
|
|
|
1142
3518
|
const statusScript = `
|
|
1143
3519
|
import hre from 'hardhat';
|
|
1144
3520
|
import { readFileSync } from 'fs';
|
|
1145
|
-
const { ethers } = hre;
|
|
3521
|
+
const { ethers } = await hre.network.connect();
|
|
1146
3522
|
async function main() {
|
|
1147
3523
|
const provider = ethers.provider;
|
|
1148
3524
|
const network = await provider.getNetwork();
|
|
@@ -1204,7 +3580,7 @@ try {
|
|
|
1204
3580
|
|
|
1205
3581
|
const pauseScript = `
|
|
1206
3582
|
import hre from 'hardhat';
|
|
1207
|
-
const { ethers } = hre;
|
|
3583
|
+
const { ethers } = await hre.network.connect();
|
|
1208
3584
|
async function main() {
|
|
1209
3585
|
const token = await ethers.getContractAt('ObjectLayerToken', '${deployment.address}');
|
|
1210
3586
|
const tx = await token.pause();
|
|
@@ -1237,7 +3613,7 @@ try {
|
|
|
1237
3613
|
|
|
1238
3614
|
const unpauseScript = `
|
|
1239
3615
|
import hre from 'hardhat';
|
|
1240
|
-
const { ethers } = hre;
|
|
3616
|
+
const { ethers } = await hre.network.connect();
|
|
1241
3617
|
async function main() {
|
|
1242
3618
|
const token = await ethers.getContractAt('ObjectLayerToken', '${deployment.address}');
|
|
1243
3619
|
const tx = await token.unpause();
|
|
@@ -1396,7 +3772,7 @@ try {
|
|
|
1396
3772
|
|
|
1397
3773
|
const balanceScript = `
|
|
1398
3774
|
import hre from 'hardhat';
|
|
1399
|
-
const { ethers } = hre;
|
|
3775
|
+
const { ethers } = await hre.network.connect();
|
|
1400
3776
|
async function main() {
|
|
1401
3777
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
1402
3778
|
const balance = await token.balanceOf('${options.address}', ${options.tokenId});
|
|
@@ -1453,7 +3829,7 @@ try {
|
|
|
1453
3829
|
|
|
1454
3830
|
const transferScript = `
|
|
1455
3831
|
import hre from 'hardhat';
|
|
1456
|
-
const { ethers } = hre;
|
|
3832
|
+
const { ethers } = await hre.network.connect();
|
|
1457
3833
|
async function main() {
|
|
1458
3834
|
const [signer] = await ethers.getSigners();
|
|
1459
3835
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
@@ -1509,7 +3885,7 @@ try {
|
|
|
1509
3885
|
|
|
1510
3886
|
const burnScript = `
|
|
1511
3887
|
import hre from 'hardhat';
|
|
1512
|
-
const { ethers } = hre;
|
|
3888
|
+
const { ethers } = await hre.network.connect();
|
|
1513
3889
|
async function main() {
|
|
1514
3890
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
1515
3891
|
const tx = await token.burn('${options.address}', ${options.tokenId}, ${options.amount});
|
|
@@ -1620,7 +3996,7 @@ try {
|
|
|
1620
3996
|
|
|
1621
3997
|
const batchScript = `
|
|
1622
3998
|
import hre from 'hardhat';
|
|
1623
|
-
const { ethers } = hre;
|
|
3999
|
+
const { ethers } = await hre.network.connect();
|
|
1624
4000
|
async function main() {
|
|
1625
4001
|
const [deployer] = await ethers.getSigners();
|
|
1626
4002
|
const token = await ethers.getContractAt('ObjectLayerToken', '${contractAddress}');
|
|
@@ -1653,6 +4029,216 @@ try {
|
|
|
1653
4029
|
}
|
|
1654
4030
|
});
|
|
1655
4031
|
|
|
4032
|
+
const runner = program.command('run-workflow').description('Run a Cyberia script from the "scripts" directory');
|
|
4033
|
+
|
|
4034
|
+
runner
|
|
4035
|
+
.command('import-default-items')
|
|
4036
|
+
.option('--dev', 'Force development environment (loads .env.development for IPFS localhost, etc.)')
|
|
4037
|
+
.description('Import default Object Layer items, skill config, and dialogues into MongoDB')
|
|
4038
|
+
.action(async (options) => {
|
|
4039
|
+
const devFlag = options.dev ? ' --dev' : '';
|
|
4040
|
+
shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}`);
|
|
4041
|
+
shellExec(`node bin/cyberia run-workflow seed-skill-config${devFlag}`);
|
|
4042
|
+
shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}`);
|
|
4043
|
+
});
|
|
4044
|
+
|
|
4045
|
+
runner
|
|
4046
|
+
.command('seed-skill-config')
|
|
4047
|
+
.option('--instance-code <code>', 'CyberiaInstance code to update (default: $INSTANCE_CODE env or "default")')
|
|
4048
|
+
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
4049
|
+
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
4050
|
+
.option('--dev', 'Force development environment')
|
|
4051
|
+
.description('Upsert default skillConfig entries into a CyberiaInstance document')
|
|
4052
|
+
.action(async (options) => {
|
|
4053
|
+
if (!options.envPath) options.envPath = `./.env`;
|
|
4054
|
+
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
4055
|
+
|
|
4056
|
+
if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
|
|
4057
|
+
const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
|
|
4058
|
+
if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
const deployId = process.env.DEFAULT_DEPLOY_ID;
|
|
4062
|
+
const host = process.env.DEFAULT_DEPLOY_HOST;
|
|
4063
|
+
const path = process.env.DEFAULT_DEPLOY_PATH;
|
|
4064
|
+
const instanceCode = options.instanceCode || process.env.INSTANCE_CODE || 'default';
|
|
4065
|
+
|
|
4066
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
4067
|
+
if (!fs.existsSync(confServerPath)) {
|
|
4068
|
+
logger.error(`Server config not found: ${confServerPath}`);
|
|
4069
|
+
process.exit(1);
|
|
4070
|
+
}
|
|
4071
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
4072
|
+
const { db } = confServer[host][path];
|
|
4073
|
+
|
|
4074
|
+
db.host = options.mongoHost
|
|
4075
|
+
? options.mongoHost
|
|
4076
|
+
: options.dev
|
|
4077
|
+
? db.host
|
|
4078
|
+
: db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
|
|
4079
|
+
|
|
4080
|
+
logger.info('seed-skill-config', { instanceCode, deployId, host, path, db });
|
|
4081
|
+
|
|
4082
|
+
await DataBaseProvider.load({ apis: ['cyberia-instance', 'cyberia-instance-conf'], host, path, db });
|
|
4083
|
+
|
|
4084
|
+
const CyberiaInstance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaInstance;
|
|
4085
|
+
const CyberiaInstanceConf = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaInstanceConf;
|
|
4086
|
+
|
|
4087
|
+
const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
|
|
4088
|
+
|
|
4089
|
+
if (!instance) {
|
|
4090
|
+
logger.info(
|
|
4091
|
+
`CyberiaInstance "${instanceCode}" not found — seeding skillConfig into conf using fallback defaults. ` +
|
|
4092
|
+
`To link to a live instance, create or import it with: node bin/cyberia instance ${instanceCode} --import`,
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
// Always upsert the conf with DefaultSkillConfig — idempotent regardless of instance existence.
|
|
4097
|
+
const conf = await CyberiaInstanceConf.findOneAndUpdate(
|
|
4098
|
+
{ instanceCode },
|
|
4099
|
+
{ $set: { skillConfig: DefaultSkillConfig } },
|
|
4100
|
+
{ upsert: true, returnDocument: 'after' },
|
|
4101
|
+
);
|
|
4102
|
+
|
|
4103
|
+
// If a live instance exists, ensure its conf ref is linked.
|
|
4104
|
+
if (instance && (!instance.conf || String(instance.conf) !== String(conf._id))) {
|
|
4105
|
+
await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: conf._id });
|
|
4106
|
+
}
|
|
4107
|
+
|
|
4108
|
+
logger.info(
|
|
4109
|
+
`skillConfig seeded for instance "${instanceCode}" (${DefaultSkillConfig.length} entries)`,
|
|
4110
|
+
DefaultSkillConfig.map((e) => `${e.triggerItemId} → [${e.logicEventIds.join(', ')}]`),
|
|
4111
|
+
);
|
|
4112
|
+
|
|
4113
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
4114
|
+
});
|
|
4115
|
+
|
|
4116
|
+
runner
|
|
4117
|
+
.command('seed-dialogues')
|
|
4118
|
+
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
4119
|
+
.option('--mongo-host <mongo-host>', 'Mongo host override')
|
|
4120
|
+
.option('--dev', 'Force development environment')
|
|
4121
|
+
.description('Upsert DefaultCyberiaDialogues into the cyberia-dialogue collection (idempotent)')
|
|
4122
|
+
.action(async (options) => {
|
|
4123
|
+
if (!options.envPath) options.envPath = `./.env`;
|
|
4124
|
+
if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
|
|
4125
|
+
|
|
4126
|
+
if (options.dev && process.env.DEFAULT_DEPLOY_ID) {
|
|
4127
|
+
const devEnvPath = `./engine-private/conf/${process.env.DEFAULT_DEPLOY_ID}/.env.development`;
|
|
4128
|
+
if (fs.existsSync(devEnvPath)) dotenv.config({ path: devEnvPath, override: true });
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
const deployId = process.env.DEFAULT_DEPLOY_ID;
|
|
4132
|
+
const host = process.env.DEFAULT_DEPLOY_HOST;
|
|
4133
|
+
const path = process.env.DEFAULT_DEPLOY_PATH;
|
|
4134
|
+
|
|
4135
|
+
const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
|
|
4136
|
+
if (!fs.existsSync(confServerPath)) {
|
|
4137
|
+
logger.error(`Server config not found: ${confServerPath}`);
|
|
4138
|
+
process.exit(1);
|
|
4139
|
+
}
|
|
4140
|
+
const confServer = loadConfServerJson(confServerPath, { resolve: true });
|
|
4141
|
+
const { db } = confServer[host][path];
|
|
4142
|
+
|
|
4143
|
+
db.host = options.mongoHost
|
|
4144
|
+
? options.mongoHost
|
|
4145
|
+
: options.dev
|
|
4146
|
+
? db.host
|
|
4147
|
+
: db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
|
|
4148
|
+
|
|
4149
|
+
logger.info('seed-dialogues', { deployId, host, path, db });
|
|
4150
|
+
|
|
4151
|
+
await DataBaseProvider.load({ apis: ['cyberia-dialogue'], host, path, db });
|
|
4152
|
+
|
|
4153
|
+
const CyberiaDialogue = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaDialogue;
|
|
4154
|
+
|
|
4155
|
+
// Upsert each dialogue record keyed by (code, order) — idempotent.
|
|
4156
|
+
let upserted = 0;
|
|
4157
|
+
for (const dlg of DefaultCyberiaDialogues) {
|
|
4158
|
+
await CyberiaDialogue.findOneAndUpdate(
|
|
4159
|
+
{ code: dlg.code, order: dlg.order },
|
|
4160
|
+
{ $set: { speaker: dlg.speaker, text: dlg.text, mood: dlg.mood } },
|
|
4161
|
+
{ upsert: true },
|
|
4162
|
+
);
|
|
4163
|
+
upserted++;
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
logger.info(`seed-dialogues: ${upserted} dialogue records upserted`);
|
|
4167
|
+
|
|
4168
|
+
await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
|
|
4169
|
+
});
|
|
4170
|
+
|
|
4171
|
+
runner
|
|
4172
|
+
.command('generate-semantic-examples')
|
|
4173
|
+
.option('--seed <seed>', 'Base seed string (each type gets a unique suffix appended)', 'example')
|
|
4174
|
+
.option('--frame-count <frameCount>', 'Number of frames to generate per item (default: 4)', parseInt)
|
|
4175
|
+
.option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
|
|
4176
|
+
.option('--dev', 'Force development environment')
|
|
4177
|
+
.description('Generate one procedural example of every registered semantic prefix')
|
|
4178
|
+
.action(async (options) => {
|
|
4179
|
+
const SEMANTIC_TYPES = [
|
|
4180
|
+
// 'floor-desert',
|
|
4181
|
+
// 'floor-grass',
|
|
4182
|
+
// 'floor-water',
|
|
4183
|
+
// 'floor-stone',
|
|
4184
|
+
// 'floor-lava',
|
|
4185
|
+
'skin-random',
|
|
4186
|
+
'skin-dark',
|
|
4187
|
+
'skin-light',
|
|
4188
|
+
'skin-vivid',
|
|
4189
|
+
'skin-natural',
|
|
4190
|
+
'skin-shaved',
|
|
4191
|
+
// 'resource-desert-petal',
|
|
4192
|
+
// 'resource-desert-stone',
|
|
4193
|
+
// 'resource-desert-polygon',
|
|
4194
|
+
// 'resource-desert-thread',
|
|
4195
|
+
// 'resource-grass-petal',
|
|
4196
|
+
// 'resource-grass-stone',
|
|
4197
|
+
// 'resource-grass-polygon',
|
|
4198
|
+
// 'resource-grass-thread',
|
|
4199
|
+
// 'resource-water-petal',
|
|
4200
|
+
// 'resource-water-stone',
|
|
4201
|
+
// 'resource-water-polygon',
|
|
4202
|
+
// 'resource-water-thread',
|
|
4203
|
+
// 'resource-stone-petal',
|
|
4204
|
+
// 'resource-stone-stone',
|
|
4205
|
+
// 'resource-stone-polygon',
|
|
4206
|
+
// 'resource-stone-thread',
|
|
4207
|
+
// 'resource-lava-petal',
|
|
4208
|
+
// 'resource-lava-stone',
|
|
4209
|
+
// 'resource-lava-polygon',
|
|
4210
|
+
// 'resource-lava-thread',
|
|
4211
|
+
];
|
|
4212
|
+
|
|
4213
|
+
const baseSeed = options.seed || 'example';
|
|
4214
|
+
const frameCount = options.frameCount || 2;
|
|
4215
|
+
const envFlag = options.envPath ? ` --env-path ${options.envPath}` : '';
|
|
4216
|
+
const devFlag = options.dev ? ' --dev' : '';
|
|
4217
|
+
|
|
4218
|
+
logger.info(
|
|
4219
|
+
`Generating ${SEMANTIC_TYPES.length} semantic examples (seed base: "${baseSeed}", frames: ${frameCount})`,
|
|
4220
|
+
);
|
|
4221
|
+
|
|
4222
|
+
for (const prefix of SEMANTIC_TYPES) {
|
|
4223
|
+
const seed = `${baseSeed}-${prefix}`;
|
|
4224
|
+
const cmd = `node bin/cyberia ol ${prefix} --generate --seed ${seed} --frame-count ${frameCount}${envFlag}${devFlag}`;
|
|
4225
|
+
logger.info(` → ${cmd}`);
|
|
4226
|
+
shellExec(cmd);
|
|
4227
|
+
}
|
|
4228
|
+
|
|
4229
|
+
logger.info('All semantic examples generated.');
|
|
4230
|
+
});
|
|
4231
|
+
|
|
4232
|
+
runner
|
|
4233
|
+
.command('build-manifest')
|
|
4234
|
+
.description('Build k8s resource manifest YAML files from templates')
|
|
4235
|
+
.action(() => {
|
|
4236
|
+
shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' --kubeadm`);
|
|
4237
|
+
shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' --kind --dev`);
|
|
4238
|
+
shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' --kubeadm`);
|
|
4239
|
+
shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' --kind --dev`);
|
|
4240
|
+
});
|
|
4241
|
+
|
|
1656
4242
|
if (underpostProgram.commands.find((c) => c._name == process.argv[2]))
|
|
1657
4243
|
throw new Error('Trigger underpost passthrough');
|
|
1658
4244
|
|