@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,14 +1,462 @@
1
1
  import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
2
+ import {
3
+ ENTITY_TYPE_DEFAULTS,
4
+ resolveEntityDefaultBuild,
5
+ resolveEntityInventory,
6
+ } from '../cyberia-server-defaults/cyberia-server-defaults.js';
7
+ import { collectInstanceItemIds, selectInstanceSkills } from '../cyberia-instance/cyberia-instance-items.js';
2
8
  import { loggerFactory } from '../../server/ops/logger.js';
3
9
  import { DataQuery } from '../../server/storage/data-query.js';
4
10
 
5
11
  const logger = loggerFactory(import.meta);
6
12
 
13
+ /** The stored shape, as every consumer of a resolved default expects to read it. */
14
+ const toEntityDefault = (source) => ({
15
+ entityType: source.entityType || '',
16
+ liveItemIds: [...(source.liveItemIds || [])],
17
+ deadItemIds: [...(source.deadItemIds || [])],
18
+ dropItemIds: [...(source.dropItemIds || [])],
19
+ inventoryItemsIds: [...(source.inventoryItemsIds || [])],
20
+ overrideItemsIdsState: (source.overrideItemsIdsState || [])
21
+ .filter((rule) => rule?.itemId)
22
+ .map((rule) => ({
23
+ itemId: rule.itemId,
24
+ ...('boolean' === typeof rule.active ? { active: rule.active } : {}),
25
+ ...(Number.isFinite(rule.quantity) ? { quantity: rule.quantity } : {}),
26
+ ...(Number.isFinite(rule.dropChance) ? { dropChance: rule.dropChance } : {}),
27
+ })),
28
+ behavior: source.behavior || '',
29
+ });
30
+
31
+ /** The key resolution ties on: same entity type, same live set, whatever order it is written in. */
32
+ const buildKey = (doc) => `${doc?.entityType || ''}|${[...(doc?.liveItemIds || [])].sort().join(',')}`;
33
+
34
+ /**
35
+ * Names the references that answer for the same entities, changing nothing.
36
+ *
37
+ * Resolution is by containment with the most specific match winning, so two documents declaring
38
+ * the same type and the same live ids are not a refinement of each other — whichever the conf
39
+ * lists first wins, which is not a decision anyone made. That is worth telling an operator about.
40
+ *
41
+ * It is not worth unlinking over. A reference is a world's declared membership, and picking which
42
+ * of two to discard by timestamp guesses at intent and throws away the other instance's wiring
43
+ * when it guesses wrong. The report says which record to delete; the deleting stays deliberate.
44
+ *
45
+ * @param {string[]} references - Document ids, in conf order.
46
+ * @param {Array<{_id: any, entityType?: string, liveItemIds?: string[]}>} defaults
47
+ * @returns {string[]} Each reference after the first that answers for an already-answered build.
48
+ */
49
+ const findDuplicateBuilds = (references, defaults) => {
50
+ const byId = new Map(defaults.map((doc) => [String(doc._id), doc]));
51
+ const answered = new Set();
52
+ // A reference with no document is the `dropped` path's business: it cannot be compared, so it
53
+ // is never called a duplicate of something unseen.
54
+ return references.filter((id) => {
55
+ if (!byId.has(id)) return false;
56
+ const key = buildKey(byId.get(id));
57
+ if (answered.has(key)) return true;
58
+ answered.add(key);
59
+ return false;
60
+ });
61
+ };
62
+
63
+ /**
64
+ * itemId → item type for the ids a world carries, or an empty map when this provider has no
65
+ * ObjectLayer model. Types only settle a contested equipment slot, so a world without them keeps
66
+ * every row the state its list derives — the same "unknown types pass through" the runtime applies.
67
+ *
68
+ * @param {Set<string>} itemIds - Ids to resolve.
69
+ * @param {{host: string, path: string}} options - Provider context.
70
+ * @returns {Promise<Object<string,string>>}
71
+ */
72
+ const readItemTypes = async (itemIds, options) => {
73
+ const types = {};
74
+ if (itemIds.size === 0) return types;
75
+ let ObjectLayer;
76
+ try {
77
+ ObjectLayer = DataBaseProviderService.getModel('ObjectLayer', options);
78
+ } catch {
79
+ return types;
80
+ }
81
+ if (!ObjectLayer?.find) return types;
82
+ for (const doc of await ObjectLayer.find({ 'data.item.id': { $in: [...itemIds] } }, { 'data.item': 1 }).lean()) {
83
+ const item = doc?.data?.item;
84
+ if (item?.id && item?.type) types[item.id] = item.type;
85
+ }
86
+ return types;
87
+ };
88
+
89
+ /**
90
+ * Rewrites one map's entities so each carries what its default says it wears.
91
+ *
92
+ * A placed entity names itself by the ids the author put on it, and the simulation seeds the rest
93
+ * from the default that name resolves to. The map document has to agree: an override that
94
+ * activates a carried item is a decision about the entity, and it is only real once the entity on
95
+ * the map holds that item too. Everything reading a map — the editor, the instance payload, the
96
+ * spawn — then sees one answer instead of two.
97
+ *
98
+ * The rewrite is a replacement, not a merge: a matched entity ends up carrying its build's live
99
+ * ids and whatever that build wears, and nothing else. Merging cannot converge — an id the sync
100
+ * added stops being recognisable the moment the default stops naming it, and it would sit on the
101
+ * map for good. Stating the whole set is what makes the second run a no-op and what makes removing
102
+ * an item from a default actually remove it from the world.
103
+ *
104
+ * The live ids are always kept, even when an override unseats one: they are the key the entity
105
+ * resolves by, and dropping one would send the next sync to a different, less specific default.
106
+ * Which of them is actually worn is settled at spawn, from the same inventory this reads.
107
+ *
108
+ * Only a default the entity genuinely carries speaks for it. The fallback match — the first of its
109
+ * type — answers what a bot looks like in general, never what is on this one, so an entity that
110
+ * contains no default's live set is left exactly as its author placed it.
111
+ *
112
+ * @param {Array<object>} entities - The map's entities.
113
+ * @param {Array<object>} defaults - The world's entity-type defaults, most specific first.
114
+ * @param {Object<string,string>} itemTypes - itemId → item type, for the equipment rules.
115
+ * @returns {{entities: Array<object>, changed: number}} The rewritten entities and how many moved.
116
+ */
117
+ const applyDefaultsToEntities = (entities = [], defaults = [], itemTypes = {}) => {
118
+ let changed = 0;
119
+ const next = entities.map((entity) => {
120
+ const placed = entity.objectLayerItemIds || [];
121
+ const build = resolveEntityDefaultBuild({ entityType: entity.entityType, itemIds: placed }, defaults);
122
+ const identity = build?.liveItemIds || [];
123
+ if (identity.length === 0 || !identity.every((itemId) => placed.includes(itemId))) return entity;
124
+ const worn = resolveEntityInventory(build, { itemTypes })
125
+ .filter((row) => row.active)
126
+ .map((row) => row.itemId);
127
+ const itemIds = [...new Set([...identity, ...worn])];
128
+ if (itemIds.length === placed.length && itemIds.every((id, index) => id === placed[index])) return entity;
129
+ changed++;
130
+ return { ...entity, objectLayerItemIds: itemIds };
131
+ });
132
+ return { entities: next, changed };
133
+ };
134
+
7
135
  class CyberiaEntityTypeDefaultService {
8
136
  // No per-itemId uniqueness: resolution is by subset containment (most-specific
9
137
  // match wins), so the same itemId may appear in many documents — e.g.
10
138
  // [purple, atlas_pistol_mk2]→hostile alongside [purple]→passive. See the model.
11
139
 
140
+ /**
141
+ * Materialises the entity-type defaults an instance conf references.
142
+ *
143
+ * This is the only way a conf's `entityDefaults` becomes usable data. It reads the ids the conf
144
+ * names and nothing else: a document belongs to a world because that world points at it, never
145
+ * because their item ids happen to overlap. That distinction is the whole fix — matching by
146
+ * item id is what let two instances sharing a skin resolve into each other's wiring.
147
+ *
148
+ * Order follows the conf, so the most specific documents an operator listed first stay first.
149
+ * An id naming no document is dropped and reported: the reference outliving its document is a
150
+ * broken world, not a silent one.
151
+ *
152
+ * @param {import('mongoose').Model} Model - CyberiaEntityTypeDefault model.
153
+ * @param {Array<string|import('mongoose').Types.ObjectId>} entityDefaultIds - Conf references.
154
+ * @returns {Promise<Array<object>>} Referenced defaults, in reference order.
155
+ */
156
+ static resolveWith = async (Model, entityDefaultIds = []) => {
157
+ const ids = (entityDefaultIds || []).map((id) => String(id?._id ?? id)).filter(Boolean);
158
+ if (!Model || ids.length === 0) return [];
159
+ const docs = await Model.find({ _id: { $in: ids } }).lean();
160
+ const byId = new Map(docs.map((doc) => [String(doc._id), doc]));
161
+ const missing = ids.filter((id) => !byId.has(id));
162
+ if (missing.length > 0) {
163
+ logger.warn('Instance conf references entity-type defaults that no longer exist', { missing });
164
+ }
165
+ return ids.filter((id) => byId.has(id)).map((id) => toEntityDefault(byId.get(id)));
166
+ };
167
+
168
+ /** {@link resolveWith}, addressing the model through the provider context. */
169
+ static resolve = async (entityDefaultIds = [], options) =>
170
+ CyberiaEntityTypeDefaultService.resolveWith(
171
+ DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options),
172
+ entityDefaultIds,
173
+ );
174
+
175
+ /**
176
+ * The defaults a world runs on: what its conf references, completed by the canonical set.
177
+ *
178
+ * Every entity type the referenced documents do not cover falls back to ENTITY_TYPE_DEFAULTS, so
179
+ * an instance only has to own the rows it actually changes.
180
+ *
181
+ * @param {Array<object>} resolved - Defaults an instance references, already materialised.
182
+ * @returns {Array<object>} Those defaults followed by the uncovered canonical ones.
183
+ */
184
+ static completeWithCanonical = (resolved = []) => {
185
+ const covered = new Set(resolved.map((entityDefault) => entityDefault.entityType));
186
+ return [
187
+ ...resolved.map(toEntityDefault),
188
+ ...ENTITY_TYPE_DEFAULTS.filter((canonical) => !covered.has(canonical.entityType)).map(toEntityDefault),
189
+ ];
190
+ };
191
+
192
+ /** {@link completeWithCanonical} over what a conf references. */
193
+ static resolveWithCanonical = async (entityDefaultIds = [], options) =>
194
+ CyberiaEntityTypeDefaultService.completeWithCanonical(
195
+ await CyberiaEntityTypeDefaultService.resolve(entityDefaultIds, options),
196
+ );
197
+
198
+ /**
199
+ * Makes exactly the named instances reference this default.
200
+ *
201
+ * States the whole membership, so re-sending converges: a conf that drops out of the list has
202
+ * the reference removed, and one that joins gets it appended. Order inside a conf is
203
+ * significant (most specific first), so an existing reference is left where it is.
204
+ *
205
+ * @param {object} req - Express request; `params.id` is the default's _id, `body.instanceCodes`
206
+ * the complete set of instances that should reference it.
207
+ * @param {object} res
208
+ * @param {{host: string, path: string}} options - Provider context.
209
+ * @returns {Promise<{instanceCodes: string[], linked: string[], unlinked: string[]}>}
210
+ */
211
+ static setInstances = async (req, res, options) => {
212
+ const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
213
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
214
+ const id = String(req.params.id);
215
+ if (!(await CyberiaEntityTypeDefault.exists({ _id: id }))) {
216
+ throw new Error(`cyberia-entity-type-default not found for _id="${id}"`);
217
+ }
218
+ const wanted = new Set((req.body?.instanceCodes || []).map(String));
219
+
220
+ const confs = await CyberiaInstanceConf.find({}, { instanceCode: 1, entityDefaults: 1 }).lean();
221
+ const unknown = [...wanted].filter((code) => !confs.some((conf) => conf.instanceCode === code));
222
+ if (unknown.length > 0) throw new Error(`cyberia-instance-conf not found for: ${unknown.join(', ')}`);
223
+
224
+ const linked = [];
225
+ const unlinked = [];
226
+ for (const conf of confs) {
227
+ const refs = (conf.entityDefaults || []).map(String);
228
+ const has = refs.includes(id);
229
+ const should = wanted.has(conf.instanceCode);
230
+ if (has === should) continue;
231
+ await CyberiaInstanceConf.updateOne(
232
+ { _id: conf._id },
233
+ { $set: { entityDefaults: should ? [...refs, id] : refs.filter((ref) => ref !== id), updatedAt: new Date() } },
234
+ );
235
+ (should ? linked : unlinked).push(conf.instanceCode);
236
+ }
237
+ const compacted = await CyberiaEntityTypeDefaultService.compactInstanceRefs(options);
238
+ logger.info('Updated entity-type default instance links', { id, linked, unlinked });
239
+ return { instanceCodes: [...wanted].sort(), linked, unlinked, compacted };
240
+ };
241
+
242
+ /**
243
+ * Drops conf references whose entity-type default no longer exists.
244
+ *
245
+ * A reference outliving its document is a world half-declared: the boot path skips it, the
246
+ * export cannot find it, and nobody can tell from the conf whether the wiring was meant to be
247
+ * there. Deleting a default unlinks it everywhere (see `delete`), so this exists for what
248
+ * slipped past that — a document removed straight from the collection, or a conf restored from
249
+ * a backup whose defaults were not.
250
+ *
251
+ * @param {{host: string, path: string}} options - Provider context.
252
+ * @param {{instanceCode?: string}} [scope] - Limit to one instance; omit for the whole collection.
253
+ * @returns {Promise<Array<{instanceCode: string, dropped: string[]}>>} What each conf lost.
254
+ */
255
+ static compactInstanceRefs = async (options, { instanceCode } = {}) => {
256
+ const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
257
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
258
+ const confs = await CyberiaInstanceConf.find(instanceCode ? { instanceCode } : {}, {
259
+ instanceCode: 1,
260
+ entityDefaults: 1,
261
+ }).lean();
262
+ const referenced = [...new Set(confs.flatMap((conf) => (conf.entityDefaults || []).map(String)))];
263
+ if (referenced.length === 0) return [];
264
+ const existing = new Set(
265
+ (await CyberiaEntityTypeDefault.find({ _id: { $in: referenced } }, { _id: 1 }).lean()).map((doc) =>
266
+ String(doc._id),
267
+ ),
268
+ );
269
+
270
+ const compacted = [];
271
+ for (const conf of confs) {
272
+ const refs = (conf.entityDefaults || []).map(String);
273
+ const kept = refs.filter((ref) => existing.has(ref));
274
+ if (kept.length === refs.length) continue;
275
+ await CyberiaInstanceConf.updateOne(
276
+ { _id: conf._id },
277
+ { $set: { entityDefaults: kept, updatedAt: new Date() } },
278
+ );
279
+ compacted.push({ instanceCode: conf.instanceCode, dropped: refs.filter((ref) => !existing.has(ref)) });
280
+ }
281
+ if (compacted.length > 0) logger.info('Dropped orphaned entity-type default references', { compacted });
282
+ return compacted;
283
+ };
284
+
285
+ /**
286
+ * Points an instance's conf at every entity-type default its own content needs.
287
+ *
288
+ * Matching is the runtime's rule, not a guess: a document belongs to this world when all of its
289
+ * `liveItemIds` appear together on some entity its maps place — the same subset containment
290
+ * `game/entity_defaults.go` resolves an entity with. So whatever an author drops on a map, the
291
+ * conf ends up referencing the wiring that will actually be applied to it.
292
+ *
293
+ * Additive, then compacting: references already there are kept in place (order carries
294
+ * specificity), matches missing from the conf are appended, and references whose document is
295
+ * gone are dropped. Nothing an author linked by hand is lost — a player or coin default is
296
+ * never placed on a map, so a wholesale replace would silently discard it.
297
+ *
298
+ * A match is reported rather than linked whenever linking it would decide something for the
299
+ * operator. Two worlds built on the same art hold documents with identical `liveItemIds`, so
300
+ * matching alone cannot tell them apart: one another instance references is left to that
301
+ * instance (`skipped`), and one answering for a build this conf already references is left
302
+ * where it is (`conflicts`), since the two would then differ only by list order. Both stay
303
+ * claimable by hand in the Entity engine. That is the cross-instance contamination the
304
+ * reference model exists to prevent — a document is in a world because the world points at it.
305
+ *
306
+ * Membership is only ever added here. A reference an operator made is never withdrawn on a
307
+ * guess: the sole removal is `dropped`, a reference whose document no longer exists.
308
+ *
309
+ * The skills the world runs are reported, not written. They follow from its content by the
310
+ * shared rule in cyberia-instance-items.js — a skill belongs when its trigger item is named by
311
+ * a map entity, an entity-type default, a vendor or assembler catalog, or a quest. Nothing to
312
+ * store means nothing to keep in step: the export and the boot payload derive the same list
313
+ * from the same content.
314
+ *
315
+ * @param {{instanceCode: string, mapCodes?: string[]}} params - Target instance; `mapCodes`
316
+ * overrides the stored set, which is what lets an editor sync against an unsaved selection.
317
+ * @param {{host: string, path: string}} options - Provider context.
318
+ * Placed entities are rewritten too: each carries the items the default it resolves to says it
319
+ * wears, so a map document and the simulation agree on what is on an entity.
320
+ *
321
+ * @returns {Promise<{instanceCode: string, mapCodes: string[], linked: string[], skipped: string[],
322
+ * dropped: string[], entityDefaults: string[], skills: string[],
323
+ * entitiesUpdated: Array<{mapCode: string, entities: number}>, conflicts: string[],
324
+ * duplicates: string[]}>}
325
+ */
326
+ static syncInstance = async ({ instanceCode, mapCodes }, options) => {
327
+ if (!instanceCode) throw new Error('instanceCode is required to sync entity-type defaults');
328
+ const CyberiaAction = DataBaseProviderService.getModel('CyberiaAction', options);
329
+ const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
330
+ const CyberiaInstance = DataBaseProviderService.getModel('CyberiaInstance', options);
331
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
332
+ const CyberiaMap = DataBaseProviderService.getModel('CyberiaMap', options);
333
+ const CyberiaQuest = DataBaseProviderService.getModel('CyberiaQuest', options);
334
+ const CyberiaSkill = DataBaseProviderService.getModel('CyberiaSkill', options);
335
+
336
+ const confs = await CyberiaInstanceConf.find({}, { instanceCode: 1, entityDefaults: 1 }).lean();
337
+ const conf = confs.find((entry) => entry.instanceCode === instanceCode);
338
+ if (!conf) throw new Error(`cyberia-instance-conf not found for instanceCode="${instanceCode}"`);
339
+ const codes = mapCodes?.length
340
+ ? [...mapCodes]
341
+ : (await CyberiaInstance.findOne({ code: instanceCode }).select('cyberiaMapCodes').lean())?.cyberiaMapCodes ??
342
+ [];
343
+
344
+ const maps = codes.length ? await CyberiaMap.find({ code: { $in: codes } }, { code: 1, entities: 1 }).lean() : [];
345
+ const placed = maps.flatMap((map) => (map.entities || []).map((entity) => new Set(entity.objectLayerItemIds || [])));
346
+
347
+ const defaults = await CyberiaEntityTypeDefault.find(
348
+ {},
349
+ { entityType: 1, liveItemIds: 1, updatedAt: 1 },
350
+ ).lean();
351
+ const matched = defaults
352
+ .filter(({ liveItemIds }) => (liveItemIds || []).length > 0)
353
+ .filter(({ liveItemIds }) => placed.some((itemIds) => liveItemIds.every((id) => itemIds.has(id))))
354
+ .map((doc) => String(doc._id));
355
+
356
+ const claimedElsewhere = new Set(
357
+ confs
358
+ .filter((entry) => entry.instanceCode !== instanceCode)
359
+ .flatMap((entry) => (entry.entityDefaults || []).map(String)),
360
+ );
361
+ const byId = new Map(defaults.map((doc) => [String(doc._id), doc]));
362
+ const existing = new Set(byId.keys());
363
+ const refs = (conf.entityDefaults || []).map(String);
364
+ const dropped = refs.filter((ref) => !existing.has(ref));
365
+ const kept = refs.filter((ref) => existing.has(ref));
366
+ // What this world already has an answer for. A match that answers for a build a reference
367
+ // already covers is a second opinion, never an upgrade: adopting it would put a document the
368
+ // operator never linked into the world, and — sharing the winner's key — it would decide by
369
+ // list order which of the two the entities actually resolve to. Reported, not linked.
370
+ const answered = new Set(kept.map((ref) => buildKey(byId.get(ref))));
371
+ const candidates = matched.filter((id) => !kept.includes(id));
372
+ const skipped = candidates.filter((id) => claimedElsewhere.has(id));
373
+ const linked = [];
374
+ const conflicts = [];
375
+ for (const id of candidates.filter((candidate) => !claimedElsewhere.has(candidate))) {
376
+ const key = buildKey(byId.get(id));
377
+ if (answered.has(key)) {
378
+ conflicts.push(id);
379
+ continue;
380
+ }
381
+ answered.add(key);
382
+ linked.push(id);
383
+ }
384
+ const entityDefaults = [...kept, ...linked];
385
+ const duplicates = findDuplicateBuilds(entityDefaults, defaults);
386
+
387
+ // Every item the world can hold: what it references, completed by the canonical defaults for
388
+ // the entity types it does not cover. A skill triggers from one of these or from nothing.
389
+ const referenced = await CyberiaEntityTypeDefault.find({ _id: { $in: entityDefaults } }).lean();
390
+ const running = CyberiaEntityTypeDefaultService.completeWithCanonical(referenced.map(toEntityDefault));
391
+ const carried = new Set(running.flatMap((doc) => resolveEntityInventory(doc).map((row) => row.itemId)));
392
+
393
+ // Which skills the world runs is derived, never stored. Reported so an operator can see what
394
+ // a sync changed, and computed by the one rule the export and the boot payload also use — a
395
+ // stored list was a second answer to the same question, and it disagreed: a trigger only a
396
+ // quest or a vendor named was reachable in play, exported with the world, and missing here.
397
+ const [actions, quests] = codes.length
398
+ ? await Promise.all([
399
+ CyberiaAction.find({ sourceMapCode: { $in: codes } }, { shopItems: 1, craftRecipes: 1 }).lean(),
400
+ CyberiaQuest.find({ sourceMapCode: { $in: codes } }, { steps: 1, rewards: 1 }).lean(),
401
+ ])
402
+ : [[], []];
403
+ const owned = collectInstanceItemIds({ maps, entityDefaults: running, actions, quests });
404
+ const skills = selectInstanceSkills(await CyberiaSkill.find({}, { triggerItemId: 1 }).lean(), owned)
405
+ .map((skill) => skill.triggerItemId)
406
+ .sort();
407
+
408
+ // Maps carry what their entities wear. The equipment rules settle a contested slot, so the
409
+ // types come along: an override that names a second skin decides which one the entity has on.
410
+ const itemTypes = await readItemTypes(carried, options);
411
+ const entitiesUpdated = [];
412
+ for (const map of maps) {
413
+ const { entities, changed } = applyDefaultsToEntities(map.entities, running, itemTypes);
414
+ if (changed === 0) continue;
415
+ await CyberiaMap.updateOne({ _id: map._id }, { $set: { entities } });
416
+ entitiesUpdated.push({ mapCode: map.code, entities: changed });
417
+ }
418
+
419
+ if (linked.length > 0 || dropped.length > 0) {
420
+ // A conf written before skills were derived still carries the field. It reaches nothing —
421
+ // it is off the schema, and fillInstanceConfDefaults strips it from every read that matters
422
+ // — so it is cleared here rather than in a write of its own.
423
+ await CyberiaInstanceConf.updateOne(
424
+ { _id: conf._id },
425
+ { $set: { entityDefaults, updatedAt: new Date() }, $unset: { skillConfig: '' } },
426
+ );
427
+ }
428
+ logger.info('Synced entity-type defaults', {
429
+ instanceCode,
430
+ maps: codes.length,
431
+ linked,
432
+ skipped,
433
+ dropped,
434
+ skills,
435
+ entitiesUpdated,
436
+ ...(conflicts.length > 0 ? { conflicts } : {}),
437
+ ...(duplicates.length > 0 ? { duplicates } : {}),
438
+ });
439
+ return {
440
+ instanceCode,
441
+ mapCodes: codes,
442
+ linked,
443
+ skipped,
444
+ dropped,
445
+ entityDefaults,
446
+ skills,
447
+ entitiesUpdated,
448
+ conflicts,
449
+ duplicates,
450
+ };
451
+ };
452
+
453
+ /** REST form of {@link syncInstance}: `params.instanceCode`, optional `body.mapCodes`. */
454
+ static sync = async (req, res, options) =>
455
+ await CyberiaEntityTypeDefaultService.syncInstance(
456
+ { instanceCode: req.params.instanceCode, mapCodes: req.body?.mapCodes },
457
+ options,
458
+ );
459
+
12
460
  static post = async (req, res, options) => {
13
461
  /** @type {import('./cyberia-entity-type-default.model.js').CyberiaEntityTypeDefaultModel} */
14
462
  const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
@@ -35,12 +483,27 @@ class CyberiaEntityTypeDefaultService {
35
483
  const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
36
484
  return await CyberiaEntityTypeDefault.findByIdAndUpdate(req.params.id, req.body);
37
485
  };
486
+ // Unlinks before it deletes: a conf pointing at a document that is gone is the one way this
487
+ // collection produces orphans, and the delete is where it would happen.
38
488
  static delete = async (req, res, options) => {
39
489
  /** @type {import('./cyberia-entity-type-default.model.js').CyberiaEntityTypeDefaultModel} */
40
490
  const CyberiaEntityTypeDefault = DataBaseProviderService.getModel('CyberiaEntityTypeDefault', options);
41
- if (req.params.id) return await CyberiaEntityTypeDefault.findByIdAndDelete(req.params.id);
42
- else return await CyberiaEntityTypeDefault.deleteMany();
491
+ const CyberiaInstanceConf = DataBaseProviderService.getModel('CyberiaInstanceConf', options);
492
+ if (req.params.id) {
493
+ const id = String(req.params.id);
494
+ const { modifiedCount } = await CyberiaInstanceConf.updateMany(
495
+ { entityDefaults: id },
496
+ { $pull: { entityDefaults: id }, $set: { updatedAt: new Date() } },
497
+ );
498
+ if (modifiedCount > 0) logger.info('Unlinked deleted entity-type default', { id, confs: modifiedCount });
499
+ return await CyberiaEntityTypeDefault.findByIdAndDelete(id);
500
+ }
501
+ await CyberiaInstanceConf.updateMany(
502
+ { entityDefaults: { $ne: [] } },
503
+ { $set: { entityDefaults: [], updatedAt: new Date() } },
504
+ );
505
+ return await CyberiaEntityTypeDefault.deleteMany();
43
506
  };
44
507
  }
45
508
 
46
- export { CyberiaEntityTypeDefaultService };
509
+ export { applyDefaultsToEntities, buildKey, findDuplicateBuilds, CyberiaEntityTypeDefaultService, toEntityDefault };