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
@@ -1,13 +1,17 @@
1
1
  /**
2
2
  * Provides utilities and engine logic for processing and managing Cyberia Online's object layer assets (skins, floors, weapons, etc.).
3
+ * Centralizes shared logic consumed by both the Cyberia CLI and the REST API service layer.
3
4
  * @module src/server/object-layer.js
4
5
  * @namespace CyberiaObjectLayer
5
6
  */
6
7
 
7
8
  import fs from 'fs-extra';
9
+ import path from 'path';
8
10
  import { PNG } from 'pngjs';
9
11
  import sharp from 'sharp';
10
12
  import { Jimp, intToRGBA, rgbaToInt } from 'jimp';
13
+ import crypto from 'crypto';
14
+ import stringify from 'fast-json-stable-stringify';
11
15
 
12
16
  import { range } from '../client/components/core/CommonJs.js';
13
17
  import { random } from '../client/components/core/CommonJs.js';
@@ -21,14 +25,71 @@ const logger = loggerFactory(import.meta);
21
25
  * @property {string} objectLayerType - The type of object layer (e.g., 'skin', 'floor').
22
26
  * @property {string} objectLayerId - The unique ID of the object layer asset.
23
27
  * @property {string} direction - The direction folder name (e.g., '08', '12').
24
- * @memberof CyberiaObjectLayer
25
28
  * @property {string} frame - The frame file name.
29
+ * @memberof CyberiaObjectLayer
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} ObjectLayerRenderFramesData
34
+ * @property {Object<string, number[][][]>} frames - Map of direction names to arrays of frame matrices.
35
+ * @property {Array<number[]>} colors - Global color palette shared across all frames.
36
+ * @property {number} frame_duration - Duration of each frame in milliseconds.
37
+ * @property {boolean} is_stateless - Whether the render layer is stateless (no animation state).
38
+ * @memberof CyberiaObjectLayer
26
39
  */
27
40
 
41
+ /**
42
+ * @typedef {Object} ObjectLayerData
43
+ * @property {Object} data - Object layer data payload.
44
+ * @property {Object} data.item - Item descriptor.
45
+ * @property {string} data.item.id - Unique identifier for the item.
46
+ * @property {string} data.item.type - Type of the item (e.g., 'skin', 'floor').
47
+ * @property {string} [data.item.description] - Human-readable description.
48
+ * @property {boolean} [data.item.activable] - Whether the item can be activated.
49
+ * @property {Object} data.stats - Statistical attributes of the object layer.
50
+ * @property {string} data.seed - Random UUID v4 for unique state generation.
51
+ * @property {string} [data.atlasSpriteSheetCid] - IPFS CID for the consolidated atlas sprite sheet PNG.
52
+ * @property {ObjectLayerRenderFramesData} [objectLayerRenderFramesData] - Render frames data (transient, used before persisting).
53
+ * @property {import('mongoose').Types.ObjectId} [objectLayerRenderFramesId] - Reference to persisted ObjectLayerRenderFrames document.
54
+ * @property {string} [sha256] - SHA-256 hash of the object layer data.
55
+ * @memberof CyberiaObjectLayer
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} BuildFromDirectoryResult
60
+ * @property {ObjectLayerRenderFramesData} objectLayerRenderFramesData - The assembled render frames data.
61
+ * @property {Object} objectLayerData - The assembled object layer data (without render frames reference or sha256).
62
+ * @memberof CyberiaObjectLayer
63
+ */
64
+
65
+ /**
66
+ * @typedef {Object} CreateDocumentsOptions
67
+ * @property {boolean} [generateAtlas=true] - Whether to generate the atlas sprite sheet after creating documents.
68
+ * @property {Object} [atlasServiceContext] - Context required by AtlasSpriteSheetService.generate (req, res, options).
69
+ * @property {Object} [atlasServiceContext.req] - Express-like request object (must include auth).
70
+ * @property {Object} [atlasServiceContext.res] - Express-like response object.
71
+ * @property {Object} [atlasServiceContext.options] - Server options (host, path).
72
+ * @memberof CyberiaObjectLayer
73
+ */
74
+
75
+ /**
76
+ * @typedef {Object} CreateDocumentsResult
77
+ * @property {Object} objectLayer - The persisted ObjectLayer mongoose document.
78
+ * @property {Object} objectLayerRenderFramesDoc - The persisted ObjectLayerRenderFrames mongoose document.
79
+ * @memberof CyberiaObjectLayer
80
+ */
81
+
82
+ /**
83
+ * Engine class providing static utilities for Cyberia Online object layer asset processing,
84
+ * frame extraction, directory iteration, image building, and centralized document creation logic.
85
+ * @class ObjectLayerEngine
86
+ * @memberof CyberiaObjectLayer
87
+ */
28
88
  export class ObjectLayerEngine {
29
89
  /**
90
+ * Iterates through the directory structure of object layer PNG assets for a given type.
91
+ * Walks `./src/client/public/cyberia/assets/{objectLayerType}/{id}/{direction}/{frame}`.
30
92
  * @static
31
- * @description Iterates through the directory structure of object layer PNG assets for a given type.
32
93
  * @param {string} [objectLayerType='skin'] - The type of object layer to iterate over (e.g., 'skin', 'floor').
33
94
  * @param {function(ObjectLayerCallbackPayload): Promise<void>} [callback=() => {}] - The async function to execute for each image file found.
34
95
  * @returns {Promise<void>}
@@ -61,10 +122,10 @@ export class ObjectLayerEngine {
61
122
  }
62
123
 
63
124
  /**
125
+ * Asynchronously reads a PNG file and resolves with its raw bitmap data, width, and height.
64
126
  * @static
65
- * @description Asynchronously reads a PNG file and resolves with its raw bitmap data, width, and height.
66
127
  * @param {string} filePath - The path to the PNG file.
67
- * @returns {Promise<{width: number, height: number, data: Buffer} | {error: true, message: string}>} - The image data or an error object.
128
+ * @returns {Promise<{width: number, height: number, data: Buffer} | {error: true, message: string}>} The image data or an error object.
68
129
  * @memberof CyberiaObjectLayer
69
130
  */
70
131
  static readPngAsync(filePath) {
@@ -87,12 +148,12 @@ export class ObjectLayerEngine {
87
148
  }
88
149
 
89
150
  /**
90
- * @static
91
- * @description Processes an image file (PNG or GIF) to generate a frame matrix and a color palette (map_color).
151
+ * Processes an image file (PNG or GIF) to generate a frame matrix and a color palette (map_color).
92
152
  * It quantizes the image based on a factor derived from image height (mazeFactor).
153
+ * @static
93
154
  * @param {string} path - The path to the image file.
94
155
  * @param {Array<number[]>} [colors=[]] - The existing color palette array to append new colors to.
95
- * @returns {Promise<{frame: number[][], colors: Array<number[]>}>} - The frame matrix and the updated color palette.
156
+ * @returns {Promise<{frame: number[][], colors: Array<number[]>}>} The frame matrix and the updated color palette.
96
157
  * @memberof CyberiaObjectLayer
97
158
  */
98
159
  static async frameFactory(path, colors = []) {
@@ -146,10 +207,10 @@ export class ObjectLayerEngine {
146
207
  }
147
208
 
148
209
  /**
210
+ * Converts a numerical folder direction (e.g., '08', '14') into an array of corresponding keyframe names (e.g., 'down_idle', 'left_walking').
149
211
  * @static
150
- * @description Converts a numerical folder direction (e.g., '08', '14') into an array of corresponding keyframe names (e.g., 'down_idle', 'left_walking').
151
212
  * @param {string} direction - The numerical direction string.
152
- * @returns {string[]} - An array of keyframe direction names.
213
+ * @returns {string[]} An array of keyframe direction names.
153
214
  * @memberof CyberiaObjectLayer
154
215
  */
155
216
  static getKeyFramesDirectionsFromNumberFolderDirection(direction) {
@@ -186,14 +247,14 @@ export class ObjectLayerEngine {
186
247
  }
187
248
 
188
249
  /**
189
- * @static
190
- * @description Processes an image file through frameFactory and adds the resulting frame to the render data structure.
250
+ * Processes an image file through {@link ObjectLayerEngine.frameFactory} and adds the resulting frame to the render data structure.
191
251
  * Updates the color palette and pushes the frame to all keyframe directions corresponding to the given direction code.
192
252
  * Initializes colors array, frames object, and direction arrays if they don't exist.
193
- * @param {Object} objectLayerRenderFramesData - The render data object containing frames and colors.
253
+ * @static
254
+ * @param {ObjectLayerRenderFramesData} objectLayerRenderFramesData - The render data object containing frames and colors.
194
255
  * @param {string} imagePath - The path to the image file to process.
195
256
  * @param {string} directionCode - The numerical direction code (e.g., '08', '14').
196
- * @returns {Promise<Object>} - The updated render data object.
257
+ * @returns {Promise<ObjectLayerRenderFramesData>} The updated render data object.
197
258
  * @memberof CyberiaObjectLayer
198
259
  */
199
260
  static async processAndPushFrame(objectLayerRenderFramesData, imagePath, directionCode) {
@@ -231,8 +292,8 @@ export class ObjectLayerEngine {
231
292
  }
232
293
 
233
294
  /**
295
+ * Builds a PNG image file from a tile matrix and color map using Jimp and Sharp.
234
296
  * @static
235
- * @description Builds a PNG image file from a tile matrix and color map using Jimp and Sharp.
236
297
  * @param {Object} options - Options object.
237
298
  * @param {Object} options.tile - The tile data.
238
299
  * @param {Array<number[]>} options.tile.map_color - The color palette.
@@ -295,9 +356,9 @@ export class ObjectLayerEngine {
295
356
  }
296
357
 
297
358
  /**
359
+ * Generates a random set of character statistics for an item, with values between 0 and 10.
298
360
  * @static
299
- * @description Generates a random set of character statistics for an item, with values between 0 and 10.
300
- * @returns {{effect: number, resistance: number, agility: number, range: number, intelligence: number, utility: number}} - The random stats object.
361
+ * @returns {{effect: number, resistance: number, agility: number, range: number, intelligence: number, utility: number}} The random stats object.
301
362
  * @memberof CyberiaObjectLayer
302
363
  */
303
364
  static generateRandomStats() {
@@ -310,22 +371,592 @@ export class ObjectLayerEngine {
310
371
  utility: random(0, 10),
311
372
  };
312
373
  }
374
+
375
+ // ──────────────────────────────────────────────────────────────────────────
376
+ // Centralized document lifecycle methods
377
+ // ──────────────────────────────────────────────────────────────────────────
378
+
379
+ /**
380
+ * Scans a local asset directory for PNG frame images and assembles both
381
+ * {@link ObjectLayerRenderFramesData} and {@link ObjectLayerData} from the directory contents
382
+ * and an optional `metadata.json` file.
383
+ *
384
+ * This is the shared first step consumed by both the Cyberia CLI `--import` flow
385
+ * and the REST API service `post` / `put` `/metadata` endpoints.
386
+ *
387
+ * @static
388
+ * @param {Object} params - Parameters.
389
+ * @param {string} params.folder - Absolute or relative path to the asset folder
390
+ * (e.g., `./src/client/public/cyberia/assets/skin/myskin`). Must contain numeric
391
+ * direction sub-folders (`08`, `18`, …) with PNG frame files.
392
+ * @param {string} params.objectLayerType - The item type string (e.g., 'skin', 'floor').
393
+ * @param {string} params.objectLayerId - The item id string.
394
+ * @param {Object} [params.metadataOverride=null] - When provided, used as the authoritative
395
+ * metadata instead of reading `metadata.json` from disk. The REST API passes `req.body`
396
+ * here; the CLI passes `null` so the file is read from disk.
397
+ * @returns {Promise<BuildFromDirectoryResult>} The assembled render frames data and object layer data.
398
+ * @memberof CyberiaObjectLayer
399
+ */
400
+ static async buildObjectLayerDataFromDirectory({ folder, objectLayerType, objectLayerId, metadataOverride = null }) {
401
+ let metadata = metadataOverride;
402
+
403
+ // If no override was supplied, try to read metadata.json from the folder
404
+ if (!metadata) {
405
+ const metadataPath = `${folder}/metadata.json`;
406
+ if (fs.existsSync(metadataPath)) {
407
+ metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
408
+ }
409
+ }
410
+
411
+ // Build objectLayerRenderFramesData
412
+ let objectLayerRenderFramesData;
413
+ if (metadata && metadata.objectLayerRenderFramesData) {
414
+ objectLayerRenderFramesData = {
415
+ frame_duration: metadata.objectLayerRenderFramesData.frame_duration || 250,
416
+ is_stateless: metadata.objectLayerRenderFramesData.is_stateless || false,
417
+ frames: {},
418
+ colors: [],
419
+ };
420
+ } else if (metadata && metadata.data && metadata.data.render) {
421
+ objectLayerRenderFramesData = {
422
+ frame_duration: metadata.data.render.frame_duration || 250,
423
+ is_stateless: metadata.data.render.is_stateless || false,
424
+ frames: {},
425
+ colors: [],
426
+ };
427
+ } else {
428
+ objectLayerRenderFramesData = {
429
+ frame_duration: 250,
430
+ is_stateless: false,
431
+ frames: {},
432
+ colors: [],
433
+ };
434
+ }
435
+
436
+ // Build objectLayerData
437
+ let objectLayerData;
438
+ if (metadata && metadata.data) {
439
+ objectLayerData = {
440
+ data: {
441
+ item: metadata.data.item || {
442
+ id: objectLayerId,
443
+ type: objectLayerType,
444
+ description: '',
445
+ activable: true,
446
+ },
447
+ stats: metadata.data.stats || ObjectLayerEngine.generateRandomStats(),
448
+ seed: metadata.data.seed || crypto.randomUUID(),
449
+ },
450
+ };
451
+ } else {
452
+ objectLayerData = {
453
+ data: {
454
+ item: {
455
+ id: objectLayerId,
456
+ type: objectLayerType,
457
+ description: '',
458
+ activable: true,
459
+ },
460
+ stats: ObjectLayerEngine.generateRandomStats(),
461
+ seed: crypto.randomUUID(),
462
+ },
463
+ };
464
+ }
465
+
466
+ // Process all PNG files from direction sub-folders
467
+ if (fs.existsSync(folder)) {
468
+ const directionFolders = await fs.readdir(folder);
469
+ for (const directionCode of directionFolders) {
470
+ const directionPath = `${folder}/${directionCode}`;
471
+
472
+ // Skip non-directories (metadata.json, etc.)
473
+ try {
474
+ const stat = await fs.stat(directionPath);
475
+ if (!stat.isDirectory()) continue;
476
+ } catch (error) {
477
+ logger.warn(`Skipping ${directionCode}: ${error.message}`);
478
+ continue;
479
+ }
480
+
481
+ const frameFiles = await fs.readdir(directionPath);
482
+ // Sort frame files numerically
483
+ frameFiles.sort((a, b) => {
484
+ const numA = parseInt(a.split('.')[0]);
485
+ const numB = parseInt(b.split('.')[0]);
486
+ return numA - numB;
487
+ });
488
+
489
+ for (const frameFile of frameFiles) {
490
+ if (!frameFile.endsWith('.png')) continue;
491
+
492
+ const framePath = `${directionPath}/${frameFile}`;
493
+ await ObjectLayerEngine.processAndPushFrame(objectLayerRenderFramesData, framePath, directionCode);
494
+ }
495
+ }
496
+ }
497
+
498
+ return { objectLayerRenderFramesData, objectLayerData };
499
+ }
500
+
501
+ /**
502
+ * Computes a SHA-256 hash of the given object layer data using deterministic JSON serialisation.
503
+ * @static
504
+ * @param {Object} data - The `data` sub-document of an ObjectLayer (item, stats, seed, atlasSpriteSheetCid, …).
505
+ * @returns {string} Hex-encoded SHA-256 hash.
506
+ * @memberof CyberiaObjectLayer
507
+ */
508
+ static computeSha256(data) {
509
+ return crypto.createHash('sha256').update(stringify(data)).digest('hex');
510
+ }
511
+
512
+ /**
513
+ * Map of keyframe direction names to their numeric folder direction codes.
514
+ * Inverse of {@link ObjectLayerEngine.getKeyFramesDirectionsFromNumberFolderDirection}.
515
+ * @static
516
+ * @type {Object<string, string>}
517
+ * @memberof CyberiaObjectLayer
518
+ */
519
+ static directionNameToCode = {
520
+ down_idle: '08',
521
+ none_idle: '08',
522
+ default_idle: '08',
523
+ up_idle: '02',
524
+ left_idle: '04',
525
+ up_left_idle: '04',
526
+ down_left_idle: '04',
527
+ right_idle: '06',
528
+ up_right_idle: '06',
529
+ down_right_idle: '06',
530
+ down_walking: '18',
531
+ up_walking: '12',
532
+ left_walking: '14',
533
+ up_left_walking: '14',
534
+ down_left_walking: '14',
535
+ right_walking: '16',
536
+ up_right_walking: '16',
537
+ down_right_walking: '16',
538
+ };
539
+
540
+ /**
541
+ * Writes frame PNGs and an optional metadata.json to one or more base asset
542
+ * directories. This is the shared write-to-disk step consumed by both the
543
+ * Cyberia CLI `--generate` / `--import` flows and the REST API service
544
+ * `post` / `put` `/metadata` endpoints.
545
+ *
546
+ * For each base path the layout produced is:
547
+ * ```
548
+ * {basePath}/assets/{itemType}/{itemId}/{directionCode}/{frameIndex}.png
549
+ * {basePath}/assets/{itemType}/{itemId}/metadata.json (optional)
550
+ * ```
551
+ *
552
+ * @static
553
+ * @param {Object} params
554
+ * @param {string[]} params.basePaths - One or more root paths
555
+ * (e.g. `['./src/client/public/cyberia/', './public/host/path/']`).
556
+ * The conventional `assets/` prefix is appended automatically.
557
+ * @param {string} params.itemType - Object layer type ('floor', 'skin', …).
558
+ * @param {string} params.itemId - Unique item identifier.
559
+ * @param {ObjectLayerRenderFramesData} params.objectLayerRenderFramesData
560
+ * - The render frames data containing `frames`, `colors`,
561
+ * `frame_duration` and `is_stateless`.
562
+ * @param {Object} [params.objectLayerData=null] - When provided, a
563
+ * `metadata.json` file is written alongside the frame PNGs.
564
+ * @param {number} [params.cellPixelDim=20] - Pixel size per grid cell.
565
+ * @returns {Promise<string[]>} Flat list of every file path written.
566
+ * @memberof CyberiaObjectLayer
567
+ */
568
+ static async writeStaticFrameAssets({
569
+ basePaths,
570
+ itemType,
571
+ itemId,
572
+ objectLayerRenderFramesData,
573
+ objectLayerData = null,
574
+ cellPixelDim = 20,
575
+ }) {
576
+ const writtenPaths = [];
577
+ const dirToCode = ObjectLayerEngine.directionNameToCode;
578
+
579
+ for (const basePath of basePaths) {
580
+ // Track which directionCode/frameIndex combos we already wrote for
581
+ // this basePath so duplicate direction names (e.g. down_idle,
582
+ // none_idle, default_idle all map to '08') only write once.
583
+ const written = new Set();
584
+
585
+ for (const [dirName, dirFrames] of Object.entries(objectLayerRenderFramesData.frames)) {
586
+ const code = dirToCode[dirName];
587
+ if (!code) continue;
588
+
589
+ for (let fi = 0; fi < dirFrames.length; fi++) {
590
+ const key = `${code}/${fi}`;
591
+ if (written.has(key)) continue;
592
+ written.add(key);
593
+
594
+ const dirFolder = path.join(basePath, 'assets', itemType, itemId, code);
595
+ await fs.ensureDir(dirFolder);
596
+
597
+ const filePath = path.join(dirFolder, `${fi}.png`);
598
+
599
+ await ObjectLayerEngine.buildImgFromTile({
600
+ tile: {
601
+ map_color: objectLayerRenderFramesData.colors,
602
+ frame_matrix: dirFrames[fi],
603
+ },
604
+ cellPixelDim,
605
+ opacityFilter: (x, y, color) => 255,
606
+ imagePath: filePath,
607
+ });
608
+
609
+ writtenPaths.push(filePath);
610
+ }
611
+ }
612
+
613
+ // Write metadata.json when objectLayerData is supplied
614
+ if (objectLayerData) {
615
+ const metaDir = path.join(basePath, 'assets', itemType, itemId);
616
+ await fs.ensureDir(metaDir);
617
+ const metadataPath = path.join(metaDir, 'metadata.json');
618
+
619
+ await fs.writeJson(
620
+ metadataPath,
621
+ {
622
+ data: objectLayerData.data,
623
+ objectLayerRenderFramesData: {
624
+ frame_duration: objectLayerRenderFramesData.frame_duration,
625
+ is_stateless: objectLayerRenderFramesData.is_stateless,
626
+ },
627
+ generated: true,
628
+ generatorVersion: '1.0.0',
629
+ },
630
+ { spaces: 2 },
631
+ );
632
+ writtenPaths.push(metadataPath);
633
+ }
634
+ }
635
+
636
+ return writtenPaths;
637
+ }
638
+
639
+ /**
640
+ * Creates new ObjectLayerRenderFrames and ObjectLayer documents in MongoDB from the
641
+ * provided data, computes an initial SHA-256, and optionally generates the atlas sprite sheet.
642
+ *
643
+ * When `generateAtlas` is `true` (the default) the method delegates to
644
+ * `AtlasSpriteSheetService.generate` and then recomputes the definitive SHA-256
645
+ * (which now includes `data.atlasSpriteSheetCid`) and persists an IPFS CID.
646
+ *
647
+ * @static
648
+ * @param {Object} params - Parameters.
649
+ * @param {Object} params.ObjectLayer - Mongoose ObjectLayer model.
650
+ * @param {Object} params.ObjectLayerRenderFrames - Mongoose ObjectLayerRenderFrames model.
651
+ * @param {ObjectLayerRenderFramesData} params.objectLayerRenderFramesData - Render frames payload.
652
+ * @param {Object} params.objectLayerData - Object layer payload (must include `data`).
653
+ * @param {CreateDocumentsOptions} [params.createOptions={}] - Additional options controlling atlas generation and IPFS pinning.
654
+ * @returns {Promise<CreateDocumentsResult>} The persisted documents.
655
+ * @memberof CyberiaObjectLayer
656
+ */
657
+ static async createObjectLayerDocuments({
658
+ ObjectLayer,
659
+ ObjectLayerRenderFrames,
660
+ objectLayerRenderFramesData,
661
+ objectLayerData,
662
+ createOptions = {},
663
+ }) {
664
+ const { generateAtlas = true, atlasServiceContext = null } = createOptions;
665
+
666
+ // 1. Persist ObjectLayerRenderFrames
667
+ const objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
668
+
669
+ // 2. Attach reference + compute temporary SHA-256
670
+ objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
671
+ objectLayerData.data.atlasSpriteSheetCid = objectLayerData.data.atlasSpriteSheetCid || '';
672
+ objectLayerData.sha256 = ObjectLayerEngine.computeSha256(objectLayerData.data);
673
+
674
+ // 3. Upsert ObjectLayer (handle duplicate sha256 gracefully)
675
+ let objectLayer;
676
+ const existingObjectLayer = await ObjectLayer.findOne({ sha256: objectLayerData.sha256 });
677
+ if (existingObjectLayer) {
678
+ logger.info(`ObjectLayer with sha256 ${objectLayerData.sha256} already exists, updating...`);
679
+ objectLayer = await ObjectLayer.findByIdAndUpdate(existingObjectLayer._id, objectLayerData, {
680
+ new: true,
681
+ }).populate('objectLayerRenderFramesId');
682
+ } else {
683
+ objectLayer = await (await ObjectLayer.create(objectLayerData)).populate('objectLayerRenderFramesId');
684
+ logger.info(`ObjectLayer created successfully with id: ${objectLayer._id}`);
685
+ }
686
+
687
+ // 4. Optional atlas generation + final SHA-256 / IPFS CID
688
+ if (generateAtlas && atlasServiceContext) {
689
+ objectLayer = await ObjectLayerEngine._generateAtlasAndFinalize({
690
+ objectLayer,
691
+ ObjectLayer,
692
+ atlasServiceContext,
693
+ isNew: true,
694
+ });
695
+ }
696
+
697
+ return { objectLayer, objectLayerRenderFramesDoc };
698
+ }
699
+
700
+ /**
701
+ * Updates an existing ObjectLayer and its ObjectLayerRenderFrames from the provided data,
702
+ * recomputes SHA-256, and optionally regenerates the atlas sprite sheet.
703
+ *
704
+ * @static
705
+ * @param {Object} params - Parameters.
706
+ * @param {string} params.objectLayerId - The `_id` of the ObjectLayer document to update.
707
+ * @param {Object} params.ObjectLayer - Mongoose ObjectLayer model.
708
+ * @param {Object} params.ObjectLayerRenderFrames - Mongoose ObjectLayerRenderFrames model.
709
+ * @param {ObjectLayerRenderFramesData} params.objectLayerRenderFramesData - Render frames payload.
710
+ * @param {Object} params.objectLayerData - Object layer payload (must include `data`).
711
+ * @param {CreateDocumentsOptions} [params.updateOptions={}] - Additional options controlling atlas generation and IPFS pinning.
712
+ * @returns {Promise<CreateDocumentsResult>} The persisted documents.
713
+ * @memberof CyberiaObjectLayer
714
+ */
715
+ static async updateObjectLayerDocuments({
716
+ objectLayerId,
717
+ ObjectLayer,
718
+ ObjectLayerRenderFrames,
719
+ objectLayerRenderFramesData,
720
+ objectLayerData,
721
+ updateOptions = {},
722
+ }) {
723
+ const { generateAtlas = true, atlasServiceContext = null } = updateOptions;
724
+
725
+ // 1. Update or create ObjectLayerRenderFrames
726
+ let objectLayerRenderFramesDoc;
727
+ const existingObjectLayer = await ObjectLayer.findById(objectLayerId);
728
+ if (existingObjectLayer && existingObjectLayer.objectLayerRenderFramesId) {
729
+ objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.findByIdAndUpdate(
730
+ existingObjectLayer.objectLayerRenderFramesId,
731
+ objectLayerRenderFramesData,
732
+ { new: true },
733
+ );
734
+ objectLayerData.objectLayerRenderFramesId = existingObjectLayer.objectLayerRenderFramesId;
735
+ } else {
736
+ objectLayerRenderFramesDoc = await ObjectLayerRenderFrames.create(objectLayerRenderFramesData);
737
+ objectLayerData.objectLayerRenderFramesId = objectLayerRenderFramesDoc._id;
738
+ }
739
+
740
+ // 2. Compute temporary SHA-256
741
+ objectLayerData.data.atlasSpriteSheetCid = objectLayerData.data.atlasSpriteSheetCid || '';
742
+ objectLayerData.sha256 = ObjectLayerEngine.computeSha256(objectLayerData.data);
743
+
744
+ // 3. Persist ObjectLayer update
745
+ let objectLayer;
746
+ try {
747
+ objectLayer = await ObjectLayer.findByIdAndUpdate(objectLayerId, objectLayerData, {
748
+ new: true,
749
+ }).populate('objectLayerRenderFramesId');
750
+ if (!objectLayer) {
751
+ throw new Error('ObjectLayer not found for update');
752
+ }
753
+ logger.info(`ObjectLayer updated successfully with id: ${objectLayerId}`);
754
+ } catch (error) {
755
+ logger.error('Error updating ObjectLayer:', error);
756
+ throw error;
757
+ }
758
+
759
+ // 4. Optional atlas generation + final SHA-256 / IPFS CID
760
+ if (generateAtlas && atlasServiceContext) {
761
+ objectLayer = await ObjectLayerEngine._generateAtlasAndFinalize({
762
+ objectLayer,
763
+ ObjectLayer,
764
+ atlasServiceContext,
765
+ isNew: false,
766
+ });
767
+ }
768
+
769
+ return { objectLayer, objectLayerRenderFramesDoc };
770
+ }
771
+
772
+ /**
773
+ * Recomputes the definitive SHA-256, pins the object layer data JSON to IPFS,
774
+ * and persists both fields on the ObjectLayer document.
775
+ *
776
+ * Intended for use after atlas generation has set `data.atlasSpriteSheetCid`.
777
+ *
778
+ * @static
779
+ * @param {Object} params - Parameters.
780
+ * @param {Object} params.objectLayer - The mongoose ObjectLayer document (must be populated).
781
+ * @param {Object} [params.ipfsClient=null] - The IpfsClient module; when `null`, IPFS pinning is skipped.
782
+ * @param {function} [params.createPinRecord=null] - The `createPinRecord` helper; when `null`, pin records are skipped.
783
+ * @param {string} [params.userId] - Authenticated user ID for IPFS pin record creation.
784
+ * @param {Object} [params.options] - Server options (host, path) forwarded to `createPinRecord`.
785
+ * @returns {Promise<Object>} The saved ObjectLayer document.
786
+ * @memberof CyberiaObjectLayer
787
+ */
788
+ static async computeAndSaveFinalSha256({ objectLayer, ipfsClient = null, createPinRecord = null, userId, options }) {
789
+ const finalSha256 = ObjectLayerEngine.computeSha256(objectLayer.data);
790
+
791
+ if (ipfsClient) {
792
+ try {
793
+ const itemId = objectLayer.data.item.id;
794
+ const ipfsResult = await ipfsClient.addJsonToIpfs(
795
+ objectLayer.data,
796
+ `${itemId}_data.json`,
797
+ `/object-layer/${itemId}/${itemId}_data.json`,
798
+ );
799
+ if (ipfsResult) {
800
+ objectLayer.cid = ipfsResult.cid;
801
+ if (userId && createPinRecord) {
802
+ await createPinRecord({ cid: ipfsResult.cid, userId, options });
803
+ }
804
+ }
805
+ } catch (ipfsError) {
806
+ logger.warn('Failed to add object layer data to IPFS:', ipfsError.message);
807
+ }
808
+ }
809
+
810
+ objectLayer.sha256 = finalSha256;
811
+ objectLayer.markModified('data');
812
+ await objectLayer.save();
813
+
814
+ return objectLayer;
815
+ }
816
+
817
+ /**
818
+ * Internal helper that generates an atlas sprite sheet and then finalizes the SHA-256 / IPFS CID.
819
+ * @static
820
+ * @param {Object} params - Parameters.
821
+ * @param {Object} params.objectLayer - The mongoose ObjectLayer document.
822
+ * @param {Object} params.ObjectLayer - Mongoose ObjectLayer model (for re-reading).
823
+ * @param {Object} params.atlasServiceContext - Context with `{ req, res, options, AtlasSpriteSheetService, IpfsClient, createPinRecord }`.
824
+ * @param {boolean} params.isNew - Whether this is a newly created object layer (used for logging).
825
+ * @returns {Promise<Object>} The finalized ObjectLayer document.
826
+ * @memberof CyberiaObjectLayer
827
+ * @private
828
+ */
829
+ static async _generateAtlasAndFinalize({ objectLayer, ObjectLayer, atlasServiceContext, isNew }) {
830
+ const { req, res, options, AtlasSpriteSheetService, IpfsClient: ipfsClient, createPinRecord } = atlasServiceContext;
831
+
832
+ // Generate atlas sprite sheet
833
+ try {
834
+ await AtlasSpriteSheetService.generate(
835
+ { params: { id: objectLayer._id }, objectLayer, auth: req ? req.auth : undefined },
836
+ res,
837
+ options,
838
+ );
839
+ } catch (atlasError) {
840
+ logger.error(`Failed to auto-${isNew ? 'generate' : 'update'} atlas for ObjectLayer:`, atlasError);
841
+ }
842
+
843
+ // Re-read the objectLayer so data.atlasSpriteSheetCid is up-to-date
844
+ objectLayer = await ObjectLayer.findById(objectLayer._id).populate('objectLayerRenderFramesId');
845
+
846
+ // Compute definitive SHA-256 and IPFS CID
847
+ const userId = req && req.auth && req.auth.user ? req.auth.user._id : undefined;
848
+ objectLayer = await ObjectLayerEngine.computeAndSaveFinalSha256({
849
+ objectLayer,
850
+ ipfsClient: ipfsClient || null,
851
+ createPinRecord: createPinRecord || null,
852
+ userId,
853
+ options,
854
+ });
855
+
856
+ return objectLayer;
857
+ }
313
858
  }
314
859
 
315
860
  /**
861
+ * Mapping of item type names to numerical IDs.
316
862
  * @constant
317
- * @description Mapping of item type names to numerical IDs.
318
863
  * @type {{floor: number, skin: number, weapon: number, skill: number, coin: number}}
319
864
  * @memberof CyberiaObjectLayer
320
865
  */
321
866
  export const itemTypes = { floor: 0, skin: 1, weapon: 2, skill: 3, coin: 4 };
322
867
 
323
- // Export equivalent for backward compatibility with existing destructured imports.
868
+ // ──────────────────────────────────────────────────────────────────────────
869
+ // Backward-compatible named exports matching the original destructured imports.
870
+ // ──────────────────────────────────────────────────────────────────────────
871
+
872
+ /**
873
+ * @see {@link ObjectLayerEngine.pngDirectoryIteratorByObjectLayerType}
874
+ * @function pngDirectoryIteratorByObjectLayerType
875
+ * @memberof CyberiaObjectLayer
876
+ */
324
877
  export const pngDirectoryIteratorByObjectLayerType = ObjectLayerEngine.pngDirectoryIteratorByObjectLayerType;
878
+
879
+ /**
880
+ * @see {@link ObjectLayerEngine.readPngAsync}
881
+ * @function readPngAsync
882
+ * @memberof CyberiaObjectLayer
883
+ */
325
884
  export const readPngAsync = ObjectLayerEngine.readPngAsync;
885
+
886
+ /**
887
+ * @see {@link ObjectLayerEngine.frameFactory}
888
+ * @function frameFactory
889
+ * @memberof CyberiaObjectLayer
890
+ */
326
891
  export const frameFactory = ObjectLayerEngine.frameFactory;
892
+
893
+ /**
894
+ * @see {@link ObjectLayerEngine.getKeyFramesDirectionsFromNumberFolderDirection}
895
+ * @function getKeyFramesDirectionsFromNumberFolderDirection
896
+ * @memberof CyberiaObjectLayer
897
+ */
327
898
  export const getKeyFramesDirectionsFromNumberFolderDirection =
328
899
  ObjectLayerEngine.getKeyFramesDirectionsFromNumberFolderDirection;
900
+
901
+ /**
902
+ * @see {@link ObjectLayerEngine.processAndPushFrame}
903
+ * @function processAndPushFrame
904
+ * @memberof CyberiaObjectLayer
905
+ */
329
906
  export const processAndPushFrame = ObjectLayerEngine.processAndPushFrame;
907
+
908
+ /**
909
+ * @see {@link ObjectLayerEngine.buildImgFromTile}
910
+ * @function buildImgFromTile
911
+ * @memberof CyberiaObjectLayer
912
+ */
330
913
  export const buildImgFromTile = ObjectLayerEngine.buildImgFromTile;
914
+
915
+ /**
916
+ * @see {@link ObjectLayerEngine.generateRandomStats}
917
+ * @function generateRandomStats
918
+ * @memberof CyberiaObjectLayer
919
+ */
331
920
  export const generateRandomStats = ObjectLayerEngine.generateRandomStats;
921
+
922
+ /**
923
+ * @see {@link ObjectLayerEngine.buildObjectLayerDataFromDirectory}
924
+ * @function buildObjectLayerDataFromDirectory
925
+ * @memberof CyberiaObjectLayer
926
+ */
927
+ export const buildObjectLayerDataFromDirectory = ObjectLayerEngine.buildObjectLayerDataFromDirectory;
928
+
929
+ /**
930
+ * @see {@link ObjectLayerEngine.computeSha256}
931
+ * @function computeSha256
932
+ * @memberof CyberiaObjectLayer
933
+ */
934
+ export const computeSha256 = ObjectLayerEngine.computeSha256;
935
+
936
+ /**
937
+ * @see {@link ObjectLayerEngine.createObjectLayerDocuments}
938
+ * @function createObjectLayerDocuments
939
+ * @memberof CyberiaObjectLayer
940
+ */
941
+ export const createObjectLayerDocuments = ObjectLayerEngine.createObjectLayerDocuments;
942
+
943
+ /**
944
+ * @see {@link ObjectLayerEngine.updateObjectLayerDocuments}
945
+ * @function updateObjectLayerDocuments
946
+ * @memberof CyberiaObjectLayer
947
+ */
948
+ export const updateObjectLayerDocuments = ObjectLayerEngine.updateObjectLayerDocuments;
949
+
950
+ /**
951
+ * @see {@link ObjectLayerEngine.computeAndSaveFinalSha256}
952
+ * @function computeAndSaveFinalSha256
953
+ * @memberof CyberiaObjectLayer
954
+ */
955
+ export const computeAndSaveFinalSha256 = ObjectLayerEngine.computeAndSaveFinalSha256;
956
+
957
+ /**
958
+ * @see {@link ObjectLayerEngine.writeStaticFrameAssets}
959
+ * @function writeStaticFrameAssets
960
+ * @memberof CyberiaObjectLayer
961
+ */
962
+ export const writeStaticFrameAssets = ObjectLayerEngine.writeStaticFrameAssets;