cyberia 3.2.12 → 3.2.70

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 (194) hide show
  1. package/.env.example +127 -68
  2. package/.github/workflows/cyberia-client.cd.yml +40 -0
  3. package/.github/workflows/cyberia-server.cd.yml +40 -0
  4. package/.github/workflows/docker-image.cyberia-client.ci.yml +49 -0
  5. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +48 -0
  6. package/.github/workflows/docker-image.cyberia-server.ci.yml +69 -0
  7. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +69 -0
  8. package/.github/workflows/docker-image.engine-cyberia.ci.yml +52 -0
  9. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +52 -0
  10. package/.github/workflows/engine-cyberia.cd.yml +34 -24
  11. package/.github/workflows/engine-cyberia.ci.yml +27 -5
  12. package/.github/workflows/ghpkg.ci.yml +89 -1
  13. package/.github/workflows/gitlab.ci.yml +1 -1
  14. package/.github/workflows/hardhat.ci.yml +1 -1
  15. package/.github/workflows/npmpkg.ci.yml +19 -12
  16. package/.github/workflows/publish.ci.yml +2 -2
  17. package/.github/workflows/publish.cyberia.ci.yml +5 -16
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -8
  19. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  20. package/CHANGELOG.md +451 -1
  21. package/CLI-HELP.md +1040 -1130
  22. package/Dockerfile +141 -43
  23. package/Dockerfile.dev +143 -0
  24. package/Dockerfile.test +165 -0
  25. package/README.md +1 -1
  26. package/baremetal/commission-workflows.json +1 -0
  27. package/bin/build.js +128 -136
  28. package/bin/build.template.js +25 -179
  29. package/bin/cyberia.js +1089 -188
  30. package/bin/deploy.js +6 -3
  31. package/bin/index.js +1089 -188
  32. package/bump.config.js +1 -0
  33. package/compose.env +131 -0
  34. package/conf.js +19 -1
  35. package/deployment.yaml +74 -2
  36. package/docker-compose.yml +316 -0
  37. package/hardhat/hardhat.config.js +2 -2
  38. package/hardhat/package-lock.json +620 -2041
  39. package/hardhat/package.json +7 -5
  40. package/hardhat/scripts/deployObjectLayerToken.js +18 -18
  41. package/hardhat/test/ObjectLayerToken.js +378 -274
  42. package/ipfs/configure-ipfs.sh +13 -0
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  44. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +74 -2
  46. package/manifests/deployment/dd-cyberia-development/grpc-service.yaml +17 -0
  47. package/manifests/deployment/dd-cyberia-development/pv-pvc.yaml +32 -0
  48. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  49. package/mongodb/entrypoint.sh +76 -0
  50. package/nginx.conf +87 -0
  51. package/package.json +34 -22
  52. package/pv-pvc.yaml +32 -0
  53. package/scripts/disk-clean.sh +85 -60
  54. package/scripts/kubeadm-node-setup.sh +317 -0
  55. package/scripts/link-local-underpost-cli.sh +6 -0
  56. package/scripts/rocky-kickstart.sh +877 -185
  57. package/scripts/rpmfusion-ffmpeg-setup.sh +26 -50
  58. package/scripts/test-monitor.sh +248 -0
  59. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +43 -7
  60. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +17 -25
  61. package/src/api/cyberia-action/cyberia-action.controller.js +19 -0
  62. package/src/api/cyberia-action/cyberia-action.model.js +21 -29
  63. package/src/api/cyberia-action/cyberia-action.router.js +42 -7
  64. package/src/api/cyberia-action/cyberia-action.service.js +20 -4
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +58 -57
  66. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +37 -6
  67. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +8 -2
  68. package/src/api/cyberia-entity/cyberia-entity.router.js +39 -7
  69. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +74 -0
  70. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +67 -0
  71. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +63 -0
  72. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +46 -0
  73. package/src/api/cyberia-instance/cyberia-fallback-world.js +62 -10
  74. package/src/api/cyberia-instance/cyberia-instance.model.js +32 -2
  75. package/src/api/cyberia-instance/cyberia-instance.router.js +6 -6
  76. package/src/api/cyberia-instance/cyberia-world-generator.js +116 -3
  77. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +3 -0
  78. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +39 -7
  79. package/src/api/cyberia-map/cyberia-map.router.js +21 -6
  80. package/src/api/cyberia-quest/cyberia-quest.controller.js +38 -0
  81. package/src/api/cyberia-quest/cyberia-quest.router.js +47 -7
  82. package/src/api/cyberia-quest/cyberia-quest.service.js +59 -4
  83. package/src/api/cyberia-saga/cyberia-saga.controller.js +74 -0
  84. package/src/api/cyberia-saga/cyberia-saga.model.js +59 -0
  85. package/src/api/cyberia-saga/cyberia-saga.router.js +63 -0
  86. package/src/api/cyberia-saga/cyberia-saga.service.js +42 -0
  87. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +551 -122
  88. package/src/api/cyberia-skill/cyberia-skill.controller.js +74 -0
  89. package/src/api/cyberia-skill/cyberia-skill.model.js +50 -0
  90. package/src/api/cyberia-skill/cyberia-skill.router.js +63 -0
  91. package/src/api/cyberia-skill/cyberia-skill.service.js +42 -0
  92. package/src/api/ipfs/ipfs.service.js +28 -15
  93. package/src/api/object-layer/object-layer.router.js +25 -15
  94. package/src/api/object-layer/object-layer.service.js +15 -20
  95. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +39 -7
  96. package/src/cli/baremetal.js +717 -16
  97. package/src/cli/cluster.js +80 -5
  98. package/src/cli/deploy.js +326 -292
  99. package/src/cli/docker-compose.js +648 -0
  100. package/src/cli/env.js +12 -6
  101. package/src/cli/fs.js +75 -30
  102. package/src/cli/image.js +187 -55
  103. package/src/cli/index.js +157 -6
  104. package/src/cli/kickstart.js +142 -20
  105. package/src/cli/monitor.js +387 -6
  106. package/src/cli/release.js +72 -15
  107. package/src/cli/repository.js +339 -40
  108. package/src/cli/run.js +678 -186
  109. package/src/cli/secrets.js +82 -48
  110. package/src/cli/ssh.js +234 -0
  111. package/src/cli/static.js +2 -2
  112. package/src/client/components/core/PanelForm.js +44 -44
  113. package/src/client/components/cyberia/ActionEngineCyberia.js +1867 -0
  114. package/src/client/components/cyberia/EntityEngineCyberia.js +585 -0
  115. package/src/client/components/cyberia/InstanceEngineCyberia.js +219 -13
  116. package/src/client/components/cyberia/MapEngineCyberia.js +154 -71
  117. package/src/client/components/cyberia/ObjectLayerEngineModal.js +40 -63
  118. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +34 -20
  119. package/src/client/components/cyberia/SharedDefaultsCyberia.js +195 -4
  120. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +66 -0
  121. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +8 -0
  122. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +8 -0
  123. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +92 -20
  124. package/src/client/public/cyberia-docs/ARCHITECTURE.md +272 -50
  125. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +3 -3
  126. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +39 -22
  127. package/src/client/public/cyberia-docs/CYBERIA-LORE.md +88 -0
  128. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +394 -0
  129. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +28 -12
  130. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  131. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +27 -5
  132. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  133. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  134. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +99 -0
  135. package/src/client/services/cyberia-instance/cyberia-instance.management.js +2 -2
  136. package/src/client/services/cyberia-map/cyberia-map.management.js +2 -2
  137. package/src/client/services/cyberia-saga/cyberia-saga.service.js +99 -0
  138. package/src/client/services/cyberia-skill/cyberia-skill.service.js +99 -0
  139. package/src/client/services/object-layer/object-layer.management.js +6 -6
  140. package/src/{server → client-builder}/client-build-docs.js +15 -5
  141. package/src/{server → client-builder}/client-build-live.js +3 -3
  142. package/src/{server → client-builder}/client-build.js +25 -22
  143. package/src/{server → client-builder}/client-dev-server.js +3 -3
  144. package/src/{server → client-builder}/client-icons.js +2 -2
  145. package/src/{server → client-builder}/ssr.js +5 -5
  146. package/src/client.build.js +1 -3
  147. package/src/client.dev.js +1 -1
  148. package/src/db/mongo/MongoBootstrap.js +12 -12
  149. package/src/db/mongo/MongooseDB.js +2 -1
  150. package/src/grpc/cyberia/grpc-server.js +255 -70
  151. package/src/index.js +12 -1
  152. package/src/mailer/EmailRender.js +1 -1
  153. package/src/{server → projects/cyberia}/atlas-sprite-sheet-generator.js +2 -2
  154. package/src/{server → projects/cyberia}/besu-genesis-generator.js +3 -3
  155. package/src/projects/cyberia/catalog-cyberia.js +81 -0
  156. package/src/projects/cyberia/gemini-client.js +175 -0
  157. package/src/projects/cyberia/generate-saga.js +2107 -0
  158. package/src/{server → projects/cyberia}/ipfs-client.js +6 -4
  159. package/src/{server → projects/cyberia}/object-layer.js +12 -108
  160. package/src/{server → projects/cyberia}/semantic-layer-generator-floor.js +1 -1
  161. package/src/{server → projects/cyberia}/semantic-layer-generator-resource.js +1 -1
  162. package/src/{server → projects/cyberia}/semantic-layer-generator-skin.js +1 -1
  163. package/src/{server → projects/cyberia}/semantic-layer-generator.js +2 -2
  164. package/src/{server → projects/cyberia}/shape-generator.js +2 -2
  165. package/src/projects/underpost/catalog-underpost.js +60 -0
  166. package/src/runtime/cyberia-client/Dockerfile +31 -83
  167. package/src/runtime/cyberia-client/Dockerfile.dev +23 -30
  168. package/src/runtime/cyberia-server/Dockerfile +23 -42
  169. package/src/runtime/cyberia-server/Dockerfile.dev +20 -35
  170. package/src/runtime/engine-cyberia/Dockerfile +143 -0
  171. package/src/runtime/engine-cyberia/Dockerfile.dev +143 -0
  172. package/src/runtime/engine-cyberia/Dockerfile.test +165 -0
  173. package/src/runtime/engine-cyberia/compose.env +131 -0
  174. package/src/runtime/engine-cyberia/docker-compose.yml +316 -0
  175. package/src/runtime/engine-cyberia/ipfs/configure-ipfs.sh +13 -0
  176. package/src/runtime/engine-cyberia/mongodb/entrypoint.sh +76 -0
  177. package/src/runtime/engine-cyberia/nginx.conf +87 -0
  178. package/src/runtime/express/Express.js +2 -2
  179. package/src/runtime/nginx/Nginx.js +250 -0
  180. package/src/runtime/wp/Dockerfile +3 -3
  181. package/src/server/catalog.js +72 -0
  182. package/src/server/conf.js +415 -60
  183. package/src/server/runtime-status.js +252 -0
  184. package/src/server/start.js +42 -11
  185. package/src/server.js +6 -2
  186. package/test/cyberia-instance-conf-defaults.test.js +140 -0
  187. package/test/deploy-monitor.test.js +267 -0
  188. package/test/shape-generator.test.js +7 -1
  189. package/typedoc.dd-cyberia.json +3 -1
  190. package/typedoc.json +3 -1
  191. package/manifests/deployment/dd-test-development/deployment.yaml +0 -256
  192. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  193. /package/src/client/ssr/{Render.js → RootDocument.js} +0 -0
  194. /package/src/{server → client-builder}/client-formatted.js +0 -0
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Module for managing server side rendering
3
- * @module src/server/ssr.js
3
+ * @module src/client-builder/ssr.js
4
4
  * @namespace ServerSideRendering
5
5
  */
6
6
 
@@ -10,19 +10,19 @@ import vm from 'node:vm';
10
10
  import Underpost from '../index.js';
11
11
 
12
12
  import { srcFormatted, JSONweb } from './client-formatted.js';
13
- import { loggerFactory } from './logger.js';
14
- import { getRootDirectory } from './process.js';
13
+ import { loggerFactory } from '../server/logger.js';
14
+ import { getRootDirectory } from '../server/process.js';
15
15
 
16
16
  const logger = loggerFactory(import.meta);
17
17
 
18
18
  /**
19
19
  * Creates a server-side rendering component function from a given file path.
20
20
  * It reads the component file, formats it, and executes it in a sandboxed Node.js VM context to extract the component.
21
- * @param {string} [componentPath='./src/client/ssr/Render.js'] - The path to the SSR component file.
21
+ * @param {string} [componentPath='./src/client/ssr/RootDocument.js'] - The path to the SSR component file.
22
22
  * @returns {Promise<Function>} A promise that resolves to the SSR component function.
23
23
  * @memberof ServerSideRendering
24
24
  */
25
- const ssrFactory = async (componentPath = `./src/client/ssr/Render.js`) => {
25
+ const ssrFactory = async (componentPath = `./src/client/ssr/RootDocument.js`) => {
26
26
  const context = { SrrComponent: () => {}, npm_package_version: Underpost.version };
27
27
  vm.createContext(context);
28
28
  vm.runInContext(await srcFormatted(fs.readFileSync(componentPath, 'utf8')), context);
@@ -6,7 +6,7 @@
6
6
  import { loggerFactory } from './server/logger.js';
7
7
  import { Config } from './server/conf.js';
8
8
  import { ProcessController } from './server/process.js';
9
- import { clientLiveBuild } from './server/client-build-live.js';
9
+ import { clientLiveBuild } from './client-builder/client-build-live.js';
10
10
 
11
11
  await Config.build();
12
12
 
@@ -15,5 +15,3 @@ const logger = loggerFactory(import.meta);
15
15
  await logger.setUpInfo();
16
16
 
17
17
  await clientLiveBuild();
18
-
19
- // ProcessController.init(logger);
package/src/client.dev.js CHANGED
@@ -6,7 +6,7 @@
6
6
  import { loggerFactory } from './server/logger.js';
7
7
  import { ProcessController } from './server/process.js';
8
8
  import { Config, buildClientStaticConf } from './server/conf.js';
9
- import { createClientDevServer } from './server/client-dev-server.js';
9
+ import { createClientDevServer } from './client-builder/client-dev-server.js';
10
10
 
11
11
  const logger = loggerFactory(import.meta);
12
12
 
@@ -290,19 +290,16 @@ class MongoBootstrap {
290
290
  * @returns {Promise<number>} Number of pods that failed to become ready (0 = all good).
291
291
  */
292
292
  static async waitForPods(namespace, replicaCount) {
293
- const { default: Underpost } = await import('../../index.js');
294
- const results = await Promise.all(
295
- Array.from({ length: replicaCount }, async (_, i) => {
296
- const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
297
- const ready = await Underpost.test.statusMonitor(podName, 'Running', 'pods', 1000, 60 * 10);
298
- return { index: i, ready };
299
- }),
300
- );
301
- const failed = results.filter((r) => !r.ready).map((r) => `${MONGODB_STATEFULSET_NAME}-${r.index}`);
302
- if (failed.length > 0) {
303
- logger.error('MongoDB pods did not become ready', { failed });
293
+ let failedCount = 0;
294
+ for (let i = 0; i < replicaCount; i++) {
295
+ const podName = `${MONGODB_STATEFULSET_NAME}-${i}`;
296
+ const result = shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
297
+ if (result.code !== 0) {
298
+ logger.error(`Pod ${podName} did not become ready`);
299
+ failedCount++;
300
+ }
304
301
  }
305
- return failed.length;
302
+ return failedCount;
306
303
  }
307
304
 
308
305
  /**
@@ -620,6 +617,9 @@ class MongoBootstrap {
620
617
  process.env.DB_PASSWORD ||
621
618
  readTrimmedFile('./engine-private/mongodb-password');
622
619
 
620
+ // Ensure the pod is ready before querying
621
+ shellExec(`kubectl wait --for=condition=Ready pod/${podName} -n ${namespace} --timeout=60s`);
622
+
623
623
  const evalExpr = 'rs.status().members.filter(m=>m.stateStr=="PRIMARY").map(m=>m.name)';
624
624
 
625
625
  const cli = disableAuth ? 'mongo' : 'mongosh';
@@ -86,7 +86,8 @@ class MongooseDBService {
86
86
 
87
87
  const user = config.user || process.env.DB_USER || '';
88
88
  const password = config.password || process.env.DB_PASSWORD || '';
89
- const directConnection = hosts.length === 1;
89
+ const hasExplicitReplicaSet = !!(config.replicaSet || process.env.DB_REPLICA_SET);
90
+ const directConnection = hosts.length === 1 && !hasExplicitReplicaSet;
90
91
  const replicaSet = directConnection
91
92
  ? ''
92
93
  : config.replicaSet || process.env.DB_REPLICA_SET || MONGODB_DEFAULT_REPLICA_SET;
@@ -17,11 +17,12 @@ import { DataBaseProviderService } from '../../db/DataBaseProvider.js';
17
17
  import { loggerFactory } from '../../server/logger.js';
18
18
  import {
19
19
  CYBERIA_INSTANCE_CONF_DEFAULTS as FALLBACK_CONFIG_DEFAULTS,
20
+ DEFAULT_PLAYER_SPAWN,
20
21
  ENTITY_TYPE_DEFAULTS,
21
- // STATUS_ICONS deliberately not imported here — see toInstanceConfig.
22
- // Server simulation only cares about the numeric u8 IDs (which travel on
23
- // the AOI wire). Icon stems + border colours live in SharedDefaultsCyberia.
22
+ DefaultCyberiaActions,
23
+ DefaultCyberiaQuests,
24
24
  } from '../../api/cyberia-server-defaults/cyberia-server-defaults.js';
25
+ import { DefaultCyberiaItems } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
25
26
  import { generateFallbackWorld } from '../../api/cyberia-instance/cyberia-fallback-world.js';
26
27
 
27
28
  const logger = loggerFactory(import.meta);
@@ -53,20 +54,6 @@ function getModels(dbKey) {
53
54
  return bucket.models;
54
55
  }
55
56
 
56
- function countSharedItemIds(source = [], target = []) {
57
- if (!Array.isArray(source) || source.length === 0 || !Array.isArray(target) || target.length === 0) {
58
- return 0;
59
- }
60
- const targetSet = new Set(target.filter(Boolean));
61
- let count = 0;
62
- for (const itemId of source) {
63
- if (targetSet.has(itemId)) {
64
- count += 1;
65
- }
66
- }
67
- return count;
68
- }
69
-
70
57
  function normalizeEntityDefault(entityDefault = {}, canonical = {}) {
71
58
  const defaultObjectLayers = entityDefault.defaultObjectLayers ?? canonical.defaultObjectLayers ?? [];
72
59
  return {
@@ -79,68 +66,140 @@ function normalizeEntityDefault(entityDefault = {}, canonical = {}) {
79
66
  active: !!ol.active,
80
67
  quantity: ol.quantity || 0,
81
68
  })),
69
+ behavior: entityDefault.behavior ?? canonical.behavior ?? '',
82
70
  };
83
71
  }
84
72
 
85
- function selectCanonicalEntityDefaultIndex(entityDefault, canonicalDefaults, usedIndexes) {
86
- const pickBestIndex = (candidateIndexes) => {
87
- let firstSameTypeIndex = -1;
88
- let bestLiveOverlapIndex = -1;
89
- let bestLiveOverlap = 0;
90
- let bestLiveItemCount = Number.POSITIVE_INFINITY;
91
-
92
- for (const index of candidateIndexes) {
93
- const canonical = canonicalDefaults[index];
94
- if (canonical.entityType !== entityDefault.entityType) {
95
- continue;
96
- }
97
- if (firstSameTypeIndex === -1) {
98
- firstSameTypeIndex = index;
99
- }
100
- const liveOverlap = countSharedItemIds(entityDefault.liveItemIds, canonical.liveItemIds);
101
- if (
102
- liveOverlap > 0 &&
103
- (bestLiveOverlapIndex === -1 ||
104
- liveOverlap > bestLiveOverlap ||
105
- (liveOverlap === bestLiveOverlap && (canonical.liveItemIds?.length ?? 0) < bestLiveItemCount))
106
- ) {
107
- bestLiveOverlapIndex = index;
108
- bestLiveOverlap = liveOverlap;
109
- bestLiveItemCount = canonical.liveItemIds?.length ?? 0;
73
+ // Merge instance-level itemIds flagged `defaultPlayerInventory` into the player
74
+ // entityDefault's defaultObjectLayers so cyberia-server seeds them into every new
75
+ // player's inventory (see handlers.go player spawn). Items are added inactive —
76
+ // present in the inventory, not auto-equipped — so they never violate the
77
+ // one-active-per-type equipment rule.
78
+ //
79
+ // Also **removes** items that are flagged `defaultPlayerInventory: false` from
80
+ // the player's defaultObjectLayers. This ensures the instance's explicit intent
81
+ // is honoured even when the conf's entityDefaults (e.g. from a backup/import)
82
+ // baked those items into the player layer list.
83
+ function applyInstanceDefaultPlayerInventory(config, instanceItemIds = []) {
84
+ const entries = instanceItemIds || [];
85
+
86
+ // ── 1. Remove items explicitly flagged as NOT default player inventory ─────
87
+ const removeIds = new Set(
88
+ entries.filter((entry) => entry && entry.defaultPlayerInventory === false && entry.id).map((entry) => entry.id),
89
+ );
90
+ if (removeIds.size > 0) {
91
+ for (const ed of config.entityDefaults || []) {
92
+ if (ed.defaultObjectLayers) {
93
+ ed.defaultObjectLayers = ed.defaultObjectLayers.filter((ol) => !removeIds.has(ol.itemId));
110
94
  }
111
95
  }
96
+ }
112
97
 
113
- if (bestLiveOverlapIndex !== -1) {
114
- return bestLiveOverlapIndex;
115
- }
116
- return firstSameTypeIndex;
117
- };
98
+ // ── 2. Add items flagged as default player inventory ──────────────────────
99
+ const addIds = entries.filter((entry) => entry && entry.defaultPlayerInventory && entry.id).map((entry) => entry.id);
100
+ if (addIds.length === 0) return config;
118
101
 
119
- const unusedCandidateIndexes = canonicalDefaults.map((_, index) => index).filter((index) => !usedIndexes.has(index));
102
+ const playerDefault = (config.entityDefaults || []).find((d) => d.entityType === 'player');
103
+ if (!playerDefault) return config;
120
104
 
121
- const preferredIndex = pickBestIndex(unusedCandidateIndexes);
122
- if (preferredIndex !== -1) {
123
- return preferredIndex;
105
+ playerDefault.defaultObjectLayers = playerDefault.defaultObjectLayers || [];
106
+ const existing = new Set(playerDefault.defaultObjectLayers.map((ol) => ol.itemId));
107
+ for (const id of addIds) {
108
+ if (existing.has(id)) continue;
109
+ playerDefault.defaultObjectLayers.push({ itemId: id, active: false, quantity: 1 });
110
+ existing.add(id);
124
111
  }
125
- return pickBestIndex(canonicalDefaults.map((_, index) => index));
112
+ return config;
126
113
  }
127
114
 
128
- function mergeEntityDefaults(entityDefaults = []) {
129
- const mergedDefaults = ENTITY_TYPE_DEFAULTS.map((canonical) => normalizeEntityDefault(canonical, canonical));
130
- const usedCanonicalIndexes = new Set();
131
-
132
- for (const entityDefault of entityDefaults) {
133
- const canonicalIndex = selectCanonicalEntityDefaultIndex(entityDefault, ENTITY_TYPE_DEFAULTS, usedCanonicalIndexes);
134
- if (canonicalIndex === -1) {
135
- mergedDefaults.push(normalizeEntityDefault(entityDefault));
136
- continue;
137
- }
115
+ // Map CyberiaSkill collection docs to the proto skillConfig shape. The skill
116
+ // collection is the authoritative own-model source: it carries full skill
117
+ // metadata (summonedEntityItemId, name, description) that the instance-conf
118
+ // skillConfig schema deliberately does not store.
119
+ function skillDocsToConfig(skillDocs = []) {
120
+ return skillDocs
121
+ .filter((d) => d && d.triggerItemId)
122
+ .map((d) => ({
123
+ triggerItemId: d.triggerItemId,
124
+ skills: (d.skills || []).map((sk) => ({
125
+ logicEventId: sk.logicEventId || '',
126
+ name: sk.name || '',
127
+ description: sk.description || '',
128
+ summonedEntityItemId: sk.summonedEntityItemId || '',
129
+ })),
130
+ }));
131
+ }
132
+
133
+ // Load authoritative skill configs from the CyberiaSkill collection. Returns []
134
+ // when the model is not loaded (api not mounted) or the collection is empty, so
135
+ // callers transparently keep the conf / fallback skillConfig.
136
+ async function loadSkillConfigDocs(models) {
137
+ if (!models.CyberiaSkill) return [];
138
+ return await models.CyberiaSkill.find({}).lean();
139
+ }
140
+
141
+ // Map CyberiaEntityTypeDefault collection docs to the proto entityDefaults shape.
142
+ // This collection is the authoritative own-model source for per-entity-type item
143
+ // defaults (live/dead/drop + seed object layers). Resolution is by liveItemIds
144
+ // membership, so every variant (e.g. each resource skin) must travel as its own
145
+ // entry — never collapsed by entityType.
146
+ function entityTypeDefaultDocsToConfig(docs = []) {
147
+ return docs
148
+ .filter((d) => d && d.entityType)
149
+ .map((d) => ({
150
+ entityType: d.entityType,
151
+ liveItemIds: [...(d.liveItemIds || [])],
152
+ deadItemIds: [...(d.deadItemIds || [])],
153
+ dropItemIds: [...(d.dropItemIds || [])],
154
+ defaultObjectLayers: (d.defaultObjectLayers || []).map((ol) => ({
155
+ itemId: ol.itemId,
156
+ active: !!ol.active,
157
+ quantity: ol.quantity || 0,
158
+ })),
159
+ behavior: d.behavior || '',
160
+ }));
161
+ }
162
+
163
+ // Load authoritative entity-type defaults from the CyberiaEntityTypeDefault
164
+ // collection. Returns [] when the model is not mounted or empty, so callers
165
+ // transparently keep the conf / canonical entityDefaults.
166
+ async function loadEntityTypeDefaultDocs(models) {
167
+ if (!models.CyberiaEntityTypeDefault) return [];
168
+ return await models.CyberiaEntityTypeDefault.find({}).lean();
169
+ }
170
+
171
+ // Feed every live/dead/drop and default-object-layer item id from the collection
172
+ // defaults into the atlas id set so the Go server resolves their sprites even
173
+ // when the entity appears in no map (e.g. a new resource skin).
174
+ function addEntityDefaultAtlasIds(entityDefaults, atlasItemIds) {
175
+ for (const d of entityDefaults || []) {
176
+ for (const id of d.liveItemIds || []) if (id) atlasItemIds.add(id);
177
+ for (const id of d.deadItemIds || []) if (id) atlasItemIds.add(id);
178
+ for (const id of d.dropItemIds || []) if (id) atlasItemIds.add(id);
179
+ for (const ol of d.defaultObjectLayers || []) if (ol.itemId) atlasItemIds.add(ol.itemId);
180
+ }
181
+ }
138
182
 
139
- mergedDefaults[canonicalIndex] = normalizeEntityDefault(entityDefault, ENTITY_TYPE_DEFAULTS[canonicalIndex]);
140
- usedCanonicalIndexes.add(canonicalIndex);
183
+ // Feed skill trigger items and every concrete summoned-entity id into the atlas
184
+ // id set so the Go server resolves their sprites even when the entity appears in
185
+ // no map. Runtime placeholders ('$active_skin', …) are excluded.
186
+ function addSkillAtlasIds(skillDocs, atlasItemIds) {
187
+ for (const d of skillDocs || []) {
188
+ if (d.triggerItemId) atlasItemIds.add(d.triggerItemId);
189
+ for (const sk of d.skills || []) {
190
+ const id = sk.summonedEntityItemId;
191
+ if (id && !id.startsWith('$')) atlasItemIds.add(id);
192
+ }
141
193
  }
194
+ }
142
195
 
143
- return mergedDefaults;
196
+ function mergeEntityDefaults(entityDefaults = []) {
197
+ const merged = entityDefaults.map((entityDefault) => normalizeEntityDefault(entityDefault));
198
+ const coveredTypes = new Set(merged.map((entityDefault) => entityDefault.entityType));
199
+ for (const canonical of ENTITY_TYPE_DEFAULTS) {
200
+ if (!coveredTypes.has(canonical.entityType)) merged.push(normalizeEntityDefault(canonical, canonical));
201
+ }
202
+ return merged;
144
203
  }
145
204
 
146
205
  // ── Mongoose doc → protobuf message converters ───────────────────
@@ -248,6 +307,52 @@ function toInstanceMsg(doc) {
248
307
  })),
249
308
  topologyMode: doc.topologyMode || 'hybrid',
250
309
  seed: doc.seed || '',
310
+ playerSpawn: {
311
+ sourceMapCode: doc.playerSpawn?.sourceMapCode || '',
312
+ sourceCellX: doc.playerSpawn?.sourceCellX || 0,
313
+ sourceCellY: doc.playerSpawn?.sourceCellY || 0,
314
+ random: !!doc.playerSpawn?.random,
315
+ },
316
+ };
317
+ }
318
+
319
+ // CyberiaAction → CyberiaActionMessage (proto camelCase fields).
320
+ function toActionMsg(a) {
321
+ return {
322
+ code: a.code || '',
323
+ label: a.label || '',
324
+ sourceMapCode: a.sourceMapCode || '',
325
+ sourceCellX: a.sourceCellX || 0,
326
+ sourceCellY: a.sourceCellY || 0,
327
+ dialogCode: a.dialogCode || '',
328
+ questDialogueCodes: (a.questDialogueCodes || []).map((qd) => ({
329
+ questCode: qd.questCode || '',
330
+ dialogCode: qd.dialogCode || '',
331
+ })),
332
+ };
333
+ }
334
+
335
+ // CyberiaQuest → CyberiaQuestMessage.
336
+ function toQuestMsg(q) {
337
+ return {
338
+ code: q.code || '',
339
+ title: q.title || '',
340
+ description: q.description || '',
341
+ unlocksQuestCodes: q.unlocksQuestCodes || [],
342
+ prerequisiteCodes: q.prerequisiteCodes || [],
343
+ sourceMapCode: q.sourceMapCode || '',
344
+ sourceCellX: q.sourceCellX || 0,
345
+ sourceCellY: q.sourceCellY || 0,
346
+ steps: (q.steps || []).map((s) => ({
347
+ id: s.id || '',
348
+ description: s.description || '',
349
+ objectives: (s.objectives || []).map((o) => ({
350
+ type: o.type || '',
351
+ itemId: o.itemId || '',
352
+ quantity: o.quantity || 1,
353
+ })),
354
+ })),
355
+ rewards: (q.rewards || []).map((r) => ({ itemId: r.itemId || '', quantity: r.quantity || 1 })),
251
356
  };
252
357
  }
253
358
 
@@ -445,11 +550,34 @@ function buildHandlers(dbKey) {
445
550
  if (ol.itemId) fallbackItemIds.add(ol.itemId);
446
551
  }
447
552
  }
553
+ // Include every canonical item so the Go server can resolve the type
554
+ // of anything a player may pick up or equip (quest rewards like
555
+ // hatchet, alt weapons, etc.) — required for the one-per-type
556
+ // equipment rule, which silently no-ops when item type is unknown.
557
+ for (const it of DefaultCyberiaItems || []) {
558
+ if (it?.item?.id) fallbackItemIds.add(it.item.id);
559
+ }
560
+
561
+ // Own-model skills (if seeded) override the fallback skillConfig; their
562
+ // summoned-entity atlases are resolved alongside the world's.
563
+ const fallbackSkillDocs = await loadSkillConfigDocs(models);
564
+ addSkillAtlasIds(fallbackSkillDocs, fallbackItemIds);
565
+
566
+ // Own-model entity defaults (if seeded) override the fallback
567
+ // entityDefaults; their live/dead/drop atlases resolve alongside.
568
+ const fallbackEntityDefaultDocs = await loadEntityTypeDefaultDocs(models);
569
+ const fallbackEntityDefaultsConfig = entityTypeDefaultDocsToConfig(fallbackEntityDefaultDocs);
570
+ addEntityDefaultAtlasIds(fallbackEntityDefaultsConfig, fallbackItemIds);
448
571
 
449
572
  const fallbackOlDocs = fallbackItemIds.size
450
573
  ? await models.ObjectLayer.find({ 'data.item.id': { $in: [...fallbackItemIds] } }).lean()
451
574
  : [];
452
575
 
576
+ const fallbackConfig = toInstanceConfig(fallbackConf);
577
+ if (fallbackEntityDefaultsConfig.length)
578
+ fallbackConfig.entityDefaults = mergeEntityDefaults(fallbackEntityDefaultsConfig);
579
+ if (fallbackSkillDocs.length) fallbackConfig.skillConfig = skillDocsToConfig(fallbackSkillDocs);
580
+
453
581
  callback(null, {
454
582
  instance: toInstanceMsg({
455
583
  _id: '',
@@ -461,6 +589,7 @@ function buildHandlers(dbKey) {
461
589
  portals: world.portals,
462
590
  topologyMode: 'procedural',
463
591
  seed: instanceCode,
592
+ playerSpawn: world.instance.playerSpawn,
464
593
  }),
465
594
  maps: world.maps.map((m) => ({
466
595
  mongoId: '',
@@ -473,8 +602,12 @@ function buildHandlers(dbKey) {
473
602
  entities: (m.entities || []).map(toEntityMsg),
474
603
  })),
475
604
  objectLayers: fallbackOlDocs.map(toObjectLayerMsg),
476
- config: toInstanceConfig(fallbackConf),
605
+ config: fallbackConfig,
477
606
  version: 'fallback',
607
+ // Mission content for the procedural fallback comes straight from
608
+ // the canonical defaults — no DB, fully self-contained.
609
+ actions: DefaultCyberiaActions.map(toActionMsg),
610
+ quests: DefaultCyberiaQuests.map(toQuestMsg),
478
611
  });
479
612
  return;
480
613
  }
@@ -506,9 +639,23 @@ function buildHandlers(dbKey) {
506
639
  }
507
640
  }
508
641
 
509
- // Include system OL items from BOTH canonical ENTITY_TYPE_DEFAULTS AND
510
- // the DB conf so the Go server always caches all default atlases even if
511
- // the DB doc has incomplete entityDefaults.
642
+ // Instance-level itemIds (e.g. default-player-inventory items) may not
643
+ // appear in any map entity include them so their atlases resolve.
644
+ // Tolerate legacy flat-string entries alongside the { id, } shape.
645
+ for (const entry of inst.itemIds || []) {
646
+ const id = typeof entry === 'string' ? entry : entry?.id;
647
+ if (id) itemIds.add(id);
648
+ }
649
+
650
+ // Authoritative per-entity-type defaults come from the
651
+ // CyberiaEntityTypeDefault collection (own model); empty collection leaves
652
+ // the conf/canonical defaults in place.
653
+ const entityDefaultDocs = await loadEntityTypeDefaultDocs(models);
654
+ const entityDefaultsConfig = entityTypeDefaultDocsToConfig(entityDefaultDocs);
655
+
656
+ // Include system OL items from canonical ENTITY_TYPE_DEFAULTS, the DB conf,
657
+ // AND the collection so the Go server always caches every default atlas
658
+ // even when an entity (e.g. a new resource skin) appears in no map.
512
659
  for (const d of [...ENTITY_TYPE_DEFAULTS, ...(conf.entityDefaults || [])]) {
513
660
  for (const id of d.liveItemIds || []) itemIds.add(id);
514
661
  for (const id of d.deadItemIds || []) itemIds.add(id);
@@ -517,6 +664,12 @@ function buildHandlers(dbKey) {
517
664
  if (ol.itemId) itemIds.add(ol.itemId);
518
665
  }
519
666
  }
667
+ addEntityDefaultAtlasIds(entityDefaultsConfig, itemIds);
668
+
669
+ // Authoritative skills come from the CyberiaSkill collection (own model);
670
+ // resolve their atlases before the OL query so summoned entities render.
671
+ const skillDocs = await loadSkillConfigDocs(models);
672
+ addSkillAtlasIds(skillDocs, itemIds);
520
673
 
521
674
  const olDocs = itemIds.size
522
675
  ? await models.ObjectLayer.find({ 'data.item.id': { $in: [...itemIds] } }).lean()
@@ -527,14 +680,46 @@ function buildHandlers(dbKey) {
527
680
  const versionParts = [String(inst.updatedAt || inst._id)];
528
681
  for (const m of mapDocs) versionParts.push(String(m.updatedAt || m._id));
529
682
  if (conf.updatedAt) versionParts.push(String(conf.updatedAt));
683
+ // Skill edits live in their own collection — fold them in so a skill
684
+ // change triggers a Go-side world rebuild instead of serving stale skills.
685
+ for (const sk of skillDocs) versionParts.push(String(sk.updatedAt || sk._id));
686
+ // Entity-type-default edits live in their own collection too — fold them
687
+ // in so adding/editing a default (e.g. a new resource) rebuilds the world.
688
+ for (const ed of entityDefaultDocs) versionParts.push(String(ed.updatedAt || ed._id));
530
689
  const version = crypto.createHash('sha256').update(versionParts.join('|')).digest('hex');
531
690
 
691
+ // Mission content for this instance: actions and quests bound to its
692
+ // maps (by sourceMapCode). Delivered with the world so the Go server
693
+ // never opens a separate REST channel, and never receives content from
694
+ // other instances' maps.
695
+ const actionDocs =
696
+ mapCodes.length && models.CyberiaAction
697
+ ? await models.CyberiaAction.find({ sourceMapCode: { $in: mapCodes } }).lean()
698
+ : [];
699
+ const questDocs =
700
+ mapCodes.length && models.CyberiaQuest
701
+ ? await models.CyberiaQuest.find({ sourceMapCode: { $in: mapCodes } }).lean()
702
+ : [];
703
+
704
+ const baseConfig = toInstanceConfig(conf);
705
+ // Own-model entity defaults win over the conf's entityDefaults (overlaid on
706
+ // canonical via mergeEntityDefaults), so every collection variant — e.g.
707
+ // each resource skin — reaches the Go runtime keyed by liveItemIds. Applied
708
+ // BEFORE default-player-inventory so the player layer additions survive.
709
+ if (entityDefaultsConfig.length) baseConfig.entityDefaults = mergeEntityDefaults(entityDefaultsConfig);
710
+ const config = applyInstanceDefaultPlayerInventory(baseConfig, inst.itemIds);
711
+ // Own-model skills win over the conf's stripped skillConfig (which lacks
712
+ // summonedEntityItemId); empty collection leaves the conf/fallback as-is.
713
+ if (skillDocs.length) config.skillConfig = skillDocsToConfig(skillDocs);
714
+
532
715
  callback(null, {
533
716
  instance: toInstanceMsg(inst),
534
717
  maps: mapDocs.map(toMapMsg),
535
718
  objectLayers: olDocs.map(toObjectLayerMsg),
536
- config: toInstanceConfig(conf),
719
+ config,
537
720
  version,
721
+ actions: actionDocs.map(toActionMsg),
722
+ quests: questDocs.map(toQuestMsg),
538
723
  });
539
724
  } catch (err) {
540
725
  logger.error('getFullInstance:', err);
package/src/index.js CHANGED
@@ -10,6 +10,7 @@ import UnderpostKickStart from './cli/kickstart.js';
10
10
  import UnderpostCluster from './cli/cluster.js';
11
11
  import UnderpostDB from './cli/db.js';
12
12
  import UnderpostDeploy from './cli/deploy.js';
13
+ import UnderpostDockerCompose from './cli/docker-compose.js';
13
14
  import UnderpostKubectl from './cli/kubectl.js';
14
15
  import UnderpostRootEnv from './cli/env.js';
15
16
  import UnderpostFileStorage from './cli/fs.js';
@@ -44,7 +45,7 @@ class Underpost {
44
45
  * @type {String}
45
46
  * @memberof Underpost
46
47
  */
47
- static version = 'v3.2.12';
48
+ static version = 'v3.2.70';
48
49
 
49
50
  /**
50
51
  * Required Node.js major version
@@ -146,6 +147,15 @@ class Underpost {
146
147
  static get deploy() {
147
148
  return UnderpostDeploy.API;
148
149
  }
150
+ /**
151
+ * Docker Compose pipeline cli API
152
+ * @static
153
+ * @type {UnderpostDockerCompose.API}
154
+ * @memberof Underpost
155
+ */
156
+ static get dockerCompose() {
157
+ return UnderpostDockerCompose.API;
158
+ }
149
159
  /**
150
160
  * File Storage cli API
151
161
  * @static
@@ -321,6 +331,7 @@ export {
321
331
  UnderpostCluster,
322
332
  UnderpostDB,
323
333
  UnderpostDeploy,
334
+ UnderpostDockerCompose,
324
335
  UnderpostKubectl,
325
336
  UnderpostRootEnv,
326
337
  UnderpostFileStorage,
@@ -1,4 +1,4 @@
1
- import { ssrFactory } from '../server/ssr.js';
1
+ import { ssrFactory } from '../client-builder/ssr.js';
2
2
 
3
3
  /**
4
4
  * Module for handling the rendering and styling of HTML emails using SSR components.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Atlas Sprite Sheet Generator for Cyberia Online.
3
3
  * Consolidates all object layer frames (8 directions, multiple modes) into a single PNG atlas.
4
- * @module src/server/atlas-sprite-sheet-generator.js
4
+ * @module src/projects/cyberia/atlas-sprite-sheet-generator.js
5
5
  * @namespace CyberiaAtlasSpriteSheetGenerator
6
6
  */
7
7
 
@@ -9,7 +9,7 @@ import fs from 'fs-extra';
9
9
  import path from 'path';
10
10
  import sharp from 'sharp';
11
11
  import { Jimp, rgbaToInt } from 'jimp';
12
- import { loggerFactory } from './logger.js';
12
+ import { loggerFactory } from '../../server/logger.js';
13
13
 
14
14
  const logger = loggerFactory(import.meta);
15
15
 
@@ -9,7 +9,7 @@
9
9
  * This eliminates hardcoded keys/hashes in manifests/besu/ and ensures every
10
10
  * new deployment gets a clean, unique chain identity.
11
11
  *
12
- * @module src/server/besu-genesis-generator.js
12
+ * @module src/projects/cyberia/besu-genesis-generator.js
13
13
  * @namespace BesuGenesisGenerator
14
14
  */
15
15
 
@@ -17,8 +17,8 @@ import crypto from 'crypto';
17
17
  import fs from 'fs-extra';
18
18
  import path from 'path';
19
19
  import { keccak256 as ethersKeccak256 } from 'ethers';
20
- import { loggerFactory } from '../server/logger.js';
21
- import { shellExec } from '../server/process.js';
20
+ import { loggerFactory } from '../../server/logger.js';
21
+ import { shellExec } from '../../server/process.js';
22
22
 
23
23
  const logger = loggerFactory(import.meta);
24
24