cyberia 3.0.1 → 3.0.2

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 (49) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +1 -0
  2. package/CHANGELOG.md +56 -1
  3. package/CLI-HELP.md +2 -4
  4. package/README.md +139 -0
  5. package/bin/build.js +5 -0
  6. package/bin/cyberia.js +385 -71
  7. package/bin/deploy.js +18 -26
  8. package/bin/file.js +3 -0
  9. package/bin/index.js +385 -71
  10. package/conf.js +32 -3
  11. package/deployment.yaml +2 -2
  12. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  13. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  14. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  15. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  16. package/manifests/ipfs/configmap.yaml +7 -0
  17. package/package.json +8 -8
  18. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
  19. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
  20. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
  21. package/src/api/file/file.controller.js +3 -13
  22. package/src/api/file/file.ref.json +0 -21
  23. package/src/api/ipfs/ipfs.controller.js +104 -0
  24. package/src/api/ipfs/ipfs.model.js +71 -0
  25. package/src/api/ipfs/ipfs.router.js +31 -0
  26. package/src/api/ipfs/ipfs.service.js +193 -0
  27. package/src/api/object-layer/README.md +139 -0
  28. package/src/api/object-layer/object-layer.controller.js +3 -0
  29. package/src/api/object-layer/object-layer.model.js +15 -1
  30. package/src/api/object-layer/object-layer.router.js +6 -10
  31. package/src/api/object-layer/object-layer.service.js +311 -182
  32. package/src/cli/cluster.js +30 -38
  33. package/src/cli/index.js +0 -1
  34. package/src/cli/run.js +14 -0
  35. package/src/client/components/core/LoadingAnimation.js +2 -3
  36. package/src/client/components/core/Modal.js +1 -1
  37. package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
  38. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
  39. package/src/client/services/ipfs/ipfs.service.js +144 -0
  40. package/src/client/services/object-layer/object-layer.management.js +161 -8
  41. package/src/index.js +1 -1
  42. package/src/runtime/express/Express.js +1 -1
  43. package/src/server/auth.js +18 -18
  44. package/src/server/ipfs-client.js +433 -0
  45. package/src/server/object-layer.js +649 -18
  46. package/src/server/semantic-layer-generator.js +1083 -0
  47. package/src/server/shape-generator.js +952 -0
  48. package/test/shape-generator.test.js +457 -0
  49. package/bin/ssl.js +0 -63
package/bin/cyberia.js CHANGED
@@ -1,28 +1,52 @@
1
1
  #! /usr/bin/env node
2
2
 
3
+ /**
4
+ * Cyberia Online CLI for object layer management.
5
+ * Provides commands for importing, viewing, and managing object layer assets,
6
+ * render frames, and atlas sprite sheets from the command line.
7
+ *
8
+ * Delegates shared object layer creation logic to {@link ObjectLayerEngine} in
9
+ * `src/server/object-layer.js` to keep a single source of truth shared with
10
+ * the REST API service layer.
11
+ *
12
+ * @module bin/cyberia.js
13
+ * @namespace CyberiaCLI
14
+ */
15
+
3
16
  import dotenv from 'dotenv';
4
17
  import { Command } from 'commander';
5
18
  import fs from 'fs-extra';
6
- import { shellExec, shellCd } from '../src/server/process.js';
19
+ import { shellExec } from '../src/server/process.js';
7
20
  import { loggerFactory } from '../src/server/logger.js';
8
21
  import { DataBaseProvider } from '../src/db/DataBaseProvider.js';
9
22
  import {
23
+ ObjectLayerEngine,
10
24
  pngDirectoryIteratorByObjectLayerType,
11
25
  getKeyFramesDirectionsFromNumberFolderDirection,
12
- processAndPushFrame,
13
26
  buildImgFromTile,
14
- generateRandomStats,
15
27
  itemTypes,
16
28
  } from '../src/server/object-layer.js';
17
29
  import { AtlasSpriteSheetGenerator } from '../src/server/atlas-sprite-sheet-generator.js';
30
+ import {
31
+ generateFrame,
32
+ generateMultiFrame,
33
+ lookupSemantic,
34
+ semanticRegistry,
35
+ } from '../src/server/semantic-layer-generator.js';
36
+ import { IpfsClient } from '../src/server/ipfs-client.js';
37
+ import { createPinRecord } from '../src/api/ipfs/ipfs.service.js';
18
38
  import { program as underpostProgram } from '../src/cli/index.js';
19
39
  import crypto from 'crypto';
20
- import stringify from 'fast-json-stable-stringify';
40
+ import nodePath from 'path';
21
41
  import Underpost from '../src/index.js';
42
+
43
+ /** @type {Function} */
22
44
  const logger = loggerFactory(import.meta);
45
+
23
46
  try {
24
47
  const program = new Command();
25
48
 
49
+ /** @type {string} */
26
50
  const version = Underpost.version;
27
51
 
28
52
  program
@@ -42,11 +66,40 @@ try {
42
66
  .option('--show-atlas-sprite-sheet', 'Show consolidated atlas sprite sheet PNG for given item-id')
43
67
  .option('--import [object-layer-type]', 'Commas separated object layer types e.g. skin,floors')
44
68
  .option('--show-frame [direction-frame]', 'View object layer frame for given item-id e.g. 08_0 (default: 08_0)')
69
+ .option('--generate', 'Generate procedural object layers from semantic item-id (e.g. floor-desert)')
70
+ .option('--count <count>', 'Shape element count multiplier for --generate (default: 3)', parseFloat)
71
+ .option('--seed <seed>', 'Deterministic seed string for --generate (e.g. fx-42)')
72
+ .option('--frame-index <frameIndex>', 'Starting frame index for --generate (default: 0)', parseInt)
73
+ .option('--frame-count <frameCount>', 'Number of frames to generate for --generate (default: 1)', parseInt)
74
+ .option('--density <density>', 'Density factor 0..1 for --generate (default: 0.5)', parseFloat)
45
75
  .option('--env-path <env-path>', 'Env path e.g. ./engine-private/conf/dd-cyberia/.env.development')
46
76
  .option('--mongo-host <mongo-host>', 'Mongo host override')
47
77
  .option('--storage-file-path <storage-file-path>', 'Storage file path override')
48
78
  .option('--drop', 'Drop existing data before importing')
49
79
  .action(
80
+ /**
81
+ * Main action handler for the `ol` command.
82
+ * Manages object layer import, frame viewing, atlas generation, and atlas display.
83
+ *
84
+ * @param {string|undefined} itemId - Optional item ID argument.
85
+ * @param {Object} options - Command options parsed by Commander.
86
+ * @param {boolean|string} options.import - Object layer types to import (e.g., 'all', 'skin,floor') or `false`.
87
+ * @param {boolean|string} options.showFrame - Direction-frame string (e.g., '08_0') or `true` for default.
88
+ * @param {string} options.envPath - Path to the `.env` file.
89
+ * @param {string} options.mongoHost - MongoDB host override.
90
+ * @param {string} options.storageFilePath - Path to a storage filter JSON file.
91
+ * @param {boolean|string} options.toAtlasSpriteSheet - Atlas dimension or `true` for auto-calc.
92
+ * @param {boolean} options.showAtlasSpriteSheet - Whether to display the atlas sprite sheet.
93
+ * @param {boolean} options.drop - Whether to drop existing data before importing.
94
+ * @param {boolean} options.generate - Whether to run procedural generation for the item-id.
95
+ * @param {number} options.count - Shape element count multiplier for generation.
96
+ * @param {string} options.seed - Deterministic seed string for generation.
97
+ * @param {number} options.frameIndex - Starting frame index for generation.
98
+ * @param {number} options.frameCount - Number of frames to generate.
99
+ * @param {number} options.density - Density factor 0..1 for generation.
100
+ * @returns {Promise<void>}
101
+ * @memberof CyberiaCLI
102
+ */
50
103
  async (
51
104
  itemId,
52
105
  options = {
@@ -57,13 +110,22 @@ try {
57
110
  storageFilePath: '',
58
111
  toAtlasSpriteSheet: '',
59
112
  showAtlasSpriteSheet: false,
113
+ generate: false,
114
+ count: 3,
115
+ seed: '',
116
+ frameIndex: 0,
117
+ frameCount: 1,
118
+ density: 0.5,
60
119
  },
61
120
  ) => {
62
121
  if (!options.envPath) options.envPath = `./.env`;
63
122
  if (fs.existsSync(options.envPath)) dotenv.config({ path: options.envPath, override: true });
64
123
 
124
+ /** @type {string} */
65
125
  const deployId = process.env.DEFAULT_DEPLOY_ID;
126
+ /** @type {string} */
66
127
  const host = process.env.DEFAULT_DEPLOY_HOST;
128
+ /** @type {string} */
67
129
  const path = process.env.DEFAULT_DEPLOY_PATH;
68
130
 
69
131
  const confServerPath = `./engine-private/conf/${deployId}/conf.server.json`;
@@ -81,16 +143,20 @@ try {
81
143
  });
82
144
 
83
145
  await DataBaseProvider.load({
84
- apis: ['object-layer', 'object-layer-render-frames', 'atlas-sprite-sheet', 'file'],
146
+ apis: ['object-layer', 'object-layer-render-frames', 'atlas-sprite-sheet', 'file', 'ipfs'],
85
147
  host,
86
148
  path,
87
149
  db,
88
150
  });
89
151
 
152
+ /** @type {import('mongoose').Model} */
90
153
  const ObjectLayer = DataBaseProvider.instance[`${host}${path}`].mongoose.models.ObjectLayer;
154
+ /** @type {import('mongoose').Model} */
91
155
  const ObjectLayerRenderFrames =
92
156
  DataBaseProvider.instance[`${host}${path}`].mongoose.models.ObjectLayerRenderFrames;
157
+ /** @type {import('mongoose').Model} */
93
158
  const AtlasSpriteSheet = DataBaseProvider.instance[`${host}${path}`].mongoose.models.AtlasSpriteSheet;
159
+ /** @type {import('mongoose').Model} */
94
160
  const File = DataBaseProvider.instance[`${host}${path}`].mongoose.models.File;
95
161
 
96
162
  if (options.drop) {
@@ -99,90 +165,140 @@ try {
99
165
  shellExec(`cd src/client/public/cyberia && underpost run clean .`);
100
166
  }
101
167
 
168
+ /** @type {Object|null} */
102
169
  const storage = options.storageFilePath ? JSON.parse(fs.readFileSync(options.storageFilePath, 'utf8')) : null;
103
170
 
104
- const objectLayers = {};
105
-
171
+ // ── Handle --import ──────────────────────────────────────────────
106
172
  if (options.import) {
107
- const argItemTypes = options.import === 'all' ? Object.keys(itemTypes) : options.import.split(',');
173
+ /** @type {boolean} */
174
+ const isImportAll = options.import === 'all';
175
+
176
+ /** @type {string[]} */
177
+ const argItemTypes = isImportAll ? Object.keys(itemTypes) : options.import.split(',');
178
+
179
+ /**
180
+ * Accumulated object layer data keyed by objectLayerId.
181
+ * @type {Object<string, import('../src/server/object-layer.js').ObjectLayerData>}
182
+ */
183
+ const objectLayers = {};
184
+
108
185
  for (const argItemType of argItemTypes) {
109
186
  await pngDirectoryIteratorByObjectLayerType(
110
187
  argItemType,
111
- async ({ path, objectLayerType, objectLayerId, direction, frame }) => {
188
+ async ({ path: framePath, objectLayerType, objectLayerId, direction, frame }) => {
112
189
  if (
113
190
  storage &&
114
191
  !storage[`src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/08/0.png`]
115
192
  )
116
193
  return;
117
- console.log(path, { objectLayerType, objectLayerId, direction, frame });
194
+
195
+ console.log(framePath, { objectLayerType, objectLayerId, direction, frame });
196
+
197
+ // On first encounter of an objectLayerId, build its data from the asset directory
118
198
  if (!objectLayers[objectLayerId]) {
119
- const metadataPath = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}/metadata.json`;
120
- const metadata = fs.existsSync(metadataPath)
121
- ? JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
122
- : null;
123
-
124
- if (metadata) {
125
- // Use metadata from file but ensure objectLayerRenderFramesData is initialized
126
- objectLayers[objectLayerId] = metadata;
127
-
128
- // Ensure data.seed exists (required field)
129
- if (!objectLayers[objectLayerId].data.seed) {
130
- objectLayers[objectLayerId].data.seed = crypto.randomUUID();
131
- }
132
-
133
- // Ensure objectLayerRenderFramesData exists
134
- if (!objectLayers[objectLayerId].objectLayerRenderFramesData) {
135
- objectLayers[objectLayerId].objectLayerRenderFramesData = {
136
- frames: {},
137
- colors: [],
138
- frame_duration: metadata.data?.render?.frame_duration || 250,
139
- is_stateless: metadata.data?.render?.is_stateless || false,
140
- };
141
- }
142
- } else {
143
- // Create default structure
144
- objectLayers[objectLayerId] = {
145
- data: {
146
- item: {
147
- id: objectLayerId,
148
- type: objectLayerType,
149
- description: '',
150
- activable: true,
151
- },
152
- stats: generateRandomStats(),
153
- seed: crypto.randomUUID(),
154
- },
155
- objectLayerRenderFramesData: {
156
- frames: {},
157
- colors: [],
158
- frame_duration: 250,
159
- is_stateless: false,
160
- },
161
- };
162
- }
199
+ const folder = `./src/client/public/cyberia/assets/${objectLayerType}/${objectLayerId}`;
200
+ const { objectLayerRenderFramesData, objectLayerData } =
201
+ await ObjectLayerEngine.buildObjectLayerDataFromDirectory({
202
+ folder,
203
+ objectLayerType,
204
+ objectLayerId,
205
+ });
206
+
207
+ objectLayers[objectLayerId] = {
208
+ ...objectLayerData,
209
+ objectLayerRenderFramesData,
210
+ _processed: true,
211
+ };
163
212
  }
164
- await processAndPushFrame(objectLayers[objectLayerId].objectLayerRenderFramesData, path, direction);
165
213
  },
166
214
  );
167
215
  }
168
- for (const objectLayerId of Object.keys(objectLayers)) {
169
- // Create ObjectLayerRenderFrames document
170
- const objectLayerRenderFramesDoc = await new ObjectLayerRenderFrames(
171
- objectLayers[objectLayerId].objectLayerRenderFramesData,
172
- ).save();
173
216
 
174
- // Update ObjectLayer with reference to render frames (top-level, not in data)
175
- objectLayers[objectLayerId].objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
176
-
177
- // Generate SHA256 hash using fast-json-stable-stringify (seed is part of data)
178
- objectLayers[objectLayerId].sha256 = crypto
179
- .createHash('sha256')
180
- .update(stringify(objectLayers[objectLayerId].data))
181
- .digest('hex');
217
+ for (const objectLayerId of Object.keys(objectLayers)) {
218
+ const entry = objectLayers[objectLayerId];
219
+
220
+ // Skip atlas generation when importing all object layers at once (bulk import).
221
+ // Individual imports or explicit --to-atlas-sprite-sheet calls will still generate atlases.
222
+ const shouldGenerateAtlas = !isImportAll;
223
+
224
+ if (shouldGenerateAtlas) {
225
+ // Use the centralized createObjectLayerDocuments which handles atlas generation
226
+ // Since we're in CLI context without a full Express req/res, we build a minimal
227
+ // atlas generation flow using AtlasSpriteSheetGenerator directly after creation.
228
+ const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
229
+ ObjectLayer,
230
+ ObjectLayerRenderFrames,
231
+ objectLayerRenderFramesData: entry.objectLayerRenderFramesData,
232
+ objectLayerData: { data: entry.data },
233
+ createOptions: {
234
+ generateAtlas: false,
235
+ },
236
+ });
237
+
238
+ // Generate atlas sprite sheet for individual imports
239
+ try {
240
+ const itemKey = objectLayer.data.item.id;
241
+ const populatedObjectLayer = await ObjectLayer.findById(objectLayer._id).populate(
242
+ 'objectLayerRenderFramesId',
243
+ );
244
+
245
+ const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
246
+ populatedObjectLayer.objectLayerRenderFramesId,
247
+ itemKey,
248
+ 20,
249
+ );
250
+
251
+ const fileDoc = await new File({
252
+ name: `${itemKey}-atlas.png`,
253
+ data: buffer,
254
+ size: buffer.length,
255
+ mimetype: 'image/png',
256
+ md5: crypto.createHash('md5').update(buffer).digest('hex'),
257
+ }).save();
258
+
259
+ let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': itemKey });
260
+
261
+ if (atlasDoc) {
262
+ atlasDoc.fileId = fileDoc._id;
263
+ atlasDoc.metadata = metadata;
264
+ await atlasDoc.save();
265
+ logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
266
+ } else {
267
+ atlasDoc = await new AtlasSpriteSheet({
268
+ fileId: fileDoc._id,
269
+ metadata,
270
+ }).save();
271
+ logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
272
+ }
182
273
 
183
- console.log(await ObjectLayer.create(objectLayers[objectLayerId]));
274
+ populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
275
+ await populatedObjectLayer.save();
276
+
277
+ logger.info(`Atlas sprite sheet completed for item: ${itemKey}`);
278
+ } catch (atlasError) {
279
+ logger.error(`Failed to generate atlas for ${objectLayerId}:`, atlasError);
280
+ }
281
+
282
+ console.log(objectLayer);
283
+ } else {
284
+ // --import all: create documents without atlas generation
285
+ const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
286
+ ObjectLayer,
287
+ ObjectLayerRenderFrames,
288
+ objectLayerRenderFramesData: entry.objectLayerRenderFramesData,
289
+ objectLayerData: { data: entry.data },
290
+ createOptions: {
291
+ generateAtlas: false,
292
+ },
293
+ });
294
+
295
+ logger.info(`ObjectLayer created (atlas skipped for bulk import): ${objectLayerId}`);
296
+ console.log(objectLayer);
297
+ }
184
298
  }
185
299
  }
300
+
301
+ // ── Handle --show-frame ──────────────────────────────────────────
186
302
  if (options.showFrame !== undefined) {
187
303
  if (!itemId) {
188
304
  logger.error('item-id is required for --show-frame');
@@ -190,8 +306,10 @@ try {
190
306
  }
191
307
 
192
308
  // Parse direction and frame (default: 08_0)
309
+ /** @type {string} */
193
310
  const showFrameInput = options.showFrame === true ? '08_0' : options.showFrame;
194
311
  const [direction, frameIndex] = showFrameInput.split('_');
312
+ /** @type {number} */
195
313
  const frameIndexNum = parseInt(frameIndex) || 0;
196
314
 
197
315
  logger.info(`Showing frame for item: ${itemId}, direction: ${direction}, frame: ${frameIndexNum}`);
@@ -250,10 +368,11 @@ try {
250
368
  shellExec(`firefox ${outputPath}`);
251
369
  }
252
370
 
253
- // Handle --to-atlas-sprite-sheet
371
+ // ── Handle --to-atlas-sprite-sheet ───────────────────────────────
254
372
  if (options.toAtlasSpriteSheet !== undefined) {
255
373
  // If toAtlasSpriteSheet is true (flag without value), use null for auto-calc
256
374
  // If it's a string/number, parse it as integer
375
+ /** @type {number|null} */
257
376
  const maxAtlasDim = options.toAtlasSpriteSheet === true ? null : parseInt(options.toAtlasSpriteSheet) || null;
258
377
 
259
378
  if (!itemId) {
@@ -262,6 +381,7 @@ try {
262
381
  }
263
382
 
264
383
  if (maxAtlasDim) {
384
+ /** @type {string} */
265
385
  const sizeRecommendation =
266
386
  maxAtlasDim < 2048
267
387
  ? ' (Warning: May be too small for all frames)'
@@ -299,6 +419,7 @@ try {
299
419
  maxAtlasDim,
300
420
  );
301
421
 
422
+ /** @type {number} */
302
423
  const frameCount = Object.values(metadata.frames).reduce((sum, frames) => sum + frames.length, 0);
303
424
  logger.info(
304
425
  `Atlas generated: ${metadata.atlasWidth}x${metadata.atlasHeight} pixels (${frameCount} frames packed)`,
@@ -340,7 +461,7 @@ try {
340
461
  logger.info(`Atlas sprite sheet completed for item: ${itemKey}`);
341
462
  }
342
463
 
343
- // Handle --show-atlas-sprite-sheet
464
+ // ── Handle --show-atlas-sprite-sheet ─────────────────────────────
344
465
  if (options.showAtlasSpriteSheet) {
345
466
  if (!itemId) {
346
467
  logger.error('item-id is required for --show-atlas-sprite-sheet');
@@ -384,6 +505,199 @@ try {
384
505
  );
385
506
  }
386
507
 
508
+ // ── Handle --generate ────────────────────────────────────────────
509
+ if (options.generate) {
510
+ if (!itemId) {
511
+ logger.error(
512
+ 'item-id is required for --generate (e.g. floor-desert, floor-grass, floor-water, floor-stone, floor-lava)',
513
+ );
514
+ logger.info('Available semantic prefixes: ' + Object.keys(semanticRegistry).join(', '));
515
+ process.exit(1);
516
+ }
517
+
518
+ const descriptor = lookupSemantic(itemId);
519
+ if (!descriptor) {
520
+ logger.error(`No semantic descriptor found for item-id "${itemId}".`);
521
+ logger.info('Available semantic prefixes: ' + Object.keys(semanticRegistry).join(', '));
522
+ process.exit(1);
523
+ }
524
+
525
+ const genSeed = options.seed || `gen-${crypto.randomUUID().slice(0, 8)}`;
526
+ const genCount = options.count || 3;
527
+ const genFrameIndex = options.frameIndex || 0;
528
+ const genFrameCount = options.frameCount || 1;
529
+ const genDensity = options.density != null ? options.density : 0.5;
530
+
531
+ // Append a random suffix to make the item-id unique per run
532
+ const randStr = crypto.randomUUID().slice(0, 8);
533
+ const uniqueItemId = `${itemId}-${randStr}`;
534
+
535
+ logger.info('Generating procedural object layers', {
536
+ itemId: uniqueItemId,
537
+ basePrefix: itemId,
538
+ seed: genSeed,
539
+ count: genCount,
540
+ startFrame: genFrameIndex,
541
+ frameCount: genFrameCount,
542
+ density: genDensity,
543
+ semanticTags: descriptor.semanticTags,
544
+ itemType: descriptor.itemType,
545
+ layers: Object.keys(descriptor.layers),
546
+ });
547
+
548
+ // 1. Generate multi-frame result (deterministic, temporally coherent)
549
+ // Pass the base itemId for semantic lookup, but override the stored
550
+ // item.id with uniqueItemId so every run produces a distinct asset.
551
+ const multiFrameResult = generateMultiFrame({
552
+ itemId,
553
+ seed: genSeed,
554
+ frameCount: genFrameCount,
555
+ startFrame: genFrameIndex,
556
+ count: genCount,
557
+ density: genDensity,
558
+ });
559
+
560
+ // Overwrite the item id in the generated data with the unique variant
561
+ multiFrameResult.objectLayerData.data.item.id = uniqueItemId;
562
+
563
+ logger.info(
564
+ `Generated ${multiFrameResult.frameCount} frame(s) with ${multiFrameResult.objectLayerRenderFramesData.colors.length} unique colors`,
565
+ );
566
+
567
+ // 2. Write static asset PNGs to both source and public directories
568
+ const srcBasePath = './src/client/public/cyberia/';
569
+ const publicBasePath = `./public/${host}${path}`;
570
+ const writtenFiles = await ObjectLayerEngine.writeStaticFrameAssets({
571
+ basePaths: [srcBasePath, publicBasePath],
572
+ itemType: descriptor.itemType,
573
+ itemId: uniqueItemId,
574
+ objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
575
+ objectLayerData: multiFrameResult.objectLayerData,
576
+ cellPixelDim: 20,
577
+ });
578
+
579
+ logger.info(`Wrote ${writtenFiles.length} asset file(s):`);
580
+ for (const f of writtenFiles) {
581
+ logger.info(` → ${f}`);
582
+ }
583
+
584
+ // 3. Persist to MongoDB (ObjectLayerRenderFrames + ObjectLayer)
585
+ const { objectLayer } = await ObjectLayerEngine.createObjectLayerDocuments({
586
+ ObjectLayer,
587
+ ObjectLayerRenderFrames,
588
+ objectLayerRenderFramesData: multiFrameResult.objectLayerRenderFramesData,
589
+ objectLayerData: multiFrameResult.objectLayerData,
590
+ createOptions: {
591
+ generateAtlas: false,
592
+ },
593
+ });
594
+
595
+ logger.info(`ObjectLayer persisted to MongoDB: ${objectLayer._id} (item: ${objectLayer.data.item.id})`);
596
+
597
+ // 4. Generate atlas sprite sheet + pin to IPFS
598
+ let atlasCid = '';
599
+ try {
600
+ const atlasItemKey = objectLayer.data.item.id;
601
+ const populatedObjectLayer = await ObjectLayer.findById(objectLayer._id).populate(
602
+ 'objectLayerRenderFramesId',
603
+ );
604
+
605
+ const { buffer, metadata } = await AtlasSpriteSheetGenerator.generateAtlas(
606
+ populatedObjectLayer.objectLayerRenderFramesId,
607
+ atlasItemKey,
608
+ 20,
609
+ );
610
+
611
+ // Save atlas file to File collection
612
+ const fileDoc = await new File({
613
+ name: `${atlasItemKey}-atlas.png`,
614
+ data: buffer,
615
+ size: buffer.length,
616
+ mimetype: 'image/png',
617
+ md5: crypto.createHash('md5').update(buffer).digest('hex'),
618
+ }).save();
619
+
620
+ // Pin atlas PNG to IPFS + copy into MFS
621
+ try {
622
+ const ipfsResult = await IpfsClient.addBufferToIpfs(
623
+ buffer,
624
+ `${atlasItemKey}_atlas_sprite_sheet.png`,
625
+ `/object-layer/${atlasItemKey}/${atlasItemKey}_atlas_sprite_sheet.png`,
626
+ );
627
+ if (ipfsResult) {
628
+ atlasCid = ipfsResult.cid;
629
+ logger.info(`Atlas sprite sheet pinned to IPFS – CID: ${atlasCid}`);
630
+ }
631
+ } catch (ipfsError) {
632
+ logger.warn('Failed to add atlas sprite sheet to IPFS:', ipfsError.message);
633
+ }
634
+
635
+ // Upsert AtlasSpriteSheet document (with CID)
636
+ let atlasDoc = await AtlasSpriteSheet.findOne({ 'metadata.itemKey': atlasItemKey });
637
+ if (atlasDoc) {
638
+ atlasDoc.fileId = fileDoc._id;
639
+ atlasDoc.cid = atlasCid;
640
+ atlasDoc.metadata = metadata;
641
+ await atlasDoc.save();
642
+ logger.info(`Updated existing AtlasSpriteSheet document: ${atlasDoc._id}`);
643
+ } else {
644
+ atlasDoc = await new AtlasSpriteSheet({
645
+ fileId: fileDoc._id,
646
+ cid: atlasCid,
647
+ metadata,
648
+ }).save();
649
+ logger.info(`Created new AtlasSpriteSheet document: ${atlasDoc._id}`);
650
+ }
651
+
652
+ // Link atlas to ObjectLayer and set data.atlasSpriteSheetCid
653
+ populatedObjectLayer.atlasSpriteSheetId = atlasDoc._id;
654
+ populatedObjectLayer.data.atlasSpriteSheetCid = atlasCid;
655
+ populatedObjectLayer.markModified('data.atlasSpriteSheetCid');
656
+ await populatedObjectLayer.save();
657
+
658
+ // Also write atlas PNG to both static asset directories
659
+ for (const bp of [srcBasePath, publicBasePath]) {
660
+ const atlasOutputDir = nodePath.join(bp, 'assets', descriptor.itemType, uniqueItemId);
661
+ await fs.ensureDir(atlasOutputDir);
662
+ const atlasOutputPath = nodePath.join(atlasOutputDir, `${atlasItemKey}-atlas.png`);
663
+ await fs.writeFile(atlasOutputPath, buffer);
664
+ logger.info(
665
+ `Atlas sprite sheet generated: ${metadata.atlasWidth}x${metadata.atlasHeight} → ${atlasOutputPath}`,
666
+ );
667
+ }
668
+ } catch (atlasError) {
669
+ logger.error(`Failed to generate atlas for ${uniqueItemId}:`, atlasError);
670
+ }
671
+
672
+ // 5. Compute final SHA-256, pin OL data JSON to IPFS, create pin records
673
+ try {
674
+ const finalObjectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
675
+ const finalized = await ObjectLayerEngine.computeAndSaveFinalSha256({
676
+ objectLayer: finalObjectLayer,
677
+ ipfsClient: IpfsClient,
678
+ createPinRecord,
679
+ userId: undefined, // CLI context has no authenticated user
680
+ options: { host, path },
681
+ });
682
+ logger.info(`Final SHA-256: ${finalized.sha256}`);
683
+ if (finalized.cid) {
684
+ logger.info(`ObjectLayer data pinned to IPFS – CID: ${finalized.cid}`);
685
+ }
686
+ } catch (finalizeError) {
687
+ logger.error('Failed to finalize SHA-256 / IPFS:', finalizeError);
688
+ }
689
+
690
+ logger.info(`✓ Generation complete for "${uniqueItemId}" (seed: ${genSeed}, frames: ${genFrameCount})`);
691
+
692
+ // Log per-layer summary
693
+ if (multiFrameResult.frames.length > 0) {
694
+ const firstFrame = multiFrameResult.frames[0];
695
+ for (const layer of firstFrame.layers) {
696
+ logger.info(` Layer "${layer.layerKey}" (${layer.layerId}): ${layer.keys.length} element(s)`);
697
+ }
698
+ }
699
+ }
700
+
387
701
  await DataBaseProvider.instance[`${host}${path}`].mongoose.close();
388
702
  },
389
703
  )
package/bin/deploy.js CHANGED
@@ -374,7 +374,9 @@ try {
374
374
  shellExec(`node bin run kill 4002`);
375
375
  shellExec(`node bin run kill 4003`);
376
376
  shellExec(`npm run update-template`);
377
- shellExec(`cd ../pwa-microservices-template && npm install`);
377
+ shellExec(
378
+ `cd ../pwa-microservices-template && npm install && echo "\nENABLE_FILE_LOGS=true" >> .env.development`,
379
+ );
378
380
  shellExec(`cd ../pwa-microservices-template && npm run build && timeout 5s npm run dev`, {
379
381
  async: true,
380
382
  });
@@ -509,33 +511,23 @@ ${shellExec(`git log | grep Author: | sort -u`, { stdout: true }).split(`\n`).jo
509
511
  // https://besu.hyperledger.org/
510
512
  // https://github.com/hyperledger/besu/archive/refs/tags/24.9.1.tar.gz
511
513
 
512
- switch (process.platform) {
513
- case 'linux':
514
- {
515
- shellCd(`..`);
516
-
517
- // Download the Linux binary
518
- shellExec(`wget https://github.com/hyperledger/besu/releases/download/24.9.1/besu-24.9.1.tar.gz`);
514
+ shellCd(`..`);
519
515
 
520
- // Unzip the file:
521
- shellExec(`tar -xvzf besu-24.9.1.tar.gz`);
516
+ // Download the Linux binary
517
+ shellExec(`wget https://github.com/hyperledger/besu/releases/download/24.9.1/besu-24.9.1.tar.gz`);
522
518
 
523
- shellCd(`besu-24.9.1`);
519
+ // Unzip the file:
520
+ shellExec(`tar -xvzf besu-24.9.1.tar.gz`);
524
521
 
525
- shellExec(`bin/besu --help`);
522
+ shellCd(`besu-24.9.1`);
526
523
 
527
- // Set env path
528
- // export PATH=$PATH:/home/dd/besu-24.9.1/bin
524
+ shellExec(`bin/besu --help`);
529
525
 
530
- // Open src
531
- // shellExec(`sudo code /home/dd/besu-24.9.1 --user-data-dir="/root/.vscode-root" --no-sandbox`);
532
- }
533
-
534
- break;
526
+ // Set env path
527
+ // export PATH=$PATH:/home/dd/besu-24.9.1/bin
535
528
 
536
- default:
537
- break;
538
- }
529
+ // Open src
530
+ // shellExec(`sudo code /home/dd/besu-24.9.1 --user-data-dir="/root/.vscode-root" --no-sandbox`);
539
531
 
540
532
  break;
541
533
  }
@@ -977,10 +969,10 @@ nvidia/gpu-operator \
977
969
  `${key}`.toUpperCase().match('MAC')
978
970
  ? 'changethis'
979
971
  : isNaN(parseFloat(privateEnv[key]))
980
- ? `${privateEnv[key]}`.match(`@`)
981
- ? 'admin@default.net'
982
- : 'changethis'
983
- : privateEnv[key];
972
+ ? `${privateEnv[key]}`.match(`@`)
973
+ ? 'admin@default.net'
974
+ : 'changethis'
975
+ : privateEnv[key];
984
976
  }
985
977
  return env;
986
978
  };
package/bin/file.js CHANGED
@@ -97,6 +97,9 @@ try {
97
97
  './manifests/deployment/dd-template-development',
98
98
  './src/server/object-layer.js',
99
99
  './src/server/atlas-sprite-sheet-generator.js',
100
+ './src/server/shape-generator.js',
101
+ './src/server/semantic-layer-generator.js',
102
+ './test/shape-generator.test.js',
100
103
  'bin/cyberia.js',
101
104
  ]) {
102
105
  if (fs.existsSync(deletePath)) fs.removeSync('../pwa-microservices-template/' + deletePath);