@underpostnet/cyberia 3.3.73 → 3.3.76

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 (228) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +5 -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/publish.cyberia.ci.yml +44 -0
  10. package/AGENTS.md +61 -12
  11. package/CHANGELOG.md +126 -41
  12. package/CLI-HELP.md +19 -12
  13. package/Dockerfile +1 -1
  14. package/Dockerfile.dev +1 -1
  15. package/Dockerfile.test +2 -2
  16. package/README.md +28 -28
  17. package/bin/build.js +37 -17
  18. package/bin/cyberia.js +1247 -1373
  19. package/bin/index.js +1247 -1373
  20. package/compose.env +17 -5
  21. package/conf.js +7 -3
  22. package/deploy/cyberia-client/deploy.sh +10 -11
  23. package/deploy/cyberia-client/package.sh +0 -1
  24. package/deploy/cyberia-client/state.sh +3 -2
  25. package/deploy/cyberia-server/deploy.sh +10 -11
  26. package/deploy/cyberia-server/package.sh +0 -1
  27. package/deploy/cyberia-server/state.sh +3 -2
  28. package/deploy/dd-cyberia/deploy.sh +5 -5
  29. package/deploy/dd-cyberia/init.sh +15 -14
  30. package/deploy/dd-cyberia/package.sh +0 -1
  31. package/deploy/dd-cyberia/state.sh +2 -2
  32. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  33. package/deploy/lib/config.sh +29 -0
  34. package/deploy/lib/github-actions-logging.sh +92 -76
  35. package/deploy/lib/host.sh +27 -18
  36. package/deploy/lib/state.sh +4 -1
  37. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  38. package/deploy/release/deploy.sh +0 -2
  39. package/deployment.yaml +1 -1
  40. package/docker-compose.yml +23 -17
  41. package/hardhat/package-lock.json +2 -2
  42. package/hardhat/package.json +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  44. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  46. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  47. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  48. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  49. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  50. package/manifests/deployment/mongo-express/service.yaml +16 -0
  51. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  52. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  53. package/package.json +19 -17
  54. package/scripts/test-monitor.sh +3 -3
  55. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  56. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  59. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  60. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  61. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  62. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  63. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  64. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  66. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  67. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  68. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  69. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  71. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  72. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  73. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  74. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  75. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  76. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  77. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  79. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  80. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  81. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  82. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  83. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  84. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  85. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  86. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  89. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  90. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  91. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  92. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  93. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  94. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  95. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  96. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  97. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  98. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  99. package/src/api/file/file.ref.js +153 -0
  100. package/src/api/file/file.ref.json +11 -2
  101. package/src/api/object-layer/object-layer.model.js +47 -12
  102. package/src/api/object-layer/object-layer.router.js +10 -3
  103. package/src/api/object-layer/object-layer.service.js +113 -120
  104. package/src/cli/app.js +12 -1
  105. package/src/cli/client.js +2 -2
  106. package/src/cli/cluster.js +7 -0
  107. package/src/cli/deploy.js +4 -4
  108. package/src/cli/docker-compose.js +1 -1
  109. package/src/cli/fs-selection.js +115 -0
  110. package/src/cli/fs.js +158 -342
  111. package/src/cli/index.js +22 -10
  112. package/src/cli/repository.js +28 -0
  113. package/src/cli/run.js +71 -197
  114. package/src/cli/test.js +4 -0
  115. package/src/cli/wireguard.js +32 -31
  116. package/src/client/components/core/Docs.js +77 -100
  117. package/src/client/components/core/Modal.js +56 -1
  118. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  119. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  120. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  121. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  122. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  123. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  124. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  125. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  126. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  127. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  128. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  129. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  130. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  131. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  132. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  133. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  134. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  135. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  136. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  137. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  138. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  139. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  140. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  141. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  142. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  143. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  144. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  145. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  146. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  147. package/src/client/services/object-layer/object-layer.management.js +6 -10
  148. package/src/client/services/object-layer/object-layer.service.js +3 -1
  149. package/src/client-builder/client-build-docs.js +52 -52
  150. package/src/client-builder/client-build.js +67 -44
  151. package/src/client-builder/client-bundle.js +347 -0
  152. package/src/db/mongo/MongoExpress.js +187 -0
  153. package/src/grpc/cyberia/grpc-server.js +5 -7
  154. package/src/index.js +1 -1
  155. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  156. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  157. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  158. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  159. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  160. package/src/projects/cyberia/gemini-client.js +3 -3
  161. package/src/projects/cyberia/generate-saga.js +4 -11
  162. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  163. package/src/projects/cyberia/instance-backup.js +210 -0
  164. package/src/projects/cyberia/instance-data.js +177 -257
  165. package/src/projects/cyberia/map-preview-generator.js +32 -68
  166. package/src/projects/cyberia/object-layer.js +152 -319
  167. package/src/projects/cyberia/seed-audio.js +139 -0
  168. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  169. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  170. package/src/projects/cyberia/stat-balance.js +81 -0
  171. package/src/projects/cyberia/stat-commands.js +23 -0
  172. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  173. package/src/runtime/cyberia-client/Dockerfile +8 -4
  174. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  175. package/src/runtime/cyberia-server/Dockerfile +9 -2
  176. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  177. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  178. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  179. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  180. package/src/runtime/engine-cyberia/compose.env +17 -5
  181. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  182. package/src/server/build/coverage.js +113 -42
  183. package/src/server/build/package.js +55 -12
  184. package/src/server/build/testing.js +59 -9
  185. package/src/server/network/middlewares.js +8 -2
  186. package/src/server/ops/logger.js +70 -35
  187. package/src/server/runtime/conf.js +4 -2
  188. package/src/server/storage/data-query.js +16 -0
  189. package/src/server/storage/downloader.js +62 -14
  190. package/src/server/storage/zip.js +153 -0
  191. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  192. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  193. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  194. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  195. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  196. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  197. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  198. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  199. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  200. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  201. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  202. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  203. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  204. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  205. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  206. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  207. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  208. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  209. package/test/unit/client-build-docs.test.js +97 -70
  210. package/test/unit/client-bundle.test.js +313 -0
  211. package/test/unit/conf-resolution.test.js +17 -0
  212. package/test/unit/coverage-artifact.test.js +120 -29
  213. package/test/unit/cyberia/instance-backup.test.js +183 -0
  214. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  215. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  216. package/test/unit/cyberia/stat-balance.test.js +69 -0
  217. package/test/unit/deploy-log-table.test.js +76 -5
  218. package/test/unit/downloader-integrity.test.js +98 -0
  219. package/test/unit/file-reference-registry.test.js +141 -0
  220. package/test/unit/fs-storage-paths.test.js +596 -0
  221. package/test/unit/logger-redaction.test.js +29 -0
  222. package/test/unit/package.test.js +70 -16
  223. package/test/unit/prepare-host.test.js +84 -1
  224. package/test/unit/test-tiers.test.js +27 -0
  225. package/test/unit/zip-archive.test.js +142 -0
  226. package/vitest.config.js +11 -3
  227. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  228. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Mongo Express Deployment Module
3
+ * @module src/db/mongo/MongoExpress
4
+ * @namespace MongoExpress
5
+ * @description Deploys the mongo-express web client used to inspect the cluster's MongoDB
6
+ * StatefulSet collections. The workload is a plain Deployment plus a ClusterIP Service, so the
7
+ * same manifests converge identically under Kind, Kubeadm and K3s; the only runtime-dependent
8
+ * decision is whether the deployed mongod enforces authentication, which selects the overlay.
9
+ */
10
+
11
+ import { loggerFactory } from '../../server/ops/logger.js';
12
+ import { shellExec } from '../../server/runtime/process.js';
13
+ // Cyclic by construction (index -> cluster -> MongoExpress -> index), same as MongoBootstrap.js.
14
+ // Safe because the binding is only dereferenced inside method bodies, never at module scope.
15
+ import Underpost from '../../index.js';
16
+ import { MONGODB_DEFAULT_PORT, MONGODB_SERVICE_NAME, MONGODB_STATEFULSET_NAME } from './MongooseDB.js';
17
+
18
+ const logger = loggerFactory(import.meta);
19
+
20
+ const MONGO_EXPRESS_NAME = 'mongo-express';
21
+ const MONGO_EXPRESS_SERVICE_NAME = 'mongo-express-service';
22
+ const MONGO_EXPRESS_IMAGE = 'mongo-express:1.0.2';
23
+ const MONGO_EXPRESS_PORT = 8081;
24
+ const MONGO_EXPRESS_NODE_PORT = 32081;
25
+ const MONGO_EXPRESS_BASE_URL = '/mongo/';
26
+ // The UI reads the root credentials and gates its own session with the same pair, so it consumes
27
+ // the StatefulSet's secret rather than owning a second credential of its own.
28
+ const MONGO_EXPRESS_SECRET_NAME = 'mongodb-secret';
29
+ const MONGO_EXPRESS_MANIFEST_DIR = 'manifests/deployment/mongo-express';
30
+ const MONGO_EXPRESS_NO_AUTH_MANIFEST_DIR = 'manifests/deployment/mongo-express-no-auth';
31
+
32
+ // Literal and unescaped: kubectl's jsonpath quotes carry escape sequences of their own, and no
33
+ // mongod argument contains a pipe.
34
+ const MONGOD_ARGS_FIELD_SEPARATOR = '|';
35
+
36
+ /**
37
+ * Parses the mongod argument vector reported by `kubectl -o jsonpath` for the StatefulSet.
38
+ * @param {string} raw - Raw jsonpath stdout: the StatefulSet name, then one field per argument.
39
+ * @returns {Array<string>|null} Argument vector, or null when no StatefulSet was reported.
40
+ * @memberof MongoExpress
41
+ */
42
+ const parseMongodArgs = (raw) => {
43
+ const value = `${raw || ''}`;
44
+ if (!value.trim()) return null;
45
+ const [, ...args] = value.split(MONGOD_ARGS_FIELD_SEPARATOR);
46
+ return args.map((arg) => arg.trim()).filter(Boolean);
47
+ };
48
+
49
+ /**
50
+ * Decides whether mongo-express must authenticate against the deployed mongod.
51
+ *
52
+ * The deployed StatefulSet is the authority: `manifests/mongodb` starts mongod with `--auth` and
53
+ * `manifests/mongodb-4.4` does not, and either can be the one running regardless of which flag
54
+ * this invocation carries. The flags only answer for a cluster with no StatefulSet deployed yet,
55
+ * where `--mongodb` (the auth-enabled manifest set) is the default the platform installs.
56
+ * @param {object} [params={}] - Resolution inputs.
57
+ * @param {Array<string>|null} [params.args=null] - mongod argument vector, or null when undeployed.
58
+ * @param {object} [params.options={}] - Cluster options (`mongodb`, `mongodb4`).
59
+ * @returns {boolean} True when the admin credential pair must be projected.
60
+ * @memberof MongoExpress
61
+ */
62
+ const mongoAuthEnabledFactory = ({ args = null, options = {} } = {}) => {
63
+ if (Array.isArray(args)) return args.includes('--auth');
64
+ return options.mongodb4 !== true;
65
+ };
66
+
67
+ /**
68
+ * Resolves the kustomization root the auth mode selects.
69
+ * @param {object} [params={}] - Path inputs.
70
+ * @param {string} [params.underpostRoot='.'] - Repository root holding `manifests/`.
71
+ * @param {boolean} [params.authEnabled=true] - Whether mongod enforces authentication.
72
+ * @returns {string} Kustomization directory path.
73
+ * @memberof MongoExpress
74
+ */
75
+ const mongoExpressManifestPathFactory = ({ underpostRoot = '.', authEnabled = true } = {}) =>
76
+ `${underpostRoot}/${authEnabled ? MONGO_EXPRESS_MANIFEST_DIR : MONGO_EXPRESS_NO_AUTH_MANIFEST_DIR}`;
77
+
78
+ /**
79
+ * Resolves the opt-in NodePort Service manifest path. It lives in the base directory and is left
80
+ * out of both kustomizations: exposing an admin UI on the node network is a caller's decision.
81
+ * @param {object} [params={}] - Path inputs.
82
+ * @param {string} [params.underpostRoot='.'] - Repository root holding `manifests/`.
83
+ * @returns {string} NodePort manifest path.
84
+ * @memberof MongoExpress
85
+ */
86
+ const mongoExpressNodePortManifestFactory = ({ underpostRoot = '.' } = {}) =>
87
+ `${underpostRoot}/${MONGO_EXPRESS_MANIFEST_DIR}/mongo-express-nodeport.yaml`;
88
+
89
+ /**
90
+ * @class MongoExpress
91
+ * @description Static deployment surface for the mongo-express admin client.
92
+ * @memberof MongoExpress
93
+ */
94
+ class MongoExpress {
95
+ /**
96
+ * Reads the mongod argument vector from the deployed MongoDB StatefulSet.
97
+ *
98
+ * The projection leads with the StatefulSet name so a deployed server that declares no arguments
99
+ * stays distinguishable from no server at all — one is mongod on its defaults, the other is a
100
+ * cluster with no data tier yet, and both would otherwise reduce to empty output.
101
+ * @param {string} [namespace='default'] - Namespace holding the StatefulSet.
102
+ * @returns {Array<string>|null} Argument vector, or null when no StatefulSet is deployed.
103
+ * @memberof MongoExpress
104
+ */
105
+ static readMongodArgs(namespace = 'default') {
106
+ const raw = shellExec(
107
+ `kubectl get statefulset ${MONGODB_STATEFULSET_NAME} -n ${namespace} --ignore-not-found -o jsonpath=` +
108
+ `'{.metadata.name}{"${MONGOD_ARGS_FIELD_SEPARATOR}"}` +
109
+ `{range .spec.template.spec.containers[?(@.name=="${MONGODB_STATEFULSET_NAME}")].args[*]}` +
110
+ `{@}{"${MONGOD_ARGS_FIELD_SEPARATOR}"}{end}'`,
111
+ { stdout: true, silent: true, silentOnError: true },
112
+ );
113
+ return parseMongodArgs(raw);
114
+ }
115
+
116
+ /**
117
+ * Deploys mongo-express and, once it is Ready, applies the exposure and placement the caller
118
+ * asked for. Cluster-type agnostic by construction: only the image pull differs between Kind
119
+ * (loaded into the node's image store) and Kubeadm/K3s (pulled through the CRI).
120
+ * @param {object} [params={}] - Deployment parameters.
121
+ * @param {string} [params.namespace='default'] - Target namespace.
122
+ * @param {string} [params.underpostRoot='.'] - Repository root holding `manifests/`.
123
+ * @param {object} [params.options={}] - Cluster options (`pullImage`, `nodePort`, `nodeName`, `mongodb4`).
124
+ * @returns {Promise<boolean>} Whether the deployment reported Ready.
125
+ * @memberof MongoExpress
126
+ */
127
+ static async deploy({ namespace = 'default', underpostRoot = '.', options = {} } = {}) {
128
+ // Secret before workload: the Deployment's secretKeyRef must resolve at pod admission, and
129
+ // the encrypted store stays the source of truth wherever a manifest exists.
130
+ if (!Underpost.secret.applyIfPresent(MONGO_EXPRESS_SECRET_NAME, namespace))
131
+ Underpost.secret.applyFromOriginSeed(MONGO_EXPRESS_SECRET_NAME, namespace);
132
+
133
+ if (options.pullImage) Underpost.cluster.pullImage(MONGO_EXPRESS_IMAGE, options);
134
+
135
+ const args = MongoExpress.readMongodArgs(namespace);
136
+ const authEnabled = mongoAuthEnabledFactory({ args, options });
137
+ const manifestPath = mongoExpressManifestPathFactory({ underpostRoot, authEnabled });
138
+ logger.info('Deploying mongo-express', {
139
+ namespace,
140
+ authEnabled,
141
+ manifestPath,
142
+ mongodb: args ? 'deployed' : 'not deployed',
143
+ });
144
+ shellExec(`kubectl apply -k ${manifestPath} -n ${namespace}`);
145
+
146
+ const ready = await Underpost.test.statusMonitor(MONGO_EXPRESS_NAME, 'Running', 'pods', 1000, 60 * 10);
147
+ if (!ready) {
148
+ logger.error('mongo-express did not report Running', { namespace });
149
+ return false;
150
+ }
151
+
152
+ if (options.nodePort)
153
+ shellExec(`kubectl apply -f ${mongoExpressNodePortManifestFactory({ underpostRoot })} -n ${namespace}`);
154
+ if (options.nodeName)
155
+ Underpost.cluster.pinToNode({
156
+ kind: 'deployment',
157
+ name: MONGO_EXPRESS_NAME,
158
+ namespace,
159
+ node: options.nodeName,
160
+ });
161
+
162
+ logger.info('mongo-express ready', {
163
+ server: `${MONGODB_STATEFULSET_NAME}-0.${MONGODB_SERVICE_NAME}:${MONGODB_DEFAULT_PORT}`,
164
+ basicAuth: `secret/${MONGO_EXPRESS_SECRET_NAME}`,
165
+ clusterIp: `${MONGO_EXPRESS_SERVICE_NAME}:${MONGO_EXPRESS_PORT}${MONGO_EXPRESS_BASE_URL}`,
166
+ nodePort: options.nodePort ? `<node>:${MONGO_EXPRESS_NODE_PORT}${MONGO_EXPRESS_BASE_URL}` : null,
167
+ portForward: `kubectl port-forward -n ${namespace} svc/${MONGO_EXPRESS_SERVICE_NAME} ${MONGO_EXPRESS_PORT}:${MONGO_EXPRESS_PORT}`,
168
+ });
169
+ return true;
170
+ }
171
+ }
172
+
173
+ export {
174
+ MONGO_EXPRESS_BASE_URL,
175
+ MONGO_EXPRESS_IMAGE,
176
+ MONGO_EXPRESS_NAME,
177
+ MONGO_EXPRESS_NODE_PORT,
178
+ MONGO_EXPRESS_PORT,
179
+ MONGO_EXPRESS_SECRET_NAME,
180
+ MONGO_EXPRESS_SERVICE_NAME,
181
+ MongoExpress,
182
+ mongoAuthEnabledFactory,
183
+ mongoExpressManifestPathFactory,
184
+ mongoExpressNodePortManifestFactory,
185
+ parseMongodArgs,
186
+ };
187
+ export default MongoExpress;
@@ -1,10 +1,9 @@
1
1
  /**
2
2
  * gRPC transport for the Cyberia Engine data pipeline.
3
3
  *
4
- * Runs alongside Express on a separate port (default 50051).
5
- * Thin adapter over src/projects/cyberia/instance-data.js the shared
6
- * world-load/boot assembly also served by the REST fallback at
7
- * /api/cyberia-instance/boot/* — so both transports stay equivalent.
4
+ * Runs beside Express on its own port (default 50051). Thin adapter over
5
+ * src/projects/cyberia/instance-data.js, the same assembly the REST fallback
6
+ * at /api/cyberia-instance/boot/* serves, so both transports stay equivalent.
8
7
  *
9
8
  * @module src/grpc/cyberia/grpc-server.js
10
9
  */
@@ -16,7 +15,6 @@ import { fileURLToPath } from 'url';
16
15
  import { loggerFactory } from '../../server/ops/logger.js';
17
16
  import {
18
17
  buildFallbackConfig,
19
- buildFallbackInstanceConfig,
20
18
  fetchFullInstance,
21
19
  fetchMapData,
22
20
  fetchObjectLayer,
@@ -131,7 +129,7 @@ class GrpcServer {
131
129
 
132
130
  server.addService(proto.CyberiaDataService.service, buildHandlers(dbKey));
133
131
 
134
- // gRPC runs over Kubernetes internal network (ClusterIP) always insecure
132
+ // gRPC stays on the cluster-internal network, so credentials are insecure.
135
133
  const creds = grpc.ServerCredentials.createInsecure();
136
134
 
137
135
  return new Promise((resolve, reject) => {
@@ -159,4 +157,4 @@ class GrpcServer {
159
157
  }
160
158
  }
161
159
 
162
- export { GrpcServer, buildFallbackConfig, buildFallbackInstanceConfig };
160
+ export { GrpcServer, buildFallbackConfig };
package/src/index.js CHANGED
@@ -52,7 +52,7 @@ class Underpost {
52
52
  * @type {String}
53
53
  * @memberof Underpost
54
54
  */
55
- static version = 'v3.3.73';
55
+ static version = 'v3.3.76';
56
56
 
57
57
  /**
58
58
  * Required Node.js major version
@@ -5,14 +5,28 @@
5
5
  * @namespace CyberiaAtlasSpriteSheetGenerator
6
6
  */
7
7
 
8
- import fs from 'fs-extra';
9
- import path from 'path';
10
- import sharp from 'sharp';
11
8
  import { Jimp, rgbaToInt } from 'jimp';
12
9
  import { loggerFactory } from '../../server/ops/logger.js';
10
+ import { getKeyframeDirectionsByCode } from '../../client/components/cyberia/SharedDefaultsCyberia.js';
13
11
 
14
12
  const logger = loggerFactory(import.meta);
15
13
 
14
+ /** Direction code of the still an item is previewed by: down idle. */
15
+ export const IDLE_PREVIEW_DIRECTION_CODE = '08';
16
+
17
+ /**
18
+ * Pixels per cell of the human-resolution atlas, when no factor is given.
19
+ * @memberof CyberiaAtlasSpriteSheetGenerator
20
+ */
21
+ export const DEFAULT_ATLAS_UPSCALE_FACTOR = 20;
22
+
23
+ /**
24
+ * Pixels per cell of the minified atlas. The client runtime downloads this one,
25
+ * so it stays at one pixel per cell.
26
+ * @memberof CyberiaAtlasSpriteSheetGenerator
27
+ */
28
+ const MINIFY_CELL_PIXEL_DIM = 1;
29
+
16
30
  /**
17
31
  * @typedef {Object} AtlasFrame
18
32
  * @property {number} x - X position in atlas
@@ -38,7 +52,7 @@ export class AtlasSpriteSheetGenerator {
38
52
  * @returns {Promise<Jimp>} The generated image
39
53
  * @memberof CyberiaAtlasSpriteSheetGenerator
40
54
  */
41
- static async frameMatrixToImage(frameMatrix, colors, cellPixelDim = 20) {
55
+ static async frameMatrixToImage(frameMatrix, colors, cellPixelDim = 1) {
42
56
  if (!frameMatrix || frameMatrix.length === 0 || frameMatrix[0].length === 0) {
43
57
  throw new Error('Invalid frame matrix');
44
58
  }
@@ -79,59 +93,47 @@ export class AtlasSpriteSheetGenerator {
79
93
  * @memberof CyberiaAtlasSpriteSheetGenerator
80
94
  */
81
95
  static calculateOptimalDimension(frameImages) {
82
- if (!frameImages || frameImages.length === 0) {
83
- logger.warn('No frames provided for dimension calculation, using default 2048');
84
- return 2048;
85
- }
86
-
87
- // Get max frame dimensions with defensive checks
88
- const widths = frameImages.map((f) => f.width).filter((w) => !isNaN(w) && w > 0);
89
- const heights = frameImages.map((f) => f.height).filter((h) => !isNaN(h) && h > 0);
90
-
91
- if (widths.length === 0 || heights.length === 0) {
92
- logger.warn('Invalid frame dimensions found, using default 2048');
93
- return 2048;
96
+ if (
97
+ !frameImages?.length ||
98
+ frameImages.some(
99
+ ({ width, height }) => !Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0,
100
+ )
101
+ ) {
102
+ throw new Error('Invalid frame dimensions');
94
103
  }
95
-
96
- const maxFrameWidth = Math.max(...widths);
97
- const maxFrameHeight = Math.max(...heights);
98
-
99
- logger.info(`Frame dimensions: max ${maxFrameWidth}x${maxFrameHeight}, count: ${frameImages.length}`);
100
-
101
- // Calculate grid layout (try to make it roughly square)
102
- const frameCount = frameImages.length;
103
- const cols = Math.ceil(Math.sqrt(frameCount));
104
- const rows = Math.ceil(frameCount / cols);
105
-
106
- // Calculate required dimensions
107
- const requiredWidth = cols * maxFrameWidth;
108
- const requiredHeight = rows * maxFrameHeight;
109
- const requiredDim = Math.max(requiredWidth, requiredHeight);
110
-
111
- logger.info(`Grid layout: ${cols}x${rows}, required: ${requiredWidth}x${requiredHeight}`);
112
-
113
- // Round up to next power of 2
114
- const optimalDim = AtlasSpriteSheetGenerator.nextPowerOf2(requiredDim);
115
-
116
- // Ensure minimum 1024, maximum 8192
117
- const finalDim = Math.max(1024, Math.min(8192, optimalDim));
118
-
119
- logger.info(`Calculated optimal dimension: ${finalDim}x${finalDim}`);
120
-
121
- return finalDim;
104
+ const cols = Math.ceil(Math.sqrt(frameImages.length));
105
+ const rows = Math.ceil(frameImages.length / cols);
106
+ return AtlasSpriteSheetGenerator.nextPowerOf2(
107
+ Math.max(
108
+ cols * Math.max(...frameImages.map((frame) => frame.width)),
109
+ rows * Math.max(...frameImages.map((frame) => frame.height)),
110
+ ),
111
+ );
122
112
  }
123
113
 
124
114
  /**
125
- * Consolidates all frames from an ObjectLayerRenderFrames into a single atlas sprite sheet
115
+ * Consolidates all frames of an ObjectLayerRenderFrames into one atlas sprite sheet.
116
+ *
117
+ * One packing produces two renders of the same layout: `minifyBuffer` at one
118
+ * pixel per cell, which the client runtime downloads, and `buffer` at
119
+ * `upscaleFactor` pixels per cell for human viewing. `metadata` describes the
120
+ * minified render, so atlas metadata and the served blob always agree.
121
+ *
126
122
  * @static
127
123
  * @param {Object} objectLayerRenderFrames - The ObjectLayerRenderFrames document
128
124
  * @param {string} itemKey - The item key for the atlas
129
- * @param {number} [cellPixelDim=20] - Pixel dimension per cell
125
+ * @param {number} [upscaleFactor=DEFAULT_ATLAS_UPSCALE_FACTOR] - Pixels per cell of the human-resolution render
130
126
  * @param {number} [maxAtlasDim=null] - Maximum atlas dimension (auto-calculated if null)
131
- * @returns {Promise<{buffer: Buffer, metadata: Object}>} Atlas buffer and metadata
127
+ * @returns {Promise<{buffer: Buffer, minifyBuffer: Buffer, metadata: Object}>} Both renders and the shared metadata
132
128
  * @memberof CyberiaAtlasSpriteSheetGenerator
133
129
  */
134
- static async generateAtlas(objectLayerRenderFrames, itemKey, cellPixelDim = 20, maxAtlasDim = null) {
130
+ static async generateAtlas(
131
+ objectLayerRenderFrames,
132
+ itemKey,
133
+ upscaleFactor = DEFAULT_ATLAS_UPSCALE_FACTOR,
134
+ maxAtlasDim = null,
135
+ ) {
136
+ if (!Number.isInteger(upscaleFactor) || upscaleFactor < 1) throw new Error('Invalid pixel scale');
135
137
  const { frames, colors } = objectLayerRenderFrames;
136
138
  const frameDuration = Number(objectLayerRenderFrames?.frame_duration);
137
139
 
@@ -157,7 +159,7 @@ export class AtlasSpriteSheetGenerator {
157
159
  'none_idle',
158
160
  ];
159
161
 
160
- // Generate all frame images
162
+ // The packing works on the minified render; the upscaled render reuses its layout.
161
163
  const frameImages = [];
162
164
 
163
165
  for (const direction of directionOrder) {
@@ -167,10 +169,15 @@ export class AtlasSpriteSheetGenerator {
167
169
  for (let frameIndex = 0; frameIndex < directionFrames.length; frameIndex++) {
168
170
  const frameMatrix = directionFrames[frameIndex];
169
171
  try {
170
- const frameImage = await AtlasSpriteSheetGenerator.frameMatrixToImage(frameMatrix, colors, cellPixelDim);
172
+ const frameImage = await AtlasSpriteSheetGenerator.frameMatrixToImage(
173
+ frameMatrix,
174
+ colors,
175
+ MINIFY_CELL_PIXEL_DIM,
176
+ );
171
177
 
172
178
  frameImages.push({
173
179
  image: frameImage,
180
+ frameMatrix,
174
181
  direction,
175
182
  frameIndex,
176
183
  width: frameImage.bitmap.width,
@@ -194,10 +201,8 @@ export class AtlasSpriteSheetGenerator {
194
201
  );
195
202
  }
196
203
 
197
- // Validate maxAtlasDim
198
- if (isNaN(maxAtlasDim) || maxAtlasDim <= 0) {
199
- logger.error(`Invalid maxAtlasDim: ${maxAtlasDim}, using default 2048`);
200
- maxAtlasDim = 2048;
204
+ if (!Number.isInteger(maxAtlasDim) || maxAtlasDim <= 0 || maxAtlasDim > 4096) {
205
+ throw new Error('Atlas dimension must be between 1 and 4096');
201
206
  }
202
207
 
203
208
  // Simple grid packing algorithm
@@ -215,43 +220,29 @@ export class AtlasSpriteSheetGenerator {
215
220
  );
216
221
  }
217
222
 
218
- // Create atlas canvas
219
- const atlasImage = new Jimp({ width: atlasWidth, height: atlasHeight, color: 0x00000000 });
223
+ const minifyImage = new Jimp({ width: atlasWidth, height: atlasHeight, color: 0x00000000 });
224
+ const upscaledImage = new Jimp({
225
+ width: atlasWidth * upscaleFactor,
226
+ height: atlasHeight * upscaleFactor,
227
+ color: 0x00000000,
228
+ });
220
229
 
221
- // Build metadata structure first
222
230
  const frameMetadata = {};
223
231
 
224
- // Composite frames onto atlas
225
- let loggedCount = 0;
226
232
  for (const packedFrame of packedFrames) {
227
- const { image, x, y, direction, frameIndex } = packedFrame;
228
-
229
- // Log first few frames for debugging
230
- if (loggedCount < 3) {
231
- logger.info(
232
- `Frame ${loggedCount + 1}: ${direction}[${frameIndex}] → (${x},${y}) ${image.bitmap.width}x${image.bitmap.height}`,
233
- );
234
- loggedCount++;
235
- }
236
-
237
- // Manually copy pixels to ensure correct positioning
238
- for (let srcY = 0; srcY < image.bitmap.height; srcY++) {
239
- for (let srcX = 0; srcX < image.bitmap.width; srcX++) {
240
- const destX = x + srcX;
241
- const destY = y + srcY;
233
+ const { image, frameMatrix, x, y, direction, frameIndex } = packedFrame;
242
234
 
243
- // Skip if out of bounds
244
- if (destX >= atlasWidth || destY >= atlasHeight) continue;
235
+ AtlasSpriteSheetGenerator.blitFrame(minifyImage, image, x, y);
245
236
 
246
- // Get pixel color from source image
247
- const color = image.getPixelColor(srcX, srcY);
248
-
249
- // Set pixel in atlas at destination position
250
- atlasImage.setPixelColor(color, destX, destY);
251
- }
237
+ // The same cell grid at a larger scale, so the upscaled atlas is an exact
238
+ // multiple of the layout the metadata describes.
239
+ if (upscaleFactor > MINIFY_CELL_PIXEL_DIM) {
240
+ const upscaledFrame = await AtlasSpriteSheetGenerator.frameMatrixToImage(frameMatrix, colors, upscaleFactor);
241
+ AtlasSpriteSheetGenerator.blitFrame(upscaledImage, upscaledFrame, x * upscaleFactor, y * upscaleFactor);
242
+ } else {
243
+ AtlasSpriteSheetGenerator.blitFrame(upscaledImage, image, x, y);
252
244
  }
253
245
 
254
- // Store metadata in correct order
255
246
  if (!frameMetadata[direction]) {
256
247
  frameMetadata[direction] = [];
257
248
  }
@@ -265,19 +256,80 @@ export class AtlasSpriteSheetGenerator {
265
256
  };
266
257
  }
267
258
 
268
- // Convert to PNG buffer
269
- const buffer = await atlasImage.getBuffer('image/png');
270
-
271
259
  const metadata = {
272
260
  itemKey,
273
261
  atlasWidth,
274
262
  atlasHeight,
275
- cellPixelDim,
263
+ cellPixelDim: MINIFY_CELL_PIXEL_DIM,
264
+ upscaleFactor,
276
265
  frame_duration: Number.isFinite(frameDuration) ? frameDuration : 100,
277
266
  frames: frameMetadata,
278
267
  };
279
268
 
280
- return { buffer, metadata };
269
+ return {
270
+ buffer: await upscaledImage.getBuffer('image/png'),
271
+ minifyBuffer: await minifyImage.getBuffer('image/png'),
272
+ metadata,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * The frame an item is previewed by: the first frame of the first down-idle keyframe the
278
+ * atlas carries, or the first frame of any direction when it has none.
279
+ *
280
+ * @static
281
+ * @param {Object} metadata - Atlas metadata as {@link generateAtlas} returns it.
282
+ * @returns {{x:number,y:number,width:number,height:number}|null} Frame box in minified cells.
283
+ * @memberof CyberiaAtlasSpriteSheetGenerator
284
+ */
285
+ static idlePreviewFrame(metadata) {
286
+ const frames = metadata?.frames ?? {};
287
+ const preferred = getKeyframeDirectionsByCode(IDLE_PREVIEW_DIRECTION_CODE);
288
+ for (const direction of [...preferred, ...Object.keys(frames)]) {
289
+ const frame = frames[direction]?.[0];
290
+ if (frame) return frame;
291
+ }
292
+ return null;
293
+ }
294
+
295
+ /**
296
+ * Cuts the idle-preview still out of the human-resolution render, using the same layout
297
+ * `metadata.frames` describes for the minified one scaled by `metadata.upscaleFactor`.
298
+ *
299
+ * @static
300
+ * @param {Buffer} buffer - PNG bytes of the human-resolution atlas render.
301
+ * @param {Object} metadata - Atlas metadata as {@link generateAtlas} returns it.
302
+ * @returns {Promise<Buffer|null>} PNG bytes of the still, or null when the atlas has no frame.
303
+ * @memberof CyberiaAtlasSpriteSheetGenerator
304
+ */
305
+ static async idlePreviewFromAtlas(buffer, metadata) {
306
+ const frame = AtlasSpriteSheetGenerator.idlePreviewFrame(metadata);
307
+ if (!frame) return null;
308
+ const scale = Number(metadata.upscaleFactor) || 1;
309
+ const image = await Jimp.read(buffer);
310
+ image.crop({ x: frame.x * scale, y: frame.y * scale, w: frame.width * scale, h: frame.height * scale });
311
+ return await image.getBuffer('image/png');
312
+ }
313
+
314
+ /**
315
+ * Copies a frame image onto an atlas canvas at the given origin.
316
+ * @static
317
+ * @param {Jimp} atlasImage - Destination atlas canvas.
318
+ * @param {Jimp} frameImage - Source frame image.
319
+ * @param {number} originX - Destination X origin.
320
+ * @param {number} originY - Destination Y origin.
321
+ * @returns {void}
322
+ * @memberof CyberiaAtlasSpriteSheetGenerator
323
+ */
324
+ static blitFrame(atlasImage, frameImage, originX, originY) {
325
+ for (let srcY = 0; srcY < frameImage.bitmap.height; srcY++) {
326
+ for (let srcX = 0; srcX < frameImage.bitmap.width; srcX++) {
327
+ const destX = originX + srcX;
328
+ const destY = originY + srcY;
329
+ if (destX >= atlasImage.bitmap.width || destY >= atlasImage.bitmap.height) continue;
330
+ atlasImage.setPixelColor(frameImage.getPixelColor(srcX, srcY), destX, destY);
331
+ }
332
+ }
281
333
  }
282
334
 
283
335
  /**
@@ -302,7 +354,6 @@ export class AtlasSpriteSheetGenerator {
302
354
  let rowHeight = 0;
303
355
  let maxWidth = 0;
304
356
  let totalHeight = 0;
305
- let exceedsCount = 0;
306
357
 
307
358
  for (const frame of sortedFrames) {
308
359
  // Check if frame fits in current row
@@ -313,9 +364,8 @@ export class AtlasSpriteSheetGenerator {
313
364
  rowHeight = 0;
314
365
  }
315
366
 
316
- // Check if we exceed max height
317
- if (currentY + frame.height > maxDim) {
318
- exceedsCount++;
367
+ if (frame.width > maxDim || currentY + frame.height > maxDim) {
368
+ throw new Error(`Frames exceed atlas dimension ${maxDim}`);
319
369
  }
320
370
 
321
371
  packedFrames.push({
@@ -330,20 +380,8 @@ export class AtlasSpriteSheetGenerator {
330
380
  totalHeight = Math.max(totalHeight, currentY + frame.height);
331
381
  }
332
382
 
333
- // Log once if frames exceeded dimensions
334
- if (exceedsCount > 0) {
335
- const recommendedSize = AtlasSpriteSheetGenerator.nextPowerOf2(Math.max(maxWidth, totalHeight));
336
- logger.warn(
337
- `${exceedsCount} frames exceed atlas dimension (${maxDim}x${maxDim}). ` +
338
- `Actual required: ${maxWidth}x${totalHeight}. ` +
339
- `Recommended: --to-atlas-sprite-sheet ${recommendedSize}`,
340
- );
341
- }
342
-
343
- // Use maxDim as the atlas dimensions (already power-of-2 and optimal from auto-calculation)
344
- // If all frames fit within a smaller power-of-2, use that instead
345
- const atlasWidth = maxDim;
346
- const atlasHeight = maxDim;
383
+ const atlasWidth = maxWidth;
384
+ const atlasHeight = totalHeight;
347
385
 
348
386
  return {
349
387
  packedFrames,
@@ -369,45 +407,4 @@ export class AtlasSpriteSheetGenerator {
369
407
  n |= n >> 16;
370
408
  return n + 1;
371
409
  }
372
-
373
- /**
374
- * Generates atlas and saves to file system
375
- * @static
376
- * @param {Object} objectLayerRenderFrames - The ObjectLayerRenderFrames document
377
- * @param {string} itemKey - The item key for the atlas
378
- * @param {string} outputPath - Output file path
379
- * @param {number} [cellPixelDim=20] - Pixel dimension per cell
380
- * @param {number} [maxAtlasDim=null] - Maximum atlas dimension (auto-calculated if null)
381
- * @returns {Promise<{buffer: Buffer, metadata: Object, outputPath: string}>}
382
- * @memberof CyberiaAtlasSpriteSheetGenerator
383
- */
384
- static async generateAtlasToFile(
385
- objectLayerRenderFrames,
386
- itemKey,
387
- outputPath,
388
- cellPixelDim = 20,
389
- maxAtlasDim = null,
390
- ) {
391
- const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
392
- objectLayerRenderFrames,
393
- itemKey,
394
- cellPixelDim,
395
- maxAtlasDim,
396
- );
397
-
398
- await fs.ensureDir(path.dirname(outputPath));
399
- await fs.writeFile(outputPath, buffer);
400
-
401
- logger.info(`Atlas sprite sheet generated: ${outputPath}`);
402
-
403
- return { buffer, metadata, outputPath };
404
- }
405
410
  }
406
-
407
- // Export convenience functions
408
- export const generateAtlas = AtlasSpriteSheetGenerator.generateAtlas;
409
- export const generateAtlasToFile = AtlasSpriteSheetGenerator.generateAtlasToFile;
410
- export const frameMatrixToImage = AtlasSpriteSheetGenerator.frameMatrixToImage;
411
- export const packFramesGrid = AtlasSpriteSheetGenerator.packFramesGrid;
412
- export const nextPowerOf2 = AtlasSpriteSheetGenerator.nextPowerOf2;
413
- export const calculateOptimalDimension = AtlasSpriteSheetGenerator.calculateOptimalDimension;