@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,56 +1,19 @@
1
+ import { STAT_TYPES, STAT_MODIFIER_MAX } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
1
2
  import { Schema, model } from 'mongoose';
2
- import { CYBERIA_INSTANCE_CONF_DEFAULTS as D } from '../cyberia-server-defaults/cyberia-server-defaults.js';
3
+ import { CYBERIA_INSTANCE_CONF_DEFAULTS as D, resolveProgressionRules, PROGRESSION_RULE_LIMITS } from '../cyberia-server-defaults/cyberia-server-defaults.js';
4
+
5
+ const BaseStatsSchema = new Schema(Object.fromEntries(STAT_TYPES.map((key) => [key, {
6
+ type: Number, min: 0, max: STAT_MODIFIER_MAX, validate: Number.isInteger,
7
+ }])), { _id: false, strict: 'throw' });
8
+ const ProgressionRulesSchema = new Schema({
9
+ ...Object.fromEntries(Object.entries(PROGRESSION_RULE_LIMITS).map(([key, [min, max]]) => [key, {
10
+ type: Number, min, max, validate: Number.isInteger,
11
+ }])),
12
+ baseStats: BaseStatsSchema,
13
+ perLevelStats: BaseStatsSchema,
14
+ }, { _id: false, strict: 'throw' });
15
+ ProgressionRulesSchema.pre('validate', function () { resolveProgressionRules(this.toObject()); });
3
16
 
4
- // ObjectLayer inventory slot: itemId + whether it starts active + initial quantity.
5
- // Used by EntityDefaultSchema.defaultObjectLayers.
6
- const ObjectLayerSlotSchema = new Schema(
7
- {
8
- itemId: { type: String, required: true },
9
- active: { type: Boolean, default: false },
10
- quantity: { type: Number, default: 1 },
11
- },
12
- { _id: false },
13
- );
14
-
15
- // Per-entity-type simulation defaults. ONLY authoritative item-id wiring
16
- // lives here — presentation (palette, colour keys) is the client's
17
- // responsibility and travels through the CyberiaClientHints REST contract.
18
- const EntityDefaultSchema = new Schema(
19
- {
20
- // Entity category string (matches entity_type_str / bot Behavior in game engine)
21
- entityType: { type: String, required: true },
22
- // Default ObjectLayer item IDs when the entity is alive and carries no assigned items.
23
- liveItemIds: { type: [String], default: [] },
24
- // Default ObjectLayer item IDs for the dead / ghost / respawning state.
25
- deadItemIds: { type: [String], default: [] },
26
- // Resource-only inventory items granted on extraction/depletion.
27
- // These are not auto-activated on the entity itself.
28
- dropItemIds: { type: [String], default: [] },
29
- // Full default ObjectLayer inventory for this entity type.
30
- // Each entry specifies itemId, whether it starts active, and its initial quantity.
31
- // The coin slot must always have active:false — coins are non-activable.
32
- // When non-empty this supersedes liveItemIds for inventory initialization.
33
- defaultObjectLayers: { type: [ObjectLayerSlotSchema], default: [] },
34
- // Canonical entity behavior (SharedDefaultsCyberia.ENTITY_BEHAVIORS). Empty
35
- // lets the runtime derive it (armed → hostile, else passive).
36
- behavior: { type: String, default: '' },
37
- },
38
- { _id: false },
39
- );
40
-
41
- const SkillConfigEntrySchema = new Schema(
42
- {
43
- triggerItemId: { type: String, required: true },
44
- logicEventIds: { type: [String], default: [] },
45
- },
46
- { _id: false },
47
- );
48
-
49
- // ── StatusIconEntrySchema ────────────────────────────────────────────────────
50
- // Numeric Entity Status Indicator (ESI) IDs. The Go server stamps one of
51
- // these u8 IDs on every entity in the AOI binary wire format. Visual
52
- // resolution (icon stem, border colour, bounce) is the C client's job and
53
- // arrives through the /api/cyberia-client-hints REST contract — NOT here.
54
17
  const StatusIconEntrySchema = new Schema(
55
18
  {
56
19
  id: { type: Number, required: true },
@@ -61,10 +24,9 @@ const StatusIconEntrySchema = new Schema(
61
24
  );
62
25
 
63
26
  // ── EconomyRulesSchema ───────────────────────────────────────────────────────
64
- // Mirrors the EconomyRules proto message and the economyRules sub-document in
65
- // cyberia-server-defaults.js. All fields default from those canonical
66
- // values so a freshly created document is immediately playable.
67
- // See OFF_CHAIN_ECONOMY.md for the full Fountain & Sink architecture.
27
+ // The EconomyRules message shape. Every field defaults from
28
+ // cyberia-server-defaults.js, so a new document is playable at once.
29
+ // See OFF_CHAIN_ECONOMY.md for the Fountain & Sink architecture.
68
30
  const EconomyRulesSchema = new Schema(
69
31
  {
70
32
  // ── Fountains ───────────────────────────────────────────────────────────
@@ -101,12 +63,12 @@ const EquipmentRulesSchema = new Schema(
101
63
 
102
64
  const SkillRulesSchema = new Schema(
103
65
  {
104
- projectileSpawnChance: { type: Number, default: D.skillRules.projectileSpawnChance },
66
+ projectileSpawnChance: { type: Number, default: D.skillRules.projectileSpawnChance, min: 0, max: 1 },
105
67
  projectileLifetimeMs: { type: Number, default: D.skillRules.projectileLifetimeMs },
106
68
  projectileWidth: { type: Number, default: D.skillRules.projectileWidth },
107
69
  projectileHeight: { type: Number, default: D.skillRules.projectileHeight },
108
70
  projectileSpeedMultiplier: { type: Number, default: D.skillRules.projectileSpeedMultiplier },
109
- doppelgangerSpawnChance: { type: Number, default: D.skillRules.doppelgangerSpawnChance },
71
+ doppelgangerSpawnChance: { type: Number, default: D.skillRules.doppelgangerSpawnChance, min: 0, max: 1 },
110
72
  doppelgangerLifetimeMs: { type: Number, default: D.skillRules.doppelgangerLifetimeMs },
111
73
  doppelgangerSpawnRadius: { type: Number, default: D.skillRules.doppelgangerSpawnRadius },
112
74
  doppelgangerInitialLifeFraction: { type: Number, default: D.skillRules.doppelgangerInitialLifeFraction },
@@ -115,12 +77,11 @@ const SkillRulesSchema = new Schema(
115
77
  );
116
78
 
117
79
  /**
118
- * Game server configuration for a Cyberia instance.
119
- * Separated from CyberiaInstance so the GUI never overwrites live server parameters
120
- * when editing instance identity / map-graph fields.
80
+ * Simulation configuration for a Cyberia instance. It is separate from
81
+ * CyberiaInstance so an edit to instance identity or map graph cannot
82
+ * overwrite live simulation parameters.
121
83
  *
122
- * Linked from CyberiaInstance.conf (ObjectId ref).
123
- * Looked up by instanceCode for CLI / gRPC use.
84
+ * Linked from CyberiaInstance.conf (ObjectId ref), looked up by instanceCode.
124
85
  */
125
86
  const CyberiaInstanceConfSchema = new Schema(
126
87
  {
@@ -151,7 +112,7 @@ const CyberiaInstanceConfSchema = new Schema(
151
112
  playerBaseSpeed: { type: Number, default: D.playerBaseSpeed },
152
113
  playerBaseLifeRegenMin: { type: Number, default: D.playerBaseLifeRegenMin },
153
114
  playerBaseLifeRegenMax: { type: Number, default: D.playerBaseLifeRegenMax },
154
- sumStatsLimit: { type: Number, default: D.sumStatsLimit },
115
+ progressionRules: { type: ProgressionRulesSchema, default: D.progressionRules },
155
116
  maxActiveLayers: { type: Number, default: D.maxActiveLayers },
156
117
  initialLifeFraction: { type: Number, default: D.initialLifeFraction },
157
118
 
@@ -163,15 +124,26 @@ const CyberiaInstanceConfSchema = new Schema(
163
124
  // Fountain & Sink parameters. See EconomyRulesSchema and OFF_CHAIN_ECONOMY.md.
164
125
  economyRules: { type: EconomyRulesSchema },
165
126
 
166
- // ── Regen ────────────────────────────────────────────────────────
167
- lifeRegenChance: { type: Number, default: D.lifeRegenChance },
168
- maxChance: { type: Number, default: D.maxChance },
169
-
170
- // ── Entity type rendering defaults ───────────────────────────────
171
- // Replaces flat fields: userDefaultItemId, botDefaultItemId, ghostItemId,
172
- // coinItemId, defaultFloorItemId, weaponDefaultItemId.
173
- // Each entry: { entityType, liveItemIds, deadItemIds, dropItemIds, colorKey }.
174
- entityDefaults: { type: [EntityDefaultSchema], default: D.entityDefaults },
127
+ // ── Chances, as fractions of 1 ───────────────────────────────────
128
+ lifeRegenChance: { type: Number, default: D.lifeRegenChance, min: 0, max: 1 },
129
+ maxChance: { type: Number, default: D.maxChance, min: 0, max: 1 },
130
+
131
+ // ── Entity type defaults ─────────────────────────────────────────
132
+ // References into the CyberiaEntityTypeDefault collection, which owns the
133
+ // per-entity-type item wiring (live/dead/drop sets, seed inventory, behavior).
134
+ //
135
+ // A reference, never a copy. Embedding the documents here meant the same
136
+ // wiring existed twice — once in the collection, once inside every instance
137
+ // conf — and the two were reconciled by matching item ids, so two instances
138
+ // sharing a skin resolved into each other's defaults on import and export.
139
+ // An id names exactly one document, which is what makes that impossible.
140
+ //
141
+ // An empty list is not "no defaults": it means this instance adds nothing to
142
+ // the canonical ENTITY_TYPE_DEFAULTS, which every world resolves against.
143
+ entityDefaults: {
144
+ type: [{ type: Schema.Types.ObjectId, ref: 'CyberiaEntityTypeDefault' }],
145
+ default: D.entityDefaults,
146
+ },
175
147
 
176
148
  // ── Entity Status Indicators ────────────────────────────────────
177
149
  // Overhead icon mapping + per-status border colour.
@@ -179,9 +151,11 @@ const CyberiaInstanceConfSchema = new Schema(
179
151
  statusIcons: { type: [StatusIconEntrySchema], default: D.statusIcons },
180
152
 
181
153
  // ── Skill system ─────────────────────────────────────────────────
182
- // Each entry maps a trigger item to an ordered list of logic handler keys.
183
- // Spawning entities (e.g. projectiles) is handled inside the logic handler itself.
184
- skillConfig: { type: [SkillConfigEntrySchema], default: [] },
154
+ // Which skills an instance runs is not stored: the CyberiaSkill collection owns the
155
+ // definitions, and an instance runs the ones whose trigger item its own content names.
156
+ // See collectInstanceItemIds / selectInstanceSkills (cyberia-instance-items.js). Storing
157
+ // the list here meant maintaining a second answer to that question, and the two disagreed —
158
+ // a trigger a quest asked for exported but never reached the simulation.
185
159
 
186
160
  // Numeric tuning parameters for each skill archetype.
187
161
  skillRules: { type: SkillRulesSchema },
@@ -1,11 +1,83 @@
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 {
5
+ CYBERIA_INSTANCE_CONF_DEFAULTS,
6
+ fillInstanceConfDefaults,
7
+ resolveProgressionRules,
8
+ } from '../cyberia-server-defaults/cyberia-server-defaults.js';
4
9
 
5
10
  const logger = loggerFactory(import.meta);
6
11
 
12
+ const readPath = (object, path) => path.split('.').reduce((node, key) => (node == null ? undefined : node[key]), object);
13
+
14
+ const writePath = (object, path, value) => {
15
+ const keys = path.split('.');
16
+ let node = object;
17
+ for (const key of keys.slice(0, -1)) {
18
+ if (node[key] == null || typeof node[key] !== 'object') node[key] = {};
19
+ node = node[key];
20
+ }
21
+ node[keys.at(-1)] = value;
22
+ };
23
+
24
+ /* The canonical default for a path, or for the nearest ancestor that has one: a value nested
25
+ * inside an array element has no default of its own, so the whole array falls back. */
26
+ const defaultFor = (path) => {
27
+ const keys = path.split('.');
28
+ for (let depth = keys.length; depth > 0; depth--) {
29
+ const candidate = keys.slice(0, depth).join('.');
30
+ const value = readPath(CYBERIA_INSTANCE_CONF_DEFAULTS, candidate);
31
+ if (value !== undefined) return { path: candidate, value: structuredClone(value) };
32
+ }
33
+ return null;
34
+ };
35
+
36
+ const validationPaths = async (Model, conf) => {
37
+ try {
38
+ await new Model(conf).validate();
39
+ return [];
40
+ } catch (error) {
41
+ return Object.keys(error?.errors ?? {});
42
+ }
43
+ };
44
+
7
45
  class CyberiaInstanceConfService {
46
+ /**
47
+ * Makes a conf whole and valid: fills every missing field, then resets every field the schema
48
+ * rejects to its canonical default.
49
+ *
50
+ * A backup written before a bound tightened, or edited by hand, must still round-trip: the
51
+ * world it describes is worth more than one stale number in it, and resetting that number is
52
+ * what an operator would do by hand. Each reset is logged by path so the decision is visible.
53
+ * A default the schema itself rejects is a code fault, not bad data, and still throws.
54
+ *
55
+ * @param {object} conf - Conf as read from a backup or from the database.
56
+ * @param {import('mongoose').Model} CyberiaInstanceConf - The model whose schema decides.
57
+ * @returns {Promise<{conf: object, resets: Array<{path: string, from: unknown, to: unknown}>}>}
58
+ */
59
+ static async coerceToSchema(conf, CyberiaInstanceConf) {
60
+ const out = fillInstanceConfDefaults(conf);
61
+ const resets = [];
62
+ // One pass resets the rejected paths; the second proves the defaults themselves pass.
63
+ for (let pass = 0; pass < 2; pass++) {
64
+ const failing = await validationPaths(CyberiaInstanceConf, out);
65
+ if (failing.length === 0) return { conf: out, resets };
66
+ if (pass === 1) throw new Error(`CyberiaInstanceConf defaults fail validation at: ${failing.join(', ')}`);
67
+ for (const path of failing) {
68
+ const fallback = defaultFor(path);
69
+ if (!fallback) throw new Error(`CyberiaInstanceConf has no default to reset "${path}" to`);
70
+ const from = readPath(out, fallback.path);
71
+ writePath(out, fallback.path, fallback.value);
72
+ resets.push({ path: fallback.path, from, to: fallback.value });
73
+ logger.warn(`CyberiaInstanceConf "${fallback.path}" reset to default`, { from, to: fallback.value });
74
+ }
75
+ }
76
+ return { conf: out, resets };
77
+ }
78
+
8
79
  static post = async (req, res, options) => {
80
+ if (Object.hasOwn(req.body, 'progressionRules')) req.body.progressionRules = resolveProgressionRules(req.body.progressionRules);
9
81
  /** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
10
82
  const CyberiaInstanceConf =
11
83
  DataBaseProviderService.getModel("CyberiaInstanceConf", options);
@@ -29,10 +101,11 @@ class CyberiaInstanceConfService {
29
101
  return { data, total, page, totalPages };
30
102
  };
31
103
  static put = async (req, res, options) => {
104
+ if (Object.hasOwn(req.body, 'progressionRules')) req.body.progressionRules = resolveProgressionRules(req.body.progressionRules);
32
105
  /** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
33
106
  const CyberiaInstanceConf =
34
107
  DataBaseProviderService.getModel("CyberiaInstanceConf", options);
35
- return await CyberiaInstanceConf.findByIdAndUpdate(req.params.id, req.body);
108
+ return await CyberiaInstanceConf.findByIdAndUpdate(req.params.id, req.body, { runValidators: true });
36
109
  };
37
110
  static delete = async (req, res, options) => {
38
111
  /** @type {import('./cyberia-instance-conf.model.js').CyberiaInstanceConfModel} */
@@ -28,7 +28,9 @@ class CyberiaMapService {
28
28
  return { codes };
29
29
  }
30
30
 
31
- if (req.params.id) return await CyberiaMap.findById(req.params.id).populate(populateCreator);
31
+ // Maps are addressed by code, the key the engine navigates by.
32
+ if (req.params.id)
33
+ return await CyberiaMap.findOne(DataQuery.naturalKeyFilter('code', req.params.id)).populate(populateCreator);
32
34
 
33
35
  // Parse query parameters using DataQuery helper
34
36
  const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
@@ -48,6 +50,8 @@ class CyberiaMapService {
48
50
  if (!map) throw new Error('map not found');
49
51
  if (req.auth.user.role !== 'admin' && String(map.creator) !== String(req.auth.user._id))
50
52
  throw new Error('insufficient permission');
53
+ const candidate = new CyberiaMap({ ...map.toObject(), ...req.body });
54
+ await candidate.validate();
51
55
  const File = DataBaseProviderService.getModel("File", options);
52
56
  if (req.body.thumbnail && map.thumbnail && String(req.body.thumbnail) !== String(map.thumbnail)) {
53
57
  await File.findByIdAndDelete(map.thumbnail);
@@ -55,7 +59,7 @@ class CyberiaMapService {
55
59
  if (req.body.preview && map.preview && String(req.body.preview) !== String(map.preview)) {
56
60
  await File.findByIdAndDelete(map.preview);
57
61
  }
58
- return await CyberiaMap.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after' });
62
+ return await CyberiaMap.findByIdAndUpdate(req.params.id, req.body, { returnDocument: 'after', runValidators: true });
59
63
  };
60
64
  static delete = async (req, res, options) => {
61
65
  /** @type {import('./cyberia-map.model.js').CyberiaMapModel} */
@@ -0,0 +1,31 @@
1
+ import {
2
+ buildCrudController,
3
+ controllerHandler,
4
+ sendSuccess,
5
+ setCrossOriginHeaders,
6
+ } from '../../server/network/middlewares.js';
7
+ import { CyberiaMapAudioConfService } from './cyberia-map-audio-conf.service.js';
8
+
9
+ const context = (options) => ({ host: options.host || 'default', path: options.path || '/' });
10
+
11
+ const CyberiaMapAudioConfController = buildCrudController(CyberiaMapAudioConfService, {
12
+ // Read by map code: the browser client's own path into this API, so it carries the same
13
+ // public-read cross-origin policy the generated CRUD reads do.
14
+ getByMapCode: controllerHandler(
15
+ async (req, res, options) => {
16
+ setCrossOriginHeaders(req, res);
17
+ return sendSuccess(res, await CyberiaMapAudioConfService.getByMapCode(req.params.mapCode, context(options)));
18
+ },
19
+ { errorStatus: 500 },
20
+ ),
21
+ assign: controllerHandler(
22
+ async (req, res, options) =>
23
+ sendSuccess(
24
+ res,
25
+ await CyberiaMapAudioConfService.assign({ ...req.body, mapCode: req.params.mapCode }, context(options)),
26
+ ),
27
+ { errorStatus: 400 },
28
+ ),
29
+ });
30
+
31
+ export { CyberiaMapAudioConfController };
@@ -0,0 +1,56 @@
1
+ import { Schema, model } from 'mongoose';
2
+ import { AUDIO_BUSES } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
3
+
4
+ // Omitted fields inherit client or map defaults.
5
+ const AudioSettingsSchema = new Schema(
6
+ {
7
+ bus: { type: String, enum: AUDIO_BUSES },
8
+ volume: { type: Number, min: 0, max: 1 },
9
+ loop: { type: Boolean },
10
+ crossfadeMs: { type: Number, min: 0, max: 10000 },
11
+ pitch: { type: Number, min: 0.25, max: 4 },
12
+ pan: { type: Number, min: 0, max: 1 },
13
+ priority: { type: Number, min: 0, max: 255, validate: Number.isInteger },
14
+ },
15
+ { _id: false },
16
+ );
17
+
18
+ const EventAudioSchema = new Schema(
19
+ {
20
+ logicEventId: { type: String, required: true, trim: true },
21
+ audioCode: { type: String, required: true, trim: true },
22
+ settings: { type: AudioSettingsSchema, default: () => ({}) },
23
+ },
24
+ { _id: false },
25
+ );
26
+
27
+ const CyberiaMapAudioConfSchema = new Schema(
28
+ {
29
+ // CyberiaMap.code. One audio configuration per map.
30
+ mapCode: { type: String, required: true, trim: true },
31
+
32
+ // CyberiaAudio.code played while no event binding is active.
33
+ defaultMusic: { type: String, default: '', trim: true },
34
+
35
+ // Logic-event bindings, merged by logicEventId.
36
+ events: { type: [EventAudioSchema], default: [] },
37
+
38
+ // Map-wide defaults. A binding's own settings override these.
39
+ settings: { type: AudioSettingsSchema, default: () => ({}) },
40
+ },
41
+ { timestamps: true },
42
+ );
43
+
44
+ CyberiaMapAudioConfSchema.index({ mapCode: 1 }, { unique: true });
45
+
46
+ const CyberiaMapAudioConfModel = model('CyberiaMapAudioConf', CyberiaMapAudioConfSchema);
47
+
48
+ const ProviderSchema = CyberiaMapAudioConfSchema;
49
+
50
+ export {
51
+ AudioSettingsSchema,
52
+ CyberiaMapAudioConfModel,
53
+ CyberiaMapAudioConfSchema,
54
+ EventAudioSchema,
55
+ ProviderSchema,
56
+ };
@@ -0,0 +1,33 @@
1
+ import express from 'express';
2
+ import { registerCrudRoutes } from '../../server/network/middlewares.js';
3
+ import { moderatorGuard } from '../../server/security/auth.js';
4
+ import { CyberiaMapAudioConfController } from './cyberia-map-audio-conf.controller.js';
5
+
6
+ class CyberiaMapAudioConfRouter {
7
+ /**
8
+ * @param {import('../types.js').RouterOptions} options
9
+ * @returns {import('express').Router}
10
+ */
11
+ static router(options) {
12
+ const router = express.Router();
13
+
14
+ // Map-code routes are registered before the CRUD routes: the generic `/:id`
15
+ // handlers capture every single-segment path once they are mounted.
16
+ router.get(
17
+ '/map-code/:mapCode',
18
+ async (req, res) => await CyberiaMapAudioConfController.getByMapCode(req, res, options),
19
+ );
20
+ router.post(
21
+ '/map-code/:mapCode',
22
+ options.authMiddleware,
23
+ moderatorGuard,
24
+ async (req, res) => await CyberiaMapAudioConfController.assign(req, res, options),
25
+ );
26
+
27
+ return registerCrudRoutes(router, CyberiaMapAudioConfController, options);
28
+ }
29
+ }
30
+
31
+ const ApiRouter = (options) => CyberiaMapAudioConfRouter.router(options);
32
+
33
+ export { ApiRouter, CyberiaMapAudioConfRouter };
@@ -0,0 +1,189 @@
1
+ /**
2
+ * @module src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js
3
+ * @namespace CyberiaMapAudioConfServiceServer
4
+ */
5
+
6
+ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
7
+ import { loggerFactory } from '../../server/ops/logger.js';
8
+ import { DataQuery } from '../../server/storage/data-query.js';
9
+
10
+ const logger = loggerFactory(import.meta);
11
+
12
+ // Resolved per call, not per module: reading a configuration must not require the
13
+ // caller to have loaded the cyberia-audio model it never touches.
14
+ const getConfModel = (options) => DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
15
+ const getAudioModel = (options) => DataBaseProviderService.getModel('CyberiaAudio', options);
16
+
17
+ /**
18
+ * Parses a `<logic-event-id>:<audio-code>` binding as accepted by the CLI's `--set-event` flag.
19
+ * The separator is the first colon, so an audio code may contain one.
20
+ *
21
+ * @param {string} value - Raw flag value.
22
+ * @returns {{logicEventId: string, audioCode: string}} Parsed binding.
23
+ * @throws {Error} When either side of the separator is empty.
24
+ */
25
+ const parseEventAudioBinding = (value) => {
26
+ const separator = `${value ?? ''}`.indexOf(':');
27
+ const logicEventId = separator === -1 ? '' : `${value}`.slice(0, separator).trim();
28
+ const audioCode = separator === -1 ? '' : `${value}`.slice(separator + 1).trim();
29
+ if (!logicEventId || !audioCode) throw new Error(`Expected <logic-event-id>:<audio-code>, received "${value}"`);
30
+ return { logicEventId, audioCode };
31
+ };
32
+
33
+ /**
34
+ * Merges incoming bindings into the ones a map already carries, keyed by logic event.
35
+ *
36
+ * Naming an event replaces that binding and leaves the rest in place, so a CLI call configures
37
+ * what it mentions and nothing else.
38
+ *
39
+ * @param {Array<{logicEventId: string}>} current - Bindings already stored.
40
+ * @param {Array<{logicEventId: string}>} incoming - Bindings being assigned.
41
+ * @returns {Array<object>} The merged list, in stored-then-new order.
42
+ */
43
+ const mergeEventBindings = (current = [], incoming = []) => {
44
+ const merged = [...current];
45
+ for (const binding of incoming) {
46
+ const index = merged.findIndex((entry) => entry.logicEventId === binding.logicEventId);
47
+ if (index === -1) merged.push(binding);
48
+ else {
49
+ const settings = binding.settings ?? merged[index].settings;
50
+ merged[index] = { ...binding, ...(settings ? { settings } : {}) };
51
+ }
52
+ }
53
+ return merged;
54
+ };
55
+
56
+ /** Accepts a bare asset code or an object carrying one. */
57
+ const readAudioCode = (input) => {
58
+ if (input === null || input === undefined || input === '') return '';
59
+ const code = typeof input === 'string' ? input : input.audioCode ?? input.code;
60
+ if (!code) throw new Error('Missing audio asset code');
61
+ return `${code}`.trim();
62
+ };
63
+
64
+ class CyberiaMapAudioConfService {
65
+ /**
66
+ * Asserts that an imported cyberia-audio asset carries this code.
67
+ *
68
+ * The code is the whole reference a binding stores, so this is where referential integrity is
69
+ * enforced: an unimported asset is refused rather than written as a dangling name. Nothing is
70
+ * copied out of the asset — the binding needs no more of it than its identity.
71
+ *
72
+ * @param {string} audioCode - CyberiaAudio.code.
73
+ * @param {{host: string, path: string}} options - Provider context.
74
+ * @returns {Promise<string>} The same code, once it is known to resolve.
75
+ * @throws {Error} When no imported cyberia-audio document carries that code.
76
+ */
77
+ static assertAudioCode = async (audioCode, options) => {
78
+ const exists = await getAudioModel(options).exists({ code: audioCode });
79
+ if (!exists) {
80
+ throw new Error(`Unknown audio asset "${audioCode}" — import it first with: cyberia audio ${audioCode} --import`);
81
+ }
82
+ return audioCode;
83
+ };
84
+
85
+ /**
86
+ * Fetches the audio configuration of a single map.
87
+ * @param {string} mapCode - CyberiaMap code.
88
+ * @param {{host: string, path: string}} options - Provider context.
89
+ * @returns {Promise<object|null>} Configuration document, or null when the map has none.
90
+ */
91
+ static getByMapCode = async (mapCode, options) => {
92
+ return await getConfModel(options).findOne({ mapCode }).lean();
93
+ };
94
+
95
+ /**
96
+ * Bulk-assigns map audio rules, upserting the map's configuration document.
97
+ *
98
+ * Two callers with two intents share this: the CLI amends one binding at a time and must leave
99
+ * the others alone, while a seed states the whole configuration and must converge on it —
100
+ * otherwise a binding whose logic event was renamed or dropped lives on forever, and the client
101
+ * keeps asking for an asset nothing declares any more. `replaceEvents` is which of the two
102
+ * this call is.
103
+ *
104
+ * @param {Object} rules - Assignment payload.
105
+ * @param {string} rules.mapCode - Target CyberiaMap code.
106
+ * @param {string|null} [rules.defaultMusic] - Default music asset code, or '' / null to clear it.
107
+ * @param {Array<{logicEventId: string, audioCode: string, settings?: object}>} [rules.events] - Bindings.
108
+ * @param {boolean} [rules.replaceEvents=false] - Make `events` the map's complete binding set.
109
+ * @param {object} [rules.settings] - Map-wide volume/loop/crossfade defaults.
110
+ * @param {{host: string, path: string}} options - Provider context.
111
+ * @returns {Promise<object>} The updated configuration document.
112
+ */
113
+ static assign = async ({ mapCode, defaultMusic, events = [], replaceEvents = false, settings }, options) => {
114
+ if (!mapCode) throw new Error('mapCode is required to assign map audio rules');
115
+ const CyberiaMapAudioConf = getConfModel(options);
116
+
117
+ const bindings = await Promise.all(
118
+ events.map(async ({ logicEventId, settings: entrySettings, ...reference }) => {
119
+ if (!logicEventId) throw new Error(`Missing logicEventId for an audio binding on map "${mapCode}"`);
120
+ return {
121
+ logicEventId: `${logicEventId}`.trim(),
122
+ audioCode: await CyberiaMapAudioConfService.assertAudioCode(readAudioCode(reference), options),
123
+ ...(entrySettings ? { settings: entrySettings } : {}),
124
+ };
125
+ }),
126
+ );
127
+
128
+ const defaultMusicCode = defaultMusic === undefined ? undefined : readAudioCode(defaultMusic);
129
+ if (defaultMusicCode) await CyberiaMapAudioConfService.assertAudioCode(defaultMusicCode, options);
130
+
131
+ const conf = (await CyberiaMapAudioConf.findOne({ mapCode })) || new CyberiaMapAudioConf({ mapCode });
132
+
133
+ if (defaultMusicCode !== undefined) conf.defaultMusic = defaultMusicCode;
134
+ if (settings) conf.settings = { ...(conf.settings?.toObject?.() ?? conf.settings ?? {}), ...settings };
135
+
136
+ const dropped = replaceEvents
137
+ ? (conf.events ?? [])
138
+ .map(({ logicEventId }) => logicEventId)
139
+ .filter((logicEventId) => !bindings.some((binding) => binding.logicEventId === logicEventId))
140
+ : [];
141
+ if (replaceEvents) conf.events = bindings;
142
+ else if (bindings.length > 0) conf.events = mergeEventBindings(conf.events, bindings);
143
+
144
+ await conf.save();
145
+ logger.info(`Assigned map audio rules for "${mapCode}"`, {
146
+ defaultMusic: conf.defaultMusic || null,
147
+ events: conf.events.length,
148
+ ...(dropped.length > 0 ? { dropped } : {}),
149
+ });
150
+ return conf;
151
+ };
152
+
153
+ static post = async (req, res, options) => {
154
+ /** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
155
+ const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
156
+ return await new CyberiaMapAudioConf(req.body).save();
157
+ };
158
+
159
+ static get = async (req, res, options) => {
160
+ /** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
161
+ const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
162
+ if (req.params.id) return await CyberiaMapAudioConf.findById(req.params.id);
163
+
164
+ const { query, sort, skip, limit, page } = DataQuery.parse(req.query);
165
+
166
+ const [data, total] = await Promise.all([
167
+ CyberiaMapAudioConf.find(query).sort(sort).limit(limit).skip(skip),
168
+ CyberiaMapAudioConf.countDocuments(query),
169
+ ]);
170
+
171
+ const totalPages = Math.ceil(total / limit);
172
+ return { data, total, page, totalPages };
173
+ };
174
+
175
+ static put = async (req, res, options) => {
176
+ /** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
177
+ const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
178
+ return await CyberiaMapAudioConf.findByIdAndUpdate(req.params.id, req.body);
179
+ };
180
+
181
+ static delete = async (req, res, options) => {
182
+ /** @type {import('./cyberia-map-audio-conf.model.js').CyberiaMapAudioConfModel} */
183
+ const CyberiaMapAudioConf = DataBaseProviderService.getModel('CyberiaMapAudioConf', options);
184
+ if (req.params.id) return await CyberiaMapAudioConf.findByIdAndDelete(req.params.id);
185
+ else return await CyberiaMapAudioConf.deleteMany();
186
+ };
187
+ }
188
+
189
+ export { CyberiaMapAudioConfService, mergeEventBindings, parseEventAudioBinding };
@@ -9,10 +9,10 @@ class CyberiaQuestRouter {
9
9
  */
10
10
  static router(options) {
11
11
  const router = express.Router();
12
- // Direct lookup by code — C client fetches quest metadata by code.
12
+ // Direct lookup of quest metadata by code.
13
13
  router.get(`/code/:code`, async (req, res) => await CyberiaQuestController.getByCode(req, res, options));
14
- // Quest offers located by binding cell C client resolves the Quest tab from
15
- // here using the interacted entity's cell, decoupled from CyberiaAction.
14
+ // Quest offers by binding cell. The client resolves its Quest tab from the
15
+ // interacted entity's cell, with no CyberiaAction lookup.
16
16
  router.get(
17
17
  `/cell/:mapCode/:cellX/:cellY`,
18
18
  async (req, res) => await CyberiaQuestController.getByCell(req, res, options),
@@ -50,9 +50,9 @@ class CyberiaQuestService {
50
50
  if (!code) throw new Error('code parameter is required');
51
51
  const data = await CyberiaQuest.findOne({ code }).lean();
52
52
  if (data) return data;
53
- // Fallback world delivers quests to the Go server via gRPC from the
54
- // canonical defaults without persisting them. Serve those same defaults so
55
- // the client can resolve metadata even when Mongo has no seeded quest.
53
+ // The fallback world serves quests from the canonical defaults and never
54
+ // persists them. Serve the same defaults, so the client resolves metadata
55
+ // with no seeded quest in Mongo.
56
56
  const fallback = DefaultCyberiaQuests.find((q) => q.code === code);
57
57
  if (fallback) return fallback;
58
58
  throw new Error(`No quest found for code: ${code}`);
@@ -4,7 +4,7 @@ import { Schema, model } from 'mongoose';
4
4
 
5
5
  const CyberiaQuestProgressSchema = new Schema(
6
6
  {
7
- // Matches the Go relay server's player UUID
7
+ // Matches the simulation server's player UUID
8
8
  playerId: { type: String, required: true, trim: true },
9
9
  // References CyberiaQuest.code
10
10
  questCode: { type: String, required: true, trim: true },