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.
Files changed (377) hide show
  1. package/.env.example +0 -2
  2. package/.github/workflows/engine-cyberia.cd.yml +10 -8
  3. package/.github/workflows/engine-cyberia.ci.yml +12 -29
  4. package/.github/workflows/ghpkg.ci.yml +4 -4
  5. package/.github/workflows/npmpkg.ci.yml +28 -11
  6. package/.github/workflows/publish.ci.yml +21 -2
  7. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -5
  8. package/.github/workflows/pwa-microservices-template-test.ci.yml +3 -3
  9. package/.github/workflows/release.cd.yml +14 -10
  10. package/CHANGELOG.md +783 -1
  11. package/CLI-HELP.md +95 -18
  12. package/Dockerfile +0 -2
  13. package/README.md +290 -220
  14. package/bin/build.js +24 -7
  15. package/bin/cyberia.js +2838 -252
  16. package/bin/deploy.js +747 -125
  17. package/bin/file.js +9 -0
  18. package/bin/index.js +2838 -252
  19. package/bin/vs.js +1 -1
  20. package/conf.js +99 -65
  21. package/deployment.yaml +18 -164
  22. package/hardhat/hardhat.config.js +13 -13
  23. package/hardhat/ignition/modules/ObjectLayerToken.js +1 -1
  24. package/hardhat/package-lock.json +2559 -5864
  25. package/hardhat/package.json +14 -23
  26. package/hardhat/scripts/deployObjectLayerToken.js +1 -1
  27. package/hardhat/test/ObjectLayerToken.js +4 -2
  28. package/hardhat/types/ethers-contracts/ObjectLayerToken.ts +690 -0
  29. package/hardhat/types/ethers-contracts/common.ts +92 -0
  30. package/hardhat/types/ethers-contracts/factories/ObjectLayerToken__factory.ts +1055 -0
  31. package/hardhat/types/ethers-contracts/factories/index.ts +4 -0
  32. package/hardhat/types/ethers-contracts/hardhat.d.ts +47 -0
  33. package/hardhat/types/ethers-contracts/index.ts +6 -0
  34. package/jsconfig.json +1 -1
  35. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +6 -5
  36. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +6 -5
  37. package/manifests/deployment/dd-cyberia-development/deployment.yaml +18 -164
  38. package/manifests/deployment/dd-cyberia-development/proxy.yaml +7 -79
  39. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  40. package/manifests/deployment/dd-test-development/deployment.yaml +112 -28
  41. package/manifests/deployment/dd-test-development/proxy.yaml +46 -1
  42. package/manifests/deployment/playwright/deployment.yaml +1 -1
  43. package/nodemon.json +1 -1
  44. package/package.json +39 -24
  45. package/proxy.yaml +7 -79
  46. package/scripts/k3s-node-setup.sh +2 -2
  47. package/scripts/nat-iptables.sh +103 -18
  48. package/scripts/rhel-grpc-setup.sh +56 -0
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +58 -14
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +23 -14
  51. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  52. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +148 -20
  53. package/src/api/core/core.controller.js +10 -10
  54. package/src/api/core/core.service.js +10 -10
  55. package/src/api/crypto/crypto.controller.js +8 -8
  56. package/src/api/crypto/crypto.service.js +8 -8
  57. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  58. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  59. package/src/api/cyberia-action/cyberia-action.router.js +27 -0
  60. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  61. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +93 -0
  62. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +36 -0
  63. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +29 -0
  64. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +51 -0
  65. package/src/api/cyberia-entity/cyberia-entity.controller.js +74 -0
  66. package/src/api/cyberia-entity/cyberia-entity.model.js +24 -0
  67. package/src/api/cyberia-entity/cyberia-entity.router.js +27 -0
  68. package/src/api/cyberia-entity/cyberia-entity.service.js +42 -0
  69. package/src/api/cyberia-instance/cyberia-fallback-world.js +178 -0
  70. package/src/api/cyberia-instance/cyberia-instance.controller.js +92 -0
  71. package/src/api/cyberia-instance/cyberia-instance.model.js +87 -0
  72. package/src/api/cyberia-instance/cyberia-instance.router.js +63 -0
  73. package/src/api/cyberia-instance/cyberia-instance.service.js +156 -0
  74. package/src/api/cyberia-instance/cyberia-portal-connector.js +260 -0
  75. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  76. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +74 -0
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +574 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +231 -0
  79. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +27 -0
  80. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +46 -0
  81. package/src/api/cyberia-map/cyberia-map.controller.js +79 -0
  82. package/src/api/cyberia-map/cyberia-map.model.js +30 -0
  83. package/src/api/cyberia-map/cyberia-map.router.js +40 -0
  84. package/src/api/cyberia-map/cyberia-map.service.js +74 -0
  85. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  86. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  87. package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
  88. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  89. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  90. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  91. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
  92. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  93. package/src/api/default/default.controller.js +10 -10
  94. package/src/api/default/default.service.js +10 -10
  95. package/src/api/document/document.controller.js +12 -12
  96. package/src/api/document/document.model.js +10 -16
  97. package/src/api/file/file.controller.js +8 -8
  98. package/src/api/file/file.model.js +10 -10
  99. package/src/api/file/file.ref.json +18 -0
  100. package/src/api/file/file.service.js +36 -36
  101. package/src/api/instance/instance.controller.js +10 -10
  102. package/src/api/instance/instance.model.js +4 -10
  103. package/src/api/instance/instance.service.js +10 -10
  104. package/src/api/ipfs/ipfs.controller.js +15 -36
  105. package/src/api/ipfs/ipfs.model.js +47 -47
  106. package/src/api/ipfs/ipfs.router.js +8 -13
  107. package/src/api/ipfs/ipfs.service.js +67 -129
  108. package/src/api/object-layer/object-layer.controller.js +12 -12
  109. package/src/api/object-layer/object-layer.model.js +4 -17
  110. package/src/api/object-layer/object-layer.router.js +30 -0
  111. package/src/api/object-layer/object-layer.service.js +126 -43
  112. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  113. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  114. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
  115. package/src/api/test/test.controller.js +8 -8
  116. package/src/api/test/test.service.js +8 -8
  117. package/src/api/user/guest.service.js +99 -0
  118. package/src/api/user/user.controller.js +6 -6
  119. package/src/api/user/user.model.js +8 -13
  120. package/src/api/user/user.service.js +11 -27
  121. package/src/cli/cluster.js +68 -21
  122. package/src/cli/db.js +753 -825
  123. package/src/cli/deploy.js +215 -125
  124. package/src/cli/env.js +29 -0
  125. package/src/cli/fs.js +82 -8
  126. package/src/cli/image.js +43 -1
  127. package/src/cli/index.js +74 -3
  128. package/src/cli/kubectl.js +211 -0
  129. package/src/cli/release.js +340 -0
  130. package/src/cli/repository.js +475 -74
  131. package/src/cli/run.js +582 -43
  132. package/src/cli/secrets.js +73 -0
  133. package/src/cli/ssh.js +1 -1
  134. package/src/cli/static.js +43 -115
  135. package/src/cli/test.js +3 -3
  136. package/src/client/Cryptokoyn.index.js +18 -22
  137. package/src/client/CyberiaPortal.index.js +19 -24
  138. package/src/client/Default.index.js +21 -34
  139. package/src/client/Itemledger.index.js +20 -27
  140. package/src/client/Underpost.index.js +19 -24
  141. package/src/client/components/core/404.js +4 -4
  142. package/src/client/components/core/500.js +4 -4
  143. package/src/client/components/core/Account.js +73 -60
  144. package/src/client/components/core/AgGrid.js +23 -33
  145. package/src/client/components/core/Alert.js +12 -13
  146. package/src/client/components/core/AppStore.js +69 -0
  147. package/src/client/components/core/Auth.js +35 -37
  148. package/src/client/components/core/Badge.js +7 -13
  149. package/src/client/components/core/BtnIcon.js +15 -17
  150. package/src/client/components/core/CalendarCore.js +43 -64
  151. package/src/client/components/core/Chat.js +13 -15
  152. package/src/client/components/core/ClientEvents.js +87 -0
  153. package/src/client/components/core/ColorPaletteElement.js +309 -0
  154. package/src/client/components/core/Content.js +17 -14
  155. package/src/client/components/core/Css.js +15 -71
  156. package/src/client/components/core/CssCore.js +12 -16
  157. package/src/client/components/core/D3Chart.js +4 -4
  158. package/src/client/components/core/Docs.js +64 -91
  159. package/src/client/components/core/DropDown.js +194 -96
  160. package/src/client/components/core/EventBus.js +92 -0
  161. package/src/client/components/core/EventsUI.js +14 -17
  162. package/src/client/components/core/FileExplorer.js +96 -228
  163. package/src/client/components/core/FullScreen.js +47 -75
  164. package/src/client/components/core/Input.js +24 -69
  165. package/src/client/components/core/Keyboard.js +26 -19
  166. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  167. package/src/client/components/core/LoadingAnimation.js +25 -31
  168. package/src/client/components/core/LogIn.js +43 -43
  169. package/src/client/components/core/LogOut.js +25 -16
  170. package/src/client/components/core/Modal.js +462 -179
  171. package/src/client/components/core/NotificationManager.js +14 -18
  172. package/src/client/components/core/Panel.js +54 -51
  173. package/src/client/components/core/PanelForm.js +44 -144
  174. package/src/client/components/core/Polyhedron.js +110 -214
  175. package/src/client/components/core/PublicProfile.js +39 -32
  176. package/src/client/components/core/Recover.js +48 -44
  177. package/src/client/components/core/Responsive.js +88 -32
  178. package/src/client/components/core/RichText.js +9 -18
  179. package/src/client/components/core/Router.js +24 -3
  180. package/src/client/components/core/SearchBox.js +37 -37
  181. package/src/client/components/core/SignUp.js +39 -30
  182. package/src/client/components/core/SocketIo.js +112 -30
  183. package/src/client/components/core/SocketIoHandler.js +75 -0
  184. package/src/client/components/core/Stream.js +143 -95
  185. package/src/client/components/core/ToggleSwitch.js +8 -20
  186. package/src/client/components/core/ToolTip.js +5 -17
  187. package/src/client/components/core/Translate.js +56 -59
  188. package/src/client/components/core/Validator.js +26 -16
  189. package/src/client/components/core/Wallet.js +15 -26
  190. package/src/client/components/core/Webhook.js +40 -7
  191. package/src/client/components/core/Worker.js +163 -27
  192. package/src/client/components/core/windowGetDimensions.js +7 -7
  193. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +59 -59
  194. package/src/client/components/cryptokoyn/AppStoreCryptokoyn.js +5 -0
  195. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  196. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +9 -7
  197. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +8 -6
  198. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  199. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  200. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  201. package/src/client/components/cryptokoyn/SocketIoCryptokoyn.js +3 -51
  202. package/src/client/components/cyberia/InstanceEngineCyberia.js +781 -0
  203. package/src/client/components/cyberia/MapEngineCyberia.js +1836 -2
  204. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  205. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1220 -99
  206. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +252 -316
  207. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +136 -103
  208. package/src/client/components/cyberia-portal/AppStoreCyberiaPortal.js +5 -0
  209. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +462 -32
  210. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  211. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +9 -7
  212. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +8 -6
  213. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  214. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  215. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  216. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  217. package/src/client/components/cyberia-portal/SocketIoCyberiaPortal.js +3 -49
  218. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -4
  219. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +91 -91
  220. package/src/client/components/default/AppStoreDefault.js +5 -0
  221. package/src/client/components/default/CssDefault.js +12 -12
  222. package/src/client/components/default/LogInDefault.js +9 -7
  223. package/src/client/components/default/LogOutDefault.js +8 -6
  224. package/src/client/components/default/RouterDefault.js +47 -0
  225. package/src/client/components/default/SettingsDefault.js +4 -4
  226. package/src/client/components/default/SignUpDefault.js +6 -4
  227. package/src/client/components/default/SocketIoDefault.js +3 -51
  228. package/src/client/components/default/TranslateDefault.js +3 -3
  229. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +59 -59
  230. package/src/client/components/itemledger/AppStoreItemledger.js +5 -0
  231. package/src/client/components/itemledger/CssItemledger.js +15 -15
  232. package/src/client/components/itemledger/LogInItemledger.js +9 -7
  233. package/src/client/components/itemledger/LogOutItemledger.js +8 -6
  234. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  235. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  236. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  237. package/src/client/components/itemledger/SocketIoItemledger.js +3 -51
  238. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  239. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +92 -92
  240. package/src/client/components/underpost/AppStoreUnderpost.js +5 -0
  241. package/src/client/components/underpost/CssUnderpost.js +14 -14
  242. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  243. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  244. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  245. package/src/client/components/underpost/LogInUnderpost.js +9 -7
  246. package/src/client/components/underpost/LogOutUnderpost.js +8 -6
  247. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  248. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  249. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  250. package/src/client/components/underpost/SocketIoUnderpost.js +3 -51
  251. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  252. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  253. package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
  254. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
  255. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
  256. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
  257. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  258. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  259. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  260. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  261. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  262. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  263. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  264. package/src/client/services/core/core.service.js +35 -55
  265. package/src/client/services/crypto/crypto.service.js +8 -13
  266. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  267. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +99 -0
  268. package/src/client/services/cyberia-entity/cyberia-entity.management.js +57 -0
  269. package/src/client/services/cyberia-entity/cyberia-entity.service.js +99 -0
  270. package/src/client/services/cyberia-instance/cyberia-instance.management.js +194 -0
  271. package/src/client/services/cyberia-instance/cyberia-instance.service.js +116 -0
  272. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +99 -0
  273. package/src/client/services/cyberia-map/cyberia-map.management.js +193 -0
  274. package/src/client/services/cyberia-map/cyberia-map.service.js +120 -0
  275. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  276. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  277. package/src/client/services/default/default.management.js +159 -267
  278. package/src/client/services/default/default.service.js +10 -16
  279. package/src/client/services/document/document.service.js +14 -19
  280. package/src/client/services/file/file.service.js +8 -13
  281. package/src/client/services/instance/instance.management.js +6 -6
  282. package/src/client/services/instance/instance.service.js +10 -15
  283. package/src/client/services/ipfs/ipfs.service.js +14 -40
  284. package/src/client/services/object-layer/object-layer.management.js +14 -14
  285. package/src/client/services/object-layer/object-layer.service.js +39 -24
  286. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  287. package/src/client/services/test/test.service.js +8 -13
  288. package/src/client/services/user/guest.service.js +86 -0
  289. package/src/client/services/user/user.management.js +6 -6
  290. package/src/client/services/user/user.service.js +14 -20
  291. package/src/client/ssr/body/404.js +3 -3
  292. package/src/client/ssr/body/500.js +3 -3
  293. package/src/client/ssr/body/CacheControl.js +5 -2
  294. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  295. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  296. package/src/client/ssr/head/PwaItemledger.js +197 -60
  297. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  298. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  299. package/src/client/ssr/offline/Maintenance.js +12 -11
  300. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  301. package/src/client/ssr/pages/CyberiaServerMetrics.js +1 -1
  302. package/src/client/ssr/pages/Test.js +2 -2
  303. package/src/client/sw/core.sw.js +212 -0
  304. package/src/grpc/cyberia/grpc-server.js +642 -0
  305. package/src/index.js +24 -1
  306. package/src/runtime/cyberia-client/Dockerfile +80 -0
  307. package/src/runtime/cyberia-server/Dockerfile +37 -0
  308. package/src/runtime/express/Dockerfile +5 -1
  309. package/src/runtime/express/Express.js +18 -1
  310. package/src/runtime/lampp/Dockerfile +17 -5
  311. package/src/runtime/lampp/Lampp.js +27 -4
  312. package/src/runtime/wp/Dockerfile +62 -0
  313. package/src/runtime/wp/Wp.js +639 -0
  314. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  315. package/src/server/auth.js +24 -1
  316. package/src/server/backup.js +37 -9
  317. package/src/server/client-build-docs.js +52 -46
  318. package/src/server/client-build.js +356 -82
  319. package/src/server/client-formatted.js +140 -57
  320. package/src/server/conf.js +29 -13
  321. package/src/server/cron.js +25 -23
  322. package/src/server/data-query.js +32 -20
  323. package/src/server/dns.js +24 -1
  324. package/src/server/ipfs-client.js +253 -89
  325. package/src/server/object-layer.js +150 -114
  326. package/src/server/peer.js +8 -0
  327. package/src/server/process.js +13 -27
  328. package/src/server/runtime.js +25 -1
  329. package/src/server/semantic-layer-generator-floor.js +319 -0
  330. package/src/server/semantic-layer-generator-resource.js +259 -0
  331. package/src/server/semantic-layer-generator-skin.js +1164 -0
  332. package/src/server/semantic-layer-generator.js +211 -542
  333. package/src/server/shape-generator.js +108 -0
  334. package/src/server/start.js +19 -5
  335. package/src/server/valkey.js +141 -235
  336. package/src/ws/IoInterface.js +1 -10
  337. package/src/ws/IoServer.js +14 -33
  338. package/src/ws/core/channels/core.ws.chat.js +65 -20
  339. package/src/ws/core/channels/core.ws.mailer.js +113 -32
  340. package/src/ws/core/channels/core.ws.stream.js +90 -31
  341. package/src/ws/core/core.ws.connection.js +12 -33
  342. package/src/ws/core/core.ws.emit.js +10 -26
  343. package/src/ws/core/core.ws.server.js +25 -58
  344. package/src/ws/default/channels/default.ws.main.js +53 -12
  345. package/src/ws/default/default.ws.connection.js +26 -13
  346. package/src/ws/default/default.ws.server.js +30 -12
  347. package/tsconfig.docs.json +15 -0
  348. package/typedoc.dd-cyberia.json +29 -0
  349. package/typedoc.json +29 -0
  350. package/WHITE-PAPER.md +0 -1540
  351. package/hardhat/README.md +0 -531
  352. package/hardhat/WHITE-PAPER.md +0 -1540
  353. package/jsdoc.dd-cyberia.json +0 -59
  354. package/jsdoc.json +0 -59
  355. package/src/api/object-layer/README.md +0 -347
  356. package/src/client/components/core/ColorPalette.js +0 -5267
  357. package/src/client/components/core/JoyStick.js +0 -80
  358. package/src/client/components/cryptokoyn/CommonCryptokoyn.js +0 -29
  359. package/src/client/components/cryptokoyn/ElementsCryptokoyn.js +0 -38
  360. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  361. package/src/client/components/cyberia-portal/ElementsCyberiaPortal.js +0 -38
  362. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -58
  363. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  364. package/src/client/components/default/ElementsDefault.js +0 -38
  365. package/src/client/components/default/RoutesDefault.js +0 -49
  366. package/src/client/components/itemledger/CommonItemledger.js +0 -29
  367. package/src/client/components/itemledger/ElementsItemledger.js +0 -38
  368. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  369. package/src/client/components/underpost/CommonUnderpost.js +0 -29
  370. package/src/client/components/underpost/ElementsUnderpost.js +0 -38
  371. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  372. package/src/client/sw/default.sw.js +0 -127
  373. package/src/client/sw/template.sw.js +0 -84
  374. package/src/ws/core/management/core.ws.chat.js +0 -8
  375. package/src/ws/core/management/core.ws.mailer.js +0 -16
  376. package/src/ws/core/management/core.ws.stream.js +0 -8
  377. package/src/ws/default/management/default.ws.main.js +0 -8
package/bin/index.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('--import [object-layer-type]', 'Commas separated object layer types e.g. skin,floors')
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|string} options.import - Object layer types to import (e.g., 'all', 'skin,floor') or `false`.
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 ? options.mongoHost : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
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
- await ObjectLayer.deleteMany();
203
- await ObjectLayerRenderFrames.deleteMany();
204
- shellExec(`cd src/client/public/cyberia && underpost run clean .`);
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.import === 'all';
741
+ const isImportAll = options.importTypes === 'all';
214
742
 
215
743
  /** @type {string[]} */
216
- const argItemTypes = isImportAll ? Object.keys(itemTypes) : options.import.split(',');
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
- // Use the createObjectLayerDocuments which handles atlas generation
265
- // Since we're in CLI context without a full Express req/res, we build a minimal
266
- // atlas generation flow using AtlasSpriteSheetGenerator directly after creation.
267
- const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
268
- ObjectLayer,
269
- ObjectLayerRenderFrames,
270
- objectLayerRenderFramesData: entry.objectLayerRenderFramesData,
271
- objectLayerData: { data: entry.data },
272
- createOptions: {
273
- generateAtlas: false,
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
- // Generate atlas sprite sheet for individual imports
278
- try {
279
- const itemKey = objectLayer.data.item.id;
280
- const populatedObjectLayer = await ObjectLayer.findById(objectLayer._id).populate(
281
- 'objectLayerRenderFramesId',
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
- const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
285
- populatedObjectLayer.objectLayerRenderFramesId,
286
- itemKey,
287
- 20,
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
- const fileDoc = await new File({
291
- name: `${itemKey}-atlas.png`,
292
- data: buffer,
293
- size: buffer.length,
294
- mimetype: 'image/png',
295
- md5: crypto.createHash('md5').update(buffer).digest('hex'),
296
- }).save();
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
- // Pin atlas PNG to IPFS
299
- let importAtlasCid = '';
300
- let importAtlasMetadataCid = '';
301
- try {
302
- const ipfsResult = await IpfsClient.addBufferToIpfs(
303
- buffer,
304
- `${itemKey}_atlas_sprite_sheet.png`,
305
- `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
306
- );
307
- if (ipfsResult) {
308
- importAtlasCid = ipfsResult.cid;
309
- logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${importAtlasCid}`);
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
- } catch (ipfsError) {
312
- logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
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
- // Pin atlas metadata JSON to IPFS (fast-json-stable-stringify)
316
- try {
317
- const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
318
- metadata,
319
- `${itemKey}_atlas_sprite_sheet_metadata.json`,
320
- `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
321
- );
322
- if (metadataIpfsResult) {
323
- importAtlasMetadataCid = metadataIpfsResult.cid;
324
- logger.info(`Atlas metadata pinned to IPFS – CID: ${importAtlasMetadataCid}`);
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
- } catch (ipfsError) {
327
- logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
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
- let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
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
- if (atlasDoc) {
333
- atlasDoc.fileId = fileDoc._id;
334
- atlasDoc.cid = importAtlasCid;
335
- atlasDoc.metadata = metadata;
336
- await atlasDoc.save();
337
- logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
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
- populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
348
- if (!populatedObjectLayer.data.render) populatedObjectLayer.data.render = {};
349
- populatedObjectLayer.data.render.cid = importAtlasCid;
350
- populatedObjectLayer.data.render.metadataCid = importAtlasMetadataCid;
351
- populatedObjectLayer.markModified('data.render');
352
- await populatedObjectLayer.save();
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
- logger.info(`Atlas sprite sheet completed for item: ${itemKey}`);
355
- } catch (atlasError) {
356
- logger.error(`Failed to generate atlas for ${objectLayerId}:`, atlasError);
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
- console.log(objectLayer);
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 { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
363
- ObjectLayer,
364
- ObjectLayerRenderFrames,
365
- objectLayerRenderFramesData: entry.objectLayerRenderFramesData,
366
- objectLayerData: { data: entry.data },
367
- createOptions: {
368
- generateAtlas: false,
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(`ObjectLayer created (atlas skipped for bulk import): ${objectLayerId}`);
373
- console.log(objectLayer);
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
- logger.info(
681
- `Generated ${multiFrameResult.frameCount} frame(s) with ${multiFrameResult.objectLayerRenderFramesData.colors.length} unique colors`,
682
- );
2900
+ extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
2901
+ await upsertImportedPin({ cid: finalCid, resourceType: doc.resourceType, mfsPath: primaryPath });
2902
+ ipfsCount++;
2903
+ }
683
2904
 
684
- // 2. Write static asset PNGs to both source and public directories
685
- const srcBasePath = './src/client/public/cyberia/';
686
- const publicBasePath = `./public/${host}${path}`;
687
- const writtenFiles = await ObjectLayerEngine.writeStaticFrameAssets({
688
- basePaths: [srcBasePath, publicBasePath],
689
- itemType: descriptor.itemType,
690
- itemId: uniqueItemId,
691
- objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
692
- objectLayerData: multiFrameResult.objectLayerData,
693
- cellPixelDim: 20,
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
- logger.info(`Wrote ${writtenFiles.length} asset file(s):`);
697
- for (const f of writtenFiles) {
698
- logger.info(` → ${f}`);
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
- // 3. Persist to MongoDB (ObjectLayerRenderFrames + ObjectLayer)
702
- const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
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
- logger.info(`ObjectLayer persisted to MongoDB: ${objectLayer._id} (item: ${objectLayer.data.item.id})`);
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
- // 4. Generate atlas sprite sheet + pin to IPFS
715
- let atlasCid = '';
716
- try {
717
- const atlasItemKey = objectLayer.data.item.id;
718
- const populatedObjectLayer = await ObjectLayer.findById(objectLayer._id).populate(
719
- 'objectLayerRenderFramesId',
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
- const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
723
- populatedObjectLayer.objectLayerRenderFramesId,
724
- atlasItemKey,
725
- 20,
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
- // Save atlas file to File collection
729
- const fileDoc = await new File({
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
- // Pin atlas PNG to IPFS + copy into MFS
738
- let atlasMetadataCid = '';
739
- try {
740
- const ipfsResult = await IpfsClient.addBufferToIpfs(
741
- buffer,
742
- `${atlasItemKey}_atlas_sprite_sheet.png`,
743
- `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
744
- );
745
- if (ipfsResult) {
746
- atlasCid = ipfsResult.cid;
747
- logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
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
- // Pin atlas metadata JSON to IPFS (fast-json-stable-stringify)
754
- try {
755
- const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
756
- metadata,
757
- `${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
758
- `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet_metadata.json`,
759
- );
760
- if (metadataIpfsResult) {
761
- atlasMetadataCid = metadataIpfsResult.cid;
762
- logger.info(`Atlas metadata pinned to IPFS CID: ${atlasMetadataCid}`);
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
- // Upsert AtlasSpriteSheet document (with CID)
769
- let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': atlasItemKey });
770
- if (atlasDoc) {
771
- atlasDoc.fileId = fileDoc._id;
772
- atlasDoc.cid = atlasCid;
773
- atlasDoc.metadata = metadata;
774
- await atlasDoc.save();
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
- // Link atlas to ObjectLayer and set data.render.cid + data.render.metadataCid
786
- populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
787
- if (!populatedObjectLayer.data.render) populatedObjectLayer.data.render = {};
788
- populatedObjectLayer.data.render.cid = atlasCid;
789
- populatedObjectLayer.data.render.metadataCid = atlasMetadataCid;
790
- populatedObjectLayer.markModified('data.render');
791
- await populatedObjectLayer.save();
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
- // Also write atlas PNG to both static asset directories
794
- for (const bp of [srcBasePath, publicBasePath]) {
795
- const atlasOutputDir = nodePath.join(bp, 'assets', descriptor.itemType, uniqueItemId);
796
- await fs.ensureDir(atlasOutputDir);
797
- const atlasOutputPath = nodePath.join(atlasOutputDir, `${atlasItemKey}-atlas.png`);
798
- await fs.writeFile(atlasOutputPath, buffer);
799
- logger.info(
800
- `Atlas sprite sheet generated: ${metadata.atlasWidth}x${metadata.atlasHeight} → ${atlasOutputPath}`,
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
- // 5. Compute final SHA-256, pin OL data JSON to IPFS, create pin records
808
- try {
809
- const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
810
- const finalized = await ObjectLayerEngine.computeAndSaveFinalSha256({
811
- objectLayer: finalObjectLayer,
812
- ipfsClient: IpfsClient,
813
- createPinRecord,
814
- userId: undefined, // CLI context has no authenticated user
815
- options: { host, path },
816
- });
817
- logger.info(`Final SHA-256: ${finalized.sha256}`);
818
- if (finalized.cid) {
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
- logger.info(`✓ Generation complete for "${uniqueItemId}" (seed: ${genSeed}, frames: ${genFrameCount})`);
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
- // Log per-layer summary
828
- if (multiFrameResult.frames.length > 0) {
829
- const firstFrame = multiFrameResult.frames[0];
830
- for (const layer of firstFrame.layers) {
831
- logger.info(` Layer "${layer.layerKey}" (${layer.layerId}): ${layer.keys.length} element(s)`);
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
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
837
- },
838
- )
839
- .description('Object layer management');
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