@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
@@ -1,11 +1,15 @@
1
1
  import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
2
2
  import { loggerFactory } from '../../server/ops/logger.js';
3
3
  import { DataQuery } from '../../server/storage/data-query.js';
4
- import { AtlasSpriteSheetGenerator } from '../../projects/cyberia/atlas-sprite-sheet-generator.js';
5
- import { FileFactory } from '../file/file.service.js';
4
+ import { FileCleanup } from '../file/file.service.js';
6
5
  import { AtlasSpriteSheetDto } from './atlas-sprite-sheet.model.js';
7
6
  import { IpfsClient } from '../../projects/cyberia/ipfs-client.js';
8
- import { createPinRecord, removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
7
+ import { removePinRecordsAndUnpin } from '../ipfs/ipfs.service.js';
8
+ import {
9
+ ATLAS_FILE_FIELDS,
10
+ AtlasSpriteSheetStore,
11
+ atlasMfsPaths,
12
+ } from '../../projects/cyberia/atlas-sprite-sheet-store.js';
9
13
 
10
14
  const logger = loggerFactory(import.meta);
11
15
  const DEFAULT_ATLAS_FRAME_DURATION = 100;
@@ -51,133 +55,91 @@ async function withResolvedAtlasFrameDuration(doc, ObjectLayer) {
51
55
  };
52
56
  }
53
57
 
58
+ /**
59
+ * Removes one atlas: its IPFS pins and MFS entries, both render File documents,
60
+ * and the AtlasSpriteSheet document itself.
61
+ *
62
+ * @param {Object} params
63
+ * @param {Object} params.atlasDoc - The AtlasSpriteSheet document.
64
+ * @param {Object} params.options - Router options ({ host, path }).
65
+ * @param {string} [params.itemKey] - Item key, when the caller already resolved it.
66
+ * @param {string} [params.metadataCid] - Atlas metadata CID, when the object layer holds it.
67
+ * @returns {Promise<void>}
68
+ */
69
+ const purgeAtlasDoc = async ({ atlasDoc, options, itemKey, metadataCid }) => {
70
+ const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
71
+ const File = DataBaseProviderService.getModel('File', options);
72
+ const key = itemKey || atlasDoc.metadata?.itemKey;
73
+ const mfsPaths = key ? atlasMfsPaths(key) : null;
74
+
75
+ for (const [cid, mfsPath] of [
76
+ [atlasDoc.cid, mfsPaths?.png],
77
+ [metadataCid, mfsPaths?.metadata],
78
+ ]) {
79
+ if (!cid) continue;
80
+ try {
81
+ await removePinRecordsAndUnpin(cid, options);
82
+ if (mfsPath) await IpfsClient.removeMfsPath(mfsPath);
83
+ logger.info(`Cleaned up IPFS CID ${cid} for AtlasSpriteSheet ${atlasDoc._id}`);
84
+ } catch (ipfsErr) {
85
+ logger.warn(`Failed to clean up IPFS CID ${cid}: ${ipfsErr.message}`);
86
+ }
87
+ }
88
+
89
+ await FileCleanup.deleteDocumentFiles({ doc: atlasDoc, fileFields: ATLAS_FILE_FIELDS, File });
90
+ await AtlasSpriteSheet.findByIdAndDelete(atlasDoc._id);
91
+ };
92
+
54
93
  class AtlasSpriteSheetService {
55
- static blob = async (req, res, options) => {
94
+ // Serves the minified render, the one `metadata` describes. The client runtime
95
+ // pairs this blob with GET /metadata/:itemKey, so the two must be the same layout.
96
+ /* One render of an item's atlas, by the File field that holds it. */
97
+ static render = async ({ itemKey, field, label }, options) => {
56
98
  /** @type {import('./atlas-sprite-sheet.model.js').AtlasSpriteSheetModel} */
57
99
  const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
58
100
  /** @type {import('../file/file.model.js').FileModel} */
59
101
  const File = DataBaseProviderService.getModel('File', options);
60
102
 
61
- const itemKey = req.params.itemKey;
62
103
  const atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey }).lean();
63
104
  if (!atlasDoc) throw new Error(`Atlas not found for itemKey: ${itemKey}`);
105
+ if (!atlasDoc[field]) throw new Error(`${label} missing for itemKey: ${itemKey}`);
64
106
 
65
- const fileDoc = await File.findById(atlasDoc.fileId);
107
+ const fileDoc = await File.findById(atlasDoc[field]);
66
108
  if (!fileDoc || !fileDoc.data) throw new Error(`File not found for atlas itemKey: ${itemKey}`);
67
109
 
68
110
  return { buffer: Buffer.from(fileDoc.data), mimetype: fileDoc.mimetype || 'image/png', name: fileDoc.name };
69
111
  };
70
- static generate = async (req, res, options, generateOptions = {}) => {
71
- /** @type {import('../object-layer/object-layer.model.js').ObjectLayerModel} */
72
- const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
73
- /** @type {import('../file/file.model.js').FileModel} */
74
- const File = DataBaseProviderService.getModel('File', options);
75
- /** @type {import('./atlas-sprite-sheet.model.js').AtlasSpriteSheetModel} */
76
- const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
77
112
 
78
- let objectLayer = req.objectLayer;
79
-
80
- if (!objectLayer) {
81
- objectLayer = await ObjectLayer.findById(req.params.id).populate('objectLayerRenderFramesId');
82
- }
83
-
84
- if (!objectLayer) {
85
- throw new Error('ObjectLayer not found');
86
- }
87
-
88
- if (!objectLayer.objectLayerRenderFramesId) {
89
- throw new Error('ObjectLayer has no render frames');
90
- }
91
-
92
- const itemKey = objectLayer.data.item.id;
93
- const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
94
- objectLayer.objectLayerRenderFramesId,
95
- itemKey,
113
+ static blob = (req, res, options) =>
114
+ AtlasSpriteSheetService.render(
115
+ { itemKey: req.params.itemKey, field: 'minifyFileId', label: 'Minified atlas render' },
116
+ options,
96
117
  );
97
118
 
98
- const fileDoc = await new File(FileFactory.create(buffer, `${itemKey}.png`)).save();
99
-
100
- // Add atlas PNG to IPFS and obtain its CID
101
- let atlasCid = '';
102
- let atlasMetadataCid = '';
103
- const userId = req.auth && req.auth.user ? req.auth.user._id : undefined;
104
- try {
105
- const ipfsResult = await IpfsClient.addBufferToIpfs(
106
- buffer,
107
- `${itemKey}_atlas_sprite_sheet.png`,
108
- `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
109
- );
110
- if (ipfsResult) {
111
- atlasCid = ipfsResult.cid;
112
- logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
113
- }
114
- } catch (ipfsError) {
115
- logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
116
- }
117
-
118
- // Pin atlas metadata JSON to IPFS (fast-json-stable-stringify) and obtain its CID
119
- try {
120
- const metadataIpfsResult = await IpfsClient.addJsonToIpfs(
121
- metadata,
122
- `${itemKey}_atlas_sprite_sheet_metadata.json`,
123
- `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
124
- );
125
- if (metadataIpfsResult) {
126
- atlasMetadataCid = metadataIpfsResult.cid;
127
- logger.info(`Atlas metadata pinned to IPFS – CID: ${atlasMetadataCid}`);
128
- }
129
- } catch (ipfsError) {
130
- logger.warn('Failed to add atlas metadata to IPFS:', ipfsError.message);
131
- }
119
+ static idlePreview = (req, res, options) =>
120
+ AtlasSpriteSheetService.render(
121
+ { itemKey: req.params.itemKey, field: 'idlePreviewFileId', label: 'Idle preview' },
122
+ options,
123
+ );
124
+ static generate = async (req, res, options, generateOptions = {}) => {
125
+ /** @type {import('../object-layer/object-layer.model.js').ObjectLayerModel} */
126
+ const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
132
127
 
133
- let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
128
+ const objectLayer =
129
+ req.objectLayer || (await ObjectLayer.findById(req.params.id).populate('objectLayerRenderFramesId'));
134
130
 
135
- if (atlasDoc) {
136
- // Clean up old file if it exists
137
- if (atlasDoc.fileId) {
138
- await File.findByIdAndDelete(atlasDoc.fileId);
139
- }
140
- atlasDoc.fileId = fileDoc._id;
141
- atlasDoc.cid = atlasCid;
142
- atlasDoc.metadata = metadata;
143
- await atlasDoc.save();
144
- } else {
145
- atlasDoc = await new AtlasSpriteSheet({
146
- fileId: fileDoc._id,
147
- cid: atlasCid,
148
- metadata,
149
- }).save();
150
- }
131
+ if (!objectLayer) throw new Error('ObjectLayer not found');
132
+ if (!objectLayer.objectLayerRenderFramesId) throw new Error('ObjectLayer has no render frames');
151
133
 
152
- // Register CIDs in the IPFS registry now that atlasDoc._id is known.
153
- if (atlasCid) {
154
- try {
155
- await createPinRecord({
156
- cid: atlasCid,
157
- resourceType: 'atlas-sprite-sheet',
158
- mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`,
159
- options,
160
- });
161
- } catch (e) {
162
- logger.warn('IPFS registry update failed (atlas PNG):', e.message);
163
- }
164
- }
165
- if (atlasMetadataCid) {
166
- try {
167
- await createPinRecord({
168
- cid: atlasMetadataCid,
169
- resourceType: 'atlas-metadata',
170
- mfsPath: `/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet_metadata.json`,
171
- options,
172
- });
173
- } catch (e) {
174
- logger.warn('IPFS registry update failed (atlas metadata):', e.message);
175
- }
176
- }
134
+ const { atlasDoc, atlasCid, atlasMetadataCid } = await AtlasSpriteSheetStore.persist({
135
+ itemKey: objectLayer.data.item.id,
136
+ objectLayerRenderFrames: objectLayer.objectLayerRenderFramesId,
137
+ upscaleFactor: generateOptions.upscaleFactor,
138
+ options,
139
+ });
177
140
 
178
- // When skipObjectLayerSave is set, return CIDs without mutating the OL document.
179
- // This enables cut-over consistency: callers stage CIDs in memory and write the
180
- // ObjectLayer atomically only after all CIDs are computed.
141
+ // Callers that stage CIDs in memory write the ObjectLayer themselves, so the
142
+ // document stays untouched until every CID is known.
181
143
  if (generateOptions.skipObjectLayerSave) {
182
144
  return { atlasDoc, atlasCid, atlasMetadataCid };
183
145
  }
@@ -194,8 +156,6 @@ class AtlasSpriteSheetService {
194
156
  static deleteByObjectLayerId = async (req, res, options) => {
195
157
  /** @type {import('../object-layer/object-layer.model.js').ObjectLayerModel} */
196
158
  const ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
197
- /** @type {import('../file/file.model.js').FileModel} */
198
- const File = DataBaseProviderService.getModel('File', options);
199
159
  /** @type {import('./atlas-sprite-sheet.model.js').AtlasSpriteSheetModel} */
200
160
  const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
201
161
 
@@ -207,37 +167,12 @@ class AtlasSpriteSheetService {
207
167
  if (objectLayer.atlasSpriteSheetId) {
208
168
  const atlasDoc = await AtlasSpriteSheet.findById(objectLayer.atlasSpriteSheetId);
209
169
  if (atlasDoc) {
210
- // Remove pin records and unpin atlas CID from IPFS
211
- const atlasCid = atlasDoc.cid || objectLayer.data.render?.cid;
212
- const atlasMetadataCid = objectLayer.data.render?.metadataCid;
213
- if (atlasCid) {
214
- try {
215
- await removePinRecordsAndUnpin(atlasCid, options);
216
- // Remove the MFS entry for the atlas sprite sheet PNG
217
- const itemId = objectLayer.data.item.id;
218
- await IpfsClient.removeMfsPath(`/object-layer/${itemId}/${itemId}_atlas_sprite_sheet.png`);
219
- logger.info(`Cleaned up IPFS atlas CID ${atlasCid} for ObjectLayer ${objectLayer._id}`);
220
- } catch (ipfsErr) {
221
- logger.warn(`Failed to clean up IPFS atlas CID ${atlasCid}: ${ipfsErr.message}`);
222
- }
223
- }
224
- if (atlasMetadataCid) {
225
- try {
226
- await removePinRecordsAndUnpin(atlasMetadataCid, options);
227
- const itemId = objectLayer.data.item.id;
228
- await IpfsClient.removeMfsPath(`/object-layer/${itemId}/${itemId}_atlas_sprite_sheet_metadata.json`);
229
- logger.info(`Cleaned up IPFS atlas metadata CID ${atlasMetadataCid} for ObjectLayer ${objectLayer._id}`);
230
- } catch (ipfsErr) {
231
- logger.warn(`Failed to clean up IPFS atlas metadata CID ${atlasMetadataCid}: ${ipfsErr.message}`);
232
- }
233
- }
234
-
235
- // Delete the atlas File document from MongoDB
236
- if (atlasDoc.fileId) {
237
- await File.findByIdAndDelete(atlasDoc.fileId);
238
- }
239
- // Delete the AtlasSpriteSheet document itself
240
- await AtlasSpriteSheet.findByIdAndDelete(atlasDoc._id);
170
+ await purgeAtlasDoc({
171
+ atlasDoc,
172
+ options,
173
+ itemKey: objectLayer.data.item.id,
174
+ metadataCid: objectLayer.data.render?.metadataCid,
175
+ });
241
176
  }
242
177
  objectLayer.atlasSpriteSheetId = undefined;
243
178
  if (!objectLayer.data.render) objectLayer.data.render = {};
@@ -328,54 +263,23 @@ class AtlasSpriteSheetService {
328
263
  static delete = async (req, res, options) => {
329
264
  /** @type {import('./atlas-sprite-sheet.model.js').AtlasSpriteSheetModel} */
330
265
  const AtlasSpriteSheet = DataBaseProviderService.getModel('AtlasSpriteSheet', options);
331
- /** @type {import('../file/file.model.js').FileModel} */
332
- const File = DataBaseProviderService.getModel('File', options);
333
266
 
334
267
  if (req.params.id) {
335
268
  const atlasDoc = await AtlasSpriteSheet.findById(req.params.id);
336
269
  if (!atlasDoc) return null;
270
+ await purgeAtlasDoc({ atlasDoc, options });
271
+ return atlasDoc;
272
+ }
337
273
 
338
- // Remove pin records and unpin atlas CID from IPFS
339
- if (atlasDoc.cid) {
340
- try {
341
- await removePinRecordsAndUnpin(atlasDoc.cid, options);
342
- if (atlasDoc.metadata?.itemKey) {
343
- const itemKey = atlasDoc.metadata.itemKey;
344
- await IpfsClient.removeMfsPath(`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`);
345
- }
346
- logger.info(`Cleaned up IPFS atlas CID ${atlasDoc.cid} for AtlasSpriteSheet ${atlasDoc._id}`);
347
- } catch (ipfsErr) {
348
- logger.warn(`Failed to clean up IPFS atlas CID ${atlasDoc.cid}: ${ipfsErr.message}`);
349
- }
350
- }
351
-
352
- // Delete the referenced File document (the atlas PNG blob)
353
- if (atlasDoc.fileId) {
354
- await File.findByIdAndDelete(atlasDoc.fileId);
355
- }
356
-
357
- return await AtlasSpriteSheet.findByIdAndDelete(req.params.id);
358
- } else {
359
- // Bulk delete: iterate each atlas to clean up File, IPFS pins, and pin records
360
- const allAtlases = await AtlasSpriteSheet.find({});
361
- for (const atlasDoc of allAtlases) {
362
- try {
363
- if (atlasDoc.cid) {
364
- await removePinRecordsAndUnpin(atlasDoc.cid, options);
365
- if (atlasDoc.metadata?.itemKey) {
366
- const itemKey = atlasDoc.metadata.itemKey;
367
- await IpfsClient.removeMfsPath(`/object-layer/${itemKey}/${itemKey}_atlas_sprite_sheet.png`);
368
- }
369
- }
370
- if (atlasDoc.fileId) {
371
- await File.findByIdAndDelete(atlasDoc.fileId);
372
- }
373
- } catch (err) {
374
- logger.error(`Failed to clean up AtlasSpriteSheet ${atlasDoc._id} during bulk delete: ${err.message}`);
375
- }
274
+ const allAtlases = await AtlasSpriteSheet.find({});
275
+ for (const atlasDoc of allAtlases) {
276
+ try {
277
+ await purgeAtlasDoc({ atlasDoc, options });
278
+ } catch (err) {
279
+ logger.error(`Failed to clean up AtlasSpriteSheet ${atlasDoc._id} during bulk delete: ${err.message}`);
376
280
  }
377
- return await AtlasSpriteSheet.deleteMany();
378
281
  }
282
+ return { deletedCount: allAtlases.length };
379
283
  };
380
284
  }
381
285
 
@@ -9,8 +9,8 @@ class CyberiaActionRouter {
9
9
  */
10
10
  static router(options) {
11
11
  const router = express.Router();
12
- // Direct lookup by code the client fetches an NPC's action metadata
13
- // (label, dialogue map) by the action code the Go server sends over AOI.
12
+ // Direct lookup by code. The client fetches an NPC's action metadata
13
+ // (label, dialogue map) by the action code the AOI stream carries.
14
14
  router.get(`/code/:code`, async (req, res) => await CyberiaActionController.getByCode(req, res, options));
15
15
  return registerCrudRoutes(router, CyberiaActionController, options);
16
16
  }
@@ -34,10 +34,9 @@ class CyberiaActionService {
34
34
  if (!code) throw new Error('code parameter is required');
35
35
  const data = await CyberiaAction.findOne({ code }).lean();
36
36
  if (data) return data;
37
- // Fallback world delivers actions to the Go server from the canonical
38
- // defaults without persisting them; serve those so the client can fetch the
39
- // action's label + dialogue map by code. Quest OFFERS are NOT resolved here
40
- // the client locates offered quests via the cyberia-quest API by cell.
37
+ // The fallback world serves actions from the canonical defaults and never
38
+ // persists them. Serve the same defaults here, so the client can fetch an
39
+ // action by code. Quest offers come from the cyberia-quest API by cell.
41
40
  const fallback = DefaultCyberiaActions.find((a) => a.code === code);
42
41
  if (fallback) return fallback;
43
42
  throw new Error(`No action found for code: ${code}`);
@@ -0,0 +1,6 @@
1
+ import { buildCrudController } from '../../server/network/middlewares.js';
2
+ import { CyberiaAudioService } from './cyberia-audio.service.js';
3
+
4
+ const CyberiaAudioController = buildCrudController(CyberiaAudioService);
5
+
6
+ export { CyberiaAudioController };
@@ -0,0 +1,61 @@
1
+ /**
2
+ * One audio asset: what it is, never when it plays.
3
+ *
4
+ * The asset carries its identity (`code`), its bytes (`fileId`) and the manifest the recorder
5
+ * wrote beside them. It holds no classification of its own: whether a sound behaves as a bed, a
6
+ * one-shot or a transition is a property of the context that binds it — see
7
+ * cyberia-map-audio-conf — not of the asset, which the same map or several maps may reuse.
8
+ *
9
+ * @module src/api/cyberia-audio/cyberia-audio.model.js
10
+ * @namespace CyberiaAudioModelServer
11
+ */
12
+
13
+ import { Schema, model } from 'mongoose';
14
+
15
+ // https://mongoosejs.com/docs/2.7.x/docs/schematypes.html
16
+
17
+ // Mirrors the `<name-out>.json` manifest the cyberia-audio package writes beside every recorded
18
+ // WAV. `defaults` and `parameters` are the asset's own parameter contract, so they stay
19
+ // free-form: each asset owns its parameter names and ranges.
20
+ const ManifestSchema = new Schema(
21
+ {
22
+ id: { type: String, required: true },
23
+ // The bus the module was authored on — `src/audio-module/<bus-id>/` in the cyberia-audio
24
+ // package, and the same vocabulary a binding's `settings.bus` selects. It is the asset's
25
+ // natural route, not a decision: a map binding chooses where an asset actually plays.
26
+ bus: { type: String, default: '', trim: true },
27
+ tags: [{ type: String }],
28
+ author: { type: String, required: true },
29
+ version: { type: String, required: true },
30
+ duration: { type: Number, required: true },
31
+ sampleRate: { type: Number },
32
+ channels: { type: Number },
33
+ loop: { type: Boolean, default: false },
34
+ audio: { type: String, default: '' },
35
+ defaults: { type: Schema.Types.Mixed, default: {} },
36
+ parameters: { type: Schema.Types.Mixed, default: {} },
37
+ },
38
+ { _id: false },
39
+ );
40
+
41
+ const CyberiaAudioSchema = new Schema(
42
+ {
43
+ // The asset's canonical identity across the platform: what a map binding, an import and an
44
+ // export all refer to. It is unique on its own, so a reference needs nothing beside it.
45
+ code: { type: String, required: true, trim: true },
46
+ fileId: { type: Schema.Types.ObjectId, ref: 'File' },
47
+ manifest: { type: ManifestSchema, required: true },
48
+ },
49
+ {
50
+ timestamps: true,
51
+ },
52
+ );
53
+
54
+ // Declared here rather than as `index: true` on the field, so the unique index is defined once.
55
+ CyberiaAudioSchema.index({ code: 1 }, { unique: true });
56
+
57
+ const CyberiaAudioModel = model('CyberiaAudio', CyberiaAudioSchema);
58
+
59
+ const ProviderSchema = CyberiaAudioSchema;
60
+
61
+ export { CyberiaAudioSchema, CyberiaAudioModel, ProviderSchema };
@@ -0,0 +1,21 @@
1
+ import express from 'express';
2
+ import { registerCrudRoutes } from '../../server/network/middlewares.js';
3
+ import { CyberiaAudioController } from './cyberia-audio.controller.js';
4
+
5
+ class CyberiaAudioRouter {
6
+ /**
7
+ * @param {import('../types.js').RouterOptions} options
8
+ * @returns {import('express').Router}
9
+ */
10
+ static router(options) {
11
+ // Fully unguarded (matches prior behavior).
12
+ return registerCrudRoutes(express.Router(), CyberiaAudioController, options, {
13
+ writeGuards: [],
14
+ deleteAllGuards: [],
15
+ });
16
+ }
17
+ }
18
+
19
+ const ApiRouter = (options) => CyberiaAudioRouter.router(options);
20
+
21
+ export { ApiRouter, CyberiaAudioRouter };