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.
- package/.github/workflows/engine-cyberia.cd.yml +1 -0
- package/CHANGELOG.md +56 -1
- package/CLI-HELP.md +2 -4
- package/README.md +139 -0
- package/bin/build.js +5 -0
- package/bin/cyberia.js +385 -71
- package/bin/deploy.js +18 -26
- package/bin/file.js +3 -0
- package/bin/index.js +385 -71
- package/conf.js +32 -3
- package/deployment.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/manifests/ipfs/configmap.yaml +7 -0
- package/package.json +8 -8
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
- package/src/api/file/file.controller.js +3 -13
- package/src/api/file/file.ref.json +0 -21
- package/src/api/ipfs/ipfs.controller.js +104 -0
- package/src/api/ipfs/ipfs.model.js +71 -0
- package/src/api/ipfs/ipfs.router.js +31 -0
- package/src/api/ipfs/ipfs.service.js +193 -0
- package/src/api/object-layer/README.md +139 -0
- package/src/api/object-layer/object-layer.controller.js +3 -0
- package/src/api/object-layer/object-layer.model.js +15 -1
- package/src/api/object-layer/object-layer.router.js +6 -10
- package/src/api/object-layer/object-layer.service.js +311 -182
- package/src/cli/cluster.js +30 -38
- package/src/cli/index.js +0 -1
- package/src/cli/run.js +14 -0
- package/src/client/components/core/LoadingAnimation.js +2 -3
- package/src/client/components/core/Modal.js +1 -1
- package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
- package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
- package/src/client/services/ipfs/ipfs.service.js +144 -0
- package/src/client/services/object-layer/object-layer.management.js +161 -8
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +1 -1
- package/src/server/auth.js +18 -18
- package/src/server/ipfs-client.js +433 -0
- package/src/server/object-layer.js +649 -18
- package/src/server/semantic-layer-generator.js +1083 -0
- package/src/server/shape-generator.js +952 -0
- package/test/shape-generator.test.js +457 -0
- 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
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
171
|
+
// ── Handle --import ──────────────────────────────────────────────
|
|
106
172
|
if (options.import) {
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
175
|
-
objectLayers[objectLayerId]
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
|
|
519
|
+
// Unzip the file:
|
|
520
|
+
shellExec(`tar -xvzf besu-24.9.1.tar.gz`);
|
|
524
521
|
|
|
525
|
-
|
|
522
|
+
shellCd(`besu-24.9.1`);
|
|
526
523
|
|
|
527
|
-
|
|
528
|
-
// export PATH=$PATH:/home/dd/besu-24.9.1/bin
|
|
524
|
+
shellExec(`bin/besu --help`);
|
|
529
525
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
break;
|
|
526
|
+
// Set env path
|
|
527
|
+
// export PATH=$PATH:/home/dd/besu-24.9.1/bin
|
|
535
528
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
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);
|