@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,220 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs-extra';
3
+ import nodePath from 'path';
4
+ import { Types } from 'mongoose';
5
+ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
6
+ import { FileCleanup } from '../file/file.service.js';
7
+ import { loggerFactory } from '../../server/ops/logger.js';
8
+ import { DataQuery } from '../../server/storage/data-query.js';
9
+ import { fileRefFields } from '../file/file.ref.js';
10
+
11
+ const logger = loggerFactory(import.meta);
12
+
13
+ const AUDIO_MIMETYPE = 'audio/wav';
14
+
15
+ /** The File-referencing fields of this model, read from the registry that owns that mapping. */
16
+ const FILE_FIELDS = fileRefFields('cyberia-audio');
17
+
18
+ /**
19
+ * The generic File `_id` an asset's bytes resolve through, derived from the asset code and the
20
+ * bytes themselves.
21
+ *
22
+ * Content-addressed on purpose. The client fetches a WAV as `/api/file/blob/<fileId>` and caches
23
+ * it, so re-recording an asset under a stable id would serve the old sound from that cache
24
+ * forever. A changed render therefore lands on a new id — and the import deletes the one it
25
+ * replaced, so the change costs nothing. Identical bytes re-derive the same id, which is what
26
+ * makes re-importing an unchanged bank a no-op rather than a churn of File documents.
27
+ *
28
+ * The code is folded in so two assets that happen to render identical bytes still own separate
29
+ * File documents; sharing one would make deleting either orphan the other.
30
+ *
31
+ * @param {string} code - Asset code.
32
+ * @param {string} md5 - Hex MD5 of the WAV bytes.
33
+ * @returns {import('mongoose').Types.ObjectId}
34
+ */
35
+ const audioFileId = (code, md5) =>
36
+ new Types.ObjectId(crypto.createHash('sha256').update(`cyberia-audio:${code}:${md5}`).digest('hex').slice(0, 24));
37
+
38
+ /**
39
+ * Validates a `<name>.json` sidecar written by the cyberia-audio package, and reads it into the
40
+ * shape this platform stores.
41
+ *
42
+ * Identity is all the import requires: `id` becomes the asset's code, and the rest is the
43
+ * recorder's own metadata — including `bus`, the `src/audio-module/<bus-id>/` directory the
44
+ * module was authored on, which is the same bus vocabulary a map binding routes through.
45
+ *
46
+ * @param {object} manifest - Parsed manifest.
47
+ * @param {string} manifestPath - Source path, for error messages.
48
+ * @returns {object} The manifest as stored.
49
+ * @throws {Error} When the manifest carries no usable asset identity.
50
+ */
51
+ const readManifest = (manifest, manifestPath) => {
52
+ if (!manifest || typeof manifest !== 'object') throw new Error(`Invalid audio manifest: ${manifestPath}`);
53
+ if (!manifest.id) throw new Error(`Audio manifest has no id: ${manifestPath}`);
54
+ const { bus = '', ...rest } = manifest;
55
+ return { ...rest, bus };
56
+ };
57
+
58
+ class CyberiaAudioService {
59
+ /**
60
+ * Imports one recorded `<name>.wav` + `<name>.json` pair, upserting both the backing
61
+ * File document and the CyberiaAudio document keyed by the asset's code.
62
+ *
63
+ * @param {{wavPath: string, manifestPath: string}} record - Absolute or cwd-relative artifact pair.
64
+ * @param {{host: string, path: string}} options - Provider context.
65
+ * @returns {Promise<object>} The upserted CyberiaAudio document.
66
+ */
67
+ static importRecord = async ({ wavPath, manifestPath }, options) => {
68
+ const CyberiaAudio = DataBaseProviderService.getModel('CyberiaAudio', options);
69
+ const File = DataBaseProviderService.getModel('file', options);
70
+
71
+ const manifest = readManifest(JSON.parse(await fs.readFile(manifestPath, 'utf8')), manifestPath);
72
+ const data = await fs.readFile(wavPath);
73
+ const md5 = crypto.createHash('md5').update(data).digest('hex');
74
+ const fileData = { name: `${manifest.id}.wav`, data, size: data.length, mimetype: AUDIO_MIMETYPE, md5 };
75
+
76
+ await new CyberiaAudio({ code: manifest.id, manifest }).validate();
77
+ if (
78
+ 44 > data.length ||
79
+ data.length > 8 * 1024 * 1024 ||
80
+ data.toString('ascii', 0, 4) !== 'RIFF' ||
81
+ data.toString('ascii', 8, 12) !== 'WAVE' ||
82
+ data.readUInt32LE(4) !== data.length - 8
83
+ )
84
+ throw new Error(`Invalid WAV: ${wavPath}`);
85
+
86
+ const existing = await CyberiaAudio.findOne({ code: manifest.id });
87
+ // Derived from the bytes, so it is the same id on an unchanged re-import and a new one the
88
+ // moment the render changes. Writing the File first leaves the bytes reachable if the run is
89
+ // interrupted before the metadata upsert; the stale document is then dropped on the next run.
90
+ const fileId = audioFileId(manifest.id, md5);
91
+ await File.findOneAndUpdate(
92
+ { _id: fileId },
93
+ { $set: fileData },
94
+ { upsert: true, returnDocument: 'after', runValidators: true },
95
+ );
96
+
97
+ const doc = await CyberiaAudio.findOneAndUpdate(
98
+ { code: manifest.id },
99
+ { $set: { code: manifest.id, fileId, manifest } },
100
+ { upsert: true, returnDocument: 'after', runValidators: true },
101
+ );
102
+
103
+ // Only once the asset points at the new bytes: the superseded blob is unreferenced from here
104
+ // on, and leaving it behind is what orphans a File document.
105
+ if (existing) {
106
+ await FileCleanup.cleanupReplacedFiles({
107
+ oldDoc: existing,
108
+ newData: { fileId },
109
+ fileFields: FILE_FIELDS,
110
+ File,
111
+ });
112
+ }
113
+ return doc;
114
+ };
115
+
116
+ /**
117
+ * Deletes an asset and the File document holding its bytes.
118
+ *
119
+ * The asset is the only reference to that blob, so dropping one without the other is exactly
120
+ * what `underpost db clean-fs` would later have to sweep up.
121
+ *
122
+ * @param {Array<object>} assets - CyberiaAudio documents about to be removed.
123
+ * @param {{host: string, path: string}} options - Provider context.
124
+ * @returns {Promise<number>} Count of File documents deleted.
125
+ */
126
+ static deleteBackingFiles = async (assets, options) => {
127
+ const File = DataBaseProviderService.getModel('file', options);
128
+ let deleted = 0;
129
+ for (const asset of assets) {
130
+ const ids = await FileCleanup.deleteDocumentFiles({ doc: asset, fileFields: FILE_FIELDS, File });
131
+ deleted += ids.length;
132
+ }
133
+ return deleted;
134
+ };
135
+
136
+ /**
137
+ * Imports every valid `<name>.wav` + `<name>.json` pair found in a records directory.
138
+ * A WAV with no manifest beside it is skipped with a warning: the pair is the unit of import.
139
+ *
140
+ * @param {{recordsPath: string, codes?: Array<string>|null}} params - Source directory and optional code filter.
141
+ * @param {{host: string, path: string}} options - Provider context.
142
+ * @returns {Promise<Array<object>>} The upserted CyberiaAudio documents.
143
+ */
144
+ static importRecords = async ({ recordsPath, codes = null }, options) => {
145
+ if (!(await fs.pathExists(recordsPath))) throw new Error(`Records directory not found: ${recordsPath}`);
146
+
147
+ const stems = (await fs.readdir(recordsPath))
148
+ .filter((name) => name.toLowerCase().endsWith('.wav'))
149
+ .map((name) => nodePath.basename(name, nodePath.extname(name)))
150
+ .filter((stem) => !codes || codes.includes(stem))
151
+ .sort();
152
+
153
+ if (codes) {
154
+ for (const code of codes) if (!stems.includes(code)) throw new Error(`No ${code}.wav found in ${recordsPath}`);
155
+ }
156
+
157
+ const imported = [];
158
+ for (const stem of stems) {
159
+ const wavPath = nodePath.join(recordsPath, `${stem}.wav`);
160
+ const manifestPath = nodePath.join(recordsPath, `${stem}.json`);
161
+ if (!(await fs.pathExists(manifestPath))) {
162
+ logger.warn(`Skipping ${stem}.wav: no ${stem}.json manifest beside it`);
163
+ continue;
164
+ }
165
+ const doc = await CyberiaAudioService.importRecord({ wavPath, manifestPath }, options);
166
+ logger.info(`Imported ${doc.code}`, { _id: doc._id.toString(), fileId: doc.fileId.toString() });
167
+ imported.push(doc);
168
+ }
169
+ return imported;
170
+ };
171
+
172
+ static post = async (req, res, options) => {
173
+ /** @type {import('./cyberia-audio.model.js').CyberiaAudioModel} */
174
+ const CyberiaAudio = DataBaseProviderService.getModel('CyberiaAudio', options);
175
+ return await new CyberiaAudio(req.body).save();
176
+ };
177
+ static get = async (req, res, options) => {
178
+ /** @type {import('./cyberia-audio.model.js').CyberiaAudioModel} */
179
+ const CyberiaAudio = DataBaseProviderService.getModel('CyberiaAudio', options);
180
+ if (req.params.id) return await CyberiaAudio.findById(req.params.id);
181
+
182
+ // Parse query parameters using DataQuery helper
183
+ const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
184
+
185
+ const [data, total] = await Promise.all([
186
+ CyberiaAudio.find(query).sort(sort).limit(limit).skip(skip),
187
+ CyberiaAudio.countDocuments(query),
188
+ ]);
189
+
190
+ const totalPages = Math.ceil(total / limit);
191
+ return { data, total, page, totalPages };
192
+ };
193
+ static put = async (req, res, options) => {
194
+ /** @type {import('./cyberia-audio.model.js').CyberiaAudioModel} */
195
+ const CyberiaAudio = DataBaseProviderService.getModel('CyberiaAudio', options);
196
+ const File = DataBaseProviderService.getModel('file', options);
197
+ const existing = await CyberiaAudio.findById(req.params.id);
198
+ const updated = await CyberiaAudio.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' });
199
+ // Repointing an asset at other bytes leaves the previous blob referenced by nothing.
200
+ if (existing) {
201
+ await FileCleanup.cleanupReplacedFiles({
202
+ oldDoc: existing,
203
+ newData: req.body,
204
+ fileFields: FILE_FIELDS,
205
+ File,
206
+ });
207
+ }
208
+ return updated;
209
+ };
210
+ static delete = async (req, res, options) => {
211
+ /** @type {import('./cyberia-audio.model.js').CyberiaAudioModel} */
212
+ const CyberiaAudio = DataBaseProviderService.getModel('CyberiaAudio', options);
213
+ const assets = req.params.id ? await CyberiaAudio.find({ _id: req.params.id }) : await CyberiaAudio.find();
214
+ await CyberiaAudioService.deleteBackingFiles(assets, options);
215
+ if (req.params.id) return await CyberiaAudio.findByIdAndDelete(req.params.id);
216
+ return await CyberiaAudio.deleteMany();
217
+ };
218
+ }
219
+
220
+ export { CyberiaAudioService, audioFileId, readManifest };
@@ -9,9 +9,7 @@ class CyberiaClientHintsController {
9
9
  host: options.host || 'default',
10
10
  path: options.path || '/',
11
11
  });
12
- // Surface the resolution source as a non-authoritative header so
13
- // operators can see whether the runtime fetched from the new
14
- // collection, the compatibility read on instance-conf, the cache, or defaults.
12
+ // Diagnostic header: which source answered this request.
15
13
  res.setHeader('X-Cyberia-Hints-Source', source);
16
14
  return sendSuccess(res, data);
17
15
  },
@@ -1,15 +1,9 @@
1
1
  /**
2
- * Mongoose model for `CyberiaClientHints`.
2
+ * Mongoose model for `CyberiaClientHints`, the per-instance presentation
3
+ * overrides. Editor flows write it; the game client reads it over REST. The
4
+ * simulation never touches it.
3
5
  *
4
- * Dedicated collection for per-instance presentation overrides.
5
- *
6
- * Ownership:
7
- * - Read by: cyberia-client (via /api/cyberia-client-hints/:code), CMS UIs.
8
- * - Written by: CMS / editor flows. The Cyberia simulation server
9
- * never reads or writes this collection.
10
- * - Cacheable: yes — the service layer keeps an in-memory TTL cache.
11
- *
12
- * Schema scope: purely visual fields. Anything that influences simulation
6
+ * Schema scope: visual fields only. A field that changes the simulation
13
7
  * belongs in `CyberiaInstanceConf`.
14
8
  *
15
9
  * @module src/api/cyberia-client-hints/cyberia-client-hints.model.js
@@ -58,13 +52,10 @@ const EntityColorKeySchema = new Schema(
58
52
 
59
53
  const CyberiaClientHintsSchema = new Schema(
60
54
  {
61
- // Instance code this hint set is scoped to. Matches CyberiaInstance.code
62
- // and CyberiaInstanceConf.instanceCode. Indexed and unique to make the
63
- // service-layer cache a 1-to-1 keyed lookup.
55
+ // Instance code, matching CyberiaInstance.code. Unique: one hint set per code.
64
56
  code: { type: String, required: true, unique: true, index: true },
65
57
 
66
- // Optional palette overrides. Keys not present here fall back to the
67
- // client's compile-time defaults.
58
+ // Palette overrides. An absent key keeps the client default.
68
59
  palette: { type: [PaletteEntrySchema], default: [] },
69
60
 
70
61
  // Optional per-entity-type color-key overrides.
@@ -73,8 +64,8 @@ const CyberiaClientHintsSchema = new Schema(
73
64
  // Optional status-icon visual overrides (id → iconId + borderColor).
74
65
  statusIcons: { type: [StatusIconHintSchema], default: [] },
75
66
 
76
- // Camera, viewport, and cell-sizing tunings null/undefined means
77
- // "use the SharedDefaultsCyberia.RENDER_DEFAULTS value".
67
+ // Camera, viewport and cell-size tunings. null keeps the
68
+ // SharedDefaultsCyberia.RENDER_DEFAULTS value.
78
69
  cellSize: { type: Number, default: null },
79
70
  defaultObjWidth: { type: Number, default: null },
80
71
  defaultObjHeight: { type: Number, default: null },
@@ -85,8 +76,8 @@ const CyberiaClientHintsSchema = new Schema(
85
76
  interpolationMs: { type: Number, default: null },
86
77
  devUi: { type: Boolean, default: null },
87
78
 
88
- // Main UI font: TTF file name under engine assets/fonts/ (null = built-in
89
- // raylib font) and a uniform text-size multiplier.
79
+ // Main UI font: TTF file name under engine assets/fonts/ (null = client
80
+ // built-in font) and a uniform text-size multiplier.
90
81
  fontFamily: { type: String, default: null },
91
82
  fontFactorSize: { type: Number, default: null },
92
83
  },
@@ -1,40 +1,22 @@
1
1
  /**
2
2
  * @module src/api/cyberia-client-hints
3
3
  *
4
- * Client presentation hints — read-only REST endpoint.
4
+ * Client presentation hints — read-only REST endpoint, off the simulation path.
5
+ * It serves optional per-instance overrides of render policy: palette, camera
6
+ * defaults, status-icon visuals, interpolation window, dev-overlay flag.
5
7
  *
6
- * Purpose
7
- * -------
8
- * The cyberia-server (Go authoritative simulation) and the WS init payload
9
- * carry *only* simulation contracts. Client-render policy (palette, camera
10
- * defaults, status-icon visuals, interpolation window, dev-overlay flag)
11
- * lives off the simulation path entirely.
12
- *
13
- * This endpoint exposes optional per-instance overrides of those values.
14
- * The client is required to function with no calls to this endpoint at
15
- * all (it ships built-in defaults that match the canonical engine
16
- * defaults 1-to-1 — see cyberia-client/src/domain/presentation_defaults.h).
17
- *
18
- * Endpoint
19
- * --------
8
+ * Endpoints:
20
9
  * GET /api/cyberia-client-hints/:instanceCode
21
10
  * -> 200 { palette, entityColorKeys, statusIcons, cameraSmoothing,
22
11
  * cameraZoom, defaultWidthScreenFactor,
23
12
  * defaultHeightScreenFactor, interpolationMs, devUi }
24
- * -> 404 if no instance with that code exists in the database — the
25
- * client falls back to its built-in defaults on 404 (this is
26
- * the normal path for stateless servers / fresh deployments).
27
- *
13
+ * -> 404 when no instance carries that code. The client then uses its
14
+ * own built-in defaults, the normal path for a fresh deployment.
28
15
  * GET /api/cyberia-client-hints/
29
- * -> 200 canonical defaults same shape as above, no DB read.
16
+ * -> 200 canonical defaults, same shape, no DB read.
30
17
  *
31
- * What it intentionally does NOT do
32
- * ---------------------------------
33
- * - It does not touch gameplay state (no entity, map, economy, skill,
34
- * equipment, or stat fields).
35
- * - It is not consumed by cyberia-server; the Go process never calls
36
- * this endpoint.
37
- * - It is not authenticated. Presentation hints are not secret.
18
+ * Out of scope: gameplay state and authentication. Presentation hints hold no
19
+ * simulation field and no secret.
38
20
  */
39
21
 
40
22
  import express from 'express';
@@ -50,15 +32,11 @@ class CyberiaClientHintsRouter {
50
32
  const router = express.Router();
51
33
  router.use(crossOriginMiddleware);
52
34
 
53
- // GET /:code -> resolved hints.
54
- // Resolution order is documented in cyberia-client-hints.service.js:
55
- // 1. In-memory TTL cache.
56
- // 2. CyberiaClientHints collection (preferred).
57
- // 3. Legacy presentation fields on CyberiaInstanceConf (back-compat).
58
- // 4. Canonical client defaults (never cached so a later DB insert wins).
35
+ // GET /:code -> resolved hints. See the resolution order in
36
+ // cyberia-client-hints.service.js.
59
37
  router.get('/:code', async (req, res) => await CyberiaClientHintsController.getByCode(req, res, options));
60
38
 
61
- // GET / -> canonical defaults. No DB read. Documentation/diagnostic.
39
+ // GET / -> canonical defaults. No DB read.
62
40
  router.get('/', async (req, res) => await CyberiaClientHintsController.getDefaults(req, res, options));
63
41
 
64
42
  return router;
@@ -3,22 +3,14 @@
3
3
  *
4
4
  * Read-only service layer for client presentation hints.
5
5
  *
6
- * Resolution order (highest priority first):
7
- * 1. CyberiaClientHints collection the dedicated presentation-overrides
8
- * collection. Always preferred when present.
9
- * 2. Compatibility read on CyberiaInstanceConf with the same `code`
10
- * covers instances seeded before CyberiaClientHints existed.
11
- * 3. Canonical compile-time defaults from
12
- * SharedDefaultsCyberia.js. The C client bakes the
13
- * same defaults at compile time, so this branch returns exactly the
14
- * values the client already has.
6
+ * Resolution order, highest priority first:
7
+ * 1. CyberiaClientHints collection, the dedicated overrides collection.
8
+ * 2. Presentation fields on CyberiaInstanceConf with the same `code`.
9
+ * 3. Canonical defaults from SharedDefaultsCyberia.js, the same values the
10
+ * client holds built in.
15
11
  *
16
- * Caching: in-memory TTL cache keyed by instance code. Read-only on the
17
- * hot path; cache writes happen on cache miss + DB hit. CMS writes that
18
- * mutate the underlying collection should call `clientHintsInvalidate(code)`.
19
- *
20
- * Output is a plain JSON-friendly object whose shape matches the C
21
- * client's compile-time layout.
12
+ * An in-memory TTL cache keyed by instance code serves the hot path. A write
13
+ * to either collection must call `clientHintsInvalidate(code)`.
22
14
  */
23
15
 
24
16
  import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
@@ -31,8 +23,7 @@ import {
31
23
 
32
24
  const logger = loggerFactory(import.meta);
33
25
 
34
- // TTL chosen long enough that bursty client fetches are absorbed, short
35
- // enough that an editor change shows up within ~30s without manual flush.
26
+ // TTL absorbs bursty client fetches and still shows an editor change in ~30s.
36
27
  const CACHE_TTL_MS = 30_000;
37
28
 
38
29
  // One per instance code. value: { data, expiresAt }
@@ -56,9 +47,7 @@ function cacheSet(code, data) {
56
47
  cache.set(code, { data, expiresAt: now() + CACHE_TTL_MS });
57
48
  }
58
49
 
59
- /** Invalidate a cache entry. CMS write paths should call this after
60
- * mutating either CyberiaClientHints or CyberiaInstanceConf for
61
- * the given code. */
50
+ /** Invalidate one cache entry, or the whole cache when `code` is empty. */
62
51
  export function clientHintsInvalidate(code) {
63
52
  if (code) {
64
53
  cache.delete(code);
@@ -107,7 +96,7 @@ export async function resolveClientHints(code, options = {}) {
107
96
  return { data: CYBERIA_CLIENT_HINTS_DEFAULTS, source: 'defaults' };
108
97
  }
109
98
 
110
- // 1. Preferred CyberiaClientHints collection (src/api/cyberia-client-hints/cyberia-client-hints.model.js).
99
+ // 1. Preferred source: the CyberiaClientHints collection.
111
100
  if (HintsModel && code) {
112
101
  const hint = await HintsModel.findOne({ code }).lean().catch(() => null);
113
102
  if (hint) {
@@ -117,9 +106,7 @@ export async function resolveClientHints(code, options = {}) {
117
106
  }
118
107
  }
119
108
 
120
- // 2. Compatibility read for instances seeded before CyberiaClientHints
121
- // existed, look up the same code in CyberiaInstanceConf and read its
122
- // presentation-shaped fields directly.
109
+ // 2. Instances that keep their presentation fields on CyberiaInstanceConf.
123
110
  if (ConfModel && code) {
124
111
  const fromConf =
125
112
  (await ConfModel.findOne({ code }).lean().catch(() => null)) ||
@@ -131,22 +118,18 @@ export async function resolveClientHints(code, options = {}) {
131
118
  }
132
119
  }
133
120
 
134
- // 3. Any available CyberiaClientHints document — used when the requested
135
- // code has no record yet but another instance (e.g. the one the Go
136
- // server is currently running) does. This avoids pure built-in
137
- // defaults in fresh environments where only a different code is seeded.
121
+ // 3. Any CyberiaClientHints document, when the requested code has none but
122
+ // another instance does. Beats falling straight through to defaults.
138
123
  if (HintsModel) {
139
124
  const anyHint = await HintsModel.findOne({}).lean().catch(() => null);
140
125
  if (anyHint) {
141
126
  const merged = buildClientHints(anyHint);
142
- // Cache under the requested code so subsequent requests are fast,
143
- // but with a shorter TTL (5 s) so a proper seed wins quickly.
127
+ // Short TTL under the requested code so a later seed wins quickly.
144
128
  if (code) cache.set(code, { data: merged, expiresAt: now() + 5_000 });
145
129
  return { data: merged, source: 'presentation-hints-fallback' };
146
130
  }
147
131
  }
148
132
 
149
- // 4. Canonical defaults. Not cached we do not poison the cache with
150
- // a default that could mask a later DB insert.
133
+ // 4. Canonical defaults. Never cached, so a later DB insert wins.
151
134
  return { data: CYBERIA_CLIENT_HINTS_DEFAULTS, source: 'defaults' };
152
135
  }
@@ -8,7 +8,7 @@ import { Schema, model, Types } from 'mongoose';
8
8
  *
9
9
  * Schema fields:
10
10
  * code – primary grouping key, e.g. "default-lain" or "wason-intro".
11
- * The C client fetches all lines for a given code in one request.
11
+ * The client fetches every line of one code in a single request.
12
12
  * order – zero-based display sequence within the code group.
13
13
  * speaker – display name shown above the dialogue line.
14
14
  * text – the dialogue line itself.
@@ -9,7 +9,7 @@ class CyberiaDialogueRouter {
9
9
  */
10
10
  static router(options) {
11
11
  const router = express.Router();
12
- // Direct lookup by code — C client fetches dialogue by code (e.g. "default-lain")
12
+ // Direct lookup by code, e.g. "default-lain".
13
13
  router.get(`/code/:code`, async (req, res) => await CyberiaDialogueController.getByCode(req, res, options));
14
14
  return registerCrudRoutes(router, CyberiaDialogueController, options);
15
15
  }
@@ -1,9 +1,11 @@
1
1
  import { Schema, model, Types } from 'mongoose';
2
+ import { ENTITY_LEVEL_MIN, ENTITY_LEVEL_MAX } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
2
3
 
3
4
  // https://mongoosejs.com/docs/2.7.x/docs/schematypes.html
4
5
 
5
6
  const CyberiaEntitySchema = new Schema({
6
7
  entityType: { type: String, default: 'floor' },
8
+ level: { type: Number, min: ENTITY_LEVEL_MIN, max: ENTITY_LEVEL_MAX, validate: Number.isInteger },
7
9
  initCellX: { type: Number, default: 0 },
8
10
  initCellY: { type: Number, default: 0 },
9
11
  dimX: { type: Number, default: 1 },
@@ -29,7 +29,7 @@ class CyberiaEntityService {
29
29
  static put = async (req, res, options) => {
30
30
  /** @type {import('./cyberia-entity.model.js').CyberiaEntityModel} */
31
31
  const CyberiaEntity = DataBaseProviderService.getModel("CyberiaEntity", options);
32
- return await CyberiaEntity.findByIdAndUpdate(req.params.id, req.body);
32
+ return await CyberiaEntity.findByIdAndUpdate(req.params.id, req.body, { runValidators: true });
33
33
  };
34
34
  static delete = async (req, res, options) => {
35
35
  /** @type {import('./cyberia-entity.model.js').CyberiaEntityModel} */
@@ -1,6 +1,12 @@
1
- import { buildCrudController } from '../../server/network/middlewares.js';
1
+ import { buildCrudController, serviceHandler } from '../../server/network/middlewares.js';
2
2
  import { CyberiaEntityTypeDefaultService } from './cyberia-entity-type-default.service.js';
3
3
 
4
- const CyberiaEntityTypeDefaultController = buildCrudController(CyberiaEntityTypeDefaultService);
4
+ const CyberiaEntityTypeDefaultController = buildCrudController(CyberiaEntityTypeDefaultService, {
5
+ // Which instances run on this default. The link is stored on CyberiaInstanceConf.entityDefaults,
6
+ // so reading it is an ordinary conf read; only the write needs a route of its own.
7
+ setInstances: serviceHandler(CyberiaEntityTypeDefaultService.setInstances),
8
+ // Point one instance's conf at every default its own map content needs.
9
+ sync: serviceHandler(CyberiaEntityTypeDefaultService.sync),
10
+ });
5
11
 
6
12
  export { CyberiaEntityTypeDefaultController };
@@ -6,24 +6,27 @@ import { Schema, model } from 'mongoose';
6
6
  // item defaults that ship in cyberia-server-defaults.js (ENTITY_TYPE_DEFAULTS).
7
7
  // A document binds an entity category (entityType) to the item ids the runtime
8
8
  // rotates through by lifecycle state:
9
- // liveItemIds — ObjectLayer item ids while the entity is alive.
10
- // deadItemIds — ids swapped in on death / ghost state.
11
- // dropItemIds — ids granted to the killer on depletion (resources).
12
- // defaultObjectLayers seed inventory rows ({ itemId, active, quantity }).
9
+ // liveItemIds — ObjectLayer item ids while the entity is alive.
10
+ // deadItemIds — ids swapped in on death / ghost state.
11
+ // dropItemIds — ids granted to the killer on depletion (resources).
12
+ // inventoryItemsIds ids the entity carries but no lifecycle state activates.
13
+ // overrideItemsIdsState — per-id overrides of what the lists would otherwise derive.
13
14
  //
14
- // Resolution rule (authoritative): defaults are looked up by the entity's ACTIVE
15
- // itemId (usually the skin) — the system finds the document whose `liveItemIds`
16
- // contains that itemId, then applies the document's deadItemIds / dropItemIds /
17
- // defaultObjectLayers to drive what is shown as the entity's state changes.
15
+ // The three lifecycle lists are discriminators, not separate inventories. The entity holds one
16
+ // inventory the union of all four lists and the runtime activates the slots the context
17
+ // calls for. An id's active flag and quantity follow from the list it belongs to, so neither is
18
+ // stored per row `overrideItemsIdsState` is the one exception, and it only ever adjusts an id
19
+ // the lists already carry. See resolveEntityInventory() in cyberia-server-defaults.js.
18
20
  //
19
- // Resolution (subset matching): a document matches an entity when ALL of its
20
- // `liveItemIds` are present in the entity's active item ids, and the MOST SPECIFIC
21
- // match wins the document requiring the largest item set. This lets the same
22
- // skin map to different defaults by its full active set, e.g.
21
+ // Resolution: a document matches an entity when every one of its `liveItemIds`
22
+ // is in the entity's active item ids. The most specific match wins, the document
23
+ // that requires the largest item set. Its remaining lists then drive the entity
24
+ // through its states. So one skin can map to different defaults by its full
25
+ // active set, e.g.
23
26
  // { bot, liveItemIds:[purple, atlas_pistol_mk2], behavior:hostile }
24
27
  // { bot, liveItemIds:[purple], behavior:passive }
25
28
  // where a purple bot carrying the pistol is hostile and a bare purple bot is
26
- // passive. The Go simulation (game/entity_defaults.go) implements the same rule.
29
+ // passive. The simulation applies the same rule.
27
30
  //
28
31
  // Invariants:
29
32
  // - `entityType` is a label, not a unique key — a category appears in many
@@ -33,11 +36,18 @@ import { Schema, model } from 'mongoose';
33
36
  // at different specificity levels). `liveItemIds` is indexed (non-unique
34
37
  // multikey) only for fast membership lookups.
35
38
 
36
- const ObjectLayerDefaultSchema = new Schema(
39
+ // Overrides what the lists derive for one carried id: `active` forces the spawn state (a skin the
40
+ // equipment rules would otherwise leave inactive), `quantity` sizes a stack (a drop bundle).
41
+ // Omitting `active` keeps the derived value; an id no list carries is ignored.
42
+ const OverrideItemStateSchema = new Schema(
37
43
  {
38
- itemId: { type: String, required: true, trim: true },
39
- active: { type: Boolean, default: false },
40
- quantity: { type: Number, default: 0 },
44
+ itemId: { type: String, trim: true },
45
+ active: { type: Boolean },
46
+ quantity: { type: Number, default: 1, min: 1 },
47
+ // Probability in [0,1] that this id actually scatters when the entity dies. Absent means 1 —
48
+ // the drop always happens, which is what every world did before this existed. Only meaningful
49
+ // for an id the build carries in `dropItemIds`; on any other row it is inert.
50
+ dropChance: { type: Number, min: 0, max: 1 },
41
51
  },
42
52
  { _id: false },
43
53
  );
@@ -48,11 +58,12 @@ const CyberiaEntityTypeDefaultSchema = new Schema(
48
58
  liveItemIds: { type: [{ type: String, trim: true }], index: true },
49
59
  deadItemIds: [{ type: String, trim: true }],
50
60
  dropItemIds: [{ type: String, trim: true }],
51
- defaultObjectLayers: [ObjectLayerDefaultSchema],
61
+ inventoryItemsIds: [{ type: String, trim: true }],
62
+ overrideItemsIdsState: [OverrideItemStateSchema],
52
63
  // Canonical entity behavior bound to entities matched by liveItemIds (see
53
64
  // SharedDefaultsCyberia.ENTITY_BEHAVIORS). Empty = let the runtime derive it
54
- // (armed → hostile, else passive). The Go simulation resolves this with the
55
- // same liveItemIds matching used for the live/dead/drop sets.
65
+ // (armed → hostile, else passive). The simulation resolves it with the same
66
+ // liveItemIds match it uses for the live/dead/drop sets.
56
67
  behavior: { type: String, trim: true },
57
68
  },
58
69
  {
@@ -1,5 +1,6 @@
1
1
  import express from 'express';
2
2
  import { registerCrudRoutes } from '../../server/network/middlewares.js';
3
+ import { moderatorGuard } from '../../server/security/auth.js';
3
4
  import { CyberiaEntityTypeDefaultController } from './cyberia-entity-type-default.controller.js';
4
5
 
5
6
  class CyberiaEntityTypeDefaultRouter {
@@ -8,7 +9,21 @@ class CyberiaEntityTypeDefaultRouter {
8
9
  * @returns {import('express').Router}
9
10
  */
10
11
  static router(options) {
11
- return registerCrudRoutes(express.Router(), CyberiaEntityTypeDefaultController, options);
12
+ const router = express.Router();
13
+ // Custom actions first: the generic /:id routes below capture everything.
14
+ router.post(
15
+ `/sync-instance/:instanceCode`,
16
+ options.authMiddleware,
17
+ moderatorGuard,
18
+ async (req, res) => await CyberiaEntityTypeDefaultController.sync(req, res, options),
19
+ );
20
+ router.post(
21
+ `/:id/instances`,
22
+ options.authMiddleware,
23
+ moderatorGuard,
24
+ async (req, res) => await CyberiaEntityTypeDefaultController.setInstances(req, res, options),
25
+ );
26
+ return registerCrudRoutes(router, CyberiaEntityTypeDefaultController, options);
12
27
  }
13
28
  }
14
29