@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,35 +1,43 @@
1
1
  /**
2
2
  * Shared Cyberia instance world-load / boot data assembly.
3
3
  *
4
- * Single source of truth for the payloads cyberia-server (Go) needs to boot an
5
- * instance and hot-reload it: full instance world (instance + maps + object
6
- * layers + simulation config + actions/quests), single map/object-layer
7
- * lookups, and the object-layer manifest.
4
+ * Single source of truth for the payloads the simulation server needs to boot
5
+ * and hot-reload an instance: the full world (instance + maps + object layers +
6
+ * simulation config + actions/quests), single map / object-layer lookups, and
7
+ * the object-layer manifest.
8
8
  *
9
- * Consumed by BOTH transports so they stay byte-for-byte equivalent:
10
- * - gRPC `CyberiaDataService` (src/grpc/cyberia/grpc-server.js) primary.
11
- * - REST `/api/cyberia-instance/boot/*` fallback when the engine gRPC
12
- * server is not enabled for the deploy.
9
+ * Both boot transports adapt this module, so they stay equivalent:
10
+ * - gRPC `CyberiaDataService` (src/grpc/cyberia/grpc-server.js), primary.
11
+ * - REST `/api/cyberia-instance/boot/*`, fallback when gRPC is off.
13
12
  *
14
13
  * @module src/projects/cyberia/instance-data.js
15
14
  */
16
15
 
17
16
  import crypto from 'crypto';
18
17
  import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
18
+ import {
19
+ collectInstanceItemIds,
20
+ collectSummonedItemIds,
21
+ selectInstanceSkills,
22
+ } from '../../api/cyberia-instance/cyberia-instance-items.js';
19
23
  import { loggerFactory } from '../../server/ops/logger.js';
20
24
  import {
21
25
  CYBERIA_INSTANCE_CONF_DEFAULTS as FALLBACK_CONFIG_DEFAULTS,
22
26
  DEFAULT_DEAD_ITEM_ID,
27
+ resolveProgressionRules,
23
28
  ENTITY_TYPE_DEFAULTS,
29
+ resolveEntityInventory,
24
30
  DefaultCyberiaActions,
25
31
  DefaultCyberiaQuests,
32
+ DefaultSkillConfig,
26
33
  } from '../../api/cyberia-server-defaults/cyberia-server-defaults.js';
27
34
  import {
28
35
  DEFAULT_INSTANCE_CODE,
36
+ validateStats,
37
+ validateEntityLevel,
29
38
  DefaultCyberiaItems,
30
39
  } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
31
40
  import { generateFallbackWorld } from '../../api/cyberia-instance/cyberia-fallback-world.js';
32
- import { getFallbackDefaultItems } from '../../api/cyberia-instance/cyberia-fallback-default-items.js';
33
41
 
34
42
  const logger = loggerFactory(import.meta);
35
43
 
@@ -69,68 +77,26 @@ function getInstanceModels(context) {
69
77
  return bucket.models;
70
78
  }
71
79
 
72
- function normalizeEntityDefault(entityDefault = {}, canonical = {}) {
73
- const defaultObjectLayers = entityDefault.defaultObjectLayers ?? canonical.defaultObjectLayers ?? [];
74
- return {
80
+ // The wire still carries inventory rows; the model no longer stores them. `defaultObjectLayers`
81
+ // is derived here, once, from the discriminator lists — see resolveEntityInventory().
82
+ function normalizeEntityDefault(entityDefault = {}, canonical = {}, itemTypes) {
83
+ const normalized = {
75
84
  entityType: entityDefault.entityType ?? canonical.entityType ?? '',
76
85
  liveItemIds: [...(entityDefault.liveItemIds ?? canonical.liveItemIds ?? [])],
77
86
  deadItemIds: [...(entityDefault.deadItemIds ?? canonical.deadItemIds ?? [])],
78
87
  dropItemIds: [...(entityDefault.dropItemIds ?? canonical.dropItemIds ?? [])],
79
- defaultObjectLayers: defaultObjectLayers.map((ol) => ({
80
- itemId: ol.itemId || '',
81
- active: !!ol.active,
82
- quantity: ol.quantity || 0,
83
- })),
88
+ inventoryItemsIds: [...(entityDefault.inventoryItemsIds ?? canonical.inventoryItemsIds ?? [])],
89
+ overrideItemsIdsState: [
90
+ ...(entityDefault.overrideItemsIdsState ?? canonical.overrideItemsIdsState ?? []),
91
+ ],
84
92
  behavior: entityDefault.behavior ?? canonical.behavior ?? '',
85
93
  };
94
+ return { ...normalized, defaultObjectLayers: resolveEntityInventory(normalized, { itemTypes }) };
86
95
  }
87
96
 
88
- // Merge instance-level itemIds flagged `defaultPlayerInventory` into the player
89
- // entityDefault's defaultObjectLayers so cyberia-server seeds them into every new
90
- // player's inventory (see handlers.go player spawn). Items are added inactive
91
- // present in the inventory, not auto-equipped — so they never violate the
92
- // one-active-per-type equipment rule.
93
- //
94
- // Also **removes** items that are flagged `defaultPlayerInventory: false` from
95
- // the player's defaultObjectLayers. This ensures the instance's explicit intent
96
- // is honoured even when the conf's entityDefaults (e.g. from a backup/import)
97
- // baked those items into the player layer list.
98
- function applyInstanceDefaultPlayerInventory(config, instanceItemIds = []) {
99
- const entries = instanceItemIds || [];
100
-
101
- // ── 1. Remove items explicitly flagged as NOT default player inventory ─────
102
- const removeIds = new Set(
103
- entries.filter((entry) => entry && entry.defaultPlayerInventory === false && entry.id).map((entry) => entry.id),
104
- );
105
- if (removeIds.size > 0) {
106
- for (const ed of config.entityDefaults || []) {
107
- if (ed.defaultObjectLayers) {
108
- ed.defaultObjectLayers = ed.defaultObjectLayers.filter((ol) => !removeIds.has(ol.itemId));
109
- }
110
- }
111
- }
112
-
113
- // ── 2. Add items flagged as default player inventory ──────────────────────
114
- const addIds = entries.filter((entry) => entry && entry.defaultPlayerInventory && entry.id).map((entry) => entry.id);
115
- if (addIds.length === 0) return config;
116
-
117
- const playerDefault = (config.entityDefaults || []).find((d) => d.entityType === 'player');
118
- if (!playerDefault) return config;
119
-
120
- playerDefault.defaultObjectLayers = playerDefault.defaultObjectLayers || [];
121
- const existing = new Set(playerDefault.defaultObjectLayers.map((ol) => ol.itemId));
122
- for (const id of addIds) {
123
- if (existing.has(id)) continue;
124
- playerDefault.defaultObjectLayers.push({ itemId: id, active: false, quantity: 1 });
125
- existing.add(id);
126
- }
127
- return config;
128
- }
129
-
130
- // Map CyberiaSkill collection docs to the proto skillConfig shape. The skill
131
- // collection is the authoritative own-model source: it carries full skill
132
- // metadata (summonedEntityItemId, name, description) that the instance-conf
133
- // skillConfig schema deliberately does not store.
97
+ // Map CyberiaSkill documents to the proto skillConfig shape. The collection is the authoritative
98
+ // source and carries the full metadata (summonedEntityItemId, name, description); the wire message
99
+ // is built from the subset this instance runs, which nothing persists.
134
100
  function skillDocsToConfig(skillDocs = []) {
135
101
  return skillDocs
136
102
  .filter((d) => d && d.triggerItemId)
@@ -145,19 +111,16 @@ function skillDocsToConfig(skillDocs = []) {
145
111
  }));
146
112
  }
147
113
 
148
- // Load authoritative skill configs from the CyberiaSkill collection. Returns []
149
- // when the model is not loaded (api not mounted) or the collection is empty, so
150
- // callers transparently keep the conf / fallback skillConfig.
151
- async function loadSkillConfigDocs(models) {
114
+ // Every skill definition the deployment knows. Which of them an instance runs is decided by
115
+ // selectInstanceSkills against that instance's own content. Returns [] when the model is not
116
+ // loaded (api not mounted), so callers fall back to the canonical definitions.
117
+ async function loadSkillDocs(models) {
152
118
  if (!models.CyberiaSkill) return [];
153
119
  return await models.CyberiaSkill.find({}).lean();
154
120
  }
155
121
 
156
- // Map CyberiaEntityTypeDefault collection docs to the proto entityDefaults shape.
157
- // This collection is the authoritative own-model source for per-entity-type item
158
- // defaults (live/dead/drop + seed object layers). Resolution is by liveItemIds
159
- // membership, so every variant (e.g. each resource skin) must travel as its own
160
- // entry — never collapsed by entityType.
122
+ // Map CyberiaEntityTypeDefault docs to the wire entityDefaults shape. Lookup is
123
+ // by liveItemIds membership, so each variant travels as its own entry.
161
124
  function entityTypeDefaultDocsToConfig(docs = []) {
162
125
  return docs
163
126
  .filter((d) => d && d.entityType)
@@ -166,75 +129,56 @@ function entityTypeDefaultDocsToConfig(docs = []) {
166
129
  liveItemIds: [...(d.liveItemIds || [])],
167
130
  deadItemIds: [...(d.deadItemIds || [])],
168
131
  dropItemIds: [...(d.dropItemIds || [])],
169
- defaultObjectLayers: (d.defaultObjectLayers || []).map((ol) => ({
170
- itemId: ol.itemId,
171
- active: !!ol.active,
172
- quantity: ol.quantity || 0,
173
- })),
132
+ inventoryItemsIds: [...(d.inventoryItemsIds || [])],
133
+ overrideItemsIdsState: [...(d.overrideItemsIdsState || [])],
174
134
  behavior: d.behavior || '',
175
135
  }));
176
136
  }
177
137
 
178
- // Load authoritative entity-type defaults from the CyberiaEntityTypeDefault
179
- // collection. Returns [] when the model is not mounted or empty, so callers
180
- // transparently keep the conf / canonical entityDefaults.
181
- async function loadEntityTypeDefaultDocs(models) {
138
+ // Load the entity-type defaults THIS instance references. The conf names them by _id, so a
139
+ // document reaches a world only when that world points at it never because another instance
140
+ // happens to share an item id with it, which is what used to drag foreign wiring into a build.
141
+ async function loadEntityTypeDefaultDocs(models, conf) {
182
142
  if (!models.CyberiaEntityTypeDefault) return [];
183
- return await models.CyberiaEntityTypeDefault.find({}).lean();
184
- }
185
-
186
- // Feed every live/dead/drop and default-object-layer item id from the collection
187
- // defaults into the atlas id set so the Go server resolves their sprites even
188
- // when the entity appears in no map (e.g. a new resource skin).
189
- function addEntityDefaultAtlasIds(entityDefaults, atlasItemIds) {
190
- for (const d of entityDefaults || []) {
191
- for (const id of d.liveItemIds || []) if (id) atlasItemIds.add(id);
192
- for (const id of d.deadItemIds || []) if (id) atlasItemIds.add(id);
193
- for (const id of d.dropItemIds || []) if (id) atlasItemIds.add(id);
194
- for (const ol of d.defaultObjectLayers || []) if (ol.itemId) atlasItemIds.add(ol.itemId);
195
- }
196
- }
197
-
198
- // Feed skill trigger items and every concrete summoned-entity id into the atlas
199
- // id set so the Go server resolves their sprites even when the entity appears in
200
- // no map. Runtime placeholders ('$active_skin', …) are excluded.
201
- function addSkillAtlasIds(skillDocs, atlasItemIds) {
202
- for (const d of skillDocs || []) {
203
- if (d.triggerItemId) atlasItemIds.add(d.triggerItemId);
204
- for (const sk of d.skills || []) {
205
- const id = sk.summonedEntityItemId;
206
- if (id && !id.startsWith('$')) atlasItemIds.add(id);
207
- }
208
- }
143
+ const ids = (conf?.entityDefaults || []).map((id) => String(id?._id ?? id)).filter(Boolean);
144
+ if (ids.length === 0) return [];
145
+ const docs = await models.CyberiaEntityTypeDefault.find({ _id: { $in: ids } }).lean();
146
+ const byId = new Map(docs.map((doc) => [String(doc._id), doc]));
147
+ return ids.filter((id) => byId.has(id)).map((id) => byId.get(id));
209
148
  }
210
149
 
211
- function mergeEntityDefaults(entityDefaults = []) {
212
- const merged = entityDefaults.map((entityDefault) => normalizeEntityDefault(entityDefault));
150
+ // `itemTypes` lets the equipment rules settle a contested slot: which skin an entity wears when an
151
+ // override names one. Without it every row keeps the state its list derives, which is the right
152
+ // answer for a world whose object layers have not been read yet.
153
+ function mergeEntityDefaults(entityDefaults = [], itemTypes) {
154
+ const merged = entityDefaults.map((entityDefault) => normalizeEntityDefault(entityDefault, {}, itemTypes));
213
155
  const coveredTypes = new Set(merged.map((entityDefault) => entityDefault.entityType));
214
156
  for (const canonical of ENTITY_TYPE_DEFAULTS) {
215
- if (!coveredTypes.has(canonical.entityType)) merged.push(normalizeEntityDefault(canonical, canonical));
157
+ if (!coveredTypes.has(canonical.entityType)) merged.push(normalizeEntityDefault(canonical, canonical, itemTypes));
216
158
  }
217
159
  return merged;
218
160
  }
219
161
 
162
+ /** itemId → item type, read from the ObjectLayer documents a world already loads for its atlases. */
163
+ function itemTypesOf(objectLayerDocs = []) {
164
+ const types = {};
165
+ for (const doc of objectLayerDocs) {
166
+ const item = doc?.data?.item;
167
+ if (item?.id && item?.type) types[item.id] = item.type;
168
+ }
169
+ return types;
170
+ }
171
+
220
172
  // ── Mongoose doc → wire message converters (proto camelCase shapes) ────────
221
173
 
222
174
  function toObjectLayerMsg(doc) {
223
175
  const d = doc.data || {};
224
176
  const item = d.item || {};
225
- const stats = d.stats || {};
226
177
  const ledger = d.ledger || {};
227
178
  const render = d.render || {};
228
179
  return {
229
180
  mongoId: String(doc._id),
230
- stats: {
231
- effect: stats.effect || 0,
232
- resistance: stats.resistance || 0,
233
- agility: stats.agility || 0,
234
- range: stats.range || 0,
235
- intelligence: stats.intelligence || 0,
236
- utility: stats.utility || 0,
237
- },
181
+ stats: validateStats(d.stats),
238
182
  item: {
239
183
  id: item.id || '',
240
184
  type: item.type || '',
@@ -263,7 +207,7 @@ function parseRgba(str) {
263
207
  if (!str) return { r: 0, g: 0, b: 0, a: 0 };
264
208
  const m = str.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/);
265
209
  if (!m) return { r: 0, g: 0, b: 0, a: 0 };
266
- // CSS alpha is 0-1 (float); proto uses 0-255 (int).
210
+ // CSS alpha is 0-1 float; the wire format uses 0-255 int.
267
211
  const cssAlpha = m[4] !== undefined ? parseFloat(m[4]) : 1;
268
212
  return { r: Number(m[1]), g: Number(m[2]), b: Number(m[3]), a: Math.round(cssAlpha * 255) };
269
213
  }
@@ -272,6 +216,7 @@ function toEntityMsg(ent) {
272
216
  const rgba = parseRgba(ent.color);
273
217
  return {
274
218
  entityType: ent.entityType || 'floor',
219
+ level: ent.level === undefined ? 0 : validateEntityLevel(ent.level),
275
220
  initCellX: ent.initCellX || 0,
276
221
  initCellY: ent.initCellY || 0,
277
222
  dimX: ent.dimX || 1,
@@ -390,24 +335,23 @@ function toQuestMsg(q) {
390
335
  * Any field that is null/undefined in `gc` falls back to FALLBACK_CONFIG_DEFAULTS,
391
336
  * so partial DB documents always produce a fully playable config.
392
337
  */
338
+ // A chance is a fraction of one. Anything else a conf carries is not a chance and resolves to
339
+ // the default, the same way a partial progression curve inherits its missing fields.
340
+ const chanceOf = (value, fallback) => (Number.isFinite(value) && value >= 0 && value <= 1 ? value : fallback);
341
+
393
342
  function toInstanceConfig(gc) {
394
343
  const fb = FALLBACK_CONFIG_DEFAULTS;
395
344
  if (!gc) return buildFallbackConfig();
396
345
 
397
- // STRICT BOUNDARY this function produces the *simulation* config for
398
- // cyberia-server only. Every presentation concern is excluded:
399
- //
400
- // - palette (colors), camera tunings, screen factors, devUi,
401
- // interpolationMs, status-icon visuals, entityDefaults[].colorKey,
402
- // cellSize, defaultObj* all of these reach the client through
403
- // /api/cyberia-client-hints, never through the boot transports.
404
- //
405
- // The Go simulation does not need any of them to advance world state;
406
- // the C/WASM cyberia-client owns its own render policy. See
407
- // src/client/components/cyberia/SharedDefaultsCyberia.js.
408
-
409
- const gcDefaults = gc.entityDefaults && gc.entityDefaults.length > 0 ? gc.entityDefaults : [];
410
- const entityDefaults = mergeEntityDefaults(gcDefaults);
346
+ // STRICT BOUNDARY: this config is simulation only. Presentation values
347
+ // (palette, camera tunings, screen factors, devUi, interpolationMs,
348
+ // status-icon visuals, entityDefaults[].colorKey, cellSize, defaultObj*)
349
+ // reach the client through /api/cyberia-client-hints, never a boot transport.
350
+
351
+ // The conf carries references, not documents: the caller resolves them and overwrites this
352
+ // with the result. Canonical defaults alone are the correct answer for a world that
353
+ // references none, and for the fallback build that has no conf at all.
354
+ const entityDefaults = mergeEntityDefaults();
411
355
 
412
356
  return {
413
357
  tickRate: gc.tickRate ?? fb.tickRate,
@@ -425,12 +369,12 @@ function toInstanceConfig(gc) {
425
369
  playerBaseSpeed: gc.playerBaseSpeed ?? fb.playerBaseSpeed,
426
370
  playerBaseLifeRegenMin: gc.playerBaseLifeRegenMin ?? fb.playerBaseLifeRegenMin,
427
371
  playerBaseLifeRegenMax: gc.playerBaseLifeRegenMax ?? fb.playerBaseLifeRegenMax,
428
- sumStatsLimit: gc.sumStatsLimit ?? fb.sumStatsLimit,
372
+ progressionRules: resolveProgressionRules(gc.progressionRules),
429
373
  maxActiveLayers: gc.maxActiveLayers ?? fb.maxActiveLayers,
430
374
  initialLifeFraction: gc.initialLifeFraction ?? fb.initialLifeFraction,
431
375
  respawnDurationMs: gc.respawnDurationMs ?? fb.respawnDurationMs,
432
376
  collisionLifeLoss: gc.collisionLifeLoss ?? fb.collisionLifeLoss,
433
- // Economy — Fountain & Sink (nested, mirrors EconomyRules proto message).
377
+ // Economy — Fountain & Sink (nested EconomyRules message).
434
378
  economyRules: {
435
379
  botSpawnCoins: gc.economyRules?.botSpawnCoins ?? fb.economyRules.botSpawnCoins,
436
380
  playerSpawnCoins: gc.economyRules?.playerSpawnCoins ?? fb.economyRules.playerSpawnCoins,
@@ -441,31 +385,25 @@ function toInstanceConfig(gc) {
441
385
  portalFee: gc.economyRules?.portalFee ?? fb.economyRules.portalFee,
442
386
  craftingFeePercent: gc.economyRules?.craftingFeePercent ?? fb.economyRules.craftingFeePercent,
443
387
  },
444
- lifeRegenChance: gc.lifeRegenChance ?? fb.lifeRegenChance,
445
- maxChance: gc.maxChance ?? fb.maxChance,
388
+ lifeRegenChance: chanceOf(gc.lifeRegenChance, fb.lifeRegenChance),
389
+ maxChance: chanceOf(gc.maxChance, fb.maxChance),
446
390
  entityDefaults,
447
- skillConfig: (gc.skillConfig && gc.skillConfig.length > 0 ? gc.skillConfig : fb.skillConfig).map((sc) => ({
448
- triggerItemId: sc.triggerItemId || '',
449
- skills: (sc.skills || []).map((sk) => ({
450
- logicEventId: sk.logicEventId || '',
451
- name: sk.name || '',
452
- description: sk.description || '',
453
- summonedEntityItemId: sk.summonedEntityItemId || '',
454
- })),
455
- })),
391
+ // Filled in by the caller from the skills this world's own content triggers; a conf stores
392
+ // none, so there is nothing here to fall back to.
393
+ skillConfig: [],
456
394
  skillRules: {
457
- projectileSpawnChance: gc.skillRules?.projectileSpawnChance ?? fb.skillRules.projectileSpawnChance,
395
+ projectileSpawnChance: chanceOf(gc.skillRules?.projectileSpawnChance, fb.skillRules.projectileSpawnChance),
458
396
  projectileLifetimeMs: gc.skillRules?.projectileLifetimeMs ?? fb.skillRules.projectileLifetimeMs,
459
397
  projectileWidth: gc.skillRules?.projectileWidth ?? fb.skillRules.projectileWidth,
460
398
  projectileHeight: gc.skillRules?.projectileHeight ?? fb.skillRules.projectileHeight,
461
399
  projectileSpeedMultiplier: gc.skillRules?.projectileSpeedMultiplier ?? fb.skillRules.projectileSpeedMultiplier,
462
- doppelgangerSpawnChance: gc.skillRules?.doppelgangerSpawnChance ?? fb.skillRules.doppelgangerSpawnChance,
400
+ doppelgangerSpawnChance: chanceOf(gc.skillRules?.doppelgangerSpawnChance, fb.skillRules.doppelgangerSpawnChance),
463
401
  doppelgangerLifetimeMs: gc.skillRules?.doppelgangerLifetimeMs ?? fb.skillRules.doppelgangerLifetimeMs,
464
402
  doppelgangerSpawnRadius: gc.skillRules?.doppelgangerSpawnRadius ?? fb.skillRules.doppelgangerSpawnRadius,
465
403
  doppelgangerInitialLifeFraction:
466
404
  gc.skillRules?.doppelgangerInitialLifeFraction ?? fb.skillRules.doppelgangerInitialLifeFraction,
467
405
  },
468
- // Equipment rules — governs activation constraints (nested, mirrors EquipmentRules proto message).
406
+ // Equipment rules — activation constraints (nested EquipmentRules message).
469
407
  equipmentRules: {
470
408
  activeItemTypes: gc.equipmentRules?.activeItemTypes ?? fb.equipmentRules.activeItemTypes,
471
409
  onePerType: gc.equipmentRules?.onePerType ?? fb.equipmentRules.onePerType,
@@ -483,15 +421,6 @@ function buildFallbackConfig() {
483
421
  return JSON.parse(JSON.stringify(FALLBACK_CONFIG_DEFAULTS));
484
422
  }
485
423
 
486
- /**
487
- * Assembles the InstanceConfig for the procedural fallback world from code
488
- * defaults only — no own-model collection may override it — then applies the
489
- * fallback instance's default-player-inventory itemIds.
490
- */
491
- function buildFallbackInstanceConfig(fallbackConf, instanceItemIds) {
492
- return applyInstanceDefaultPlayerInventory(toInstanceConfig(fallbackConf), instanceItemIds);
493
- }
494
-
495
424
  // ═══════════════════════════════════════════════════════════════════
496
425
  // Transport-agnostic fetchers — one per CyberiaDataService RPC
497
426
  // ═══════════════════════════════════════════════════════════════════
@@ -555,40 +484,31 @@ async function fetchFullInstance(models, requestedInstanceCode) {
555
484
  // ── Fallback: instance not found → return a multi-map procedural world ──
556
485
  if (!inst) {
557
486
  logger.info(`Instance "${instanceCode}" not found — returning fallback world.`);
558
- const world = generateFallbackWorld({ itemIds: getFallbackDefaultItems() });
487
+ const world = generateFallbackWorld();
559
488
  const fallbackConf = buildFallbackConfig();
560
489
 
561
- // Collect all objectLayerItemIds from the generated maps so the
562
- // Go server can resolve atlases at startup.
490
+ // Collect every objectLayerItemId of the generated maps, so the atlases
491
+ // resolve at startup.
563
492
  const fallbackItemIds = new Set();
564
493
  for (const m of world.maps) {
565
494
  for (const e of m.entities || []) {
566
495
  for (const id of e.objectLayerItemIds || []) fallbackItemIds.add(id);
567
496
  }
568
497
  }
569
- // Also include system OL items from entity defaults.
570
- for (const d of fallbackConf.entityDefaults || []) {
498
+ // Also include system OL items from the canonical entity defaults — the fallback world
499
+ // references no collection document, so these are the defaults it actually runs on.
500
+ for (const d of ENTITY_TYPE_DEFAULTS) {
571
501
  for (const id of d.liveItemIds || []) fallbackItemIds.add(id);
572
502
  for (const id of d.deadItemIds || []) fallbackItemIds.add(id);
573
503
  for (const id of d.dropItemIds || []) fallbackItemIds.add(id);
574
- for (const ol of d.defaultObjectLayers || []) {
575
- if (ol.itemId) fallbackItemIds.add(ol.itemId);
576
- }
504
+ for (const id of d.inventoryItemsIds || []) fallbackItemIds.add(id);
577
505
  }
578
- // Include every canonical item so the Go server can resolve the type
579
- // of anything a player may pick up or equip (quest rewards like
580
- // hatchet, alt weapons, etc.) — required for the one-per-type
581
- // equipment rule, which silently no-ops when item type is unknown.
506
+ // Include every canonical item so the server knows the type of anything a
507
+ // player can pick up. The one-per-type rule no-ops on an unknown type.
582
508
  for (const it of DefaultCyberiaItems || []) {
583
509
  if (it?.item?.id) fallbackItemIds.add(it.item.id);
584
510
  }
585
511
 
586
- // Instance-level itemIds (default-player-inventory) may not appear in
587
- // any map entity — include them so their atlases resolve.
588
- for (const entry of world.instance.itemIds || []) {
589
- if (entry?.id) fallbackItemIds.add(entry.id);
590
- }
591
-
592
512
  // The fallback world is fully self-contained: content config comes
593
513
  // exclusively from the canonical code defaults (cyberia-server-defaults
594
514
  // + SharedDefaultsCyberia). Own-model collections (CyberiaSkill,
@@ -599,19 +519,16 @@ async function fetchFullInstance(models, requestedInstanceCode) {
599
519
  ? await models.ObjectLayer.find({ 'data.item.id': { $in: [...fallbackItemIds] } }).lean()
600
520
  : [];
601
521
 
602
- const fallbackConfig = buildFallbackInstanceConfig(fallbackConf, world.instance.itemIds);
522
+ const fallbackConfig = toInstanceConfig(fallbackConf);
523
+ // Same membership rule as a persisted world, over the canonical definitions: this path
524
+ // consults no collection, so DefaultSkillConfig is the only source of skills it has.
525
+ fallbackConfig.skillConfig = skillDocsToConfig(selectInstanceSkills(DefaultSkillConfig, fallbackItemIds));
603
526
 
604
527
  // Opaque version over everything mutable in this payload, mirroring the
605
528
  // persisted path below. A constant here would make hot reload a silent
606
529
  // no-op: WorldBuilder.ReloadWorld skips the rebuild — and therefore
607
- // ApplyInstanceConfig whenever the version is unchanged. The world
608
- // geometry itself is deterministic (code defaults + seed), so the inputs
609
- // that can actually differ are the staged default items and the
610
- // ObjectLayer docs, which are the only things read from the DB here.
611
- const fallbackVersionParts = ['fallback'];
612
- for (const entry of world.instance.itemIds || []) {
613
- fallbackVersionParts.push(`${entry.id}:${entry.defaultPlayerInventory ? 1 : 0}`);
614
- }
530
+ // ApplyInstanceConfig when content and configuration are unchanged.
531
+ const fallbackVersionParts = ['fallback', JSON.stringify(fallbackConfig)];
615
532
  for (const doc of fallbackOlDocs) fallbackVersionParts.push(String(doc.sha256 || doc._id));
616
533
  const fallbackVersion = `fallback-${crypto
617
534
  .createHash('sha256')
@@ -633,8 +550,7 @@ async function fetchFullInstance(models, requestedInstanceCode) {
633
550
  objectLayers: fallbackOlDocs.map(toObjectLayerMsg),
634
551
  config: fallbackConfig,
635
552
  version: fallbackVersion,
636
- // Mission content for the procedural fallback comes straight from
637
- // the canonical defaults — no DB, fully self-contained.
553
+ // Mission content for the fallback comes from the code defaults, no DB.
638
554
  actions: DefaultCyberiaActions.map(toActionMsg),
639
555
  quests: DefaultCyberiaQuests.map(toQuestMsg),
640
556
  };
@@ -642,10 +558,8 @@ async function fetchFullInstance(models, requestedInstanceCode) {
642
558
 
643
559
  // ── Instance found — load maps + entity OLs + config default OLs ──────
644
560
  // `populate('conf')` returns null when the ObjectId ref is missing or
645
- // orphaned (e.g. after a --conf import that preserved a stale _id).
646
- // Fall back to a direct instanceCode lookup so aoiRadius and all other
647
- // tuning fields are always read from the database, never silently
648
- // replaced by FALLBACK_CONFIG_DEFAULTS.
561
+ // orphaned. The direct instanceCode lookup keeps every tuning field coming
562
+ // from the database instead of FALLBACK_CONFIG_DEFAULTS.
649
563
  let conf = inst.conf;
650
564
  if (!conf) {
651
565
  conf = await models.CyberiaInstanceConf.findOne({ instanceCode }).lean();
@@ -657,105 +571,112 @@ async function fetchFullInstance(models, requestedInstanceCode) {
657
571
  const mapCodes = inst.cyberiaMapCodes || [];
658
572
  const mapDocs = mapCodes.length ? await models.CyberiaMap.find({ code: { $in: mapCodes } }).lean() : [];
659
573
 
660
- // Collect all item IDs referenced by map entities.
661
- const itemIds = new Set();
662
- for (const m of mapDocs) {
663
- for (const e of m.entities || []) {
664
- for (const id of e.objectLayerItemIds || []) itemIds.add(id);
665
- }
666
- }
667
-
668
- // Instance-level itemIds (e.g. default-player-inventory items) may not
669
- // appear in any map entity — include them so their atlases resolve.
670
- // Tolerate legacy flat-string entries alongside the { id, … } shape.
671
- for (const entry of inst.itemIds || []) {
672
- const id = typeof entry === 'string' ? entry : entry?.id;
673
- if (id) itemIds.add(id);
674
- }
675
-
676
- // Authoritative per-entity-type defaults come from the
677
- // CyberiaEntityTypeDefault collection (own model); empty collection leaves
678
- // the conf/canonical defaults in place.
679
- const entityDefaultDocs = await loadEntityTypeDefaultDocs(models);
574
+ // Authoritative per-entity-type defaults are the documents this instance's conf references;
575
+ // referencing none leaves the canonical defaults in place.
576
+ const entityDefaultDocs = await loadEntityTypeDefaultDocs(models, conf);
680
577
  const entityDefaultsConfig = entityTypeDefaultDocsToConfig(entityDefaultDocs);
681
578
 
682
- // Include system OL items from canonical ENTITY_TYPE_DEFAULTS, the DB conf,
683
- // AND the collection so the Go server always caches every default atlas
684
- // even when an entity (e.g. a new resource skin) appears in no map.
685
- for (const d of [...ENTITY_TYPE_DEFAULTS, ...(conf.entityDefaults || [])]) {
686
- for (const id of d.liveItemIds || []) itemIds.add(id);
687
- for (const id of d.deadItemIds || []) itemIds.add(id);
688
- for (const id of d.dropItemIds || []) itemIds.add(id);
689
- for (const ol of d.defaultObjectLayers || []) {
690
- if (ol.itemId) itemIds.add(ol.itemId);
691
- }
692
- }
579
+ // Mission content for this instance: actions and quests bound to its maps (by sourceMapCode).
580
+ // Delivered with the world so the Go server never opens a separate content channel, and never
581
+ // receives content from other instances' maps. Read before the item set because what a vendor
582
+ // sells and what a quest asks for are part of what this world owns.
583
+ const actionDocs =
584
+ mapCodes.length && models.CyberiaAction
585
+ ? await models.CyberiaAction.find({ sourceMapCode: { $in: mapCodes } }).lean()
586
+ : [];
587
+ const questDocs =
588
+ mapCodes.length && models.CyberiaQuest
589
+ ? await models.CyberiaQuest.find({ sourceMapCode: { $in: mapCodes } }).lean()
590
+ : [];
591
+
592
+ // Everything this world names, by the one rule the export and the editor's sync also use. The
593
+ // canonical defaults join the instance's own: every world runs on both, so an entity here can
594
+ // hold what they wire even when no map places it.
595
+ const owned = collectInstanceItemIds({
596
+ maps: mapDocs,
597
+ entityDefaults: [...entityDefaultsConfig, ...ENTITY_TYPE_DEFAULTS],
598
+ actions: actionDocs,
599
+ quests: questDocs,
600
+ });
601
+
602
+ const itemIds = new Set(owned);
693
603
  // The Go server falls back to this dead visual when a build declares
694
604
  // no deadItemIds — its atlas must resolve even when nothing references it.
695
605
  itemIds.add(DEFAULT_DEAD_ITEM_ID);
696
- addEntityDefaultAtlasIds(entityDefaultsConfig, itemIds);
697
606
 
698
- // Authoritative skills come from the CyberiaSkill collection (own model);
699
- // resolve their atlases before the OL query so summoned entities render.
700
- const skillDocs = await loadSkillConfigDocs(models);
701
- addSkillAtlasIds(skillDocs, itemIds);
607
+ // The skills this world runs: the collection owns the definitions, and a trigger item the
608
+ // world never names is unreachable here. The canonical definitions stand in only for a
609
+ // deployment that never seeded the collection — never for a world that legitimately selects
610
+ // none of them, which would put back skills an operator deleted. What the selected skills
611
+ // summon needs an atlas, so those ids join the set before the ObjectLayer query.
612
+ const seededSkills = await loadSkillDocs(models);
613
+ const skillDocs = selectInstanceSkills(seededSkills.length ? seededSkills : DefaultSkillConfig, owned);
614
+ for (const summoned of collectSummonedItemIds(skillDocs)) itemIds.add(summoned);
702
615
 
703
616
  const olDocs = itemIds.size ? await models.ObjectLayer.find({ 'data.item.id': { $in: [...itemIds] } }).lean() : [];
704
617
 
705
- // Opaque version string from updatedAt timestamps the Go server
706
- // compares this to skip full world rebuilds when nothing changed.
707
- const versionParts = [String(inst.updatedAt || inst._id)];
618
+ // Opaque version over the updatedAt timestamps. The server compares it to
619
+ // skip a full world rebuild when nothing changed.
620
+ const versionParts = [String(inst.updatedAt || inst._id), JSON.stringify(toInstanceConfig(conf))];
708
621
  for (const m of mapDocs) versionParts.push(String(m.updatedAt || m._id));
709
622
  if (conf.updatedAt) versionParts.push(String(conf.updatedAt));
710
623
  // Skill edits live in their own collection — fold them in so a skill
711
624
  // change triggers a Go-side world rebuild instead of serving stale skills.
712
- for (const sk of skillDocs) versionParts.push(String(sk.updatedAt || sk._id));
625
+ for (const sk of skillDocs) versionParts.push(String(sk.updatedAt || sk._id || sk.triggerItemId));
713
626
  // Entity-type-default edits live in their own collection too — fold them
714
627
  // in so adding/editing a default (e.g. a new resource) rebuilds the world.
715
628
  for (const ed of entityDefaultDocs) versionParts.push(String(ed.updatedAt || ed._id));
716
629
  const version = crypto.createHash('sha256').update(versionParts.join('|')).digest('hex');
717
630
 
718
- // Mission content for this instance: actions and quests bound to its
719
- // maps (by sourceMapCode). Delivered with the world so the Go server
720
- // never opens a separate content channel, and never receives content from
721
- // other instances' maps.
722
- const actionDocs =
723
- mapCodes.length && models.CyberiaAction
724
- ? await models.CyberiaAction.find({ sourceMapCode: { $in: mapCodes } }).lean()
725
- : [];
726
- const questDocs =
727
- mapCodes.length && models.CyberiaQuest
728
- ? await models.CyberiaQuest.find({ sourceMapCode: { $in: mapCodes } }).lean()
729
- : [];
730
-
731
631
  const baseConfig = toInstanceConfig(conf);
732
- // Own-model entity defaults win over the conf's entityDefaults (overlaid on
733
- // canonical via mergeEntityDefaults), so every collection variant — e.g.
734
- // each resource skin reaches the Go runtime keyed by liveItemIds. Applied
735
- // BEFORE default-player-inventory so the player layer additions survive.
736
- if (entityDefaultsConfig.length) baseConfig.entityDefaults = mergeEntityDefaults(entityDefaultsConfig);
737
- const config = applyInstanceDefaultPlayerInventory(baseConfig, inst.itemIds);
738
- // Own-model skills win over the conf's stripped skillConfig (which lacks
739
- // summonedEntityItemId); empty collection leaves the conf/fallback as-is.
740
- if (skillDocs.length) config.skillConfig = skillDocsToConfig(skillDocs);
632
+ // The referenced documents are the world's defaults, completed by the canonical set for every
633
+ // entity type they do not cover. Every variant — each resource skin, say — reaches the Go
634
+ // runtime keyed by liveItemIds. Applied BEFORE default-player-inventory so the player layer
635
+ // additions survive.
636
+ if (entityDefaultsConfig.length) {
637
+ baseConfig.entityDefaults = mergeEntityDefaults(entityDefaultsConfig, itemTypesOf(olDocs));
638
+ }
639
+ // The wire still carries a skillConfig; nothing stores one. It is exactly the skills selected
640
+ // above, so what the simulation runs and what the export writes come from one decision.
641
+ baseConfig.skillConfig = skillDocsToConfig(skillDocs);
741
642
 
742
643
  return {
743
644
  instance: toInstanceMsg(inst),
744
645
  maps: mapDocs.map(toMapMsg),
745
646
  objectLayers: olDocs.map(toObjectLayerMsg),
746
- config,
647
+ config: baseConfig,
747
648
  version,
748
649
  actions: actionDocs.map(toActionMsg),
749
650
  quests: questDocs.map(toQuestMsg),
750
651
  };
751
652
  }
752
653
 
654
+ /**
655
+ * Every ObjectLayer item id one instance runs on, as stored documents.
656
+ *
657
+ * Reads the world the same way the runtime does, so a tool never disagrees with the
658
+ * simulation about what a world owns. Ids the instance names but the collection does not
659
+ * hold are absent, because {@link fetchFullInstance} resolves the set against ObjectLayer.
660
+ *
661
+ * @param {object} models - Instance models, see {@link getInstanceModels}.
662
+ * @param {string} instanceCode - Instance code, must exist.
663
+ * @returns {Promise<string[]>} Item ids, no duplicates.
664
+ * @throws {Error} When no CyberiaInstance holds that code.
665
+ */
666
+ async function fetchInstanceObjectLayerItemIds(models, instanceCode) {
667
+ const instance = await models.CyberiaInstance.findOne({ code: instanceCode }, { _id: 1 }).lean();
668
+ if (!instance) throw new Error(`CyberiaInstance "${instanceCode}" not found`);
669
+ const { objectLayers } = await fetchFullInstance(models, instanceCode);
670
+ return [...new Set(objectLayers.map((objectLayer) => objectLayer?.item?.id).filter(Boolean))];
671
+ }
672
+
753
673
  export {
754
674
  buildCyberiaMmoInstanceEnv as buildInstanceEnv,
755
675
  buildCyberiaMmoInstanceEnv,
676
+ fetchInstanceObjectLayerItemIds,
756
677
  getInstanceModels,
678
+ itemTypesOf,
757
679
  normalizeEntityDefault,
758
- applyInstanceDefaultPlayerInventory,
759
680
  skillDocsToConfig,
760
681
  entityTypeDefaultDocsToConfig,
761
682
  mergeEntityDefaults,
@@ -768,7 +689,6 @@ export {
768
689
  toQuestMsg,
769
690
  toInstanceConfig,
770
691
  buildFallbackConfig,
771
- buildFallbackInstanceConfig,
772
692
  pingData,
773
693
  objectLayerQueryFilter,
774
694
  fetchObjectLayerBatch,