cyberia 3.2.5 → 3.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +2 -2
  2. package/.github/workflows/release.cd.yml +1 -2
  3. package/CHANGELOG.md +351 -1
  4. package/CLI-HELP.md +40 -13
  5. package/Dockerfile +0 -4
  6. package/README.md +242 -497
  7. package/bin/build.js +19 -5
  8. package/bin/cyberia.js +1149 -240
  9. package/bin/deploy.js +570 -1
  10. package/bin/file.js +6 -0
  11. package/bin/index.js +1149 -240
  12. package/bin/vs.js +1 -1
  13. package/conf.js +67 -89
  14. package/deployment.yaml +4 -222
  15. package/hardhat/package-lock.json +32 -32
  16. package/hardhat/package.json +3 -3
  17. package/jsconfig.json +1 -1
  18. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  19. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
  20. package/manifests/deployment/dd-cyberia-development/deployment.yaml +4 -222
  21. package/manifests/deployment/dd-cyberia-development/proxy.yaml +10 -118
  22. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  23. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  24. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  25. package/package.json +23 -14
  26. package/proxy.yaml +10 -118
  27. package/scripts/k3s-node-setup.sh +2 -2
  28. package/scripts/nat-iptables.sh +103 -18
  29. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +18 -18
  30. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -14
  31. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +76 -21
  32. package/src/api/core/core.controller.js +10 -10
  33. package/src/api/core/core.service.js +10 -10
  34. package/src/api/crypto/crypto.controller.js +8 -8
  35. package/src/api/crypto/crypto.service.js +8 -8
  36. package/src/api/cyberia-action/cyberia-action.controller.js +74 -0
  37. package/src/api/cyberia-action/cyberia-action.model.js +87 -0
  38. package/src/api/cyberia-action/cyberia-action.router.js +27 -0
  39. package/src/api/cyberia-action/cyberia-action.service.js +42 -0
  40. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +13 -13
  41. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +11 -11
  42. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +2 -2
  43. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +16 -16
  44. package/src/api/cyberia-entity/cyberia-entity.controller.js +10 -10
  45. package/src/api/cyberia-entity/cyberia-entity.service.js +10 -10
  46. package/src/api/cyberia-instance/cyberia-fallback-world.js +19 -209
  47. package/src/api/cyberia-instance/cyberia-instance.controller.js +14 -14
  48. package/src/api/cyberia-instance/cyberia-instance.model.js +3 -0
  49. package/src/api/cyberia-instance/cyberia-instance.service.js +22 -57
  50. package/src/api/cyberia-instance/cyberia-portal-connector.js +20 -246
  51. package/src/api/cyberia-instance/cyberia-world-generator.js +505 -0
  52. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +10 -10
  53. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +216 -55
  54. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +4 -1
  55. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +18 -14
  56. package/src/api/cyberia-map/cyberia-map.controller.js +10 -10
  57. package/src/api/cyberia-map/cyberia-map.service.js +10 -10
  58. package/src/api/cyberia-quest/cyberia-quest.controller.js +74 -0
  59. package/src/api/cyberia-quest/cyberia-quest.model.js +67 -0
  60. package/src/api/cyberia-quest/cyberia-quest.router.js +27 -0
  61. package/src/api/cyberia-quest/cyberia-quest.service.js +42 -0
  62. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +74 -0
  63. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +49 -0
  64. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +27 -0
  65. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +42 -0
  66. package/src/api/default/default.controller.js +10 -10
  67. package/src/api/default/default.service.js +10 -10
  68. package/src/api/document/document.controller.js +12 -12
  69. package/src/api/document/document.model.js +10 -16
  70. package/src/api/file/file.controller.js +8 -8
  71. package/src/api/file/file.model.js +10 -10
  72. package/src/api/file/file.service.js +36 -36
  73. package/src/api/instance/instance.controller.js +10 -10
  74. package/src/api/instance/instance.model.js +4 -10
  75. package/src/api/instance/instance.service.js +10 -10
  76. package/src/api/ipfs/ipfs.controller.js +12 -12
  77. package/src/api/ipfs/ipfs.model.js +4 -13
  78. package/src/api/ipfs/ipfs.service.js +14 -28
  79. package/src/api/object-layer/object-layer.controller.js +12 -12
  80. package/src/api/object-layer/object-layer.model.js +4 -17
  81. package/src/api/object-layer/object-layer.service.js +12 -12
  82. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +10 -10
  83. package/src/api/object-layer-render-frames/object-layer-render-frames.model.js +6 -16
  84. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +18 -14
  85. package/src/api/test/test.controller.js +8 -8
  86. package/src/api/test/test.service.js +8 -8
  87. package/src/api/user/guest.service.js +99 -0
  88. package/src/api/user/user.controller.js +6 -6
  89. package/src/api/user/user.model.js +8 -13
  90. package/src/api/user/user.service.js +3 -20
  91. package/src/cli/cluster.js +61 -14
  92. package/src/cli/db.js +47 -2
  93. package/src/cli/deploy.js +67 -35
  94. package/src/cli/fs.js +79 -8
  95. package/src/cli/image.js +43 -1
  96. package/src/cli/index.js +26 -1
  97. package/src/cli/release.js +57 -1
  98. package/src/cli/repository.js +69 -31
  99. package/src/cli/run.js +415 -36
  100. package/src/cli/ssh.js +1 -1
  101. package/src/cli/static.js +43 -115
  102. package/src/client/Cryptokoyn.index.js +18 -21
  103. package/src/client/CyberiaPortal.index.js +19 -23
  104. package/src/client/Default.index.js +21 -33
  105. package/src/client/Itemledger.index.js +20 -26
  106. package/src/client/Underpost.index.js +19 -23
  107. package/src/client/components/core/404.js +4 -4
  108. package/src/client/components/core/500.js +4 -4
  109. package/src/client/components/core/Account.js +73 -60
  110. package/src/client/components/core/AgGrid.js +23 -33
  111. package/src/client/components/core/Alert.js +12 -13
  112. package/src/client/components/core/AppStore.js +1 -1
  113. package/src/client/components/core/Auth.js +35 -37
  114. package/src/client/components/core/Badge.js +7 -13
  115. package/src/client/components/core/BtnIcon.js +15 -17
  116. package/src/client/components/core/CalendarCore.js +42 -63
  117. package/src/client/components/core/Chat.js +13 -15
  118. package/src/client/components/core/ClientEvents.js +87 -0
  119. package/src/client/components/core/ColorPaletteElement.js +309 -0
  120. package/src/client/components/core/Content.js +17 -14
  121. package/src/client/components/core/Css.js +15 -71
  122. package/src/client/components/core/CssCore.js +12 -16
  123. package/src/client/components/core/D3Chart.js +4 -4
  124. package/src/client/components/core/Docs.js +64 -91
  125. package/src/client/components/core/DropDown.js +69 -91
  126. package/src/client/components/core/EventBus.js +92 -0
  127. package/src/client/components/core/EventsUI.js +14 -17
  128. package/src/client/components/core/FileExplorer.js +96 -228
  129. package/src/client/components/core/FullScreen.js +47 -75
  130. package/src/client/components/core/Input.js +24 -69
  131. package/src/client/components/core/Keyboard.js +25 -18
  132. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  133. package/src/client/components/core/LoadingAnimation.js +25 -31
  134. package/src/client/components/core/LogIn.js +41 -41
  135. package/src/client/components/core/LogOut.js +23 -14
  136. package/src/client/components/core/Modal.js +462 -178
  137. package/src/client/components/core/NotificationManager.js +14 -18
  138. package/src/client/components/core/Panel.js +54 -50
  139. package/src/client/components/core/PanelForm.js +25 -125
  140. package/src/client/components/core/Polyhedron.js +110 -214
  141. package/src/client/components/core/PublicProfile.js +39 -32
  142. package/src/client/components/core/Recover.js +48 -44
  143. package/src/client/components/core/Responsive.js +88 -32
  144. package/src/client/components/core/RichText.js +9 -18
  145. package/src/client/components/core/Router.js +24 -3
  146. package/src/client/components/core/SearchBox.js +37 -37
  147. package/src/client/components/core/SignUp.js +39 -30
  148. package/src/client/components/core/SocketIo.js +31 -2
  149. package/src/client/components/core/SocketIoHandler.js +6 -6
  150. package/src/client/components/core/ToggleSwitch.js +8 -20
  151. package/src/client/components/core/ToolTip.js +5 -17
  152. package/src/client/components/core/Translate.js +56 -59
  153. package/src/client/components/core/Validator.js +26 -16
  154. package/src/client/components/core/Wallet.js +15 -26
  155. package/src/client/components/core/Worker.js +163 -27
  156. package/src/client/components/core/windowGetDimensions.js +7 -7
  157. package/src/client/components/cryptokoyn/{MenuCryptokoyn.js → AppShellCryptokoyn.js} +57 -57
  158. package/src/client/components/cryptokoyn/CssCryptokoyn.js +15 -15
  159. package/src/client/components/cryptokoyn/LogInCryptokoyn.js +6 -4
  160. package/src/client/components/cryptokoyn/LogOutCryptokoyn.js +6 -4
  161. package/src/client/components/cryptokoyn/RouterCryptokoyn.js +37 -0
  162. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +4 -4
  163. package/src/client/components/cryptokoyn/SignUpCryptokoyn.js +6 -4
  164. package/src/client/components/cyberia/InstanceEngineCyberia.js +141 -60
  165. package/src/client/components/cyberia/MapEngineCyberia.js +691 -214
  166. package/src/client/components/cyberia/ObjectLayerEngine.js +19 -0
  167. package/src/client/components/cyberia/ObjectLayerEngineModal.js +1204 -94
  168. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +196 -298
  169. package/src/client/components/cyberia-portal/{MenuCyberiaPortal.js → AppShellCyberiaPortal.js} +102 -102
  170. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +305 -61
  171. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +15 -15
  172. package/src/client/components/cyberia-portal/LogInCyberiaPortal.js +6 -4
  173. package/src/client/components/cyberia-portal/LogOutCyberiaPortal.js +6 -4
  174. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +4 -4
  175. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +60 -0
  176. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +4 -4
  177. package/src/client/components/cyberia-portal/SignUpCyberiaPortal.js +6 -4
  178. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -4
  179. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  180. package/src/client/components/default/CssDefault.js +12 -12
  181. package/src/client/components/default/LogInDefault.js +6 -4
  182. package/src/client/components/default/LogOutDefault.js +6 -4
  183. package/src/client/components/default/RouterDefault.js +47 -0
  184. package/src/client/components/default/SettingsDefault.js +4 -4
  185. package/src/client/components/default/SignUpDefault.js +6 -4
  186. package/src/client/components/default/TranslateDefault.js +3 -3
  187. package/src/client/components/itemledger/{MenuItemledger.js → AppShellItemledger.js} +57 -57
  188. package/src/client/components/itemledger/CssItemledger.js +15 -15
  189. package/src/client/components/itemledger/LogInItemledger.js +6 -4
  190. package/src/client/components/itemledger/LogOutItemledger.js +6 -4
  191. package/src/client/components/itemledger/RouterItemledger.js +38 -0
  192. package/src/client/components/itemledger/SettingsItemledger.js +4 -4
  193. package/src/client/components/itemledger/SignUpItemledger.js +6 -4
  194. package/src/client/components/itemledger/TranslateItemledger.js +3 -3
  195. package/src/client/components/underpost/{MenuUnderpost.js → AppShellUnderpost.js} +88 -88
  196. package/src/client/components/underpost/CssUnderpost.js +14 -14
  197. package/src/client/components/underpost/CyberpunkBloggerUnderpost.js +4 -4
  198. package/src/client/components/underpost/DocumentSearchProvider.js +1 -1
  199. package/src/client/components/underpost/LabGalleryUnderpost.js +12 -15
  200. package/src/client/components/underpost/LogInUnderpost.js +6 -4
  201. package/src/client/components/underpost/LogOutUnderpost.js +6 -4
  202. package/src/client/components/underpost/RouterUnderpost.js +45 -0
  203. package/src/client/components/underpost/SettingsUnderpost.js +4 -4
  204. package/src/client/components/underpost/SignUpUnderpost.js +6 -4
  205. package/src/client/components/underpost/TranslateUnderpost.js +4 -4
  206. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +235 -0
  207. package/src/client/public/cyberia-docs/ARCHITECTURE.md +443 -0
  208. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +417 -0
  209. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +313 -0
  210. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +260 -0
  211. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +241 -0
  212. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +300 -0
  213. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +279 -0
  214. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +206 -0
  215. package/src/client/public/cyberia-docs/ROADMAP.md +240 -0
  216. package/src/client/public/cyberia-docs/WHITE-PAPER.md +732 -0
  217. package/src/client/services/atlas-sprite-sheet/atlas-sprite-sheet.service.js +14 -20
  218. package/src/client/services/core/core.service.js +17 -49
  219. package/src/client/services/crypto/crypto.service.js +8 -13
  220. package/src/client/services/cyberia-action/cyberia-action.service.js +99 -0
  221. package/src/client/services/cyberia-dialogue/cyberia-dialogue.service.js +10 -16
  222. package/src/client/services/cyberia-entity/cyberia-entity.management.js +5 -5
  223. package/src/client/services/cyberia-entity/cyberia-entity.service.js +10 -16
  224. package/src/client/services/cyberia-instance/cyberia-instance.management.js +6 -6
  225. package/src/client/services/cyberia-instance/cyberia-instance.service.js +12 -18
  226. package/src/client/services/cyberia-instance-conf/cyberia-instance-conf.service.js +10 -16
  227. package/src/client/services/cyberia-map/cyberia-map.management.js +6 -6
  228. package/src/client/services/cyberia-map/cyberia-map.service.js +12 -18
  229. package/src/client/services/cyberia-quest/cyberia-quest.service.js +99 -0
  230. package/src/client/services/cyberia-quest-progress/cyberia-quest-progress.service.js +99 -0
  231. package/src/client/services/default/default.management.js +159 -267
  232. package/src/client/services/default/default.service.js +10 -16
  233. package/src/client/services/document/document.service.js +14 -19
  234. package/src/client/services/file/file.service.js +8 -13
  235. package/src/client/services/instance/instance.management.js +5 -5
  236. package/src/client/services/instance/instance.service.js +10 -15
  237. package/src/client/services/ipfs/ipfs.service.js +12 -18
  238. package/src/client/services/object-layer/object-layer.management.js +12 -12
  239. package/src/client/services/object-layer/object-layer.service.js +20 -26
  240. package/src/client/services/object-layer-render-frames/object-layer-render-frames.service.js +10 -16
  241. package/src/client/services/test/test.service.js +8 -13
  242. package/src/client/services/user/guest.service.js +86 -0
  243. package/src/client/services/user/user.management.js +5 -5
  244. package/src/client/services/user/user.service.js +14 -20
  245. package/src/client/ssr/body/404.js +3 -3
  246. package/src/client/ssr/body/500.js +3 -3
  247. package/src/client/ssr/body/CacheControl.js +5 -2
  248. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  249. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +13 -6
  250. package/src/client/ssr/head/PwaItemledger.js +197 -60
  251. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  252. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  253. package/src/client/ssr/offline/Maintenance.js +12 -11
  254. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  255. package/src/client/ssr/pages/Test.js +2 -2
  256. package/src/client/sw/core.sw.js +212 -0
  257. package/src/grpc/cyberia/grpc-server.js +179 -67
  258. package/src/index.js +1 -1
  259. package/src/runtime/cyberia-client/Dockerfile +80 -0
  260. package/src/runtime/cyberia-server/Dockerfile +37 -0
  261. package/src/runtime/express/Dockerfile +4 -4
  262. package/src/runtime/lampp/Dockerfile +8 -7
  263. package/src/runtime/wp/Dockerfile +11 -17
  264. package/src/server/atlas-sprite-sheet-generator.js +4 -2
  265. package/src/server/client-build-docs.js +45 -46
  266. package/src/server/client-build.js +334 -60
  267. package/src/server/client-formatted.js +47 -16
  268. package/src/server/conf.js +5 -4
  269. package/src/server/data-query.js +32 -20
  270. package/src/server/dns.js +22 -0
  271. package/src/server/ipfs-client.js +232 -91
  272. package/src/server/object-layer.js +1 -6
  273. package/src/server/process.js +13 -27
  274. package/src/server/semantic-layer-generator-floor.js +11 -51
  275. package/src/server/semantic-layer-generator-resource.js +259 -0
  276. package/src/server/semantic-layer-generator-skin.js +41 -171
  277. package/src/server/semantic-layer-generator.js +122 -14
  278. package/src/server/shape-generator.js +108 -0
  279. package/src/server/start.js +17 -3
  280. package/src/server/valkey.js +141 -235
  281. package/tsconfig.docs.json +15 -0
  282. package/typedoc.dd-cyberia.json +29 -0
  283. package/typedoc.json +29 -0
  284. package/WHITE-PAPER.md +0 -1540
  285. package/hardhat/README.md +0 -531
  286. package/hardhat/WHITE-PAPER.md +0 -1540
  287. package/jsdoc.dd-cyberia.json +0 -68
  288. package/jsdoc.json +0 -68
  289. package/src/api/object-layer/README.md +0 -672
  290. package/src/client/components/core/ColorPalette.js +0 -5267
  291. package/src/client/components/core/JoyStick.js +0 -80
  292. package/src/client/components/cryptokoyn/RoutesCryptokoyn.js +0 -39
  293. package/src/client/components/cyberia-portal/RoutesCyberiaPortal.js +0 -62
  294. package/src/client/components/cyberia-portal/ServerCyberiaPortal.js +0 -136
  295. package/src/client/components/default/RoutesDefault.js +0 -49
  296. package/src/client/components/itemledger/RoutesItemledger.js +0 -40
  297. package/src/client/components/underpost/RoutesUnderpost.js +0 -47
  298. package/src/client/sw/default.sw.js +0 -127
  299. package/src/client/sw/template.sw.js +0 -84
  300. package/src/grpc/cyberia/OFF_CHAIN_ECONOMY.md +0 -305
  301. package/src/grpc/cyberia/README.md +0 -326
package/bin/cyberia.js CHANGED
@@ -16,6 +16,7 @@
16
16
  import dotenv from 'dotenv';
17
17
  import { Command } from 'commander';
18
18
  import fs from 'fs-extra';
19
+ import stringify from 'fast-json-stable-stringify';
19
20
  import { shellExec } from '../src/server/process.js';
20
21
  import { loggerFactory } from '../src/server/logger.js';
21
22
  import { generateBesuManifests, deployBesu, removeBesu } from '../src/server/besu-genesis-generator.js';
@@ -28,24 +29,22 @@ 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,
46
+ DefaultCyberiaActions,
47
+ DefaultCyberiaQuests,
49
48
  } from '../src/client/components/cyberia-portal/CommonCyberiaPortal.js';
50
49
 
51
50
  /**
@@ -1705,7 +1704,11 @@ try {
1705
1704
  .command('instance [instance-code]')
1706
1705
  .option('--export [path]', 'Export instance and related documents to a backup directory')
1707
1706
  .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')
1707
+ .option(
1708
+ '--conf',
1709
+ 'When used with --export or --import, only process cyberia-instance.json and cyberia-instance-conf.json',
1710
+ )
1711
+ .option('--drop', 'Drop all documents associated with the instance code before importing or as a standalone action')
1709
1712
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
1710
1713
  .option('--mongo-host <mongo-host>', 'Mongo host override')
1711
1714
  .option('--dev', 'Force development environment')
@@ -1749,6 +1752,8 @@ try {
1749
1752
  await DataBaseProvider.load({
1750
1753
  apis: [
1751
1754
  'cyberia-instance',
1755
+ 'cyberia-instance-conf',
1756
+ 'cyberia-dialogue',
1752
1757
  'cyberia-map',
1753
1758
  'cyberia-entity',
1754
1759
  'object-layer',
@@ -1764,6 +1769,8 @@ try {
1764
1769
 
1765
1770
  const dbModels = DataBaseProvider.instance[`${host}${path}`].mongoose.models;
1766
1771
  const CyberiaInstance = dbModels.CyberiaInstance;
1772
+ const CyberiaInstanceConf = dbModels.CyberiaInstanceConf;
1773
+ const CyberiaDialogue = dbModels.CyberiaDialogue;
1767
1774
  const CyberiaMap = dbModels.CyberiaMap;
1768
1775
  const ObjectLayer = dbModels.ObjectLayer;
1769
1776
  const ObjectLayerRenderFrames = dbModels.ObjectLayerRenderFrames;
@@ -1771,6 +1778,134 @@ try {
1771
1778
  const File = dbModels.File;
1772
1779
  const Ipfs = dbModels.Ipfs;
1773
1780
 
1781
+ const toBuffer = (value) => {
1782
+ if (!value) return null;
1783
+ if (Buffer.isBuffer(value)) return value;
1784
+ if (value.type === 'Buffer' && Array.isArray(value.data)) return Buffer.from(value.data);
1785
+ if (value.buffer) return Buffer.from(value.buffer);
1786
+ return Buffer.from(value);
1787
+ };
1788
+
1789
+ const getCanonicalIpfsPaths = (itemKey) => ({
1790
+ objectLayerData: `/object-layer/${itemKey}/${itemKey}_data.json`,
1791
+ atlasSpriteSheet: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
1792
+ atlasMetadata: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
1793
+ });
1794
+
1795
+ const collectMfsPaths = (doc = {}) => {
1796
+ const paths = new Set();
1797
+ if (doc.mfsPath) paths.add(doc.mfsPath);
1798
+ for (const p of doc.mfsPaths || []) {
1799
+ if (p) paths.add(p);
1800
+ }
1801
+ return [...paths];
1802
+ };
1803
+
1804
+ const inferResourceType = (doc = {}) => {
1805
+ if (doc.resourceType) return doc.resourceType;
1806
+ for (const path of collectMfsPaths(doc)) {
1807
+ if (path.endsWith('_atlas_sprite_sheet.png')) return 'atlas-sprite-sheet';
1808
+ if (path.endsWith('_atlas_sprite_sheet_metadata.json')) return 'atlas-metadata';
1809
+ if (path.endsWith('_data.json')) return 'object-layer-data';
1810
+ }
1811
+ return null;
1812
+ };
1813
+
1814
+ const findInstanceRelatedIpfsDoc = (ipfsDocs, { linkedCid, resourceType, mfsPath }) =>
1815
+ ipfsDocs.find(
1816
+ (doc) =>
1817
+ inferResourceType(doc) === resourceType &&
1818
+ linkedCid &&
1819
+ doc.cid === linkedCid &&
1820
+ collectMfsPaths(doc).includes(mfsPath),
1821
+ ) ||
1822
+ ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && linkedCid && doc.cid === linkedCid) ||
1823
+ ipfsDocs.find((doc) => inferResourceType(doc) === resourceType && collectMfsPaths(doc).includes(mfsPath)) ||
1824
+ null;
1825
+
1826
+ const upsertCanonicalPinEntry = (pinMap, { cid, resourceType, mfsPath = '' }) => {
1827
+ if (!cid || !resourceType) return;
1828
+ const key = `${resourceType}:${cid}`;
1829
+ const nextPath = mfsPath || '';
1830
+ if (!pinMap.has(key)) {
1831
+ pinMap.set(key, {
1832
+ cid,
1833
+ resourceType,
1834
+ mfsPath: nextPath,
1835
+ mfsPaths: nextPath ? [nextPath] : [],
1836
+ });
1837
+ return;
1838
+ }
1839
+
1840
+ const existing = pinMap.get(key);
1841
+ if (nextPath && !existing.mfsPaths.includes(nextPath)) {
1842
+ existing.mfsPaths.push(nextPath);
1843
+ }
1844
+ if (!existing.mfsPath && nextPath) {
1845
+ existing.mfsPath = nextPath;
1846
+ }
1847
+ };
1848
+
1849
+ const serialiseCanonicalPins = (pinMap) =>
1850
+ [...pinMap.values()].map((entry) => ({
1851
+ cid: entry.cid,
1852
+ resourceType: entry.resourceType,
1853
+ ...(entry.mfsPath ? { mfsPath: entry.mfsPath } : {}),
1854
+ ...(entry.mfsPaths.length ? { mfsPaths: entry.mfsPaths } : {}),
1855
+ }));
1856
+
1857
+ const getDefaultDialoguesByItemId = (itemIds = []) => {
1858
+ const requestedItemIds = new Set(itemIds.filter(Boolean));
1859
+ const defaultsByItemId = new Map();
1860
+
1861
+ for (const dialogue of DefaultCyberiaDialogues) {
1862
+ // Match by code prefix: "default-<itemId>" covers the common case;
1863
+ // callers may also pass a full code directly.
1864
+ const matchingIds = [...requestedItemIds].filter(
1865
+ (id) => dialogue.code === `default-${id}` || dialogue.code === id,
1866
+ );
1867
+ if (!matchingIds.length) continue;
1868
+ for (const id of matchingIds) {
1869
+ if (!defaultsByItemId.has(id)) defaultsByItemId.set(id, []);
1870
+ defaultsByItemId.get(id).push({
1871
+ code: dialogue.code,
1872
+ order: dialogue.order ?? 0,
1873
+ speaker: dialogue.speaker ?? '',
1874
+ text: dialogue.text,
1875
+ mood: dialogue.mood ?? 'neutral',
1876
+ });
1877
+ }
1878
+ }
1879
+
1880
+ for (const dialogues of defaultsByItemId.values()) {
1881
+ dialogues.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
1882
+ }
1883
+
1884
+ return defaultsByItemId;
1885
+ };
1886
+
1887
+ const rewriteImportedCidReferences = async ({ oldCid, newCid, resourceType }) => {
1888
+ if (!oldCid || !newCid || oldCid === newCid) return;
1889
+
1890
+ if (resourceType === 'object-layer-data') {
1891
+ await ObjectLayer.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
1892
+ return;
1893
+ }
1894
+
1895
+ if (resourceType === 'atlas-sprite-sheet') {
1896
+ await AtlasSpriteSheet.updateMany({ cid: oldCid }, { $set: { cid: newCid } });
1897
+ await ObjectLayer.updateMany({ 'data.render.cid': oldCid }, { $set: { 'data.render.cid': newCid } });
1898
+ return;
1899
+ }
1900
+
1901
+ if (resourceType === 'atlas-metadata') {
1902
+ await ObjectLayer.updateMany(
1903
+ { 'data.render.metadataCid': oldCid },
1904
+ { $set: { 'data.render.metadataCid': newCid } },
1905
+ );
1906
+ }
1907
+ };
1908
+
1774
1909
  // ── EXPORT ──────────────────────────────────────────────────────
1775
1910
  if (options.export !== undefined) {
1776
1911
  const instance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
@@ -1788,13 +1923,12 @@ try {
1788
1923
  fs.ensureDirSync(backupDir);
1789
1924
  logger.info('Exporting instance', { code: instanceCode, backupDir });
1790
1925
 
1791
- fs.ensureDirSync(`${backupDir}/files`);
1792
-
1793
1926
  // Helper: export a File document to the files/ directory
1794
1927
  const exportFileDoc = async (fileId, fileKey) => {
1795
1928
  if (!fileId) return;
1796
1929
  const file = await File.findById(fileId).lean();
1797
1930
  if (!file) return;
1931
+ fs.ensureDirSync(`${backupDir}/files`);
1798
1932
  const fileExport = { ...file };
1799
1933
  // Handle both Node.js Buffer and BSON Binary types from .lean()
1800
1934
  if (fileExport.data) {
@@ -1808,11 +1942,46 @@ try {
1808
1942
 
1809
1943
  // 1. Save instance document + thumbnail
1810
1944
  fs.writeJsonSync(`${backupDir}/cyberia-instance.json`, instance, { spaces: 2 });
1811
- if (instance.thumbnail) {
1945
+ if (!options.conf && instance.thumbnail) {
1812
1946
  await exportFileDoc(instance.thumbnail, `thumb-instance-${instanceCode}`);
1813
1947
  }
1814
1948
  logger.info('Exported CyberiaInstance', { code: instanceCode });
1815
1949
 
1950
+ // 1b. Export linked CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
1951
+ // If no conf doc exists yet (instance created before auto-upsert logic), create one using
1952
+ // schema defaults — identical to the behaviour in CyberiaInstanceService.post().
1953
+ let instanceConf =
1954
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
1955
+ (instance.conf ? await CyberiaInstanceConf.findById(instance.conf).lean() : null);
1956
+ if (!instanceConf) {
1957
+ logger.info('No CyberiaInstanceConf found — creating default', { instanceCode });
1958
+ const created = await CyberiaInstanceConf.findOneAndUpdate(
1959
+ { instanceCode },
1960
+ { $setOnInsert: { instanceCode } },
1961
+ { upsert: true, returnDocument: 'after' },
1962
+ );
1963
+ // Back-fill the instance.conf ref if it was missing
1964
+ if (created && !instance.conf) {
1965
+ await CyberiaInstance.findByIdAndUpdate(instance._id, { conf: created._id });
1966
+ }
1967
+ instanceConf = created?.toObject ? created.toObject() : created;
1968
+ }
1969
+ if (instanceConf) {
1970
+ fs.writeJsonSync(`${backupDir}/cyberia-instance-conf.json`, instanceConf, { spaces: 2 });
1971
+ logger.info('Exported CyberiaInstanceConf', { instanceCode });
1972
+ } else {
1973
+ logger.warn('Could not create or find CyberiaInstanceConf', { instanceCode });
1974
+ }
1975
+
1976
+ if (options.conf) {
1977
+ logger.info('Instance export completed in --conf mode', {
1978
+ backupDir,
1979
+ exportedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
1980
+ });
1981
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
1982
+ return;
1983
+ }
1984
+
1816
1985
  // 2. Collect all map codes (instance maps + portal targets)
1817
1986
  const mapCodes = new Set(instance.cyberiaMapCodes || []);
1818
1987
  for (const portal of instance.portals || []) {
@@ -1841,6 +2010,100 @@ try {
1841
2010
  }
1842
2011
  }
1843
2012
 
2013
+ // 4b. Add instance-level itemIds
2014
+ for (const id of instance.itemIds || []) {
2015
+ if (id) objectLayerItemIds.add(id);
2016
+ }
2017
+
2018
+ // 4c. Add all itemIds referenced by CyberiaInstanceConf (entityDefaults + skillConfig).
2019
+ // This ensures liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers and
2020
+ // skill trigger items are included even if no map entity currently uses them.
2021
+ if (instanceConf) {
2022
+ for (const ed of instanceConf.entityDefaults || []) {
2023
+ for (const id of ed.liveItemIds || []) if (id) objectLayerItemIds.add(id);
2024
+ for (const id of ed.deadItemIds || []) if (id) objectLayerItemIds.add(id);
2025
+ for (const id of ed.dropItemIds || []) if (id) objectLayerItemIds.add(id);
2026
+ for (const slot of ed.defaultObjectLayers || []) {
2027
+ if (slot.itemId) objectLayerItemIds.add(slot.itemId);
2028
+ }
2029
+ }
2030
+ for (const sc of instanceConf.skillConfig || []) {
2031
+ if (sc.triggerItemId) objectLayerItemIds.add(sc.triggerItemId);
2032
+ for (const skill of sc.skills || []) {
2033
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$')) {
2034
+ objectLayerItemIds.add(skill.summonedEntityItemId);
2035
+ }
2036
+ }
2037
+ }
2038
+ }
2039
+
2040
+ // 4d. Export dialogues for all relevant object-layer items. Codes follow the pattern
2041
+ // "default-<itemId>". If an item has no dialogue docs yet but ships with
2042
+ // DefaultCyberiaDialogues, seed those defaults into Mongo first.
2043
+ if (objectLayerItemIds.size > 0) {
2044
+ const requestedItemIds = [...objectLayerItemIds];
2045
+ const requestedCodes = requestedItemIds.map((id) => `default-${id}`);
2046
+ const defaultDialoguesByItemId = getDefaultDialoguesByItemId(requestedItemIds);
2047
+ const existingDialogueDocs = await CyberiaDialogue.find({
2048
+ code: { $in: requestedCodes },
2049
+ })
2050
+ .sort({ code: 1, order: 1 })
2051
+ .lean();
2052
+
2053
+ const existingDialogueCodes = new Set(existingDialogueDocs.map((dialogue) => dialogue.code).filter(Boolean));
2054
+ let seededDialogueCount = 0;
2055
+
2056
+ for (const [itemId, dialogues] of defaultDialoguesByItemId.entries()) {
2057
+ const firstCode = dialogues[0]?.code;
2058
+ if (firstCode && existingDialogueCodes.has(firstCode)) continue;
2059
+
2060
+ for (const dialogue of dialogues) {
2061
+ await CyberiaDialogue.findOneAndUpdate(
2062
+ { code: dialogue.code, order: dialogue.order },
2063
+ {
2064
+ $set: {
2065
+ speaker: dialogue.speaker,
2066
+ text: dialogue.text,
2067
+ mood: dialogue.mood,
2068
+ },
2069
+ },
2070
+ { upsert: true },
2071
+ );
2072
+ seededDialogueCount++;
2073
+ }
2074
+ }
2075
+
2076
+ const dialogueDocs = await CyberiaDialogue.find({ code: { $in: requestedCodes } })
2077
+ .sort({ code: 1, order: 1 })
2078
+ .lean();
2079
+
2080
+ if (seededDialogueCount > 0) {
2081
+ logger.info(`Seeded ${seededDialogueCount} CyberiaDialogue default record(s) for export`);
2082
+ }
2083
+
2084
+ if (dialogueDocs.length > 0) {
2085
+ fs.ensureDirSync(`${backupDir}/cyberia-dialogues`);
2086
+ const dialoguesByCode = new Map();
2087
+
2088
+ for (const dialogue of dialogueDocs) {
2089
+ if (!dialoguesByCode.has(dialogue.code)) {
2090
+ dialoguesByCode.set(dialogue.code, []);
2091
+ }
2092
+ dialoguesByCode.get(dialogue.code).push(dialogue);
2093
+ }
2094
+
2095
+ for (const [code, dialogues] of dialoguesByCode.entries()) {
2096
+ fs.writeJsonSync(`${backupDir}/cyberia-dialogues/${encodeURIComponent(code)}.json`, dialogues, {
2097
+ spaces: 2,
2098
+ });
2099
+ }
2100
+
2101
+ logger.info(`Exported ${dialogueDocs.length} CyberiaDialogue document(s)`, {
2102
+ codes: [...dialoguesByCode.keys()],
2103
+ });
2104
+ }
2105
+ }
2106
+
1844
2107
  // 5. Export object layers with related render frames, atlas, files, and IPFS records
1845
2108
  if (objectLayerItemIds.size > 0) {
1846
2109
  const objectLayers = await ObjectLayer.find({
@@ -1851,48 +2114,245 @@ try {
1851
2114
  fs.ensureDirSync(`${backupDir}/render-frames`);
1852
2115
  fs.ensureDirSync(`${backupDir}/atlas-sprite-sheets`);
1853
2116
  fs.ensureDirSync(`${backupDir}/ipfs`);
2117
+ fs.ensureDirSync(`${backupDir}/ipfs/content`);
2118
+
2119
+ const canonicalPins = new Map();
2120
+ const expectedObjectLayerIpfsRefs = [];
2121
+ const ipfsPayloadFailures = [];
2122
+ let ipfsPayloadExportCount = 0;
2123
+ let ipfsPayloadAliasCount = 0;
2124
+
2125
+ const writeBackupPayload = (cid, payloadBuffer) => {
2126
+ if (!cid) return false;
2127
+ const payloadPath = `${backupDir}/ipfs/content/${cid}.bin`;
2128
+ if (fs.existsSync(payloadPath)) return false;
2129
+ fs.writeFileSync(payloadPath, payloadBuffer);
2130
+ ipfsPayloadExportCount++;
2131
+ return true;
2132
+ };
2133
+
2134
+ const writeBackupPayloadAlias = ({ canonicalCid, linkedCid, payloadBuffer }) => {
2135
+ if (!linkedCid || linkedCid === canonicalCid) return;
2136
+ if (writeBackupPayload(linkedCid, payloadBuffer)) {
2137
+ ipfsPayloadAliasCount++;
2138
+ }
2139
+ };
2140
+
2141
+ const exportCanonicalPayload = async ({ payloadBuffer, resourceType, mfsPath, filename, itemKey }) => {
2142
+ const hashResult = await IpfsClient.hashBufferForIpfs(payloadBuffer, filename);
2143
+ if (!hashResult?.cid) {
2144
+ ipfsPayloadFailures.push({ itemKey, resourceType, mfsPath, reason: 'Failed to hash payload via Kubo' });
2145
+ return null;
2146
+ }
1854
2147
 
1855
- const allCids = new Set();
2148
+ writeBackupPayload(hashResult.cid, payloadBuffer);
2149
+ return hashResult.cid;
2150
+ };
1856
2151
 
1857
2152
  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 });
2153
+ const itemKey = ol.data?.item?.id || ol._id.toString();
2154
+ const itemPaths = getCanonicalIpfsPaths(itemKey);
2155
+ const objectLayerExport = newInstance(ol);
2156
+
2157
+ if (!objectLayerExport.data.render) {
2158
+ objectLayerExport.data.render = {};
2159
+ }
1860
2160
 
1861
2161
  // Export ObjectLayerRenderFrames
1862
2162
  if (ol.objectLayerRenderFramesId) {
1863
2163
  const rf = await ObjectLayerRenderFrames.findById(ol.objectLayerRenderFramesId).lean();
1864
2164
  if (rf) {
1865
- fs.writeJsonSync(`${backupDir}/render-frames/${fileName}.json`, rf, { spaces: 2 });
2165
+ fs.writeJsonSync(`${backupDir}/render-frames/${itemKey}.json`, rf, { spaces: 2 });
1866
2166
  }
1867
2167
  }
1868
2168
 
1869
- // Export AtlasSpriteSheet + its File
2169
+ // Export AtlasSpriteSheet + its File using canonical payload bytes from the DB state.
1870
2170
  if (ol.atlasSpriteSheetId) {
1871
2171
  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);
2172
+ if (!atlas) {
2173
+ ipfsPayloadFailures.push({
2174
+ itemKey,
2175
+ resourceType: 'atlas-sprite-sheet',
2176
+ mfsPath: itemPaths.atlasSpriteSheet,
2177
+ reason: 'AtlasSpriteSheet document not found in MongoDB',
2178
+ });
2179
+ continue;
2180
+ }
2181
+
2182
+ const atlasExport = newInstance(atlas);
2183
+ if (atlas.fileId) {
2184
+ await exportFileDoc(atlas.fileId, `atlas-${itemKey}`);
2185
+ }
2186
+
2187
+ const atlasFile = atlas.fileId ? await File.findById(atlas.fileId).lean() : null;
2188
+ const atlasBuffer = toBuffer(atlasFile?.data);
2189
+ if (!atlasBuffer) {
2190
+ ipfsPayloadFailures.push({
2191
+ itemKey,
2192
+ resourceType: 'atlas-sprite-sheet',
2193
+ mfsPath: itemPaths.atlasSpriteSheet,
2194
+ reason: 'Atlas File payload not found in MongoDB',
2195
+ });
2196
+ continue;
2197
+ }
2198
+
2199
+ const atlasCid = await exportCanonicalPayload({
2200
+ payloadBuffer: atlasBuffer,
2201
+ resourceType: 'atlas-sprite-sheet',
2202
+ mfsPath: itemPaths.atlasSpriteSheet,
2203
+ filename: `${itemKey}_atlas_sprite_sheet.png`,
2204
+ itemKey,
2205
+ });
2206
+ if (!atlasCid) continue;
2207
+
2208
+ const linkedAtlasCid = atlas.cid || ol.data?.render?.cid || atlasCid;
2209
+ writeBackupPayloadAlias({
2210
+ canonicalCid: atlasCid,
2211
+ linkedCid: linkedAtlasCid,
2212
+ payloadBuffer: atlasBuffer,
2213
+ });
2214
+ expectedObjectLayerIpfsRefs.push({
2215
+ itemKey,
2216
+ resourceType: 'atlas-sprite-sheet',
2217
+ mfsPath: itemPaths.atlasSpriteSheet,
2218
+ linkedCid: linkedAtlasCid,
2219
+ fallbackCid: atlasCid,
2220
+ });
2221
+
2222
+ const atlasMetadataBuffer = Buffer.from(stringify(atlasExport.metadata || {}), 'utf-8');
2223
+ const atlasMetadataCid = await exportCanonicalPayload({
2224
+ payloadBuffer: atlasMetadataBuffer,
2225
+ resourceType: 'atlas-metadata',
2226
+ mfsPath: itemPaths.atlasMetadata,
2227
+ filename: `${itemKey}_atlas_sprite_sheet_metadata.json`,
2228
+ itemKey,
2229
+ });
2230
+ if (!atlasMetadataCid) continue;
2231
+
2232
+ const linkedAtlasMetadataCid = ol.data?.render?.metadataCid || atlasMetadataCid;
2233
+ writeBackupPayloadAlias({
2234
+ canonicalCid: atlasMetadataCid,
2235
+ linkedCid: linkedAtlasMetadataCid,
2236
+ payloadBuffer: atlasMetadataBuffer,
2237
+ });
2238
+ expectedObjectLayerIpfsRefs.push({
2239
+ itemKey,
2240
+ resourceType: 'atlas-metadata',
2241
+ mfsPath: itemPaths.atlasMetadata,
2242
+ linkedCid: linkedAtlasMetadataCid,
2243
+ fallbackCid: atlasMetadataCid,
2244
+ });
2245
+
2246
+ atlasExport.cid = atlasCid;
2247
+ objectLayerExport.data.render.cid = atlasCid;
2248
+ objectLayerExport.data.render.metadataCid = atlasMetadataCid;
2249
+ fs.writeJsonSync(`${backupDir}/atlas-sprite-sheets/${itemKey}.json`, atlasExport, { spaces: 2 });
2250
+ } else {
2251
+ if (objectLayerExport.data.render?.cid || objectLayerExport.data.render?.metadataCid) {
2252
+ ipfsPayloadFailures.push({
2253
+ itemKey,
2254
+ resourceType: 'atlas-sprite-sheet',
2255
+ mfsPath: itemPaths.atlasSpriteSheet,
2256
+ reason: 'ObjectLayer references atlas CIDs but no AtlasSpriteSheet document exists',
2257
+ });
2258
+ continue;
1878
2259
  }
2260
+ delete objectLayerExport.data.render.cid;
2261
+ delete objectLayerExport.data.render.metadataCid;
1879
2262
  }
1880
2263
 
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);
2264
+ const objectLayerBuffer = Buffer.from(stringify(objectLayerExport.data || {}), 'utf-8');
2265
+ const objectLayerCid = await exportCanonicalPayload({
2266
+ payloadBuffer: objectLayerBuffer,
2267
+ resourceType: 'object-layer-data',
2268
+ mfsPath: itemPaths.objectLayerData,
2269
+ filename: `${itemKey}_data.json`,
2270
+ itemKey,
2271
+ });
2272
+ if (!objectLayerCid) continue;
2273
+
2274
+ const linkedObjectLayerCid = ol.cid || objectLayerCid;
2275
+ writeBackupPayloadAlias({
2276
+ canonicalCid: objectLayerCid,
2277
+ linkedCid: linkedObjectLayerCid,
2278
+ payloadBuffer: objectLayerBuffer,
2279
+ });
2280
+ expectedObjectLayerIpfsRefs.push({
2281
+ itemKey,
2282
+ resourceType: 'object-layer-data',
2283
+ mfsPath: itemPaths.objectLayerData,
2284
+ linkedCid: linkedObjectLayerCid,
2285
+ fallbackCid: objectLayerCid,
2286
+ });
2287
+
2288
+ objectLayerExport.cid = objectLayerCid;
2289
+ fs.writeJsonSync(`${backupDir}/object-layers/${itemKey}.json`, objectLayerExport, { spaces: 2 });
2290
+ }
2291
+
2292
+ if (ipfsPayloadFailures.length > 0) {
2293
+ for (const failure of ipfsPayloadFailures) {
2294
+ logger.error('Canonical IPFS payload export failed', failure);
2295
+ }
2296
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
2297
+ process.exit(1);
1885
2298
  }
1886
2299
 
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)`);
2300
+ const relatedPinPaths = [
2301
+ ...new Set(expectedObjectLayerIpfsRefs.map((entry) => entry.mfsPath).filter(Boolean)),
2302
+ ];
2303
+ const relatedPinCids = [
2304
+ ...new Set(
2305
+ expectedObjectLayerIpfsRefs.flatMap((entry) => [entry.linkedCid, entry.fallbackCid]).filter(Boolean),
2306
+ ),
2307
+ ];
2308
+ const relatedIpfsDocs =
2309
+ relatedPinPaths.length > 0 || relatedPinCids.length > 0
2310
+ ? await Ipfs.find({
2311
+ $or: [
2312
+ ...(relatedPinPaths.length ? [{ mfsPath: { $in: relatedPinPaths } }] : []),
2313
+ ...(relatedPinPaths.length ? [{ mfsPaths: { $in: relatedPinPaths } }] : []),
2314
+ ...(relatedPinCids.length ? [{ cid: { $in: relatedPinCids } }] : []),
2315
+ ],
2316
+ }).lean()
2317
+ : [];
2318
+
2319
+ let ipfsCollectionMatchCount = 0;
2320
+ let ipfsCollectionFallbackCount = 0;
2321
+
2322
+ for (const ref of expectedObjectLayerIpfsRefs) {
2323
+ const matchingDoc = findInstanceRelatedIpfsDoc(relatedIpfsDocs, ref);
2324
+ const exportCid = matchingDoc?.cid || ref.linkedCid || ref.fallbackCid;
2325
+
2326
+ if (!exportCid) {
2327
+ logger.warn('Skipping instance IPFS pin export because the ObjectLayer ref has no linked CID', {
2328
+ itemKey: ref.itemKey,
2329
+ resourceType: ref.resourceType,
2330
+ mfsPath: ref.mfsPath,
2331
+ });
2332
+ continue;
1893
2333
  }
2334
+
2335
+ upsertCanonicalPinEntry(canonicalPins, {
2336
+ cid: exportCid,
2337
+ resourceType: ref.resourceType,
2338
+ mfsPath: ref.mfsPath,
2339
+ });
2340
+
2341
+ if (matchingDoc) ipfsCollectionMatchCount++;
2342
+ else ipfsCollectionFallbackCount++;
1894
2343
  }
1895
2344
 
2345
+ const sanitised = serialiseCanonicalPins(canonicalPins);
2346
+ fs.writeJsonSync(`${backupDir}/ipfs/pins.json`, sanitised, { spaces: 2 });
2347
+ logger.info(
2348
+ `Exported ${sanitised.length} instance-related Ipfs pin record(s) and ${ipfsPayloadExportCount} raw payload file(s)`,
2349
+ {
2350
+ matchedFromIpfsCollection: ipfsCollectionMatchCount,
2351
+ fallbackFromObjectLayerRefs: ipfsCollectionFallbackCount,
2352
+ rawPayloadAliases: ipfsPayloadAliasCount,
2353
+ },
2354
+ );
2355
+
1896
2356
  logger.info(`Exported ${objectLayers.length} ObjectLayer document(s)`, {
1897
2357
  itemIds: [...objectLayerItemIds],
1898
2358
  });
@@ -1919,7 +2379,7 @@ try {
1919
2379
  logger.info('Importing instance', { code: instanceCode, backupDir });
1920
2380
 
1921
2381
  // 0. Drop existing documents if --drop is set
1922
- if (options.drop) {
2382
+ if (options.drop && !options.conf) {
1923
2383
  const existingInstance = await CyberiaInstance.findOne({ code: instanceCode }).lean();
1924
2384
  if (existingInstance) {
1925
2385
  const dropMapCodes = new Set(existingInstance.cyberiaMapCodes || []);
@@ -1931,13 +2391,41 @@ try {
1931
2391
  // Collect thumbnail File IDs to drop
1932
2392
  const thumbFileIds = [];
1933
2393
  if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
2394
+ const dropOlItemIds = new Set();
1934
2395
 
1935
2396
  // Query other instances/maps for shared thumbnail exclusion
1936
2397
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
1937
2398
 
2399
+ // Add instance-level itemIds (may not appear in any map entity)
2400
+ for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
2401
+
2402
+ // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
2403
+ const existingConf =
2404
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
2405
+ (existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
2406
+ if (existingConf) {
2407
+ for (const ed of existingConf.entityDefaults || []) {
2408
+ for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
2409
+ for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
2410
+ for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
2411
+ for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
2412
+ }
2413
+ for (const sc of existingConf.skillConfig || []) {
2414
+ if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
2415
+ for (const skill of sc.skills || []) {
2416
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
2417
+ dropOlItemIds.add(skill.summonedEntityItemId);
2418
+ }
2419
+ }
2420
+ }
2421
+
2422
+ const otherMaps = await CyberiaMap.find(
2423
+ { code: { $nin: [...dropMapCodes] } },
2424
+ { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
2425
+ ).lean();
2426
+
1938
2427
  if (dropMapCodes.size > 0) {
1939
2428
  const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
1940
- const dropOlItemIds = new Set();
1941
2429
  for (const map of dropMaps) {
1942
2430
  if (map.thumbnail) thumbFileIds.push(map.thumbnail);
1943
2431
  for (const entity of map.entities || []) {
@@ -1947,110 +2435,105 @@ try {
1947
2435
  }
1948
2436
  }
1949
2437
 
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
- }
2438
+ const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2439
+ logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
2440
+ }
1967
2441
 
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);
2442
+ // Exclude OL item IDs referenced by maps outside this instance
2443
+ const sharedOlItemIds = new Set();
2444
+ for (const m of otherMaps) {
2445
+ for (const entity of m.entities || []) {
2446
+ for (const itemId of entity.objectLayerItemIds || []) {
2447
+ if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
2448
+ }
1974
2449
  }
2450
+ }
2451
+ for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
2452
+ if (sharedOlItemIds.size > 0) {
2453
+ logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
2454
+ }
1975
2455
 
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
- }
2456
+ // Exclude thumbnail File IDs referenced by other instances or maps
2457
+ const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
2458
+ const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
2459
+ const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
2460
+ for (let i = thumbFileIds.length - 1; i >= 0; i--) {
2461
+ if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
2462
+ }
2002
2463
 
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
- }
2464
+ if (dropOlItemIds.size > 0) {
2465
+ const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
2466
+ const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
2467
+ logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
2468
+ const olDocs = await ObjectLayer.find(
2469
+ { 'data.item.id': { $in: [...dropOlItemIds] } },
2470
+ {
2471
+ cid: 1,
2472
+ 'data.item.id': 1,
2473
+ 'data.render': 1,
2474
+ objectLayerRenderFramesId: 1,
2475
+ atlasSpriteSheetId: 1,
2476
+ },
2477
+ ).lean();
2020
2478
 
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
- }
2479
+ const cidsToUnpin = new Set();
2480
+ const renderFrameIds = [];
2481
+ const atlasIds = [];
2482
+ const itemKeysToClean = new Set();
2026
2483
 
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
- }
2484
+ for (const doc of olDocs) {
2485
+ if (doc.cid) cidsToUnpin.add(doc.cid);
2486
+ if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
2487
+ if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
2488
+ if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
2489
+ if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
2490
+ if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
2491
+ }
2032
2492
 
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++;
2493
+ // Delete AtlasSpriteSheet + referenced File docs
2494
+ if (atlasIds.length > 0) {
2495
+ const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
2496
+ const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
2497
+ for (const atlas of atlasDocs) {
2498
+ if (atlas.cid) cidsToUnpin.add(atlas.cid);
2038
2499
  }
2039
- let mfsCount = 0;
2040
- for (const itemKey of itemKeysToClean) {
2041
- const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
2042
- if (ok) mfsCount++;
2500
+ if (atlasFileIds.length > 0) {
2501
+ const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
2502
+ logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2043
2503
  }
2044
- logger.info(
2045
- `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
2046
- );
2504
+ const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
2505
+ logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
2506
+ }
2047
2507
 
2048
- const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2049
- logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2508
+ // Delete RenderFrames
2509
+ if (renderFrameIds.length > 0) {
2510
+ const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
2511
+ logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
2050
2512
  }
2051
2513
 
2052
- const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2053
- logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
2514
+ // Delete IPFS pin records
2515
+ if (cidsToUnpin.size > 0) {
2516
+ const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
2517
+ logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
2518
+ }
2519
+
2520
+ // Unpin CIDs from IPFS Kubo + Cluster and remove MFS paths
2521
+ let unpinCount = 0;
2522
+ for (const cid of cidsToUnpin) {
2523
+ const ok = await IpfsClient.unpinCid(cid);
2524
+ if (ok) unpinCount++;
2525
+ }
2526
+ let mfsCount = 0;
2527
+ for (const itemKey of itemKeysToClean) {
2528
+ const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
2529
+ if (ok) mfsCount++;
2530
+ }
2531
+ logger.info(
2532
+ `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
2533
+ );
2534
+
2535
+ const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2536
+ logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2054
2537
  }
2055
2538
 
2056
2539
  // Drop thumbnail File documents (instance + maps), excluding shared ones
@@ -2061,9 +2544,58 @@ try {
2061
2544
 
2062
2545
  await CyberiaInstance.deleteOne({ code: instanceCode });
2063
2546
  logger.info('Dropped CyberiaInstance', { code: instanceCode });
2547
+ await CyberiaInstanceConf.deleteOne({ instanceCode });
2548
+ logger.info('Dropped CyberiaInstanceConf', { instanceCode });
2064
2549
  } else {
2065
2550
  logger.info('No existing instance to drop', { code: instanceCode });
2066
2551
  }
2552
+ } else if (options.drop && options.conf) {
2553
+ logger.info(
2554
+ 'Skipping full instance drop because --conf only imports cyberia-instance.json and cyberia-instance-conf.json',
2555
+ );
2556
+ }
2557
+
2558
+ if (options.conf) {
2559
+ const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2560
+ let importedConf = null;
2561
+ if (fs.existsSync(confImportPath)) {
2562
+ const confData = fs.readJsonSync(confImportPath);
2563
+ if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2564
+ await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2565
+ // Always bump updatedAt so the Go server's version hash changes and
2566
+ // ReloadWorld re-applies the config without requiring a full restart.
2567
+ confData.updatedAt = new Date();
2568
+ importedConf = await CyberiaInstanceConf.create(confData);
2569
+ logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
2570
+ } else {
2571
+ logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
2572
+ }
2573
+
2574
+ // In --conf mode we must NOT delete + recreate the CyberiaInstance because
2575
+ // that would overwrite cyberiaMapCodes / portals / itemIds with whatever was
2576
+ // in the (possibly stale) backup, effectively removing the live maps and OLs
2577
+ // from the instance. Only update the conf ref and bump updatedAt so the Go
2578
+ // server's version hash changes and ReloadWorld re-applies the config.
2579
+ if (importedConf) {
2580
+ const result = await CyberiaInstance.updateOne(
2581
+ { code: instanceCode },
2582
+ { $set: { conf: importedConf._id, updatedAt: new Date() } },
2583
+ );
2584
+ if (result.matchedCount > 0) {
2585
+ logger.info('Updated CyberiaInstance conf ref', { code: instanceCode });
2586
+ } else {
2587
+ logger.warn(`CyberiaInstance not found in DB for code "${instanceCode}" — cannot update conf ref`);
2588
+ }
2589
+ } else {
2590
+ logger.warn(`Skipping CyberiaInstance conf ref update — no conf was imported`);
2591
+ }
2592
+
2593
+ logger.info('Instance import completed in --conf mode', {
2594
+ backupDir,
2595
+ importedFiles: ['cyberia-instance.json', 'cyberia-instance-conf.json'],
2596
+ });
2597
+ await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
2598
+ return;
2067
2599
  }
2068
2600
 
2069
2601
  // 1. Import File documents first (atlas PNG + thumbnail dependencies)
@@ -2113,6 +2645,9 @@ try {
2113
2645
  for (const f of atlasFiles) {
2114
2646
  const atlasData = fs.readJsonSync(`${atlasDir}/${f}`);
2115
2647
  await AtlasSpriteSheet.deleteOne({ _id: atlasData._id });
2648
+ if (atlasData.metadata?.itemKey) {
2649
+ await AtlasSpriteSheet.deleteOne({ 'metadata.itemKey': atlasData.metadata.itemKey });
2650
+ }
2116
2651
  await AtlasSpriteSheet.create(atlasData);
2117
2652
  atlasCount++;
2118
2653
  }
@@ -2127,45 +2662,71 @@ try {
2127
2662
  for (const file of olFiles) {
2128
2663
  const olData = fs.readJsonSync(`${olDir}/${file}`);
2129
2664
  await ObjectLayer.deleteOne({ _id: olData._id });
2665
+ if (olData.sha256) {
2666
+ await ObjectLayer.deleteOne({ sha256: olData.sha256 });
2667
+ }
2130
2668
  await ObjectLayer.create(olData);
2131
2669
  olCount++;
2132
2670
  }
2133
2671
  logger.info(`Imported ${olCount} ObjectLayer document(s)`);
2134
2672
  }
2135
2673
 
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++;
2674
+ // 4b. Regenerate static frame PNGs from imported render-frames + object-layer documents.
2675
+ // Mirrors the writeStaticFrameAssets call in `ol --import` so src/client/public/cyberia
2676
+ // and the public/<host><path> deployment dir are populated even when the cyberia
2677
+ // asset directory was wiped (e.g. git clean / rm -rf).
2678
+ const rfImportDir = `${backupDir}/render-frames`;
2679
+ const olImportDir = `${backupDir}/object-layers`;
2680
+ if (fs.existsSync(rfImportDir) && fs.existsSync(olImportDir)) {
2681
+ const srcBasePath = './src/client/public/cyberia/';
2682
+ const publicBasePath = `./public/${host}${path}`;
2683
+ let staticWriteCount = 0;
2684
+ const rfFileList = fs.readdirSync(rfImportDir).filter((f) => f.endsWith('.json'));
2685
+ for (const rfFile of rfFileList) {
2686
+ const rfData = fs.readJsonSync(`${rfImportDir}/${rfFile}`);
2687
+ const itemId = nodePath.basename(rfFile, '.json');
2688
+ const olFile = `${olImportDir}/${itemId}.json`;
2689
+ if (!fs.existsSync(olFile)) {
2690
+ logger.warn(`Skipping static asset generation for '${itemId}' no matching object-layer file`);
2691
+ continue;
2692
+ }
2693
+ const olData = fs.readJsonSync(olFile);
2694
+ const itemType = olData.data?.item?.type;
2695
+ if (!itemType) {
2696
+ logger.warn(`Skipping static asset generation for '${itemId}' — missing data.item.type`);
2697
+ continue;
2698
+ }
2699
+ // rfData matches the ObjectLayerRenderFrames schema: { frames, colors, frame_duration }
2700
+ const objectLayerRenderFramesData = {
2701
+ frames: rfData.frames || {},
2702
+ colors: rfData.colors || [],
2703
+ frame_duration: rfData.frame_duration ?? 100,
2704
+ };
2705
+ try {
2706
+ const written = await ObjectLayerEngine.writeStaticFrameAssets({
2707
+ basePaths: [srcBasePath, publicBasePath],
2708
+ itemType,
2709
+ itemId,
2710
+ objectLayerRenderFramesData,
2711
+ objectLayerData: olData,
2712
+ cellPixelDim: 20,
2713
+ });
2714
+ staticWriteCount += written.length;
2715
+ } catch (err) {
2716
+ logger.warn(`Failed to write static assets for '${itemId}': ${err.message}`);
2717
+ }
2155
2718
  }
2156
- logger.info(`IPFS re-pin: ${repinCount}/${pinnedCids.size} CIDs pinned`);
2719
+ logger.info(`Static frame PNGs written: ${staticWriteCount} file(s) across src/client/public and public/`);
2157
2720
  }
2158
2721
 
2159
- // 6. Import maps (preserveUUID: delete by code then create with exact _id)
2722
+ // 5. Import maps (preserveUUID: delete by code then create with exact _id)
2160
2723
  const mapsDir = `${backupDir}/maps`;
2161
2724
  if (fs.existsSync(mapsDir)) {
2162
2725
  const mapFiles = fs.readdirSync(mapsDir).filter((f) => f.endsWith('.json'));
2163
2726
  let mapCount = 0;
2164
2727
  for (const file of mapFiles) {
2165
2728
  const mapData = fs.readJsonSync(`${mapsDir}/${file}`);
2166
- // Remove any existing map with this code (may have different _id)
2167
2729
  await CyberiaMap.deleteOne({ code: mapData.code });
2168
- // Also remove if an old doc with this _id exists
2169
2730
  await CyberiaMap.deleteOne({ _id: mapData._id });
2170
2731
  await CyberiaMap.create(mapData);
2171
2732
  mapCount++;
@@ -2173,6 +2734,18 @@ try {
2173
2734
  logger.info(`Imported ${mapCount} CyberiaMap document(s)`);
2174
2735
  }
2175
2736
 
2737
+ // 6. Import CyberiaInstanceConf (skillRules, equipmentRules, entityDefaults, etc.)
2738
+ const confImportPath = `${backupDir}/cyberia-instance-conf.json`;
2739
+ if (fs.existsSync(confImportPath)) {
2740
+ const confData = fs.readJsonSync(confImportPath);
2741
+ if (confData._id) await CyberiaInstanceConf.deleteOne({ _id: confData._id });
2742
+ await CyberiaInstanceConf.deleteOne({ instanceCode: confData.instanceCode });
2743
+ await CyberiaInstanceConf.create(confData);
2744
+ logger.info('Imported CyberiaInstanceConf', { instanceCode: confData.instanceCode });
2745
+ } else {
2746
+ logger.warn(`CyberiaInstanceConf backup not found: ${confImportPath}`);
2747
+ }
2748
+
2176
2749
  // 7. Import instance (preserveUUID: delete by code then create with exact _id)
2177
2750
  const instancePath = `${backupDir}/cyberia-instance.json`;
2178
2751
  if (fs.existsSync(instancePath)) {
@@ -2185,6 +2758,283 @@ try {
2185
2758
  logger.warn(`Instance file not found: ${instancePath}`);
2186
2759
  }
2187
2760
 
2761
+ // 8. Import CyberiaDialogue documents
2762
+ const dialoguesDir = `${backupDir}/cyberia-dialogues`;
2763
+ if (fs.existsSync(dialoguesDir)) {
2764
+ const dialogueFiles = fs.readdirSync(dialoguesDir).filter((f) => f.endsWith('.json'));
2765
+ let dialogueCount = 0;
2766
+
2767
+ for (const file of dialogueFiles) {
2768
+ const rawDialogueData = fs.readJsonSync(`${dialoguesDir}/${file}`);
2769
+ const dialogues = Array.isArray(rawDialogueData) ? rawDialogueData : [rawDialogueData];
2770
+ const dialogueCodes = [...new Set(dialogues.map((dialogue) => dialogue.code).filter(Boolean))];
2771
+ if (dialogueCodes.length === 0) {
2772
+ logger.warn(`Skipping CyberiaDialogue backup without code: ${file}`);
2773
+ continue;
2774
+ }
2775
+
2776
+ await CyberiaDialogue.deleteMany({ code: { $in: dialogueCodes } });
2777
+
2778
+ const dialogueIds = dialogues.map((dialogue) => dialogue._id).filter(Boolean);
2779
+ if (dialogueIds.length > 0) {
2780
+ await CyberiaDialogue.deleteMany({ _id: { $in: dialogueIds } });
2781
+ }
2782
+
2783
+ await CyberiaDialogue.create(dialogues);
2784
+ dialogueCount += dialogues.length;
2785
+ }
2786
+
2787
+ logger.info(`Imported ${dialogueCount} CyberiaDialogue document(s)`);
2788
+ }
2789
+
2790
+ // 9. Restore IPFS pin records and payloads
2791
+ const ipfsFile = `${backupDir}/ipfs/pins.json`;
2792
+ if (fs.existsSync(ipfsFile)) {
2793
+ const ipfsDocs = fs.readJsonSync(ipfsFile);
2794
+ const ipfsContentDir = `${backupDir}/ipfs/content`;
2795
+ let ipfsCount = 0;
2796
+ let ipfsSkipped = 0;
2797
+
2798
+ const backupPins = new Map();
2799
+ for (const doc of ipfsDocs) {
2800
+ const resourceType = inferResourceType(doc);
2801
+ if (!resourceType) {
2802
+ logger.warn(
2803
+ `Ipfs record is missing resourceType and cannot be inferred (cid: ${doc.cid}, mfsPath: ${doc.mfsPath ?? '(none)'}) — skipping`,
2804
+ );
2805
+ ipfsSkipped++;
2806
+ continue;
2807
+ }
2808
+
2809
+ const mfsPaths = collectMfsPaths(doc);
2810
+ if (mfsPaths.length === 0) {
2811
+ upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath: '' });
2812
+ } else {
2813
+ for (const mfsPath of mfsPaths) {
2814
+ upsertCanonicalPinEntry(backupPins, { cid: doc.cid, resourceType, mfsPath });
2815
+ }
2816
+ }
2817
+ }
2818
+
2819
+ const backupPinEntries = serialiseCanonicalPins(backupPins);
2820
+ const backupCids = [...new Set(backupPinEntries.map((entry) => entry.cid).filter(Boolean))];
2821
+ if (backupCids.length > 0) {
2822
+ await Ipfs.deleteMany({ cid: { $in: backupCids } });
2823
+ }
2824
+
2825
+ const restoreAdditionalMfsPaths = async (cid, mfsPaths, primaryPath) => {
2826
+ let restoredCount = 0;
2827
+ for (const mfsPath of mfsPaths) {
2828
+ if (!mfsPath || mfsPath === primaryPath) continue;
2829
+ const ok = await IpfsClient.restoreMfsPath(cid, mfsPath);
2830
+ if (ok) restoredCount++;
2831
+ }
2832
+ return restoredCount;
2833
+ };
2834
+
2835
+ const upsertImportedPin = async ({ cid, resourceType, mfsPath }) => {
2836
+ if (!cid || !resourceType) return;
2837
+ await Ipfs.deleteMany({ cid, resourceType });
2838
+ await createPinRecord({ cid, resourceType, mfsPath: mfsPath || '', options: { host, path } });
2839
+ };
2840
+
2841
+ if (fs.existsSync(ipfsContentDir)) {
2842
+ let cidRewriteCount = 0;
2843
+ let extraMfsRestoreCount = 0;
2844
+
2845
+ for (const [index, doc] of backupPinEntries.entries()) {
2846
+ const mfsPaths = collectMfsPaths(doc);
2847
+ const primaryPath = mfsPaths[0] || '';
2848
+ const payloadPath = `${ipfsContentDir}/${doc.cid}.bin`;
2849
+
2850
+ logger.info('IPFS raw payload restore start', {
2851
+ index: index + 1,
2852
+ total: backupPinEntries.length,
2853
+ cid: doc.cid,
2854
+ resourceType: doc.resourceType,
2855
+ mfsPath: primaryPath || null,
2856
+ });
2857
+
2858
+ if (!fs.existsSync(payloadPath)) {
2859
+ logger.warn('IPFS raw payload file missing from backup', {
2860
+ cid: doc.cid,
2861
+ resourceType: doc.resourceType,
2862
+ mfsPath: primaryPath || null,
2863
+ });
2864
+ ipfsSkipped++;
2865
+ continue;
2866
+ }
2867
+
2868
+ const addResult = await IpfsClient.addToIpfs(
2869
+ fs.readFileSync(payloadPath),
2870
+ nodePath.basename(primaryPath || doc.cid),
2871
+ primaryPath || undefined,
2872
+ );
2873
+
2874
+ if (!addResult?.cid) {
2875
+ logger.warn('IPFS raw payload restore failed', {
2876
+ cid: doc.cid,
2877
+ resourceType: doc.resourceType,
2878
+ mfsPath: primaryPath || null,
2879
+ });
2880
+ ipfsSkipped++;
2881
+ continue;
2882
+ }
2883
+
2884
+ const finalCid = addResult.cid;
2885
+ if (doc.cid !== finalCid) {
2886
+ await rewriteImportedCidReferences({
2887
+ oldCid: doc.cid,
2888
+ newCid: finalCid,
2889
+ resourceType: doc.resourceType,
2890
+ });
2891
+ cidRewriteCount++;
2892
+ logger.warn('IPFS raw payload CID mismatch during import; rewriting imported references', {
2893
+ oldCid: doc.cid,
2894
+ newCid: finalCid,
2895
+ resourceType: doc.resourceType,
2896
+ mfsPath: primaryPath || null,
2897
+ });
2898
+ }
2899
+
2900
+ extraMfsRestoreCount += await restoreAdditionalMfsPaths(finalCid, mfsPaths, primaryPath);
2901
+ await upsertImportedPin({ cid: finalCid, resourceType: doc.resourceType, mfsPath: primaryPath });
2902
+ ipfsCount++;
2903
+ }
2904
+
2905
+ logger.info(
2906
+ `Imported ${ipfsCount} Ipfs pin record(s) from exact backup payloads${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
2907
+ );
2908
+ logger.info(
2909
+ `IPFS raw payload restore: ${ipfsCount}/${backupPinEntries.length} record(s) restored, ${extraMfsRestoreCount} additional MFS path(s) restored${cidRewriteCount ? `, ${cidRewriteCount} CID rewrite(s)` : ''}`,
2910
+ );
2911
+ } else {
2912
+ logger.warn(
2913
+ 'Backup has no raw IPFS payload files under ipfs/content/. Rebuilding a canonical IPFS layout from imported ObjectLayer, AtlasSpriteSheet, and File documents.',
2914
+ );
2915
+
2916
+ const importedItemIds = fs.existsSync(olDir)
2917
+ ? fs
2918
+ .readdirSync(olDir)
2919
+ .filter((f) => f.endsWith('.json'))
2920
+ .map((f) => nodePath.basename(f, '.json'))
2921
+ : [];
2922
+ const importedObjectLayers = importedItemIds.length
2923
+ ? await ObjectLayer.find({ 'data.item.id': { $in: importedItemIds } }).lean()
2924
+ : [];
2925
+
2926
+ let rebuiltObjectLayers = 0;
2927
+
2928
+ for (const [index, objectLayerDoc] of importedObjectLayers.entries()) {
2929
+ const itemKey = objectLayerDoc.data?.item?.id || objectLayerDoc._id.toString();
2930
+ const itemPaths = getCanonicalIpfsPaths(itemKey);
2931
+ const updatedData = newInstance(objectLayerDoc.data || {});
2932
+ if (!updatedData.render) updatedData.render = {};
2933
+
2934
+ logger.info('IPFS legacy canonical rebuild start', {
2935
+ index: index + 1,
2936
+ total: importedObjectLayers.length,
2937
+ itemKey,
2938
+ });
2939
+
2940
+ let atlasCid = '';
2941
+ let atlasMetadataCid = '';
2942
+
2943
+ if (objectLayerDoc.atlasSpriteSheetId) {
2944
+ const atlasDoc = await AtlasSpriteSheet.findById(objectLayerDoc.atlasSpriteSheetId).lean();
2945
+ if (atlasDoc) {
2946
+ const atlasFile = atlasDoc.fileId ? await File.findById(atlasDoc.fileId).lean() : null;
2947
+ const atlasBuffer = toBuffer(atlasFile?.data);
2948
+
2949
+ if (atlasBuffer) {
2950
+ const atlasAddResult = await IpfsClient.addBufferToIpfs(
2951
+ atlasBuffer,
2952
+ `${itemKey}_atlas_sprite_sheet.png`,
2953
+ itemPaths.atlasSpriteSheet,
2954
+ );
2955
+ if (atlasAddResult?.cid) {
2956
+ atlasCid = atlasAddResult.cid;
2957
+ await AtlasSpriteSheet.updateOne({ _id: atlasDoc._id }, { $set: { cid: atlasCid } });
2958
+ await createPinRecord({
2959
+ cid: atlasCid,
2960
+ resourceType: 'atlas-sprite-sheet',
2961
+ mfsPath: itemPaths.atlasSpriteSheet,
2962
+ options: { host, path },
2963
+ });
2964
+ ipfsCount++;
2965
+ } else {
2966
+ logger.warn(`Failed to rebuild atlas sprite sheet payload for '${itemKey}'`);
2967
+ }
2968
+ } else if (atlasDoc.fileId) {
2969
+ logger.warn(`Atlas File payload missing for '${itemKey}'`);
2970
+ }
2971
+
2972
+ const atlasMetadataResult = await IpfsClient.addJsonToIpfs(
2973
+ atlasDoc.metadata || {},
2974
+ `${itemKey}_atlas_sprite_sheet_metadata.json`,
2975
+ itemPaths.atlasMetadata,
2976
+ );
2977
+ if (atlasMetadataResult?.cid) {
2978
+ atlasMetadataCid = atlasMetadataResult.cid;
2979
+ await createPinRecord({
2980
+ cid: atlasMetadataCid,
2981
+ resourceType: 'atlas-metadata',
2982
+ mfsPath: itemPaths.atlasMetadata,
2983
+ options: { host, path },
2984
+ });
2985
+ ipfsCount++;
2986
+ } else {
2987
+ logger.warn(`Failed to rebuild atlas metadata payload for '${itemKey}'`);
2988
+ }
2989
+ }
2990
+ }
2991
+
2992
+ if (atlasCid) {
2993
+ updatedData.render.cid = atlasCid;
2994
+ } else {
2995
+ delete updatedData.render.cid;
2996
+ }
2997
+ if (atlasMetadataCid) {
2998
+ updatedData.render.metadataCid = atlasMetadataCid;
2999
+ } else {
3000
+ delete updatedData.render.metadataCid;
3001
+ }
3002
+
3003
+ const objectLayerAddResult = await IpfsClient.addJsonToIpfs(
3004
+ updatedData,
3005
+ `${itemKey}_data.json`,
3006
+ itemPaths.objectLayerData,
3007
+ );
3008
+ if (objectLayerAddResult?.cid) {
3009
+ await ObjectLayer.updateOne(
3010
+ { _id: objectLayerDoc._id },
3011
+ {
3012
+ $set: {
3013
+ cid: objectLayerAddResult.cid,
3014
+ data: updatedData,
3015
+ },
3016
+ },
3017
+ );
3018
+ await createPinRecord({
3019
+ cid: objectLayerAddResult.cid,
3020
+ resourceType: 'object-layer-data',
3021
+ mfsPath: itemPaths.objectLayerData,
3022
+ options: { host, path },
3023
+ });
3024
+ ipfsCount++;
3025
+ rebuiltObjectLayers++;
3026
+ } else {
3027
+ logger.warn(`Failed to rebuild object-layer-data payload for '${itemKey}'`);
3028
+ ipfsSkipped++;
3029
+ }
3030
+ }
3031
+
3032
+ logger.info(
3033
+ `Legacy IPFS rebuild: ${rebuiltObjectLayers}/${importedObjectLayers.length} ObjectLayer payload(s) rebuilt, ${ipfsCount} canonical pin record(s) upserted${ipfsSkipped ? `, skipped ${ipfsSkipped}` : ''}`,
3034
+ );
3035
+ }
3036
+ }
3037
+
2188
3038
  logger.info('Instance import completed', { backupDir });
2189
3039
  }
2190
3040
 
@@ -2201,13 +3051,41 @@ try {
2201
3051
  // Collect thumbnail File IDs to drop
2202
3052
  const thumbFileIds = [];
2203
3053
  if (existingInstance.thumbnail) thumbFileIds.push(existingInstance.thumbnail);
3054
+ const dropOlItemIds = new Set();
2204
3055
 
2205
3056
  // Query other instances for shared thumbnail exclusion
2206
3057
  const otherInstances = await CyberiaInstance.find({ code: { $ne: instanceCode } }, { thumbnail: 1 }).lean();
2207
3058
 
3059
+ // Add instance-level itemIds (may not appear in any map entity)
3060
+ for (const id of existingInstance.itemIds || []) if (id) dropOlItemIds.add(id);
3061
+
3062
+ // Add conf entityDefaults and skillConfig itemIds (liveItemIds, deadItemIds, dropItemIds, defaultObjectLayers)
3063
+ const existingConf =
3064
+ (await CyberiaInstanceConf.findOne({ instanceCode }).lean()) ||
3065
+ (existingInstance.conf ? await CyberiaInstanceConf.findById(existingInstance.conf).lean() : null);
3066
+ if (existingConf) {
3067
+ for (const ed of existingConf.entityDefaults || []) {
3068
+ for (const id of ed.liveItemIds || []) if (id) dropOlItemIds.add(id);
3069
+ for (const id of ed.deadItemIds || []) if (id) dropOlItemIds.add(id);
3070
+ for (const id of ed.dropItemIds || []) if (id) dropOlItemIds.add(id);
3071
+ for (const slot of ed.defaultObjectLayers || []) if (slot.itemId) dropOlItemIds.add(slot.itemId);
3072
+ }
3073
+ for (const sc of existingConf.skillConfig || []) {
3074
+ if (sc.triggerItemId) dropOlItemIds.add(sc.triggerItemId);
3075
+ for (const skill of sc.skills || []) {
3076
+ if (skill.summonedEntityItemId && !skill.summonedEntityItemId.startsWith('$'))
3077
+ dropOlItemIds.add(skill.summonedEntityItemId);
3078
+ }
3079
+ }
3080
+ }
3081
+
3082
+ const otherMaps = await CyberiaMap.find(
3083
+ { code: { $nin: [...dropMapCodes] } },
3084
+ { 'entities.objectLayerItemIds': 1, thumbnail: 1 },
3085
+ ).lean();
3086
+
2208
3087
  if (dropMapCodes.size > 0) {
2209
3088
  const dropMaps = await CyberiaMap.find({ code: { $in: [...dropMapCodes] } }).lean();
2210
- const dropOlItemIds = new Set();
2211
3089
  for (const map of dropMaps) {
2212
3090
  if (map.thumbnail) thumbFileIds.push(map.thumbnail);
2213
3091
  for (const entity of map.entities || []) {
@@ -2216,103 +3094,102 @@ try {
2216
3094
  }
2217
3095
  }
2218
3096
  }
3097
+ const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
3098
+ logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
3099
+ }
2219
3100
 
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
- }
3101
+ // Exclude OL item IDs referenced by maps outside this instance
3102
+ const sharedOlItemIds = new Set();
3103
+ for (const m of otherMaps) {
3104
+ for (const entity of m.entities || []) {
3105
+ for (const itemId of entity.objectLayerItemIds || []) {
3106
+ if (dropOlItemIds.has(itemId)) sharedOlItemIds.add(itemId);
2231
3107
  }
2232
3108
  }
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
- }
3109
+ }
3110
+ for (const shared of sharedOlItemIds) dropOlItemIds.delete(shared);
3111
+ if (sharedOlItemIds.size > 0) {
3112
+ logger.info(`Preserved ${sharedOlItemIds.size} ObjectLayer(s) shared with other maps`);
3113
+ }
2237
3114
 
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
- }
3115
+ // Exclude thumbnail File IDs referenced by other instances or maps
3116
+ const otherMapThumbs = otherMaps.map((m) => m.thumbnail?.toString()).filter(Boolean);
3117
+ const otherInstThumbs = otherInstances.map((i) => i.thumbnail?.toString()).filter(Boolean);
3118
+ const sharedThumbIds = new Set([...otherMapThumbs, ...otherInstThumbs]);
3119
+ for (let i = thumbFileIds.length - 1; i >= 0; i--) {
3120
+ if (sharedThumbIds.has(thumbFileIds[i].toString())) thumbFileIds.splice(i, 1);
3121
+ }
2245
3122
 
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();
3123
+ if (dropOlItemIds.size > 0) {
3124
+ const dropDialogueCodes = [...dropOlItemIds].map((id) => `default-${id}`);
3125
+ const dialogueResult = await CyberiaDialogue.deleteMany({ code: { $in: dropDialogueCodes } });
3126
+ logger.info(`Dropped ${dialogueResult.deletedCount} CyberiaDialogue document(s)`);
3127
+
3128
+ const olDocs = await ObjectLayer.find(
3129
+ { 'data.item.id': { $in: [...dropOlItemIds] } },
3130
+ {
3131
+ cid: 1,
3132
+ 'data.item.id': 1,
3133
+ 'data.render': 1,
3134
+ objectLayerRenderFramesId: 1,
3135
+ atlasSpriteSheetId: 1,
3136
+ },
3137
+ ).lean();
2257
3138
 
2258
- const cidsToUnpin = new Set();
2259
- const renderFrameIds = [];
2260
- const atlasIds = [];
2261
- const itemKeysToClean = new Set();
3139
+ const cidsToUnpin = new Set();
3140
+ const renderFrameIds = [];
3141
+ const atlasIds = [];
3142
+ const itemKeysToClean = new Set();
2262
3143
 
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
- }
3144
+ for (const doc of olDocs) {
3145
+ if (doc.cid) cidsToUnpin.add(doc.cid);
3146
+ if (doc.data?.render?.cid) cidsToUnpin.add(doc.data.render.cid);
3147
+ if (doc.data?.render?.metadataCid) cidsToUnpin.add(doc.data.render.metadataCid);
3148
+ if (doc.data?.item?.id) itemKeysToClean.add(doc.data.item.id);
3149
+ if (doc.objectLayerRenderFramesId) renderFrameIds.push(doc.objectLayerRenderFramesId);
3150
+ if (doc.atlasSpriteSheetId) atlasIds.push(doc.atlasSpriteSheetId);
3151
+ }
2271
3152
 
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)`);
3153
+ if (atlasIds.length > 0) {
3154
+ const atlasDocs = await AtlasSpriteSheet.find({ _id: { $in: atlasIds } }, { fileId: 1, cid: 1 }).lean();
3155
+ const atlasFileIds = atlasDocs.map((a) => a.fileId).filter(Boolean);
3156
+ for (const atlas of atlasDocs) {
3157
+ if (atlas.cid) cidsToUnpin.add(atlas.cid);
2284
3158
  }
2285
-
2286
- if (renderFrameIds.length > 0) {
2287
- const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
2288
- logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
3159
+ if (atlasFileIds.length > 0) {
3160
+ const fileResult = await File.deleteMany({ _id: { $in: atlasFileIds } });
3161
+ logger.info(`Dropped ${fileResult.deletedCount} File document(s) (atlas)`);
2289
3162
  }
3163
+ const atlasResult = await AtlasSpriteSheet.deleteMany({ _id: { $in: atlasIds } });
3164
+ logger.info(`Dropped ${atlasResult.deletedCount} AtlasSpriteSheet document(s)`);
3165
+ }
2290
3166
 
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
- }
3167
+ if (renderFrameIds.length > 0) {
3168
+ const rfResult = await ObjectLayerRenderFrames.deleteMany({ _id: { $in: renderFrameIds } });
3169
+ logger.info(`Dropped ${rfResult.deletedCount} ObjectLayerRenderFrames document(s)`);
3170
+ }
2295
3171
 
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
- );
3172
+ if (cidsToUnpin.size > 0) {
3173
+ const ipfsResult = await Ipfs.deleteMany({ cid: { $in: [...cidsToUnpin] } });
3174
+ logger.info(`Dropped ${ipfsResult.deletedCount} Ipfs pin record(s)`);
3175
+ }
2309
3176
 
2310
- const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
2311
- logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
3177
+ let unpinCount = 0;
3178
+ for (const cid of cidsToUnpin) {
3179
+ const ok = await IpfsClient.unpinCid(cid);
3180
+ if (ok) unpinCount++;
3181
+ }
3182
+ let mfsCount = 0;
3183
+ for (const itemKey of itemKeysToClean) {
3184
+ const ok = await IpfsClient.removeMfsPath(`/object-layer/${itemKey}`);
3185
+ if (ok) mfsCount++;
2312
3186
  }
3187
+ logger.info(
3188
+ `IPFS cleanup: ${unpinCount}/${cidsToUnpin.size} CIDs unpinned, ${mfsCount}/${itemKeysToClean.size} MFS paths removed`,
3189
+ );
2313
3190
 
2314
- const mapResult = await CyberiaMap.deleteMany({ code: { $in: [...dropMapCodes] } });
2315
- logger.info(`Dropped ${mapResult.deletedCount} CyberiaMap document(s)`);
3191
+ const olResult = await ObjectLayer.deleteMany({ 'data.item.id': { $in: [...dropOlItemIds] } });
3192
+ logger.info(`Dropped ${olResult.deletedCount} ObjectLayer document(s)`);
2316
3193
  }
2317
3194
 
2318
3195
  // Drop thumbnail File documents (instance + maps), excluding shared ones
@@ -2323,6 +3200,8 @@ try {
2323
3200
 
2324
3201
  await CyberiaInstance.deleteOne({ code: instanceCode });
2325
3202
  logger.info('Dropped CyberiaInstance', { code: instanceCode });
3203
+ await CyberiaInstanceConf.deleteOne({ instanceCode });
3204
+ logger.info('Dropped CyberiaInstanceConf', { instanceCode });
2326
3205
  } else {
2327
3206
  logger.info('No existing instance to drop', { code: instanceCode });
2328
3207
  }
@@ -3273,11 +4152,11 @@ try {
3273
4152
 
3274
4153
  const CyberiaDialogue = DataBaseProvider.instance[`${host}${path}`].mongoose.models.CyberiaDialogue;
3275
4154
 
3276
- // Upsert each dialogue record keyed by (itemId, order) — idempotent.
4155
+ // Upsert each dialogue record keyed by (code, order) — idempotent.
3277
4156
  let upserted = 0;
3278
4157
  for (const dlg of DefaultCyberiaDialogues) {
3279
4158
  await CyberiaDialogue.findOneAndUpdate(
3280
- { itemId: dlg.itemId, order: dlg.order },
4159
+ { code: dlg.code, order: dlg.order },
3281
4160
  { $set: { speaker: dlg.speaker, text: dlg.text, mood: dlg.mood } },
3282
4161
  { upsert: true },
3283
4162
  );
@@ -3298,17 +4177,37 @@ try {
3298
4177
  .description('Generate one procedural example of every registered semantic prefix')
3299
4178
  .action(async (options) => {
3300
4179
  const SEMANTIC_TYPES = [
3301
- 'floor-desert',
3302
- 'floor-grass',
4180
+ // 'floor-desert',
4181
+ // 'floor-grass',
3303
4182
  // 'floor-water',
3304
4183
  // 'floor-stone',
3305
4184
  // 'floor-lava',
3306
4185
  'skin-random',
3307
- // 'skin-dark',
3308
- // 'skin-light',
3309
- // 'skin-vivid',
3310
- // 'skin-natural',
4186
+ 'skin-dark',
4187
+ 'skin-light',
4188
+ 'skin-vivid',
4189
+ 'skin-natural',
3311
4190
  'skin-shaved',
4191
+ // 'resource-desert-petal',
4192
+ // 'resource-desert-stone',
4193
+ // 'resource-desert-polygon',
4194
+ // 'resource-desert-thread',
4195
+ // 'resource-grass-petal',
4196
+ // 'resource-grass-stone',
4197
+ // 'resource-grass-polygon',
4198
+ // 'resource-grass-thread',
4199
+ // 'resource-water-petal',
4200
+ // 'resource-water-stone',
4201
+ // 'resource-water-polygon',
4202
+ // 'resource-water-thread',
4203
+ // 'resource-stone-petal',
4204
+ // 'resource-stone-stone',
4205
+ // 'resource-stone-polygon',
4206
+ // 'resource-stone-thread',
4207
+ // 'resource-lava-petal',
4208
+ // 'resource-lava-stone',
4209
+ // 'resource-lava-polygon',
4210
+ // 'resource-lava-thread',
3312
4211
  ];
3313
4212
 
3314
4213
  const baseSeed = options.seed || 'example';
@@ -3330,6 +4229,16 @@ try {
3330
4229
  logger.info('All semantic examples generated.');
3331
4230
  });
3332
4231
 
4232
+ runner
4233
+ .command('build-manifest')
4234
+ .description('Build k8s resource manifest YAML files from templates')
4235
+ .action(() => {
4236
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' --kubeadm`);
4237
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-client,./cyberia-client' --kind --dev`);
4238
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' --kubeadm`);
4239
+ shellExec(`node bin run instance-build-manifest 'dd-cyberia,mmo-server,./cyberia-server' --kind --dev`);
4240
+ });
4241
+
3333
4242
  if (underpostProgram.commands.find((c) => c._name == process.argv[2]))
3334
4243
  throw new Error('Trigger underpost passthrough');
3335
4244