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/index.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/conf.js
CHANGED
|
@@ -130,6 +130,8 @@ const DefaultConf = /**/ {
|
|
|
130
130
|
public_folder: '/dist/vanilla-jsoneditor/standalone.js',
|
|
131
131
|
import_name: 'vanilla-jsoneditor',
|
|
132
132
|
import_name_build: '/dist/vanilla-jsoneditor/standalone.js',
|
|
133
|
+
styles: './node_modules/vanilla-jsoneditor/themes',
|
|
134
|
+
public_styles_folder: '/styles/vanilla-jsoneditor',
|
|
133
135
|
},
|
|
134
136
|
],
|
|
135
137
|
services: [
|
|
@@ -408,10 +410,15 @@ const DefaultConf = /**/ {
|
|
|
408
410
|
'object-layer',
|
|
409
411
|
'object-layer-render-frames',
|
|
410
412
|
'atlas-sprite-sheet',
|
|
413
|
+
'ipfs',
|
|
411
414
|
],
|
|
412
415
|
ws: 'core',
|
|
413
416
|
peer: true,
|
|
414
|
-
origins: [
|
|
417
|
+
origins: [
|
|
418
|
+
'https://www.cyberiaonline.com',
|
|
419
|
+
'https://server.cyberiaonline.com',
|
|
420
|
+
'https://client.cyberiaonline.com',
|
|
421
|
+
],
|
|
415
422
|
minifyBuild: false,
|
|
416
423
|
liteBuild: true,
|
|
417
424
|
docsBuild: false,
|
|
@@ -456,7 +463,18 @@ const DefaultConf = /**/ {
|
|
|
456
463
|
'/': {
|
|
457
464
|
client: 'cryptokoyn',
|
|
458
465
|
runtime: 'nodejs',
|
|
459
|
-
apis: [
|
|
466
|
+
apis: [
|
|
467
|
+
'core',
|
|
468
|
+
'file',
|
|
469
|
+
'user',
|
|
470
|
+
'crypto',
|
|
471
|
+
'document',
|
|
472
|
+
'instance',
|
|
473
|
+
'object-layer',
|
|
474
|
+
'object-layer-render-frames',
|
|
475
|
+
'atlas-sprite-sheet',
|
|
476
|
+
'ipfs',
|
|
477
|
+
],
|
|
460
478
|
origins: [],
|
|
461
479
|
minifyBuild: false,
|
|
462
480
|
liteBuild: true,
|
|
@@ -504,7 +522,18 @@ const DefaultConf = /**/ {
|
|
|
504
522
|
'/': {
|
|
505
523
|
client: 'itemledger',
|
|
506
524
|
runtime: 'nodejs',
|
|
507
|
-
apis: [
|
|
525
|
+
apis: [
|
|
526
|
+
'core',
|
|
527
|
+
'file',
|
|
528
|
+
'user',
|
|
529
|
+
'crypto',
|
|
530
|
+
'document',
|
|
531
|
+
'instance',
|
|
532
|
+
'object-layer',
|
|
533
|
+
'object-layer-render-frames',
|
|
534
|
+
'atlas-sprite-sheet',
|
|
535
|
+
'ipfs',
|
|
536
|
+
],
|
|
508
537
|
origins: [],
|
|
509
538
|
minifyBuild: false,
|
|
510
539
|
liteBuild: true,
|
package/deployment.yaml
CHANGED
|
@@ -18,7 +18,7 @@ spec:
|
|
|
18
18
|
spec:
|
|
19
19
|
containers:
|
|
20
20
|
- name: dd-cyberia-development-blue
|
|
21
|
-
image: localhost/rockylinux9-underpost:v3.0.
|
|
21
|
+
image: localhost/rockylinux9-underpost:v3.0.2
|
|
22
22
|
|
|
23
23
|
command:
|
|
24
24
|
- /bin/sh
|
|
@@ -156,7 +156,7 @@ spec:
|
|
|
156
156
|
spec:
|
|
157
157
|
containers:
|
|
158
158
|
- name: dd-cyberia-development-green
|
|
159
|
-
image: localhost/rockylinux9-underpost:v3.0.
|
|
159
|
+
image: localhost/rockylinux9-underpost:v3.0.2
|
|
160
160
|
|
|
161
161
|
command:
|
|
162
162
|
- /bin/sh
|