@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -0,0 +1,210 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
4
+ import { loggerFactory } from '../../server/ops/logger.js';
5
+ import { createPinRecord } from '../../api/ipfs/ipfs.service.js';
6
+ import { IpfsClient } from './ipfs-client.js';
7
+ import { documentFileIds } from '../../api/file/file.ref.js';
8
+ import { ATLAS_FILE_FIELDS, AtlasSpriteSheetStore, atlasMfsPaths } from './atlas-sprite-sheet-store.js';
9
+ import { DEFAULT_ATLAS_UPSCALE_FACTOR } from './atlas-sprite-sheet-generator.js';
10
+ import { ObjectLayerEngine } from './object-layer.js';
11
+
12
+ const logger = loggerFactory(import.meta);
13
+
14
+ /* The `files/` name each atlas render is exported under; one per registered File field, so an
15
+ * export overwrites its own previous copy instead of leaving one behind. */
16
+ const ATLAS_BACKUP_FILE_PREFIX = { fileId: 'atlas', minifyFileId: 'atlas-minify', idlePreviewFileId: 'atlas-idle' };
17
+ export const atlasBackupFileKey = (field, itemKey) => `${ATLAS_BACKUP_FILE_PREFIX[field]}-${itemKey}`;
18
+
19
+ /* A File's bytes as the export wrote them: base64, or a serialised Buffer. */
20
+ const decodeFileData = (data) => {
21
+ if (!data) return data;
22
+ if (data.$base64) return Buffer.from(data.$base64, 'base64');
23
+ if (data.type === 'Buffer' && Array.isArray(data.data)) return Buffer.from(data.data);
24
+ return data;
25
+ };
26
+
27
+ const readJsonIfPresent = (file) => (fs.existsSync(file) ? fs.readJsonSync(file) : null);
28
+
29
+ /**
30
+ * Reads everything an instance backup holds for one object layer.
31
+ *
32
+ * The object layer names its render frames and atlas by `_id`; the atlas names its render
33
+ * Files the same way. Files are matched on that `_id`, never on file name, so a backup that
34
+ * renamed its renders still resolves. The IPFS payloads are the raw bytes behind the three
35
+ * CIDs the item references, when the backup carried them.
36
+ *
37
+ * @param {{backupDir: string, itemId: string}} params
38
+ * @returns {{
39
+ * objectLayer: object, renderFrames: object|null, atlas: object|null,
40
+ * files: object[], payloads: Map<string, Buffer>,
41
+ * }}
42
+ * @throws {Error} When the backup has no object layer for the item.
43
+ */
44
+ export function readObjectLayerBackup({ backupDir, itemId }) {
45
+ const objectLayer = readJsonIfPresent(path.join(backupDir, 'object-layers', `${itemId}.json`));
46
+ if (!objectLayer) throw new Error(`Backup at ${backupDir} has no object layer '${itemId}'`);
47
+
48
+ const renderFrames = readJsonIfPresent(path.join(backupDir, 'render-frames', `${itemId}.json`));
49
+ const atlas = readJsonIfPresent(path.join(backupDir, 'atlas-sprite-sheets', `${itemId}.json`));
50
+
51
+ const wanted = new Set(documentFileIds(atlas ? [atlas] : [], ATLAS_FILE_FIELDS));
52
+ const filesDir = path.join(backupDir, 'files');
53
+ const files = [];
54
+ if (wanted.size > 0 && fs.existsSync(filesDir)) {
55
+ for (const name of fs.readdirSync(filesDir)) {
56
+ if (!name.endsWith('.json')) continue;
57
+ const doc = fs.readJsonSync(path.join(filesDir, name));
58
+ if (!wanted.has(String(doc._id))) continue;
59
+ files.push({ ...doc, data: decodeFileData(doc.data) });
60
+ }
61
+ }
62
+
63
+ const payloads = new Map();
64
+ const contentDir = path.join(backupDir, 'ipfs', 'content');
65
+ for (const cid of [objectLayer.cid, objectLayer.data?.render?.cid, objectLayer.data?.render?.metadataCid]) {
66
+ if (!cid) continue;
67
+ const payload = path.join(contentDir, `${cid}.bin`);
68
+ if (fs.existsSync(payload)) payloads.set(cid, fs.readFileSync(payload));
69
+ }
70
+
71
+ return { objectLayer, renderFrames, atlas, files, payloads };
72
+ }
73
+
74
+ /* Re-adds one payload and pins it under its canonical path. Returns the CID IPFS assigned, or
75
+ * the backup's own when IPFS is unreachable, so the documents still point at something real. */
76
+ const restorePayload = async ({ cid, payload, mfsPath, resourceType, options }) => {
77
+ if (!payload) return cid;
78
+ const added = await IpfsClient.addToIpfs(payload, path.basename(mfsPath), mfsPath);
79
+ if (!added?.cid) {
80
+ logger.warn(`IPFS add failed for ${mfsPath}; keeping the backup CID ${cid}`);
81
+ return cid;
82
+ }
83
+ if (added.cid !== cid) logger.warn(`IPFS assigned ${added.cid} to ${mfsPath}; the backup said ${cid}`);
84
+ await createPinRecord({ cid: added.cid, resourceType, mfsPath, options });
85
+ return added.cid;
86
+ };
87
+
88
+ /**
89
+ * Restores one object layer from an instance backup, making the backup the authority for that
90
+ * item id: its render frames, atlas, atlas render Files, IPFS pins and payloads, and the static
91
+ * frame PNGs, replacing whatever the database holds under that id.
92
+ *
93
+ * Payloads go to IPFS first, so the documents are written with the CIDs IPFS actually
94
+ * assigned. `_id`s are kept, so references between the restored documents stay intact.
95
+ *
96
+ * @param {{backupDir: string, itemId: string, options: {host: string, path: string}}} params
97
+ * @returns {Promise<{itemId: string, files: number, renderFrames: boolean, atlas: boolean, pins: number, staticFiles: number}>}
98
+ */
99
+ export async function restoreObjectLayerBackup({ backupDir, itemId, options }) {
100
+ const { objectLayer, renderFrames, atlas, files, payloads } = readObjectLayerBackup({ backupDir, itemId });
101
+ const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
102
+ const ObjectLayerRenderFrames = DataBaseProviderService.getModel('ObjectLayerRenderFrames', options);
103
+ const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
104
+ const File = DataBaseProviderService.getModel('File', options);
105
+
106
+ // 1. IPFS, so every CID below is one the node will serve.
107
+ const mfs = atlasMfsPaths(itemId);
108
+ const targets = [
109
+ {
110
+ key: 'cid',
111
+ get: () => objectLayer.cid,
112
+ set: (v) => (objectLayer.cid = v),
113
+ mfsPath: `/object-layer/${itemId}/${itemId}_data.json`,
114
+ resourceType: 'object-layer-data',
115
+ },
116
+ {
117
+ key: 'render',
118
+ get: () => objectLayer.data?.render?.cid,
119
+ set: (v) => (objectLayer.data.render.cid = v),
120
+ mfsPath: mfs.png,
121
+ resourceType: 'atlas-sprite-sheet',
122
+ },
123
+ {
124
+ key: 'metadata',
125
+ get: () => objectLayer.data?.render?.metadataCid,
126
+ set: (v) => (objectLayer.data.render.metadataCid = v),
127
+ mfsPath: mfs.metadata,
128
+ resourceType: 'atlas-metadata',
129
+ },
130
+ ];
131
+ let pins = 0;
132
+ for (const target of targets) {
133
+ const cid = target.get();
134
+ if (!cid) continue;
135
+ const finalCid = await restorePayload({
136
+ cid,
137
+ payload: payloads.get(cid),
138
+ mfsPath: target.mfsPath,
139
+ resourceType: target.resourceType,
140
+ options,
141
+ });
142
+ if (payloads.has(cid)) pins++;
143
+ target.set(finalCid);
144
+ if (target.key === 'render' && atlas) atlas.cid = finalCid;
145
+ }
146
+
147
+ // 2. Documents, leaves first so every reference resolves as it lands.
148
+ for (const file of files) {
149
+ await File.deleteOne({ _id: file._id });
150
+ await File.create(file);
151
+ }
152
+ if (renderFrames?._id) {
153
+ await ObjectLayerRenderFrames.deleteOne({ _id: renderFrames._id });
154
+ await ObjectLayerRenderFrames.create(renderFrames);
155
+ }
156
+ if (atlas) {
157
+ await AtlasSpriteSheet.deleteOne({ _id: atlas._id });
158
+ await AtlasSpriteSheet.deleteOne({ 'metadata.itemKey': itemId });
159
+ await AtlasSpriteSheet.create(atlas);
160
+ // A backup from before the still existed restores without one; cut it from the render now.
161
+ await AtlasSpriteSheetStore.syncIdlePreview({ itemKey: itemId, options });
162
+ await AtlasSpriteSheetStore.pruneOrphanRenders({ options });
163
+ }
164
+ // An item the database already has keeps its own _id and takes the backup's values, so an
165
+ // item id can never fork into two documents.
166
+ const existing = await ObjectLayer.findByItemId(itemId);
167
+ if (!existing && objectLayer._id) await ObjectLayer.deleteOne({ _id: objectLayer._id });
168
+ await ObjectLayer.upsertByItemId(objectLayer);
169
+
170
+ // An atlas restored without its minified render cannot be served, and a render cut from the
171
+ // frames alone is refused wherever the stored layout moved: regenerate the whole atlas from
172
+ // the frames just restored, and relink the object layer to it.
173
+ if (atlas && !atlas.minifyFileId && renderFrames) {
174
+ const live = await ObjectLayer.findByItemId(itemId);
175
+ const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
176
+ itemKey: itemId,
177
+ objectLayerRenderFrames: renderFrames,
178
+ options,
179
+ });
180
+ live.atlasSpriteSheetId = atlasDoc._id;
181
+ if (!live.data.render) live.data.render = {};
182
+ live.data.render.cid = atlasCid;
183
+ live.data.render.metadataCid = atlasMetadataCid;
184
+ live.markModified('data.render');
185
+ await live.save();
186
+ await ObjectLayerEngine.computeAndSaveFinalSha256({ objectLayer: live, options });
187
+ logger.info(`Rebuilt the atlas of '${itemId}': the backup carried no minified render`);
188
+ }
189
+
190
+ // 3. The static frame PNGs the web client serves, from the same render frames.
191
+ let staticFiles = 0;
192
+ const itemType = objectLayer.data?.item?.type;
193
+ if (renderFrames && itemType) {
194
+ const written = await ObjectLayerEngine.writeStaticFrameAssets({
195
+ basePaths: ['./src/client/public/cyberia/', `./public/${options.host}${options.path}`],
196
+ itemType,
197
+ itemId,
198
+ objectLayerRenderFramesData: {
199
+ frames: renderFrames.frames || {},
200
+ colors: renderFrames.colors || [],
201
+ frame_duration: renderFrames.frame_duration ?? 100,
202
+ },
203
+ objectLayerData: objectLayer,
204
+ cellPixelDim: DEFAULT_ATLAS_UPSCALE_FACTOR,
205
+ });
206
+ staticFiles = written.length;
207
+ }
208
+
209
+ return { itemId, files: files.length, renderFrames: !!renderFrames, atlas: !!atlas, pins, staticFiles };
210
+ }