cyberia 3.2.5 → 3.2.12

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 (381) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +8 -2
  2. package/.github/workflows/npmpkg.ci.yml +1 -0
  3. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  4. package/.github/workflows/release.cd.yml +2 -2
  5. package/.vscode/extensions.json +9 -9
  6. package/.vscode/settings.json +20 -4
  7. package/CHANGELOG.md +563 -1
  8. package/CLI-HELP.md +130 -34
  9. package/Dockerfile +0 -4
  10. package/README.md +194 -607
  11. package/bin/build.js +42 -12
  12. package/bin/build.template.js +187 -0
  13. package/bin/cyberia.js +1367 -281
  14. package/bin/deploy.js +582 -3
  15. package/bin/index.js +1367 -281
  16. package/bump.config.js +26 -0
  17. package/conf.js +195 -111
  18. package/deployment.yaml +6 -222
  19. package/hardhat/package-lock.json +118 -149
  20. package/hardhat/package.json +5 -4
  21. package/jsconfig.json +1 -1
  22. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  23. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  24. package/manifests/deployment/dd-cyberia-development/deployment.yaml +6 -222
  25. package/manifests/deployment/dd-cyberia-development/proxy.yaml +10 -118
  26. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  27. package/manifests/deployment/dd-test-development/deployment.yaml +138 -66
  28. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  29. package/manifests/kind-config-dev.yaml +8 -0
  30. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  31. package/manifests/mongodb/pv-pvc.yaml +44 -8
  32. package/manifests/mongodb/statefulset.yaml +55 -68
  33. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  34. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  35. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  36. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  37. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  38. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  39. package/manifests/valkey/statefulset.yaml +1 -1
  40. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  41. package/package.json +45 -24
  42. package/proxy.yaml +10 -118
  43. package/scripts/ipxe-setup.sh +52 -49
  44. package/scripts/k3s-node-setup.sh +83 -48
  45. package/scripts/lxd-vm-setup.sh +193 -8
  46. package/scripts/maas-nat-firewalld.sh +145 -0
  47. package/scripts/nat-iptables.sh +103 -18
  48. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +18 -18
  49. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -14
  50. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  51. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +91 -36
  52. package/src/api/core/core.controller.js +10 -10
  53. package/src/api/core/core.router.js +19 -14
  54. package/src/api/core/core.service.js +15 -15
  55. package/src/api/crypto/crypto.controller.js +8 -8
  56. package/src/api/crypto/crypto.router.js +18 -12
  57. package/src/api/crypto/crypto.service.js +11 -11
  58. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  59. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  60. package/src/api/cyberia-action/cyberia-action.router.js +31 -0
  61. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  62. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  63. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  64. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +13 -13
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +11 -11
  68. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  69. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +22 -22
  70. package/src/api/cyberia-entity/cyberia-entity.controller.js +10 -10
  71. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  72. package/src/api/cyberia-entity/cyberia-entity.service.js +15 -15
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +83 -198
  74. package/src/api/cyberia-instance/cyberia-instance.controller.js +14 -14
  75. package/src/api/cyberia-instance/cyberia-instance.model.js +3 -0
  76. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  77. package/src/api/cyberia-instance/cyberia-instance.service.js +32 -67
  78. package/src/api/cyberia-instance/cyberia-portal-connector.js +20 -246
  79. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  80. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +10 -10
  81. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +18 -49
  82. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  83. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +19 -15
  84. package/src/api/cyberia-map/cyberia-map.controller.js +10 -10
  85. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  86. package/src/api/cyberia-map/cyberia-map.service.js +17 -17
  87. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  88. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  89. package/src/api/cyberia-quest/cyberia-quest.router.js +31 -0
  90. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  91. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  92. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  93. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +31 -0
  94. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  95. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +451 -0
  96. package/src/api/default/default.controller.js +10 -10
  97. package/src/api/default/default.router.js +22 -18
  98. package/src/api/default/default.service.js +15 -15
  99. package/src/api/document/document.controller.js +12 -12
  100. package/src/api/document/document.model.js +10 -16
  101. package/src/api/document/document.router.js +28 -23
  102. package/src/api/document/document.service.js +100 -23
  103. package/src/api/file/file.controller.js +8 -8
  104. package/src/api/file/file.model.js +10 -10
  105. package/src/api/file/file.router.js +19 -13
  106. package/src/api/file/file.service.js +45 -43
  107. package/src/api/instance/instance.controller.js +10 -10
  108. package/src/api/instance/instance.model.js +4 -10
  109. package/src/api/instance/instance.router.js +29 -24
  110. package/src/api/instance/instance.service.js +16 -16
  111. package/src/api/ipfs/ipfs.controller.js +12 -12
  112. package/src/api/ipfs/ipfs.model.js +4 -13
  113. package/src/api/ipfs/ipfs.router.js +21 -16
  114. package/src/api/ipfs/ipfs.service.js +22 -36
  115. package/src/api/object-layer/object-layer.controller.js +12 -12
  116. package/src/api/object-layer/object-layer.model.js +4 -17
  117. package/src/api/object-layer/object-layer.router.js +512 -507
  118. package/src/api/object-layer/object-layer.service.js +29 -26
  119. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  120. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  121. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  122. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +19 -15
  123. package/src/api/test/test.controller.js +8 -8
  124. package/src/api/test/test.router.js +17 -12
  125. package/src/api/test/test.service.js +8 -8
  126. package/src/api/types.js +24 -0
  127. package/src/api/user/guest.service.js +100 -0
  128. package/src/api/user/user.controller.js +6 -6
  129. package/src/api/user/user.model.js +8 -13
  130. package/src/api/user/user.router.js +297 -288
  131. package/src/api/user/user.service.js +103 -55
  132. package/src/cli/baremetal.js +132 -101
  133. package/src/cli/cluster.js +732 -217
  134. package/src/cli/db.js +106 -62
  135. package/src/cli/deploy.js +260 -149
  136. package/src/cli/fs.js +90 -9
  137. package/src/cli/image.js +43 -1
  138. package/src/cli/index.js +106 -16
  139. package/src/cli/ipfs.js +4 -6
  140. package/src/cli/kubectl.js +4 -1
  141. package/src/cli/lxd.js +1099 -223
  142. package/src/cli/monitor.js +9 -3
  143. package/src/cli/release.js +336 -86
  144. package/src/cli/repository.js +136 -53
  145. package/src/cli/run.js +599 -76
  146. package/src/cli/secrets.js +11 -2
  147. package/src/cli/ssh.js +1 -1
  148. package/src/cli/static.js +43 -115
  149. package/src/cli/test.js +9 -3
  150. package/src/client/Cryptokoyn.index.js +18 -21
  151. package/src/client/CyberiaPortal.index.js +19 -23
  152. package/src/client/Default.index.js +30 -36
  153. package/src/client/Itemledger.index.js +20 -26
  154. package/src/client/Underpost.index.js +19 -23
  155. package/src/client/components/core/404.js +4 -4
  156. package/src/client/components/core/500.js +4 -4
  157. package/src/client/components/core/Account.js +73 -60
  158. package/src/client/components/core/AgGrid.js +23 -33
  159. package/src/client/components/core/Alert.js +12 -13
  160. package/src/client/components/core/AppStore.js +1 -1
  161. package/src/client/components/core/Auth.js +40 -37
  162. package/src/client/components/core/Badge.js +7 -13
  163. package/src/client/components/core/BtnIcon.js +15 -17
  164. package/src/client/components/core/CalendarCore.js +42 -63
  165. package/src/client/components/core/Chat.js +13 -15
  166. package/src/client/components/core/ClientEvents.js +163 -0
  167. package/src/client/components/core/ColorPaletteElement.js +309 -0
  168. package/src/client/components/core/Content.js +17 -14
  169. package/src/client/components/core/Css.js +15 -71
  170. package/src/client/components/core/CssCore.js +12 -16
  171. package/src/client/components/core/D3Chart.js +4 -4
  172. package/src/client/components/core/Docs.js +64 -91
  173. package/src/client/components/core/DropDown.js +69 -91
  174. package/src/client/components/core/EventBus.js +96 -0
  175. package/src/client/components/core/EventsUI.js +14 -17
  176. package/src/client/components/core/FileExplorer.js +96 -228
  177. package/src/client/components/core/FullScreen.js +47 -75
  178. package/src/client/components/core/Input.js +24 -69
  179. package/src/client/components/core/Keyboard.js +25 -18
  180. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  181. package/src/client/components/core/LoadingAnimation.js +25 -31
  182. package/src/client/components/core/LogIn.js +41 -41
  183. package/src/client/components/core/LogOut.js +23 -14
  184. package/src/client/components/core/Modal.js +544 -219
  185. package/src/client/components/core/NotificationManager.js +14 -18
  186. package/src/client/components/core/Panel.js +54 -50
  187. package/src/client/components/core/PanelForm.js +81 -177
  188. package/src/client/components/core/Polyhedron.js +110 -214
  189. package/src/client/components/core/PublicProfile.js +39 -32
  190. package/src/client/components/core/Recover.js +48 -44
  191. package/src/client/components/core/Responsive.js +88 -32
  192. package/src/client/components/core/RichText.js +9 -18
  193. package/src/client/components/core/Router.js +24 -3
  194. package/src/client/components/core/SearchBox.js +37 -37
  195. package/src/client/components/core/SignUp.js +39 -30
  196. package/src/client/components/core/SocketIo.js +31 -2
  197. package/src/client/components/core/SocketIoHandler.js +6 -6
  198. package/src/client/components/core/ToggleSwitch.js +8 -20
  199. package/src/client/components/core/ToolTip.js +5 -17
  200. package/src/client/components/core/Translate.js +56 -59
  201. package/src/client/components/core/Validator.js +26 -16
  202. package/src/client/components/core/Wallet.js +15 -26
  203. package/src/client/components/core/Worker.js +211 -276
  204. package/src/client/components/core/windowGetDimensions.js +7 -7
  205. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +57 -57
  206. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  207. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +6 -4
  208. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +6 -4
  209. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  210. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  211. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  212. package/src/client/components/cyberia/InstanceEngineCyberia.js +141 -60
  213. package/src/client/components/cyberia/MapEngineCyberia.js +691 -214
  214. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  215. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1204 -94
  216. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +196 -298
  217. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  218. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +102 -102
  219. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  220. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +6 -4
  221. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +6 -4
  222. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  223. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  224. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  225. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  226. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -4
  227. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  228. package/src/client/components/default/CssDefault.js +12 -12
  229. package/src/client/components/default/LogInDefault.js +6 -4
  230. package/src/client/components/default/LogOutDefault.js +6 -4
  231. package/src/client/components/default/RouterDefault.js +47 -0
  232. package/src/client/components/default/SettingsDefault.js +4 -4
  233. package/src/client/components/default/SignUpDefault.js +6 -4
  234. package/src/client/components/default/TranslateDefault.js +3 -3
  235. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +57 -57
  236. package/src/client/components/itemledger/CssItemledger.js +15 -15
  237. package/src/client/components/itemledger/LogInItemledger.js +6 -4
  238. package/src/client/components/itemledger/LogOutItemledger.js +6 -4
  239. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  240. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  241. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  242. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  243. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +88 -88
  244. package/src/client/components/underpost/CssUnderpost.js +14 -14
  245. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  246. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  247. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  248. package/src/client/components/underpost/LogInUnderpost.js +6 -4
  249. package/src/client/components/underpost/LogOutUnderpost.js +6 -4
  250. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  251. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  252. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  253. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  254. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  255. package/src/client/public/cyberia-docs/ARCHITECTURE.md +83 -0
  256. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +204 -0
  257. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +291 -0
  258. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +278 -0
  259. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  260. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  261. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  262. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  263. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  264. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  265. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  266. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  267. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  268. package/src/client/services/core/core.service.js +17 -49
  269. package/src/client/services/crypto/crypto.service.js +8 -13
  270. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  271. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  272. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +10 -16
  273. package/src/client/services/cyberia-entity/cyberia-entity.management.js +5 -5
  274. package/src/client/services/cyberia-entity/cyberia-entity.service.js +10 -16
  275. package/src/client/services/cyberia-instance/cyberia-instance.management.js +6 -6
  276. package/src/client/services/cyberia-instance/cyberia-instance.service.js +12 -18
  277. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +10 -16
  278. package/src/client/services/cyberia-map/cyberia-map.management.js +6 -6
  279. package/src/client/services/cyberia-map/cyberia-map.service.js +12 -18
  280. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  281. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  282. package/src/client/services/default/default.management.js +159 -267
  283. package/src/client/services/default/default.service.js +10 -16
  284. package/src/client/services/document/document.service.js +14 -19
  285. package/src/client/services/file/file.service.js +8 -13
  286. package/src/client/services/instance/instance.management.js +5 -5
  287. package/src/client/services/instance/instance.service.js +10 -15
  288. package/src/client/services/ipfs/ipfs.service.js +12 -18
  289. package/src/client/services/object-layer/object-layer.management.js +12 -12
  290. package/src/client/services/object-layer/object-layer.service.js +20 -26
  291. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  292. package/src/client/services/test/test.service.js +8 -13
  293. package/src/client/services/user/guest.service.js +86 -0
  294. package/src/client/services/user/user.management.js +5 -5
  295. package/src/client/services/user/user.service.js +14 -20
  296. package/src/client/ssr/body/404.js +3 -3
  297. package/src/client/ssr/body/500.js +3 -3
  298. package/src/client/ssr/body/CacheControl.js +5 -2
  299. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  300. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  301. package/src/client/ssr/head/PwaItemledger.js +197 -60
  302. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  303. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  304. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  305. package/src/client/ssr/{offline → views}/Maintenance.js +12 -11
  306. package/src/client/ssr/{offline → views}/NoNetworkConnection.js +3 -3
  307. package/src/client/ssr/{pages → views}/Test.js +2 -2
  308. package/src/client/sw/core.sw.js +274 -0
  309. package/src/db/DataBaseProvider.js +115 -15
  310. package/src/db/mariadb/MariaDB.js +2 -1
  311. package/src/db/mongo/MongoBootstrap.js +657 -0
  312. package/src/db/mongo/MongooseDB.js +129 -21
  313. package/src/grpc/cyberia/grpc-server.js +185 -105
  314. package/src/index.js +1 -1
  315. package/src/runtime/cyberia-client/Dockerfile +101 -0
  316. package/src/runtime/cyberia-client/Dockerfile.dev +82 -0
  317. package/src/runtime/cyberia-server/Dockerfile +62 -0
  318. package/src/runtime/cyberia-server/Dockerfile.dev +71 -0
  319. package/src/runtime/express/Dockerfile +4 -4
  320. package/src/runtime/express/Express.js +2 -2
  321. package/src/runtime/lampp/Dockerfile +8 -7
  322. package/src/runtime/wp/Dockerfile +11 -17
  323. package/src/runtime/wp/Wp.js +8 -5
  324. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  325. package/src/server/auth.js +2 -2
  326. package/src/server/client-build-docs.js +46 -47
  327. package/src/server/client-build.js +371 -132
  328. package/src/server/client-formatted.js +47 -16
  329. package/src/server/conf.js +91 -87
  330. package/src/server/data-query.js +32 -20
  331. package/src/server/dns.js +22 -0
  332. package/src/server/ipfs-client.js +232 -91
  333. package/src/server/object-layer.js +1 -6
  334. package/src/server/process.js +192 -45
  335. package/src/server/proxy.js +9 -2
  336. package/src/server/runtime.js +1 -1
  337. package/src/server/semantic-layer-generator-floor.js +11 -51
  338. package/src/server/semantic-layer-generator-resource.js +259 -0
  339. package/src/server/semantic-layer-generator-skin.js +41 -171
  340. package/src/server/semantic-layer-generator.js +122 -14
  341. package/src/server/shape-generator.js +108 -0
  342. package/src/server/start.js +34 -8
  343. package/src/server/valkey.js +143 -235
  344. package/src/ws/IoInterface.js +16 -16
  345. package/src/ws/core/channels/core.ws.chat.js +11 -11
  346. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  347. package/src/ws/core/channels/core.ws.stream.js +19 -19
  348. package/src/ws/core/core.ws.connection.js +8 -8
  349. package/src/ws/core/core.ws.server.js +6 -5
  350. package/src/ws/default/channels/default.ws.main.js +10 -10
  351. package/src/ws/default/default.ws.connection.js +4 -4
  352. package/src/ws/default/default.ws.server.js +4 -3
  353. package/tsconfig.docs.json +15 -0
  354. package/typedoc.dd-cyberia.json +29 -0
  355. package/typedoc.json +29 -0
  356. package/WHITE-PAPER.md +0 -1540
  357. package/bin/file.js +0 -196
  358. package/bin/vs.js +0 -74
  359. package/bin/zed.js +0 -84
  360. package/hardhat/README.md +0 -531
  361. package/hardhat/WHITE-PAPER.md +0 -1540
  362. package/jsdoc.dd-cyberia.json +0 -68
  363. package/jsdoc.json +0 -68
  364. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -413
  365. package/src/api/object-layer/README.md +0 -672
  366. package/src/client/components/core/ColorPalette.js +0 -5267
  367. package/src/client/components/core/JoyStick.js +0 -80
  368. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  369. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -223
  370. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -62
  371. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  372. package/src/client/components/default/RoutesDefault.js +0 -49
  373. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  374. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  375. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  376. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  377. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  378. package/src/client/sw/default.sw.js +0 -127
  379. package/src/client/sw/template.sw.js +0 -84
  380. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +0 -305
  381. package/src/grpc/cyberia/README.md +0 -326
package/bin/cyberia.js CHANGED
@@ -16,10 +16,11 @@
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';
22
- import { DataBaseProvider } from '../src/db/DataBaseProvider.js';
23
+ import { DataBaseProviderService } from '../src/db/DataBaseProvider.js';
23
24
  import { loadConfServerJson } from '../src/server/conf.js';
24
25
  import {
25
26
  ObjectLayerEngine,
@@ -28,25 +29,23 @@ import {
28
29
  getKeyFramesDirectionsFromNumberFolderDirection,
29
30
  buildImgFromTile,
30
31
  } from '../src/server/object-layer.js';
31
- import { ITEM_TYPES as itemTypes } from '../src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.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';
45
41
  import {
42
+ ITEM_TYPES as itemTypes,
46
43
  DefaultCyberiaItems,
47
44
  DefaultSkillConfig,
48
45
  DefaultCyberiaDialogues,
49
- } from '../src/client/components/cyberia-portal/CommonCyberiaPortal.js';
46
+ DefaultCyberiaActions,
47
+ DefaultCyberiaQuests,
48
+ } from '../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
50
49
 
51
50
  /**
52
51
  * Connect to the project MongoDB instance using the standard env / conf layout.
@@ -73,14 +72,14 @@ async function connectDbForChain({ envPath, mongoHost }) {
73
72
 
74
73
  db.host = mongoHost ? mongoHost : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
75
74
 
76
- await DataBaseProvider.load({
75
+ await DataBaseProviderService.load({
77
76
  apis: ['object-layer'],
78
77
  host,
79
78
  path,
80
79
  db,
81
80
  });
82
81
 
83
- const ObjectLayer = DataBaseProvider.instance[`${host}${path}`].mongoose.models.ObjectLayer;
82
+ const ObjectLayer = DataBaseProviderService.getModel('object-layer', { host, path });
84
83
  return { ObjectLayer, host, path };
85
84
  }
86
85
 
@@ -214,7 +213,7 @@ try {
214
213
  db,
215
214
  });
216
215
 
217
- await DataBaseProvider.load({
216
+ await DataBaseProviderService.load({
218
217
  apis: ['object-layer', 'object-layer-render-frames', 'atlas-sprite-sheet', 'file', 'ipfs'],
219
218
  host,
220
219
  path,
@@ -222,16 +221,15 @@ try {
222
221
  });
223
222
 
224
223
  /** @type {import('mongoose').Model} */
225
- const ObjectLayer = DataBaseProvider.instance[`${host}${path}`].mongoose.models.ObjectLayer;
224
+ const ObjectLayer = DataBaseProviderService.getModel('object-layer', { host, path });
226
225
  /** @type {import('mongoose').Model} */
227
- const ObjectLayerRenderFrames =
228
- DataBaseProvider.instance[`${host}${path}`].mongoose.models.ObjectLayerRenderFrames;
226
+ const ObjectLayerRenderFrames = DataBaseProviderService.getModel('object-layer-render-frames', { host, path });
229
227
  /** @type {import('mongoose').Model} */
230
- const AtlasSpriteSheet = DataBaseProvider.instance[`${host}${path}`].mongoose.models.AtlasSpriteSheet;
228
+ const AtlasSpriteSheet = DataBaseProviderService.getModel('atlas-sprite-sheet', { host, path });
231
229
  /** @type {import('mongoose').Model} */
232
- const File = DataBaseProvider.instance[`${host}${path}`].mongoose.models.File;
230
+ const File = DataBaseProviderService.getModel('file', { host, path });
233
231
  /** @type {import('mongoose').Model} */
234
- const Ipfs = DataBaseProvider.instance[`${host}${path}`].mongoose.models.Ipfs;
232
+ const Ipfs = DataBaseProviderService.getModel('ipfs', { host, path });
235
233
 
236
234
  if (options.drop) {
237
235
  // Parse comma-separated item IDs for targeted drop; if none provided, drop everything
@@ -1695,7 +1693,7 @@ try {
1695
1693
  }
1696
1694
  }
1697
1695
 
1698
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1696
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
1699
1697
  },
1700
1698
  )
1701
1699
  .description('Object layer management');
@@ -1705,7 +1703,11 @@ try {
1705
1703
  .command('instance [instance-code]')
1706
1704
  .option('--export [path]', 'Export instance and related documents to a backup directory')
1707
1705
  .option('--import [path]', 'Import instance and related documents from a backup directory (preserveUUID, upsert)')
1708
- .option('--drop', 'Drop existing instance, maps and object layers before importing')
1706
+ .option(
1707
+ '--conf',
1708
+ 'When used with --export or --import, only process cyberia-instance.json and cyberia-instance-conf.json',
1709
+ )
1710
+ .option('--drop', 'Drop all documents associated with the instance code before importing or as a standalone action')
1709
1711
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
1710
1712
  .option('--mongo-host <mongo-host>', 'Mongo host override')
1711
1713
  .option('--dev', 'Force development environment')
@@ -1746,9 +1748,11 @@ try {
1746
1748
 
1747
1749
  logger.info('instance env', { env: options.envPath, deployId, host, path, db });
1748
1750
 
1749
- await DataBaseProvider.load({
1751
+ await DataBaseProviderService.load({
1750
1752
  apis: [
1751
1753
  'cyberia-instance',
1754
+ 'cyberia-instance-conf',
1755
+ 'cyberia-dialogue',
1752
1756
  'cyberia-map',
1753
1757
  'cyberia-entity',
1754
1758
  'object-layer',
@@ -1762,21 +1766,150 @@ try {
1762
1766
  db,
1763
1767
  });
1764
1768
 
1765
- const dbModels = DataBaseProvider.instance[`${host}${path}`].mongoose.models;
1766
- const CyberiaInstance = dbModels.CyberiaInstance;
1767
- const CyberiaMap = dbModels.CyberiaMap;
1768
- const ObjectLayer = dbModels.ObjectLayer;
1769
- const ObjectLayerRenderFrames = dbModels.ObjectLayerRenderFrames;
1770
- const AtlasSpriteSheet = dbModels.AtlasSpriteSheet;
1771
- const File = dbModels.File;
1772
- const Ipfs = dbModels.Ipfs;
1769
+ const CyberiaInstance = DataBaseProviderService.getModel('cyberia-instance', { host, path });
1770
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('cyberia-instance-conf', { host, path });
1771
+ const CyberiaDialogue = DataBaseProviderService.getModel('cyberia-dialogue', { host, path });
1772
+ const CyberiaMap = DataBaseProviderService.getModel('cyberia-map', { host, path });
1773
+ const ObjectLayer = DataBaseProviderService.getModel('object-layer', { host, path });
1774
+ const ObjectLayerRenderFrames = DataBaseProviderService.getModel('object-layer-render-frames', { host, path });
1775
+ const AtlasSpriteSheet = DataBaseProviderService.getModel('atlas-sprite-sheet', { host, path });
1776
+ const File = DataBaseProviderService.getModel('file', { host, path });
1777
+ const Ipfs = DataBaseProviderService.getModel('ipfs', { host, path });
1778
+
1779
+ const toBuffer = (value) => {
1780
+ if (!value) return null;
1781
+ if (Buffer.isBuffer(value)) return value;
1782
+ if (value.type === 'Buffer' && Array.isArray(value.data)) return Buffer.from(value.data);
1783
+ if (value.buffer) return Buffer.from(value.buffer);
1784
+ return Buffer.from(value);
1785
+ };
1786
+
1787
+ const getCanonicalIpfsPaths = (itemKey) => ({
1788
+ objectLayerData: `/object-layer/${itemKey}/${itemKey}_data.json`,
1789
+ atlasSpriteSheet: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
1790
+ atlasMetadata: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
1791
+ });
1792
+
1793
+ const collectMfsPaths = (doc = {}) => {
1794
+ const paths = new Set();
1795
+ if (doc.mfsPath) paths.add(doc.mfsPath);
1796
+ for (const p of doc.mfsPaths || []) {
1797
+ if (p) paths.add(p);
1798
+ }
1799
+ return [...paths];
1800
+ };
1801
+
1802
+ const inferResourceType = (doc = {}) => {
1803
+ if (doc.resourceType) return doc.resourceType;
1804
+ for (const path of collectMfsPaths(doc)) {
1805
+ if (path.endsWith('_atlas_sprite_sheet.png')) return 'atlas-sprite-sheet';
1806
+ if (path.endsWith('_atlas_sprite_sheet_metadata.json')) return 'atlas-metadata';
1807
+ if (path.endsWith('_data.json')) return 'object-layer-data';
1808
+ }
1809
+ return null;
1810
+ };
1811
+
1812
+ const findInstanceRelatedIpfsDoc = (ipfsDocs, { linkedCid, resourceType, mfsPath }) =>
1813
+ ipfsDocs.find(
1814
+ (doc) =>
1815
+ inferResourceType(doc) === resourceType &&
1816
+ linkedCid &&
1817
+ doc.cid === linkedCid &&
1818
+ collectMfsPaths(doc).includes(mfsPath),
1819
+ ) ||
1820
+ ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && linkedCid && doc.cid === linkedCid) ||
1821
+ ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && collectMfsPaths(doc).includes(mfsPath)) ||
1822
+ null;
1823
+
1824
+ const upsertCanonicalPinEntry = (pinMap, { cid, resourceType, mfsPath = '' }) => {
1825
+ if (!cid || !resourceType) return;
1826
+ const key = `${resourceType}:${cid}`;
1827
+ const nextPath = mfsPath || '';
1828
+ if (!pinMap.has(key)) {
1829
+ pinMap.set(key, {
1830
+ cid,
1831
+ resourceType,
1832
+ mfsPath: nextPath,
1833
+ mfsPaths: nextPath ? [nextPath] : [],
1834
+ });
1835
+ return;
1836
+ }
1837
+
1838
+ const existing = pinMap.get(key);
1839
+ if (nextPath && !existing.mfsPaths.includes(nextPath)) {
1840
+ existing.mfsPaths.push(nextPath);
1841
+ }
1842
+ if (!existing.mfsPath && nextPath) {
1843
+ existing.mfsPath = nextPath;
1844
+ }
1845
+ };
1846
+
1847
+ const serialiseCanonicalPins = (pinMap) =>
1848
+ [...pinMap.values()].map((entry) => ({
1849
+ cid: entry.cid,
1850
+ resourceType: entry.resourceType,
1851
+ ...(entry.mfsPath ? { mfsPath: entry.mfsPath } : {}),
1852
+ ...(entry.mfsPaths.length ? { mfsPaths: entry.mfsPaths } : {}),
1853
+ }));
1854
+
1855
+ const getDefaultDialoguesByItemId = (itemIds = []) => {
1856
+ const requestedItemIds = new Set(itemIds.filter(Boolean));
1857
+ const defaultsByItemId = new Map();
1858
+
1859
+ for (const dialogue of DefaultCyberiaDialogues) {
1860
+ // Match by code prefix: "default-<itemId>" covers the common case;
1861
+ // callers may also pass a full code directly.
1862
+ const matchingIds = [...requestedItemIds].filter(
1863
+ (id) => dialogue.code === `default-${id}` || dialogue.code === id,
1864
+ );
1865
+ if (!matchingIds.length) continue;
1866
+ for (const id of matchingIds) {
1867
+ if (!defaultsByItemId.has(id)) defaultsByItemId.set(id, []);
1868
+ defaultsByItemId.get(id).push({
1869
+ code: dialogue.code,
1870
+ order: dialogue.order ?? 0,
1871
+ speaker: dialogue.speaker ?? '',
1872
+ text: dialogue.text,
1873
+ mood: dialogue.mood ?? 'neutral',
1874
+ });
1875
+ }
1876
+ }
1877
+
1878
+ for (const dialogues of defaultsByItemId.values()) {
1879
+ dialogues.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1880
+ }
1881
+
1882
+ return defaultsByItemId;
1883
+ };
1884
+
1885
+ const rewriteImportedCidReferences = async ({ oldCid, newCid, resourceType }) => {
1886
+ if (!oldCid || !newCid || oldCid === newCid) return;
1887
+
1888
+ if (resourceType === 'object-layer-data') {
1889
+ await ObjectLayer.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
1890
+ return;
1891
+ }
1892
+
1893
+ if (resourceType === 'atlas-sprite-sheet') {
1894
+ await AtlasSpriteSheet.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
1895
+ await ObjectLayer.updateMany({ 'data.render.cid': oldCid }, { $set: { 'data.render.cid': newCid } });
1896
+ return;
1897
+ }
1898
+
1899
+ if (resourceType === 'atlas-metadata') {
1900
+ await ObjectLayer.updateMany(
1901
+ { 'data.render.metadataCid': oldCid },
1902
+ { $set: { 'data.render.metadataCid': newCid } },
1903
+ );
1904
+ }
1905
+ };
1773
1906
 
1774
1907
  // ── EXPORT ──────────────────────────────────────────────────────
1775
1908
  if (options.export !== undefined) {
1776
1909
  const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
1777
1910
  if (!instance) {
1778
1911
  logger.error(`CyberiaInstance with code "${instanceCode}" not found`);
1779
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1912
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
1780
1913
  process.exit(1);
1781
1914
  }
1782
1915
 
@@ -1788,13 +1921,12 @@ try {
1788
1921
  fs.ensureDirSync(backupDir);
1789
1922
  logger.info('Exporting instance', { code: instanceCode, backupDir });
1790
1923
 
1791
- fs.ensureDirSync(`${backupDir}/files`);
1792
-
1793
1924
  // Helper: export a File document to the files/ directory
1794
1925
  const exportFileDoc = async (fileId, fileKey) => {
1795
1926
  if (!fileId) return;
1796
1927
  const file = await File.findById(fileId).lean();
1797
1928
  if (!file) return;
1929
+ fs.ensureDirSync(`${backupDir}/files`);
1798
1930
  const fileExport = { ...file };
1799
1931
  // Handle both Node.js Buffer and BSON Binary types from .lean()
1800
1932
  if (fileExport.data) {
@@ -1808,11 +1940,46 @@ try {
1808
1940
 
1809
1941
  // 1. Save instance document + thumbnail
1810
1942
  fs.writeJsonSync(`${backupDir}/cyberia-instance.json`, instance, { spaces: 2 });
1811
- if (instance.thumbnail) {
1943
+ if (!options.conf && instance.thumbnail) {
1812
1944
  await exportFileDoc(instance.thumbnail, `thumb-instance-${instanceCode}`);
1813
1945
  }
1814
1946
  logger.info('Exported CyberiaInstance', { code: instanceCode });
1815
1947
 
1948
+ // 1b. Export linked CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
1949
+ // If no conf doc exists yet (instance created before auto-upsert logic), create one using
1950
+ // schema defaults — identical to the behaviour in CyberiaInstanceService.post().
1951
+ let instanceConf =
1952
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
1953
+ (instance.conf ? await CyberiaInstanceConf.findById(instance.conf).lean() : null);
1954
+ if (!instanceConf) {
1955
+ logger.info('No CyberiaInstanceConf found — creating default', { instanceCode });
1956
+ const created = await CyberiaInstanceConf.findOneAndUpdate(
1957
+ { instanceCode },
1958
+ { $setOnInsert: { instanceCode } },
1959
+ { upsert: true, returnDocument: 'after' },
1960
+ );
1961
+ // Back-fill the instance.conf ref if it was missing
1962
+ if (created && !instance.conf) {
1963
+ await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: created._id });
1964
+ }
1965
+ instanceConf = created?.toObject ? created.toObject() : created;
1966
+ }
1967
+ if (instanceConf) {
1968
+ fs.writeJsonSync(`${backupDir}/cyberia-instance-conf.json`, instanceConf, { spaces: 2 });
1969
+ logger.info('Exported CyberiaInstanceConf', { instanceCode });
1970
+ } else {
1971
+ logger.warn('Could not create or find CyberiaInstanceConf', { instanceCode });
1972
+ }
1973
+
1974
+ if (options.conf) {
1975
+ logger.info('Instance export completed in --conf mode', {
1976
+ backupDir,
1977
+ exportedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
1978
+ });
1979
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
1980
+ return;
1981
+ }
1982
+
1816
1983
  // 2. Collect all map codes (instance maps + portal targets)
1817
1984
  const mapCodes = new Set(instance.cyberiaMapCodes || []);
1818
1985
  for (const portal of instance.portals || []) {
@@ -1841,6 +2008,100 @@ try {
1841
2008
  }
1842
2009
  }
1843
2010
 
2011
+ // 4b. Add instance-level itemIds
2012
+ for (const id of instance.itemIds || []) {
2013
+ if (id) objectLayerItemIds.add(id);
2014
+ }
2015
+
2016
+ // 4c. Add all itemIds referenced by CyberiaInstanceConf (entityDefaults + skillConfig).
2017
+ // This ensures liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers and
2018
+ // skill trigger items are included even if no map entity currently uses them.
2019
+ if (instanceConf) {
2020
+ for (const ed of instanceConf.entityDefaults || []) {
2021
+ for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
2022
+ for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
2023
+ for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
2024
+ for (const slot of ed.defaultObjectLayers || []) {
2025
+ if (slot.itemId) objectLayerItemIds.add(slot.itemId);
2026
+ }
2027
+ }
2028
+ for (const sc of instanceConf.skillConfig || []) {
2029
+ if (sc.triggerItemId) objectLayerItemIds.add(sc.triggerItemId);
2030
+ for (const skill of sc.skills || []) {
2031
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$')) {
2032
+ objectLayerItemIds.add(skill.summonedEntityItemId);
2033
+ }
2034
+ }
2035
+ }
2036
+ }
2037
+
2038
+ // 4d. Export dialogues for all relevant object-layer items. Codes follow the pattern
2039
+ // "default-<itemId>". If an item has no dialogue docs yet but ships with
2040
+ // DefaultCyberiaDialogues, seed those defaults into Mongo first.
2041
+ if (objectLayerItemIds.size > 0) {
2042
+ const requestedItemIds = [...objectLayerItemIds];
2043
+ const requestedCodes = requestedItemIds.map((id) => `default-${id}`);
2044
+ const defaultDialoguesByItemId = getDefaultDialoguesByItemId(requestedItemIds);
2045
+ const existingDialogueDocs = await CyberiaDialogue.find({
2046
+ code: { $in: requestedCodes },
2047
+ })
2048
+ .sort({ code: 1, order: 1 })
2049
+ .lean();
2050
+
2051
+ const existingDialogueCodes = new Set(existingDialogueDocs.map((dialogue) => dialogue.code).filter(Boolean));
2052
+ let seededDialogueCount = 0;
2053
+
2054
+ for (const [itemId, dialogues] of defaultDialoguesByItemId.entries()) {
2055
+ const firstCode = dialogues[0]?.code;
2056
+ if (firstCode && existingDialogueCodes.has(firstCode)) continue;
2057
+
2058
+ for (const dialogue of dialogues) {
2059
+ await CyberiaDialogue.findOneAndUpdate(
2060
+ { code: dialogue.code, order: dialogue.order },
2061
+ {
2062
+ $set: {
2063
+ speaker: dialogue.speaker,
2064
+ text: dialogue.text,
2065
+ mood: dialogue.mood,
2066
+ },
2067
+ },
2068
+ { upsert: true },
2069
+ );
2070
+ seededDialogueCount++;
2071
+ }
2072
+ }
2073
+
2074
+ const dialogueDocs = await CyberiaDialogue.find({ code: { $in: requestedCodes } })
2075
+ .sort({ code: 1, order: 1 })
2076
+ .lean();
2077
+
2078
+ if (seededDialogueCount > 0) {
2079
+ logger.info(`Seeded ${seededDialogueCount} CyberiaDialogue default record(s) for export`);
2080
+ }
2081
+
2082
+ if (dialogueDocs.length > 0) {
2083
+ fs.ensureDirSync(`${backupDir}/cyberia-dialogues`);
2084
+ const dialoguesByCode = new Map();
2085
+
2086
+ for (const dialogue of dialogueDocs) {
2087
+ if (!dialoguesByCode.has(dialogue.code)) {
2088
+ dialoguesByCode.set(dialogue.code, []);
2089
+ }
2090
+ dialoguesByCode.get(dialogue.code).push(dialogue);
2091
+ }
2092
+
2093
+ for (const [code, dialogues] of dialoguesByCode.entries()) {
2094
+ fs.writeJsonSync(`${backupDir}/cyberia-dialogues/${encodeURIComponent(code)}.json`, dialogues, {
2095
+ spaces: 2,
2096
+ });
2097
+ }
2098
+
2099
+ logger.info(`Exported ${dialogueDocs.length} CyberiaDialogue document(s)`, {
2100
+ codes: [...dialoguesByCode.keys()],
2101
+ });
2102
+ }
2103
+ }
2104
+
1844
2105
  // 5. Export object layers with related render frames, atlas, files, and IPFS records
1845
2106
  if (objectLayerItemIds.size > 0) {
1846
2107
  const objectLayers = await ObjectLayer.find({
@@ -1851,48 +2112,245 @@ try {
1851
2112
  fs.ensureDirSync(`${backupDir}/render-frames`);
1852
2113
  fs.ensureDirSync(`${backupDir}/atlas-sprite-sheets`);
1853
2114
  fs.ensureDirSync(`${backupDir}/ipfs`);
2115
+ fs.ensureDirSync(`${backupDir}/ipfs/content`);
2116
+
2117
+ const canonicalPins = new Map();
2118
+ const expectedObjectLayerIpfsRefs = [];
2119
+ const ipfsPayloadFailures = [];
2120
+ let ipfsPayloadExportCount = 0;
2121
+ let ipfsPayloadAliasCount = 0;
2122
+
2123
+ const writeBackupPayload = (cid, payloadBuffer) => {
2124
+ if (!cid) return false;
2125
+ const payloadPath = `${backupDir}/ipfs/content/${cid}.bin`;
2126
+ if (fs.existsSync(payloadPath)) return false;
2127
+ fs.writeFileSync(payloadPath, payloadBuffer);
2128
+ ipfsPayloadExportCount++;
2129
+ return true;
2130
+ };
2131
+
2132
+ const writeBackupPayloadAlias = ({ canonicalCid, linkedCid, payloadBuffer }) => {
2133
+ if (!linkedCid || linkedCid === canonicalCid) return;
2134
+ if (writeBackupPayload(linkedCid, payloadBuffer)) {
2135
+ ipfsPayloadAliasCount++;
2136
+ }
2137
+ };
2138
+
2139
+ const exportCanonicalPayload = async ({ payloadBuffer, resourceType, mfsPath, filename, itemKey }) => {
2140
+ const hashResult = await IpfsClient.hashBufferForIpfs(payloadBuffer, filename);
2141
+ if (!hashResult?.cid) {
2142
+ ipfsPayloadFailures.push({ itemKey, resourceType, mfsPath, reason: 'Failed to hash payload via Kubo' });
2143
+ return null;
2144
+ }
1854
2145
 
1855
- const allCids = new Set();
2146
+ writeBackupPayload(hashResult.cid, payloadBuffer);
2147
+ return hashResult.cid;
2148
+ };
1856
2149
 
1857
2150
  for (const ol of objectLayers) {
1858
- const fileName = ol.data?.item?.id || ol._id.toString();
1859
- fs.writeJsonSync(`${backupDir}/object-layers/${fileName}.json`, ol, { spaces: 2 });
2151
+ const itemKey = ol.data?.item?.id || ol._id.toString();
2152
+ const itemPaths = getCanonicalIpfsPaths(itemKey);
2153
+ const objectLayerExport = newInstance(ol);
2154
+
2155
+ if (!objectLayerExport.data.render) {
2156
+ objectLayerExport.data.render = {};
2157
+ }
1860
2158
 
1861
2159
  // Export ObjectLayerRenderFrames
1862
2160
  if (ol.objectLayerRenderFramesId) {
1863
2161
  const rf = await ObjectLayerRenderFrames.findById(ol.objectLayerRenderFramesId).lean();
1864
2162
  if (rf) {
1865
- fs.writeJsonSync(`${backupDir}/render-frames/${fileName}.json`, rf, { spaces: 2 });
2163
+ fs.writeJsonSync(`${backupDir}/render-frames/${itemKey}.json`, rf, { spaces: 2 });
1866
2164
  }
1867
2165
  }
1868
2166
 
1869
- // Export AtlasSpriteSheet + its File
2167
+ // Export AtlasSpriteSheet + its File using canonical payload bytes from the DB state.
1870
2168
  if (ol.atlasSpriteSheetId) {
1871
2169
  const atlas = await AtlasSpriteSheet.findById(ol.atlasSpriteSheetId).lean();
1872
- if (atlas) {
1873
- fs.writeJsonSync(`${backupDir}/atlas-sprite-sheets/${fileName}.json`, atlas, { spaces: 2 });
1874
- if (atlas.fileId) {
1875
- await exportFileDoc(atlas.fileId, `atlas-${fileName}`);
1876
- }
1877
- if (atlas.cid) allCids.add(atlas.cid);
2170
+ if (!atlas) {
2171
+ ipfsPayloadFailures.push({
2172
+ itemKey,
2173
+ resourceType: 'atlas-sprite-sheet',
2174
+ mfsPath: itemPaths.atlasSpriteSheet,
2175
+ reason: 'AtlasSpriteSheet document not found in MongoDB',
2176
+ });
2177
+ continue;
2178
+ }
2179
+
2180
+ const atlasExport = newInstance(atlas);
2181
+ if (atlas.fileId) {
2182
+ await exportFileDoc(atlas.fileId, `atlas-${itemKey}`);
2183
+ }
2184
+
2185
+ const atlasFile = atlas.fileId ? await File.findById(atlas.fileId).lean() : null;
2186
+ const atlasBuffer = toBuffer(atlasFile?.data);
2187
+ if (!atlasBuffer) {
2188
+ ipfsPayloadFailures.push({
2189
+ itemKey,
2190
+ resourceType: 'atlas-sprite-sheet',
2191
+ mfsPath: itemPaths.atlasSpriteSheet,
2192
+ reason: 'Atlas File payload not found in MongoDB',
2193
+ });
2194
+ continue;
1878
2195
  }
2196
+
2197
+ const atlasCid = await exportCanonicalPayload({
2198
+ payloadBuffer: atlasBuffer,
2199
+ resourceType: 'atlas-sprite-sheet',
2200
+ mfsPath: itemPaths.atlasSpriteSheet,
2201
+ filename: `${itemKey}_atlas_sprite_sheet.png`,
2202
+ itemKey,
2203
+ });
2204
+ if (!atlasCid) continue;
2205
+
2206
+ const linkedAtlasCid = atlas.cid || ol.data?.render?.cid || atlasCid;
2207
+ writeBackupPayloadAlias({
2208
+ canonicalCid: atlasCid,
2209
+ linkedCid: linkedAtlasCid,
2210
+ payloadBuffer: atlasBuffer,
2211
+ });
2212
+ expectedObjectLayerIpfsRefs.push({
2213
+ itemKey,
2214
+ resourceType: 'atlas-sprite-sheet',
2215
+ mfsPath: itemPaths.atlasSpriteSheet,
2216
+ linkedCid: linkedAtlasCid,
2217
+ fallbackCid: atlasCid,
2218
+ });
2219
+
2220
+ const atlasMetadataBuffer = Buffer.from(stringify(atlasExport.metadata || {}), 'utf-8');
2221
+ const atlasMetadataCid = await exportCanonicalPayload({
2222
+ payloadBuffer: atlasMetadataBuffer,
2223
+ resourceType: 'atlas-metadata',
2224
+ mfsPath: itemPaths.atlasMetadata,
2225
+ filename: `${itemKey}_atlas_sprite_sheet_metadata.json`,
2226
+ itemKey,
2227
+ });
2228
+ if (!atlasMetadataCid) continue;
2229
+
2230
+ const linkedAtlasMetadataCid = ol.data?.render?.metadataCid || atlasMetadataCid;
2231
+ writeBackupPayloadAlias({
2232
+ canonicalCid: atlasMetadataCid,
2233
+ linkedCid: linkedAtlasMetadataCid,
2234
+ payloadBuffer: atlasMetadataBuffer,
2235
+ });
2236
+ expectedObjectLayerIpfsRefs.push({
2237
+ itemKey,
2238
+ resourceType: 'atlas-metadata',
2239
+ mfsPath: itemPaths.atlasMetadata,
2240
+ linkedCid: linkedAtlasMetadataCid,
2241
+ fallbackCid: atlasMetadataCid,
2242
+ });
2243
+
2244
+ atlasExport.cid = atlasCid;
2245
+ objectLayerExport.data.render.cid = atlasCid;
2246
+ objectLayerExport.data.render.metadataCid = atlasMetadataCid;
2247
+ fs.writeJsonSync(`${backupDir}/atlas-sprite-sheets/${itemKey}.json`, atlasExport, { spaces: 2 });
2248
+ } else {
2249
+ if (objectLayerExport.data.render?.cid || objectLayerExport.data.render?.metadataCid) {
2250
+ ipfsPayloadFailures.push({
2251
+ itemKey,
2252
+ resourceType: 'atlas-sprite-sheet',
2253
+ mfsPath: itemPaths.atlasSpriteSheet,
2254
+ reason: 'ObjectLayer references atlas CIDs but no AtlasSpriteSheet document exists',
2255
+ });
2256
+ continue;
2257
+ }
2258
+ delete objectLayerExport.data.render.cid;
2259
+ delete objectLayerExport.data.render.metadataCid;
1879
2260
  }
1880
2261
 
1881
- // Collect CIDs for IPFS pin records
1882
- if (ol.cid) allCids.add(ol.cid);
1883
- if (ol.data?.render?.cid) allCids.add(ol.data.render.cid);
1884
- if (ol.data?.render?.metadataCid) allCids.add(ol.data.render.metadataCid);
2262
+ const objectLayerBuffer = Buffer.from(stringify(objectLayerExport.data || {}), 'utf-8');
2263
+ const objectLayerCid = await exportCanonicalPayload({
2264
+ payloadBuffer: objectLayerBuffer,
2265
+ resourceType: 'object-layer-data',
2266
+ mfsPath: itemPaths.objectLayerData,
2267
+ filename: `${itemKey}_data.json`,
2268
+ itemKey,
2269
+ });
2270
+ if (!objectLayerCid) continue;
2271
+
2272
+ const linkedObjectLayerCid = ol.cid || objectLayerCid;
2273
+ writeBackupPayloadAlias({
2274
+ canonicalCid: objectLayerCid,
2275
+ linkedCid: linkedObjectLayerCid,
2276
+ payloadBuffer: objectLayerBuffer,
2277
+ });
2278
+ expectedObjectLayerIpfsRefs.push({
2279
+ itemKey,
2280
+ resourceType: 'object-layer-data',
2281
+ mfsPath: itemPaths.objectLayerData,
2282
+ linkedCid: linkedObjectLayerCid,
2283
+ fallbackCid: objectLayerCid,
2284
+ });
2285
+
2286
+ objectLayerExport.cid = objectLayerCid;
2287
+ fs.writeJsonSync(`${backupDir}/object-layers/${itemKey}.json`, objectLayerExport, { spaces: 2 });
2288
+ }
2289
+
2290
+ if (ipfsPayloadFailures.length > 0) {
2291
+ for (const failure of ipfsPayloadFailures) {
2292
+ logger.error('Canonical IPFS payload export failed', failure);
2293
+ }
2294
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2295
+ process.exit(1);
1885
2296
  }
1886
2297
 
1887
- // Export IPFS pin records for all collected CIDs
1888
- if (allCids.size > 0) {
1889
- const ipfsDocs = await Ipfs.find({ cid: { $in: [...allCids] } }).lean();
1890
- if (ipfsDocs.length > 0) {
1891
- fs.writeJsonSync(`${backupDir}/ipfs/pins.json`, ipfsDocs, { spaces: 2 });
1892
- logger.info(`Exported ${ipfsDocs.length} Ipfs pin record(s)`);
2298
+ const relatedPinPaths = [
2299
+ ...new Set(expectedObjectLayerIpfsRefs.map((entry) => entry.mfsPath).filter(Boolean)),
2300
+ ];
2301
+ const relatedPinCids = [
2302
+ ...new Set(
2303
+ expectedObjectLayerIpfsRefs.flatMap((entry) => [entry.linkedCid, entry.fallbackCid]).filter(Boolean),
2304
+ ),
2305
+ ];
2306
+ const relatedIpfsDocs =
2307
+ relatedPinPaths.length > 0 || relatedPinCids.length > 0
2308
+ ? await Ipfs.find({
2309
+ $or: [
2310
+ ...(relatedPinPaths.length ? [{ mfsPath: { $in: relatedPinPaths } }] : []),
2311
+ ...(relatedPinPaths.length ? [{ mfsPaths: { $in: relatedPinPaths } }] : []),
2312
+ ...(relatedPinCids.length ? [{ cid: { $in: relatedPinCids } }] : []),
2313
+ ],
2314
+ }).lean()
2315
+ : [];
2316
+
2317
+ let ipfsCollectionMatchCount = 0;
2318
+ let ipfsCollectionFallbackCount = 0;
2319
+
2320
+ for (const ref of expectedObjectLayerIpfsRefs) {
2321
+ const matchingDoc = findInstanceRelatedIpfsDoc(relatedIpfsDocs, ref);
2322
+ const exportCid = matchingDoc?.cid || ref.linkedCid || ref.fallbackCid;
2323
+
2324
+ if (!exportCid) {
2325
+ logger.warn('Skipping instance IPFS pin export because the ObjectLayer ref has no linked CID', {
2326
+ itemKey: ref.itemKey,
2327
+ resourceType: ref.resourceType,
2328
+ mfsPath: ref.mfsPath,
2329
+ });
2330
+ continue;
1893
2331
  }
2332
+
2333
+ upsertCanonicalPinEntry(canonicalPins, {
2334
+ cid: exportCid,
2335
+ resourceType: ref.resourceType,
2336
+ mfsPath: ref.mfsPath,
2337
+ });
2338
+
2339
+ if (matchingDoc) ipfsCollectionMatchCount++;
2340
+ else ipfsCollectionFallbackCount++;
1894
2341
  }
1895
2342
 
2343
+ const sanitised = serialiseCanonicalPins(canonicalPins);
2344
+ fs.writeJsonSync(`${backupDir}/ipfs/pins.json`, sanitised, { spaces: 2 });
2345
+ logger.info(
2346
+ `Exported ${sanitised.length} instance-related Ipfs pin record(s) and ${ipfsPayloadExportCount} raw payload file(s)`,
2347
+ {
2348
+ matchedFromIpfsCollection: ipfsCollectionMatchCount,
2349
+ fallbackFromObjectLayerRefs: ipfsCollectionFallbackCount,
2350
+ rawPayloadAliases: ipfsPayloadAliasCount,
2351
+ },
2352
+ );
2353
+
1896
2354
  logger.info(`Exported ${objectLayers.length} ObjectLayer document(s)`, {
1897
2355
  itemIds: [...objectLayerItemIds],
1898
2356
  });
@@ -1912,14 +2370,14 @@ try {
1912
2370
 
1913
2371
  if (!fs.existsSync(backupDir)) {
1914
2372
  logger.error(`Backup directory not found: ${backupDir}`);
1915
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
2373
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
1916
2374
  process.exit(1);
1917
2375
  }
1918
2376
 
1919
2377
  logger.info('Importing instance', { code: instanceCode, backupDir });
1920
2378
 
1921
2379
  // 0. Drop existing documents if --drop is set
1922
- if (options.drop) {
2380
+ if (options.drop && !options.conf) {
1923
2381
  const existingInstance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
1924
2382
  if (existingInstance) {
1925
2383
  const dropMapCodes = new Set(existingInstance.cyberiaMapCodes || []);
@@ -1931,13 +2389,41 @@ try {
1931
2389
  // Collect thumbnail File IDs to drop
1932
2390
  const thumbFileIds = [];
1933
2391
  if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
2392
+ const dropOlItemIds = new Set();
1934
2393
 
1935
2394
  // Query other instances/maps for shared thumbnail exclusion
1936
2395
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
1937
2396
 
2397
+ // Add instance-level itemIds (may not appear in any map entity)
2398
+ for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
2399
+
2400
+ // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
2401
+ const existingConf =
2402
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
2403
+ (existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
2404
+ if (existingConf) {
2405
+ for (const ed of existingConf.entityDefaults || []) {
2406
+ for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
2407
+ for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
2408
+ for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
2409
+ for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
2410
+ }
2411
+ for (const sc of existingConf.skillConfig || []) {
2412
+ if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
2413
+ for (const skill of sc.skills || []) {
2414
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
2415
+ dropOlItemIds.add(skill.summonedEntityItemId);
2416
+ }
2417
+ }
2418
+ }
2419
+
2420
+ const otherMaps = await CyberiaMap.find(
2421
+ { code: { $nin: [...dropMapCodes] } },
2422
+ { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
2423
+ ).lean();
2424
+
1938
2425
  if (dropMapCodes.size > 0) {
1939
2426
  const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
1940
- const dropOlItemIds = new Set();
1941
2427
  for (const map of dropMaps) {
1942
2428
  if (map.thumbnail) thumbFileIds.push(map.thumbnail);
1943
2429
  for (const entity of map.entities || []) {
@@ -1947,110 +2433,105 @@ try {
1947
2433
  }
1948
2434
  }
1949
2435
 
1950
- // Exclude OL item IDs referenced by maps outside this instance
1951
- const otherMaps = await CyberiaMap.find(
1952
- { code: { $nin: [...dropMapCodes] } },
1953
- { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
1954
- ).lean();
1955
- const sharedOlItemIds = new Set();
1956
- for (const m of otherMaps) {
1957
- for (const entity of m.entities || []) {
1958
- for (const itemId of entity.objectLayerItemIds || []) {
1959
- if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
1960
- }
1961
- }
1962
- }
1963
- for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
1964
- if (sharedOlItemIds.size > 0) {
1965
- logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
1966
- }
2436
+ const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2437
+ logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
2438
+ }
1967
2439
 
1968
- // Exclude thumbnail File IDs referenced by other instances or maps
1969
- const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
1970
- const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
1971
- const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
1972
- for (let i = thumbFileIds.length - 1; i >= 0; i--) {
1973
- if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
2440
+ // Exclude OL item IDs referenced by maps outside this instance
2441
+ const sharedOlItemIds = new Set();
2442
+ for (const m of otherMaps) {
2443
+ for (const entity of m.entities || []) {
2444
+ for (const itemId of entity.objectLayerItemIds || []) {
2445
+ if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
2446
+ }
1974
2447
  }
2448
+ }
2449
+ for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
2450
+ if (sharedOlItemIds.size > 0) {
2451
+ logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
2452
+ }
1975
2453
 
1976
- if (dropOlItemIds.size > 0) {
1977
- // Gather ObjectLayers to collect related doc IDs and CIDs
1978
- const olDocs = await ObjectLayer.find(
1979
- { 'data.item.id': { $in: [...dropOlItemIds] } },
1980
- {
1981
- cid: 1,
1982
- 'data.item.id': 1,
1983
- 'data.render': 1,
1984
- objectLayerRenderFramesId: 1,
1985
- atlasSpriteSheetId: 1,
1986
- },
1987
- ).lean();
1988
-
1989
- const cidsToUnpin = new Set();
1990
- const renderFrameIds = [];
1991
- const atlasIds = [];
1992
- const itemKeysToClean = new Set();
1993
-
1994
- for (const doc of olDocs) {
1995
- if (doc.cid) cidsToUnpin.add(doc.cid);
1996
- if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
1997
- if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
1998
- if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
1999
- if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
2000
- if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
2001
- }
2454
+ // Exclude thumbnail File IDs referenced by other instances or maps
2455
+ const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
2456
+ const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
2457
+ const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
2458
+ for (let i = thumbFileIds.length - 1; i >= 0; i--) {
2459
+ if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
2460
+ }
2002
2461
 
2003
- // Delete AtlasSpriteSheet + referenced File docs
2004
- if (atlasIds.length > 0) {
2005
- const atlasDocs = await AtlasSpriteSheet.find(
2006
- { _id: { $in: atlasIds } },
2007
- { fileId: 1, cid: 1 },
2008
- ).lean();
2009
- const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
2010
- for (const atlas of atlasDocs) {
2011
- if (atlas.cid) cidsToUnpin.add(atlas.cid);
2012
- }
2013
- if (atlasFileIds.length > 0) {
2014
- const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
2015
- logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2016
- }
2017
- const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
2018
- logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
2019
- }
2462
+ if (dropOlItemIds.size > 0) {
2463
+ const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
2464
+ const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
2465
+ logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
2466
+ const olDocs = await ObjectLayer.find(
2467
+ { 'data.item.id': { $in: [...dropOlItemIds] } },
2468
+ {
2469
+ cid: 1,
2470
+ 'data.item.id': 1,
2471
+ 'data.render': 1,
2472
+ objectLayerRenderFramesId: 1,
2473
+ atlasSpriteSheetId: 1,
2474
+ },
2475
+ ).lean();
2020
2476
 
2021
- // Delete RenderFrames
2022
- if (renderFrameIds.length > 0) {
2023
- const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
2024
- logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
2025
- }
2477
+ const cidsToUnpin = new Set();
2478
+ const renderFrameIds = [];
2479
+ const atlasIds = [];
2480
+ const itemKeysToClean = new Set();
2026
2481
 
2027
- // Delete IPFS pin records
2028
- if (cidsToUnpin.size > 0) {
2029
- const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
2030
- logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
2031
- }
2482
+ for (const doc of olDocs) {
2483
+ if (doc.cid) cidsToUnpin.add(doc.cid);
2484
+ if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
2485
+ if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
2486
+ if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
2487
+ if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
2488
+ if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
2489
+ }
2032
2490
 
2033
- // Unpin CIDs from IPFS Kubo + Cluster and remove MFS paths
2034
- let unpinCount = 0;
2035
- for (const cid of cidsToUnpin) {
2036
- const ok = await IpfsClient.unpinCid(cid);
2037
- if (ok) unpinCount++;
2491
+ // Delete AtlasSpriteSheet + referenced File docs
2492
+ if (atlasIds.length > 0) {
2493
+ const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
2494
+ const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
2495
+ for (const atlas of atlasDocs) {
2496
+ if (atlas.cid) cidsToUnpin.add(atlas.cid);
2038
2497
  }
2039
- let mfsCount = 0;
2040
- for (const itemKey of itemKeysToClean) {
2041
- const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
2042
- if (ok) mfsCount++;
2498
+ if (atlasFileIds.length > 0) {
2499
+ const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
2500
+ logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2043
2501
  }
2044
- logger.info(
2045
- `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
2046
- );
2502
+ const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
2503
+ logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
2504
+ }
2047
2505
 
2048
- const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2049
- logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2506
+ // Delete RenderFrames
2507
+ if (renderFrameIds.length > 0) {
2508
+ const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
2509
+ logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
2050
2510
  }
2051
2511
 
2052
- const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2053
- logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
2512
+ // Delete IPFS pin records
2513
+ if (cidsToUnpin.size > 0) {
2514
+ const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
2515
+ logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
2516
+ }
2517
+
2518
+ // Unpin CIDs from IPFS Kubo + Cluster and remove MFS paths
2519
+ let unpinCount = 0;
2520
+ for (const cid of cidsToUnpin) {
2521
+ const ok = await IpfsClient.unpinCid(cid);
2522
+ if (ok) unpinCount++;
2523
+ }
2524
+ let mfsCount = 0;
2525
+ for (const itemKey of itemKeysToClean) {
2526
+ const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
2527
+ if (ok) mfsCount++;
2528
+ }
2529
+ logger.info(
2530
+ `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
2531
+ );
2532
+
2533
+ const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2534
+ logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2054
2535
  }
2055
2536
 
2056
2537
  // Drop thumbnail File documents (instance + maps), excluding shared ones
@@ -2061,9 +2542,58 @@ try {
2061
2542
 
2062
2543
  await CyberiaInstance.deleteOne({ code: instanceCode });
2063
2544
  logger.info('Dropped CyberiaInstance', { code: instanceCode });
2545
+ await CyberiaInstanceConf.deleteOne({ instanceCode });
2546
+ logger.info('Dropped CyberiaInstanceConf', { instanceCode });
2064
2547
  } else {
2065
2548
  logger.info('No existing instance to drop', { code: instanceCode });
2066
2549
  }
2550
+ } else if (options.drop && options.conf) {
2551
+ logger.info(
2552
+ 'Skipping full instance drop because --conf only imports cyberia-instance.json and cyberia-instance-conf.json',
2553
+ );
2554
+ }
2555
+
2556
+ if (options.conf) {
2557
+ const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2558
+ let importedConf = null;
2559
+ if (fs.existsSync(confImportPath)) {
2560
+ const confData = fs.readJsonSync(confImportPath);
2561
+ if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2562
+ await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2563
+ // Always bump updatedAt so the Go server's version hash changes and
2564
+ // ReloadWorld re-applies the config without requiring a full restart.
2565
+ confData.updatedAt = new Date();
2566
+ importedConf = await CyberiaInstanceConf.create(confData);
2567
+ logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
2568
+ } else {
2569
+ logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
2570
+ }
2571
+
2572
+ // In --conf mode we must NOT delete + recreate the CyberiaInstance because
2573
+ // that would overwrite cyberiaMapCodes / portals / itemIds with whatever was
2574
+ // in the (possibly stale) backup, effectively removing the live maps and OLs
2575
+ // from the instance. Only update the conf ref and bump updatedAt so the Go
2576
+ // server's version hash changes and ReloadWorld re-applies the config.
2577
+ if (importedConf) {
2578
+ const result = await CyberiaInstance.updateOne(
2579
+ { code: instanceCode },
2580
+ { $set: { conf: importedConf._id, updatedAt: new Date() } },
2581
+ );
2582
+ if (result.matchedCount > 0) {
2583
+ logger.info('Updated CyberiaInstance conf ref', { code: instanceCode });
2584
+ } else {
2585
+ logger.warn(`CyberiaInstance not found in DB for code "${instanceCode}" — cannot update conf ref`);
2586
+ }
2587
+ } else {
2588
+ logger.warn(`Skipping CyberiaInstance conf ref update — no conf was imported`);
2589
+ }
2590
+
2591
+ logger.info('Instance import completed in --conf mode', {
2592
+ backupDir,
2593
+ importedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
2594
+ });
2595
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2596
+ return;
2067
2597
  }
2068
2598
 
2069
2599
  // 1. Import File documents first (atlas PNG + thumbnail dependencies)
@@ -2113,6 +2643,9 @@ try {
2113
2643
  for (const f of atlasFiles) {
2114
2644
  const atlasData = fs.readJsonSync(`${atlasDir}/${f}`);
2115
2645
  await AtlasSpriteSheet.deleteOne({ _id: atlasData._id });
2646
+ if (atlasData.metadata?.itemKey) {
2647
+ await AtlasSpriteSheet.deleteOne({ 'metadata.itemKey': atlasData.metadata.itemKey });
2648
+ }
2116
2649
  await AtlasSpriteSheet.create(atlasData);
2117
2650
  atlasCount++;
2118
2651
  }
@@ -2127,45 +2660,71 @@ try {
2127
2660
  for (const file of olFiles) {
2128
2661
  const olData = fs.readJsonSync(`${olDir}/${file}`);
2129
2662
  await ObjectLayer.deleteOne({ _id: olData._id });
2663
+ if (olData.sha256) {
2664
+ await ObjectLayer.deleteOne({ sha256: olData.sha256 });
2665
+ }
2130
2666
  await ObjectLayer.create(olData);
2131
2667
  olCount++;
2132
2668
  }
2133
2669
  logger.info(`Imported ${olCount} ObjectLayer document(s)`);
2134
2670
  }
2135
2671
 
2136
- // 5. Import IPFS pin records and re-pin CIDs
2137
- const ipfsFile = `${backupDir}/ipfs/pins.json`;
2138
- if (fs.existsSync(ipfsFile)) {
2139
- const ipfsDocs = fs.readJsonSync(ipfsFile);
2140
- let ipfsCount = 0;
2141
- const pinnedCids = new Set();
2142
- for (const doc of ipfsDocs) {
2143
- await Ipfs.deleteOne({ _id: doc._id });
2144
- await Ipfs.create(doc);
2145
- ipfsCount++;
2146
- if (doc.cid) pinnedCids.add(doc.cid);
2147
- }
2148
- logger.info(`Imported ${ipfsCount} Ipfs pin record(s)`);
2149
-
2150
- // Re-pin CIDs to IPFS Kubo + Cluster
2151
- let repinCount = 0;
2152
- for (const cid of pinnedCids) {
2153
- const ok = await IpfsClient.pinCid(cid);
2154
- if (ok) repinCount++;
2672
+ // 4b. Regenerate static frame PNGs from imported render-frames + object-layer documents.
2673
+ // Mirrors the writeStaticFrameAssets call in `ol --import` so src/client/public/cyberia
2674
+ // and the public/<host><path> deployment dir are populated even when the cyberia
2675
+ // asset directory was wiped (e.g. git clean / rm -rf).
2676
+ const rfImportDir = `${backupDir}/render-frames`;
2677
+ const olImportDir = `${backupDir}/object-layers`;
2678
+ if (fs.existsSync(rfImportDir) && fs.existsSync(olImportDir)) {
2679
+ const srcBasePath = './src/client/public/cyberia/';
2680
+ const publicBasePath = `./public/${host}${path}`;
2681
+ let staticWriteCount = 0;
2682
+ const rfFileList = fs.readdirSync(rfImportDir).filter((f) => f.endsWith('.json'));
2683
+ for (const rfFile of rfFileList) {
2684
+ const rfData = fs.readJsonSync(`${rfImportDir}/${rfFile}`);
2685
+ const itemId = nodePath.basename(rfFile, '.json');
2686
+ const olFile = `${olImportDir}/${itemId}.json`;
2687
+ if (!fs.existsSync(olFile)) {
2688
+ logger.warn(`Skipping static asset generation for '${itemId}' no matching object-layer file`);
2689
+ continue;
2690
+ }
2691
+ const olData = fs.readJsonSync(olFile);
2692
+ const itemType = olData.data?.item?.type;
2693
+ if (!itemType) {
2694
+ logger.warn(`Skipping static asset generation for '${itemId}' — missing data.item.type`);
2695
+ continue;
2696
+ }
2697
+ // rfData matches the ObjectLayerRenderFrames schema: { frames, colors, frame_duration }
2698
+ const objectLayerRenderFramesData = {
2699
+ frames: rfData.frames || {},
2700
+ colors: rfData.colors || [],
2701
+ frame_duration: rfData.frame_duration ?? 100,
2702
+ };
2703
+ try {
2704
+ const written = await ObjectLayerEngine.writeStaticFrameAssets({
2705
+ basePaths: [srcBasePath, publicBasePath],
2706
+ itemType,
2707
+ itemId,
2708
+ objectLayerRenderFramesData,
2709
+ objectLayerData: olData,
2710
+ cellPixelDim: 20,
2711
+ });
2712
+ staticWriteCount += written.length;
2713
+ } catch (err) {
2714
+ logger.warn(`Failed to write static assets for '${itemId}': ${err.message}`);
2715
+ }
2155
2716
  }
2156
- logger.info(`IPFS re-pin: ${repinCount}/${pinnedCids.size} CIDs pinned`);
2717
+ logger.info(`Static frame PNGs written: ${staticWriteCount} file(s) across src/client/public and public/`);
2157
2718
  }
2158
2719
 
2159
- // 6. Import maps (preserveUUID: delete by code then create with exact _id)
2720
+ // 5. Import maps (preserveUUID: delete by code then create with exact _id)
2160
2721
  const mapsDir = `${backupDir}/maps`;
2161
2722
  if (fs.existsSync(mapsDir)) {
2162
2723
  const mapFiles = fs.readdirSync(mapsDir).filter((f) => f.endsWith('.json'));
2163
2724
  let mapCount = 0;
2164
2725
  for (const file of mapFiles) {
2165
2726
  const mapData = fs.readJsonSync(`${mapsDir}/${file}`);
2166
- // Remove any existing map with this code (may have different _id)
2167
2727
  await CyberiaMap.deleteOne({ code: mapData.code });
2168
- // Also remove if an old doc with this _id exists
2169
2728
  await CyberiaMap.deleteOne({ _id: mapData._id });
2170
2729
  await CyberiaMap.create(mapData);
2171
2730
  mapCount++;
@@ -2173,6 +2732,18 @@ try {
2173
2732
  logger.info(`Imported ${mapCount} CyberiaMap document(s)`);
2174
2733
  }
2175
2734
 
2735
+ // 6. Import CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
2736
+ const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2737
+ if (fs.existsSync(confImportPath)) {
2738
+ const confData = fs.readJsonSync(confImportPath);
2739
+ if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2740
+ await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2741
+ await CyberiaInstanceConf.create(confData);
2742
+ logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
2743
+ } else {
2744
+ logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
2745
+ }
2746
+
2176
2747
  // 7. Import instance (preserveUUID: delete by code then create with exact _id)
2177
2748
  const instancePath = `${backupDir}/cyberia-instance.json`;
2178
2749
  if (fs.existsSync(instancePath)) {
@@ -2185,6 +2756,283 @@ try {
2185
2756
  logger.warn(`Instance file not found: ${instancePath}`);
2186
2757
  }
2187
2758
 
2759
+ // 8. Import CyberiaDialogue documents
2760
+ const dialoguesDir = `${backupDir}/cyberia-dialogues`;
2761
+ if (fs.existsSync(dialoguesDir)) {
2762
+ const dialogueFiles = fs.readdirSync(dialoguesDir).filter((f) => f.endsWith('.json'));
2763
+ let dialogueCount = 0;
2764
+
2765
+ for (const file of dialogueFiles) {
2766
+ const rawDialogueData = fs.readJsonSync(`${dialoguesDir}/${file}`);
2767
+ const dialogues = Array.isArray(rawDialogueData) ? rawDialogueData : [rawDialogueData];
2768
+ const dialogueCodes = [...new Set(dialogues.map((dialogue) => dialogue.code).filter(Boolean))];
2769
+ if (dialogueCodes.length === 0) {
2770
+ logger.warn(`Skipping CyberiaDialogue backup without code: ${file}`);
2771
+ continue;
2772
+ }
2773
+
2774
+ await CyberiaDialogue.deleteMany({ code: { $in: dialogueCodes } });
2775
+
2776
+ const dialogueIds = dialogues.map((dialogue) => dialogue._id).filter(Boolean);
2777
+ if (dialogueIds.length > 0) {
2778
+ await CyberiaDialogue.deleteMany({ _id: { $in: dialogueIds } });
2779
+ }
2780
+
2781
+ await CyberiaDialogue.create(dialogues);
2782
+ dialogueCount += dialogues.length;
2783
+ }
2784
+
2785
+ logger.info(`Imported ${dialogueCount} CyberiaDialogue document(s)`);
2786
+ }
2787
+
2788
+ // 9. Restore IPFS pin records and payloads
2789
+ const ipfsFile = `${backupDir}/ipfs/pins.json`;
2790
+ if (fs.existsSync(ipfsFile)) {
2791
+ const ipfsDocs = fs.readJsonSync(ipfsFile);
2792
+ const ipfsContentDir = `${backupDir}/ipfs/content`;
2793
+ let ipfsCount = 0;
2794
+ let ipfsSkipped = 0;
2795
+
2796
+ const backupPins = new Map();
2797
+ for (const doc of ipfsDocs) {
2798
+ const resourceType = inferResourceType(doc);
2799
+ if (!resourceType) {
2800
+ logger.warn(
2801
+ `Ipfs record is missing resourceType and cannot be inferred (cid: ${doc.cid}, mfsPath: ${doc.mfsPath ?? '(none)'}) — skipping`,
2802
+ );
2803
+ ipfsSkipped++;
2804
+ continue;
2805
+ }
2806
+
2807
+ const mfsPaths = collectMfsPaths(doc);
2808
+ if (mfsPaths.length === 0) {
2809
+ upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath: '' });
2810
+ } else {
2811
+ for (const mfsPath of mfsPaths) {
2812
+ upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath });
2813
+ }
2814
+ }
2815
+ }
2816
+
2817
+ const backupPinEntries = serialiseCanonicalPins(backupPins);
2818
+ const backupCids = [...new Set(backupPinEntries.map((entry) => entry.cid).filter(Boolean))];
2819
+ if (backupCids.length > 0) {
2820
+ await Ipfs.deleteMany({ cid: { $in: backupCids } });
2821
+ }
2822
+
2823
+ const restoreAdditionalMfsPaths = async (cid, mfsPaths, primaryPath) => {
2824
+ let restoredCount = 0;
2825
+ for (const mfsPath of mfsPaths) {
2826
+ if (!mfsPath || mfsPath === primaryPath) continue;
2827
+ const ok = await IpfsClient.restoreMfsPath(cid, mfsPath);
2828
+ if (ok) restoredCount++;
2829
+ }
2830
+ return restoredCount;
2831
+ };
2832
+
2833
+ const upsertImportedPin = async ({ cid, resourceType, mfsPath }) => {
2834
+ if (!cid || !resourceType) return;
2835
+ await Ipfs.deleteMany({ cid, resourceType });
2836
+ await createPinRecord({ cid, resourceType, mfsPath: mfsPath || '', options: { host, path } });
2837
+ };
2838
+
2839
+ if (fs.existsSync(ipfsContentDir)) {
2840
+ let cidRewriteCount = 0;
2841
+ let extraMfsRestoreCount = 0;
2842
+
2843
+ for (const [index, doc] of backupPinEntries.entries()) {
2844
+ const mfsPaths = collectMfsPaths(doc);
2845
+ const primaryPath = mfsPaths[0] || '';
2846
+ const payloadPath = `${ipfsContentDir}/${doc.cid}.bin`;
2847
+
2848
+ logger.info('IPFS raw payload restore start', {
2849
+ index: index + 1,
2850
+ total: backupPinEntries.length,
2851
+ cid: doc.cid,
2852
+ resourceType: doc.resourceType,
2853
+ mfsPath: primaryPath || null,
2854
+ });
2855
+
2856
+ if (!fs.existsSync(payloadPath)) {
2857
+ logger.warn('IPFS raw payload file missing from backup', {
2858
+ cid: doc.cid,
2859
+ resourceType: doc.resourceType,
2860
+ mfsPath: primaryPath || null,
2861
+ });
2862
+ ipfsSkipped++;
2863
+ continue;
2864
+ }
2865
+
2866
+ const addResult = await IpfsClient.addToIpfs(
2867
+ fs.readFileSync(payloadPath),
2868
+ nodePath.basename(primaryPath || doc.cid),
2869
+ primaryPath || undefined,
2870
+ );
2871
+
2872
+ if (!addResult?.cid) {
2873
+ logger.warn('IPFS raw payload restore failed', {
2874
+ cid: doc.cid,
2875
+ resourceType: doc.resourceType,
2876
+ mfsPath: primaryPath || null,
2877
+ });
2878
+ ipfsSkipped++;
2879
+ continue;
2880
+ }
2881
+
2882
+ const finalCid = addResult.cid;
2883
+ if (doc.cid !== finalCid) {
2884
+ await rewriteImportedCidReferences({
2885
+ oldCid: doc.cid,
2886
+ newCid: finalCid,
2887
+ resourceType: doc.resourceType,
2888
+ });
2889
+ cidRewriteCount++;
2890
+ logger.warn('IPFS raw payload CID mismatch during import; rewriting imported references', {
2891
+ oldCid: doc.cid,
2892
+ newCid: finalCid,
2893
+ resourceType: doc.resourceType,
2894
+ mfsPath: primaryPath || null,
2895
+ });
2896
+ }
2897
+
2898
+ extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
2899
+ await upsertImportedPin({ cid: finalCid, resourceType: doc.resourceType, mfsPath: primaryPath });
2900
+ ipfsCount++;
2901
+ }
2902
+
2903
+ logger.info(
2904
+ `Imported ${ipfsCount} Ipfs pin record(s) from exact backup payloads${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
2905
+ );
2906
+ logger.info(
2907
+ `IPFS raw payload restore: ${ipfsCount}/${backupPinEntries.length} record(s) restored, ${extraMfsRestoreCount} additional MFS path(s) restored${cidRewriteCount ? `, ${cidRewriteCount} CID rewrite(s)` : ''}`,
2908
+ );
2909
+ } else {
2910
+ logger.warn(
2911
+ 'Backup has no raw IPFS payload files under ipfs/content/. Rebuilding a canonical IPFS layout from imported ObjectLayer, AtlasSpriteSheet, and File documents.',
2912
+ );
2913
+
2914
+ const importedItemIds = fs.existsSync(olDir)
2915
+ ? fs
2916
+ .readdirSync(olDir)
2917
+ .filter((f) => f.endsWith('.json'))
2918
+ .map((f) => nodePath.basename(f, '.json'))
2919
+ : [];
2920
+ const importedObjectLayers = importedItemIds.length
2921
+ ? await ObjectLayer.find({ 'data.item.id': { $in: importedItemIds } }).lean()
2922
+ : [];
2923
+
2924
+ let rebuiltObjectLayers = 0;
2925
+
2926
+ for (const [index, objectLayerDoc] of importedObjectLayers.entries()) {
2927
+ const itemKey = objectLayerDoc.data?.item?.id || objectLayerDoc._id.toString();
2928
+ const itemPaths = getCanonicalIpfsPaths(itemKey);
2929
+ const updatedData = newInstance(objectLayerDoc.data || {});
2930
+ if (!updatedData.render) updatedData.render = {};
2931
+
2932
+ logger.info('IPFS legacy canonical rebuild start', {
2933
+ index: index + 1,
2934
+ total: importedObjectLayers.length,
2935
+ itemKey,
2936
+ });
2937
+
2938
+ let atlasCid = '';
2939
+ let atlasMetadataCid = '';
2940
+
2941
+ if (objectLayerDoc.atlasSpriteSheetId) {
2942
+ const atlasDoc = await AtlasSpriteSheet.findById(objectLayerDoc.atlasSpriteSheetId).lean();
2943
+ if (atlasDoc) {
2944
+ const atlasFile = atlasDoc.fileId ? await File.findById(atlasDoc.fileId).lean() : null;
2945
+ const atlasBuffer = toBuffer(atlasFile?.data);
2946
+
2947
+ if (atlasBuffer) {
2948
+ const atlasAddResult = await IpfsClient.addBufferToIpfs(
2949
+ atlasBuffer,
2950
+ `${itemKey}_atlas_sprite_sheet.png`,
2951
+ itemPaths.atlasSpriteSheet,
2952
+ );
2953
+ if (atlasAddResult?.cid) {
2954
+ atlasCid = atlasAddResult.cid;
2955
+ await AtlasSpriteSheet.updateOne({ _id: atlasDoc._id }, { $set: { cid: atlasCid } });
2956
+ await createPinRecord({
2957
+ cid: atlasCid,
2958
+ resourceType: 'atlas-sprite-sheet',
2959
+ mfsPath: itemPaths.atlasSpriteSheet,
2960
+ options: { host, path },
2961
+ });
2962
+ ipfsCount++;
2963
+ } else {
2964
+ logger.warn(`Failed to rebuild atlas sprite sheet payload for '${itemKey}'`);
2965
+ }
2966
+ } else if (atlasDoc.fileId) {
2967
+ logger.warn(`Atlas File payload missing for '${itemKey}'`);
2968
+ }
2969
+
2970
+ const atlasMetadataResult = await IpfsClient.addJsonToIpfs(
2971
+ atlasDoc.metadata || {},
2972
+ `${itemKey}_atlas_sprite_sheet_metadata.json`,
2973
+ itemPaths.atlasMetadata,
2974
+ );
2975
+ if (atlasMetadataResult?.cid) {
2976
+ atlasMetadataCid = atlasMetadataResult.cid;
2977
+ await createPinRecord({
2978
+ cid: atlasMetadataCid,
2979
+ resourceType: 'atlas-metadata',
2980
+ mfsPath: itemPaths.atlasMetadata,
2981
+ options: { host, path },
2982
+ });
2983
+ ipfsCount++;
2984
+ } else {
2985
+ logger.warn(`Failed to rebuild atlas metadata payload for '${itemKey}'`);
2986
+ }
2987
+ }
2988
+ }
2989
+
2990
+ if (atlasCid) {
2991
+ updatedData.render.cid = atlasCid;
2992
+ } else {
2993
+ delete updatedData.render.cid;
2994
+ }
2995
+ if (atlasMetadataCid) {
2996
+ updatedData.render.metadataCid = atlasMetadataCid;
2997
+ } else {
2998
+ delete updatedData.render.metadataCid;
2999
+ }
3000
+
3001
+ const objectLayerAddResult = await IpfsClient.addJsonToIpfs(
3002
+ updatedData,
3003
+ `${itemKey}_data.json`,
3004
+ itemPaths.objectLayerData,
3005
+ );
3006
+ if (objectLayerAddResult?.cid) {
3007
+ await ObjectLayer.updateOne(
3008
+ { _id: objectLayerDoc._id },
3009
+ {
3010
+ $set: {
3011
+ cid: objectLayerAddResult.cid,
3012
+ data: updatedData,
3013
+ },
3014
+ },
3015
+ );
3016
+ await createPinRecord({
3017
+ cid: objectLayerAddResult.cid,
3018
+ resourceType: 'object-layer-data',
3019
+ mfsPath: itemPaths.objectLayerData,
3020
+ options: { host, path },
3021
+ });
3022
+ ipfsCount++;
3023
+ rebuiltObjectLayers++;
3024
+ } else {
3025
+ logger.warn(`Failed to rebuild object-layer-data payload for '${itemKey}'`);
3026
+ ipfsSkipped++;
3027
+ }
3028
+ }
3029
+
3030
+ logger.info(
3031
+ `Legacy IPFS rebuild: ${rebuiltObjectLayers}/${importedObjectLayers.length} ObjectLayer payload(s) rebuilt, ${ipfsCount} canonical pin record(s) upserted${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
3032
+ );
3033
+ }
3034
+ }
3035
+
2188
3036
  logger.info('Instance import completed', { backupDir });
2189
3037
  }
2190
3038
 
@@ -2201,13 +3049,41 @@ try {
2201
3049
  // Collect thumbnail File IDs to drop
2202
3050
  const thumbFileIds = [];
2203
3051
  if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
3052
+ const dropOlItemIds = new Set();
2204
3053
 
2205
3054
  // Query other instances for shared thumbnail exclusion
2206
3055
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
2207
3056
 
3057
+ // Add instance-level itemIds (may not appear in any map entity)
3058
+ for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
3059
+
3060
+ // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
3061
+ const existingConf =
3062
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
3063
+ (existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
3064
+ if (existingConf) {
3065
+ for (const ed of existingConf.entityDefaults || []) {
3066
+ for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
3067
+ for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
3068
+ for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
3069
+ for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
3070
+ }
3071
+ for (const sc of existingConf.skillConfig || []) {
3072
+ if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
3073
+ for (const skill of sc.skills || []) {
3074
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
3075
+ dropOlItemIds.add(skill.summonedEntityItemId);
3076
+ }
3077
+ }
3078
+ }
3079
+
3080
+ const otherMaps = await CyberiaMap.find(
3081
+ { code: { $nin: [...dropMapCodes] } },
3082
+ { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
3083
+ ).lean();
3084
+
2208
3085
  if (dropMapCodes.size > 0) {
2209
3086
  const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
2210
- const dropOlItemIds = new Set();
2211
3087
  for (const map of dropMaps) {
2212
3088
  if (map.thumbnail) thumbFileIds.push(map.thumbnail);
2213
3089
  for (const entity of map.entities || []) {
@@ -2216,103 +3092,102 @@ try {
2216
3092
  }
2217
3093
  }
2218
3094
  }
3095
+ const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
3096
+ logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
3097
+ }
2219
3098
 
2220
- // Exclude OL item IDs referenced by maps outside this instance
2221
- const otherMaps = await CyberiaMap.find(
2222
- { code: { $nin: [...dropMapCodes] } },
2223
- { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
2224
- ).lean();
2225
- const sharedOlItemIds = new Set();
2226
- for (const m of otherMaps) {
2227
- for (const entity of m.entities || []) {
2228
- for (const itemId of entity.objectLayerItemIds || []) {
2229
- if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
2230
- }
3099
+ // Exclude OL item IDs referenced by maps outside this instance
3100
+ const sharedOlItemIds = new Set();
3101
+ for (const m of otherMaps) {
3102
+ for (const entity of m.entities || []) {
3103
+ for (const itemId of entity.objectLayerItemIds || []) {
3104
+ if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
2231
3105
  }
2232
3106
  }
2233
- for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
2234
- if (sharedOlItemIds.size > 0) {
2235
- logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
2236
- }
3107
+ }
3108
+ for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
3109
+ if (sharedOlItemIds.size > 0) {
3110
+ logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
3111
+ }
2237
3112
 
2238
- // Exclude thumbnail File IDs referenced by other instances or maps
2239
- const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
2240
- const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
2241
- const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
2242
- for (let i = thumbFileIds.length - 1; i >= 0; i--) {
2243
- if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
2244
- }
3113
+ // Exclude thumbnail File IDs referenced by other instances or maps
3114
+ const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
3115
+ const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
3116
+ const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
3117
+ for (let i = thumbFileIds.length - 1; i >= 0; i--) {
3118
+ if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
3119
+ }
2245
3120
 
2246
- if (dropOlItemIds.size > 0) {
2247
- const olDocs = await ObjectLayer.find(
2248
- { 'data.item.id': { $in: [...dropOlItemIds] } },
2249
- {
2250
- cid: 1,
2251
- 'data.item.id': 1,
2252
- 'data.render': 1,
2253
- objectLayerRenderFramesId: 1,
2254
- atlasSpriteSheetId: 1,
2255
- },
2256
- ).lean();
3121
+ if (dropOlItemIds.size > 0) {
3122
+ const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
3123
+ const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
3124
+ logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
3125
+
3126
+ const olDocs = await ObjectLayer.find(
3127
+ { 'data.item.id': { $in: [...dropOlItemIds] } },
3128
+ {
3129
+ cid: 1,
3130
+ 'data.item.id': 1,
3131
+ 'data.render': 1,
3132
+ objectLayerRenderFramesId: 1,
3133
+ atlasSpriteSheetId: 1,
3134
+ },
3135
+ ).lean();
2257
3136
 
2258
- const cidsToUnpin = new Set();
2259
- const renderFrameIds = [];
2260
- const atlasIds = [];
2261
- const itemKeysToClean = new Set();
3137
+ const cidsToUnpin = new Set();
3138
+ const renderFrameIds = [];
3139
+ const atlasIds = [];
3140
+ const itemKeysToClean = new Set();
2262
3141
 
2263
- for (const doc of olDocs) {
2264
- if (doc.cid) cidsToUnpin.add(doc.cid);
2265
- if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
2266
- if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
2267
- if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
2268
- if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
2269
- if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
2270
- }
3142
+ for (const doc of olDocs) {
3143
+ if (doc.cid) cidsToUnpin.add(doc.cid);
3144
+ if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
3145
+ if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
3146
+ if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
3147
+ if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
3148
+ if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
3149
+ }
2271
3150
 
2272
- if (atlasIds.length > 0) {
2273
- const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
2274
- const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
2275
- for (const atlas of atlasDocs) {
2276
- if (atlas.cid) cidsToUnpin.add(atlas.cid);
2277
- }
2278
- if (atlasFileIds.length > 0) {
2279
- const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
2280
- logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2281
- }
2282
- const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
2283
- logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
3151
+ if (atlasIds.length > 0) {
3152
+ const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
3153
+ const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
3154
+ for (const atlas of atlasDocs) {
3155
+ if (atlas.cid) cidsToUnpin.add(atlas.cid);
2284
3156
  }
2285
-
2286
- if (renderFrameIds.length > 0) {
2287
- const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
2288
- logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
3157
+ if (atlasFileIds.length > 0) {
3158
+ const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
3159
+ logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2289
3160
  }
3161
+ const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
3162
+ logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
3163
+ }
2290
3164
 
2291
- if (cidsToUnpin.size > 0) {
2292
- const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
2293
- logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
2294
- }
3165
+ if (renderFrameIds.length > 0) {
3166
+ const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
3167
+ logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
3168
+ }
2295
3169
 
2296
- let unpinCount = 0;
2297
- for (const cid of cidsToUnpin) {
2298
- const ok = await IpfsClient.unpinCid(cid);
2299
- if (ok) unpinCount++;
2300
- }
2301
- let mfsCount = 0;
2302
- for (const itemKey of itemKeysToClean) {
2303
- const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
2304
- if (ok) mfsCount++;
2305
- }
2306
- logger.info(
2307
- `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
2308
- );
3170
+ if (cidsToUnpin.size > 0) {
3171
+ const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
3172
+ logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
3173
+ }
2309
3174
 
2310
- const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2311
- logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
3175
+ let unpinCount = 0;
3176
+ for (const cid of cidsToUnpin) {
3177
+ const ok = await IpfsClient.unpinCid(cid);
3178
+ if (ok) unpinCount++;
2312
3179
  }
3180
+ let mfsCount = 0;
3181
+ for (const itemKey of itemKeysToClean) {
3182
+ const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
3183
+ if (ok) mfsCount++;
3184
+ }
3185
+ logger.info(
3186
+ `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
3187
+ );
2313
3188
 
2314
- const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2315
- logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
3189
+ const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
3190
+ logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2316
3191
  }
2317
3192
 
2318
3193
  // Drop thumbnail File documents (instance + maps), excluding shared ones
@@ -2323,6 +3198,8 @@ try {
2323
3198
 
2324
3199
  await CyberiaInstance.deleteOne({ code: instanceCode });
2325
3200
  logger.info('Dropped CyberiaInstance', { code: instanceCode });
3201
+ await CyberiaInstanceConf.deleteOne({ instanceCode });
3202
+ logger.info('Dropped CyberiaInstanceConf', { instanceCode });
2326
3203
  } else {
2327
3204
  logger.info('No existing instance to drop', { code: instanceCode });
2328
3205
  }
@@ -2332,7 +3209,106 @@ try {
2332
3209
  logger.error('Specify --export, --import, or --drop flag');
2333
3210
  }
2334
3211
 
2335
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
3212
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3213
+ });
3214
+
3215
+ // ── client-hints: presentation hints management ──────────────────────────
3216
+ program
3217
+ .command('client-hints [instance-code]')
3218
+ .option('--export [path]', 'Export CyberiaClientHints document to JSON (default: ./client-hints-<code>.json)')
3219
+ .option('--import [path]', 'Upsert CyberiaClientHints from a JSON file')
3220
+ .option('--seed-defaults', 'Upsert canonical presentation-hint defaults for the given instance code')
3221
+ .option('--drop', 'Remove the CyberiaClientHints document for the given instance code')
3222
+ .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
3223
+ .option('--mongo-host <mongo-host>', 'Mongo host override')
3224
+ .option('--dev', 'Force development environment')
3225
+ .description('Manage per-instance client presentation hints (palette, camera, status icons, interpolation)')
3226
+ .action(async (instanceCode, options = {}) => {
3227
+ try {
3228
+ const envPath =
3229
+ options.envPath || `./engine-private/conf/dd-cyberia/.env.${options.dev ? 'development' : 'production'}`;
3230
+ if (fs.existsSync(envPath)) dotenv.config({ path: envPath, override: true });
3231
+
3232
+ const { CYBERIA_CLIENT_HINTS_DEFAULTS, buildClientHints } =
3233
+ await import('../src/client/components/cyberia/SharedDefaultsCyberia.js');
3234
+
3235
+ const deployId = process.env.DEFAULT_DEPLOY_ID;
3236
+ const host = process.env.DEFAULT_DEPLOY_HOST;
3237
+ const path = process.env.DEFAULT_DEPLOY_PATH;
3238
+ const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
3239
+ if (!fs.existsSync(confServerPath)) throw new Error(`Config not found: ${confServerPath}`);
3240
+ const confServer = loadConfServerJson(confServerPath, { resolve: true });
3241
+ const { db } = confServer[host][path];
3242
+ db.host = options.mongoHost ? options.mongoHost : db.host.replace('127.0.0.1', 'mongodb-0.mongodb-service');
3243
+
3244
+ await DataBaseProviderService.load({ apis: ['cyberia-client-hints'], host, path, db });
3245
+ const CyberiaClientHints = DataBaseProviderService.getModel('cyberia-client-hints', { host, path });
3246
+
3247
+ if (!instanceCode && !options.seedDefaults) {
3248
+ logger.error('instance-code required for client-hints operations (omit only with --seed-defaults on all)');
3249
+ process.exit(1);
3250
+ }
3251
+
3252
+ if (options.drop) {
3253
+ if (!instanceCode) {
3254
+ logger.error('instance-code required for --drop');
3255
+ process.exit(1);
3256
+ }
3257
+ const result = await CyberiaClientHints.deleteOne({ code: instanceCode });
3258
+ logger.info(`client-hints --drop: removed ${result.deletedCount} document(s) for code="${instanceCode}"`);
3259
+ }
3260
+
3261
+ if (options.seedDefaults) {
3262
+ const codes = instanceCode ? [instanceCode] : [];
3263
+ if (codes.length === 0) {
3264
+ logger.error('instance-code required for --seed-defaults');
3265
+ process.exit(1);
3266
+ }
3267
+ for (const code of codes) {
3268
+ await CyberiaClientHints.findOneAndUpdate(
3269
+ { code },
3270
+ { $setOnInsert: { code, ...CYBERIA_CLIENT_HINTS_DEFAULTS } },
3271
+ { upsert: true, returnDocument: 'after' },
3272
+ );
3273
+ logger.info(`client-hints --seed-defaults: seeded defaults for code="${code}"`);
3274
+ }
3275
+ }
3276
+
3277
+ if (options.import) {
3278
+ const filePath = typeof options.import === 'string' ? options.import : `./client-hints-${instanceCode}.json`;
3279
+ if (!fs.existsSync(filePath)) throw new Error(`Import file not found: ${filePath}`);
3280
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
3281
+ const code = data.code || instanceCode;
3282
+ if (!code) {
3283
+ logger.error('instance-code required (from file.code or CLI argument)');
3284
+ process.exit(1);
3285
+ }
3286
+ await CyberiaClientHints.findOneAndUpdate({ code }, { $set: { code, ...data } }, { upsert: true, new: true });
3287
+ logger.info(`client-hints --import: upserted code="${code}" from ${filePath}`);
3288
+ }
3289
+
3290
+ if (options.export) {
3291
+ if (!instanceCode) {
3292
+ logger.error('instance-code required for --export');
3293
+ process.exit(1);
3294
+ }
3295
+ const doc = await CyberiaClientHints.findOne({ code: instanceCode }).lean();
3296
+ if (!doc) {
3297
+ logger.warn(`No client-hints document found for code="${instanceCode}", exporting defaults`);
3298
+ }
3299
+ const outPath = typeof options.export === 'string' ? options.export : `./client-hints-${instanceCode}.json`;
3300
+ fs.writeFileSync(
3301
+ outPath,
3302
+ JSON.stringify(doc || { code: instanceCode, ...CYBERIA_CLIENT_HINTS_DEFAULTS }, null, 2),
3303
+ );
3304
+ logger.info(`client-hints --export: wrote ${outPath}`);
3305
+ }
3306
+
3307
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3308
+ } catch (err) {
3309
+ logger.error('client-hints command error:', err);
3310
+ process.exit(1);
3311
+ }
2336
3312
  });
2337
3313
 
2338
3314
  // ── chain: Hyperledger Besu / ERC-1155 lifecycle commands ────────────────
@@ -2532,7 +3508,7 @@ try {
2532
3508
  logger.info(` SHA-256: ${resolved.sha256}`);
2533
3509
 
2534
3510
  // Close the DB connection after resolving
2535
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
3511
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
2536
3512
  } catch (dbErr) {
2537
3513
  logger.error(`Failed to resolve canonical CID from database: ${dbErr.message}`);
2538
3514
  process.exit(1);
@@ -3100,7 +4076,7 @@ try {
3100
4076
  }
3101
4077
 
3102
4078
  try {
3103
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
4079
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3104
4080
  } catch (_) {
3105
4081
  /* ignore close errors */
3106
4082
  }
@@ -3155,12 +4131,28 @@ try {
3155
4131
  runner
3156
4132
  .command('import-default-items')
3157
4133
  .option('--dev', 'Force development environment (loads .env.development for IPFS localhost, etc.)')
3158
- .description('Import default Object Layer items, skill config, and dialogues into MongoDB')
4134
+ .description('Import default Object Layer items, skill config, dialogues, and client-hints into MongoDB')
3159
4135
  .action(async (options) => {
4136
+ // Pre-flight: every item id referenced by the fallback world must
4137
+ // exist in DefaultCyberiaItems. Drift here causes silent missing
4138
+ // sprites at runtime, so fail loudly before we touch MongoDB.
4139
+ const { auditFallbackItemIds } = await import('../src/api/cyberia-instance/cyberia-fallback-world.js');
4140
+ const missing = auditFallbackItemIds();
4141
+ if (missing.length > 0) {
4142
+ logger.error(
4143
+ 'import-default-items aborted: item ids referenced by defaults are missing from DefaultCyberiaItems:',
4144
+ missing.join(', '),
4145
+ '— add them to cyberia-server-defaults.js before seeding.',
4146
+ );
4147
+ process.exit(1);
4148
+ }
4149
+
3160
4150
  const devFlag = options.dev ? ' --dev' : '';
4151
+ const instanceCode = process.env.INSTANCE_CODE || 'cyberia-main';
3161
4152
  shellExec(`node bin/cyberia ol ${DefaultCyberiaItems.map((e) => e.item.id)} --import${devFlag}`);
3162
4153
  shellExec(`node bin/cyberia run-workflow seed-skill-config${devFlag}`);
3163
4154
  shellExec(`node bin/cyberia run-workflow seed-dialogues${devFlag}`);
4155
+ shellExec(`node bin/cyberia client-hints ${instanceCode} --seed-defaults${devFlag}`);
3164
4156
  });
3165
4157
 
3166
4158
  runner
@@ -3200,10 +4192,10 @@ try {
3200
4192
 
3201
4193
  logger.info('seed-skill-config', { instanceCode, deployId, host, path, db });
3202
4194
 
3203
- await DataBaseProvider.load({ apis: ['cyberia-instance', 'cyberia-instance-conf'], host, path, db });
4195
+ await DataBaseProviderService.load({ apis: ['cyberia-instance', 'cyberia-instance-conf'], host, path, db });
3204
4196
 
3205
- const CyberiaInstance = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaInstance;
3206
- const CyberiaInstanceConf = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaInstanceConf;
4197
+ const CyberiaInstance = DataBaseProviderService.getModel('cyberia-instance', { host, path });
4198
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('cyberia-instance-conf', { host, path });
3207
4199
 
3208
4200
  const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
3209
4201
 
@@ -3231,7 +4223,7 @@ try {
3231
4223
  DefaultSkillConfig.map((e) => `${e.triggerItemId} → [${e.logicEventIds.join(', ')}]`),
3232
4224
  );
3233
4225
 
3234
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
4226
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3235
4227
  });
3236
4228
 
3237
4229
  runner
@@ -3269,15 +4261,15 @@ try {
3269
4261
 
3270
4262
  logger.info('seed-dialogues', { deployId, host, path, db });
3271
4263
 
3272
- await DataBaseProvider.load({ apis: ['cyberia-dialogue'], host, path, db });
4264
+ await DataBaseProviderService.load({ apis: ['cyberia-dialogue'], host, path, db });
3273
4265
 
3274
- const CyberiaDialogue = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaDialogue;
4266
+ const CyberiaDialogue = DataBaseProviderService.getModel('cyberia-dialogue', { host, path });
3275
4267
 
3276
- // Upsert each dialogue record keyed by (itemId, order) — idempotent.
4268
+ // Upsert each dialogue record keyed by (code, order) — idempotent.
3277
4269
  let upserted = 0;
3278
4270
  for (const dlg of DefaultCyberiaDialogues) {
3279
4271
  await CyberiaDialogue.findOneAndUpdate(
3280
- { itemId: dlg.itemId, order: dlg.order },
4272
+ { code: dlg.code, order: dlg.order },
3281
4273
  { $set: { speaker: dlg.speaker, text: dlg.text, mood: dlg.mood } },
3282
4274
  { upsert: true },
3283
4275
  );
@@ -3286,7 +4278,7 @@ try {
3286
4278
 
3287
4279
  logger.info(`seed-dialogues: ${upserted} dialogue records upserted`);
3288
4280
 
3289
- await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
4281
+ await DataBaseProviderService.getProvider({ host, path }, 'mongoose').close();
3290
4282
  });
3291
4283
 
3292
4284
  runner
@@ -3298,17 +4290,37 @@ try {
3298
4290
  .description('Generate one procedural example of every registered semantic prefix')
3299
4291
  .action(async (options) => {
3300
4292
  const SEMANTIC_TYPES = [
3301
- 'floor-desert',
3302
- 'floor-grass',
4293
+ // 'floor-desert',
4294
+ // 'floor-grass',
3303
4295
  // 'floor-water',
3304
4296
  // 'floor-stone',
3305
4297
  // 'floor-lava',
3306
4298
  'skin-random',
3307
- // 'skin-dark',
3308
- // 'skin-light',
3309
- // 'skin-vivid',
3310
- // 'skin-natural',
4299
+ 'skin-dark',
4300
+ 'skin-light',
4301
+ 'skin-vivid',
4302
+ 'skin-natural',
3311
4303
  'skin-shaved',
4304
+ // 'resource-desert-petal',
4305
+ // 'resource-desert-stone',
4306
+ // 'resource-desert-polygon',
4307
+ // 'resource-desert-thread',
4308
+ // 'resource-grass-petal',
4309
+ // 'resource-grass-stone',
4310
+ // 'resource-grass-polygon',
4311
+ // 'resource-grass-thread',
4312
+ // 'resource-water-petal',
4313
+ // 'resource-water-stone',
4314
+ // 'resource-water-polygon',
4315
+ // 'resource-water-thread',
4316
+ // 'resource-stone-petal',
4317
+ // 'resource-stone-stone',
4318
+ // 'resource-stone-polygon',
4319
+ // 'resource-stone-thread',
4320
+ // 'resource-lava-petal',
4321
+ // 'resource-lava-stone',
4322
+ // 'resource-lava-polygon',
4323
+ // 'resource-lava-thread',
3312
4324
  ];
3313
4325
 
3314
4326
  const baseSeed = options.seed || 'example';
@@ -3330,17 +4342,91 @@ try {
3330
4342
  logger.info('All semantic examples generated.');
3331
4343
  });
3332
4344
 
3333
- if (underpostProgram.commands.find((c) => c._name == process.argv[2]))
4345
+ runner
4346
+ .command('build-manifest')
4347
+ .option(
4348
+ '--dev',
4349
+ 'Build dev-variant manifests (kind cluster, Dockerfile.dev). Default builds prod (kubeadm, Dockerfile).',
4350
+ )
4351
+ .description(
4352
+ 'Build k8s resource manifests for the Cyberia mmo-server + mmo-client instances. ' +
4353
+ 'Without --dev: production manifests (Dockerfile, kubeadm). With --dev: dev manifests (Dockerfile.dev, kind).',
4354
+ )
4355
+ .action((options) => {
4356
+ const isDev = !!options.dev;
4357
+ const flags = isDev ? '--kind --dev' : '--kubeadm';
4358
+ // shellExec is fail-fast by default: any non-zero exit throws
4359
+ // ShellExecError, which propagates to the outer catch and exits
4360
+ // the CLI non-zero — observable by GitHub Actions.
4361
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' ${flags}`);
4362
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' ${flags}`);
4363
+ // Copy canonical doc sources into the generated project READMEs.
4364
+ // Edit the canonical sources; never hand-edit these generated outputs.
4365
+ fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-CLIENT.md', './cyberia-client/README.md');
4366
+ fs.copyFileSync('./src/client/public/cyberia-docs/CYBERIA-SERVER.md', './cyberia-server/README.md');
4367
+ logger.info(`run-workflow build-manifest complete (${isDev ? 'dev' : 'prod'})`);
4368
+ });
4369
+
4370
+ runner
4371
+ .command('build-server-dashboard')
4372
+ .option(
4373
+ '--dev',
4374
+ 'Build a development variant of the dashboard with dev-specific env vars (e.g. localhost API endpoints).',
4375
+ )
4376
+ .option(
4377
+ '--output-path <path>',
4378
+ 'Override output path for the rendered HTML (default: ./cyberia-server/public/index.html). ' +
4379
+ 'Used by CI when this command is invoked from inside an engine checkout that lives ' +
4380
+ 'alongside (not inside) the cyberia-server repo — pass e.g. ../public/index.html.',
4381
+ )
4382
+ .description('Build a static HTML dashboard for cyberia-server metrics and operational status. ')
4383
+ .action((options) => {
4384
+ const outputPath = options.outputPath || './cyberia-server/public/index.html';
4385
+ shellExec(
4386
+ `node bin static --page ./src/client/ssr/views/CyberiaServerMetrics.js` +
4387
+ ` --output-path ${outputPath}` +
4388
+ ` --title 'Cyberia Server Metrics'` +
4389
+ ` --favicon /favicon.ico` +
4390
+ ` --description 'Operational dashboard for the cyberia-server MMO runtime.'` +
4391
+ ` --lang en` +
4392
+ ` --env ${options.dev ? 'development' : 'production'}`,
4393
+ );
4394
+ });
4395
+
4396
+ // Passthrough check: if the user invoked a command that is OWNED by the
4397
+ // underpost CLI (not the cyberia overlay), throw the sentinel error so
4398
+ // the catch block below can re-run argv through underpost. The match is
4399
+ // strict on process.argv[2] (the first positional after `node bin/cyberia`)
4400
+ // so we only passthrough when the top-level command name actually
4401
+ // belongs to underpost.
4402
+ if (
4403
+ process.argv[2] &&
4404
+ underpostProgram.commands.find((c) => c._name === process.argv[2]) &&
4405
+ !program.commands.find((c) => c._name === process.argv[2])
4406
+ ) {
3334
4407
  throw new Error('Trigger underpost passthrough');
4408
+ }
3335
4409
 
3336
4410
  program.parse();
3337
4411
  } catch (error) {
3338
- logger.warn(error);
3339
- process.argv = process.argv.filter((c) => c !== 'underpost');
3340
- logger.warn('Rerouting to underpost cli...');
3341
- try {
3342
- underpostProgram.parse();
3343
- } catch (error) {
4412
+ // ONLY reroute on the explicit passthrough sentinel. Any other thrown
4413
+ // error (subprocess non-zero from shellExec's fail-fast default, CLI
4414
+ // parse errors, missing modules) must propagate as a non-zero process
4415
+ // exit so GitHub Actions / CI parents observe the failure. Without this
4416
+ // guard, a genuine build failure was being silently rerouted into the
4417
+ // underpost CLI and then masked behind a misleading "unknown command"
4418
+ // line.
4419
+ if (error && error.message === 'Trigger underpost passthrough') {
4420
+ process.argv = process.argv.filter((c) => c !== 'underpost');
4421
+ logger.warn('Rerouting to underpost cli...');
4422
+ try {
4423
+ underpostProgram.parse();
4424
+ } catch (err) {
4425
+ logger.error(err);
4426
+ process.exit(1);
4427
+ }
4428
+ } else {
3344
4429
  logger.error(error);
4430
+ process.exit(1);
3345
4431
  }
3346
4432
  }