@directivegames/genesys.sdk 3.2.2 → 3.2.4
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/README.md +60 -60
- package/dist/src/core/cli.js +22 -22
- package/dist/src/templates/scripts/genesys/genesys-mcp.js +25 -25
- package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +4 -4
- package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +30 -30
- package/package.json +176 -176
- package/scripts/post-install.ts +143 -143
- package/src/asset-pack/.gitattributes +88 -88
- package/src/asset-pack/eslint.config.js +45 -45
- package/src/asset-pack/gitignore +11 -11
- package/src/asset-pack/scripts/postinstall.ts +81 -81
- package/src/asset-pack/tsconfig.json +33 -33
- package/src/templates/.cursor/mcp.json +20 -20
- package/src/templates/.cursorignore +2 -2
- package/src/templates/.gitattributes +88 -88
- package/src/templates/.vscode/settings.json +6 -6
- package/src/templates/AGENTS.md +86 -86
- package/src/templates/README.md +24 -24
- package/src/templates/eslint.config.js +45 -45
- package/src/templates/gitignore +11 -11
- package/src/templates/index.html +34 -34
- package/src/templates/pnpm-lock.yaml +3676 -3676
- package/src/templates/scripts/genesys/build-project.ts +51 -51
- package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -272
- package/src/templates/scripts/genesys/common.ts +46 -46
- package/src/templates/scripts/genesys/const.ts +9 -9
- package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -11
- package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -146
- package/src/templates/scripts/genesys/dev/launcher.ts +46 -46
- package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -229
- package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -84
- package/src/templates/scripts/genesys/doc-server.ts +16 -16
- package/src/templates/scripts/genesys/genesys-mcp.ts +526 -526
- package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -86
- package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -151
- package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -73
- package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -35
- package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -30
- package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -858
- package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -380
- package/src/templates/scripts/genesys/mcp/utils.ts +281 -281
- package/src/templates/scripts/genesys/misc.ts +42 -42
- package/src/templates/scripts/genesys/mock.ts +6 -6
- package/src/templates/scripts/genesys/place-actors.ts +179 -179
- package/src/templates/scripts/genesys/post-install.ts +30 -30
- package/src/templates/scripts/genesys/prefab.schema.json +84 -84
- package/src/templates/scripts/genesys/remove-engine-comments.ts +134 -134
- package/src/templates/scripts/genesys/run-mcp-inspector.bat +4 -4
- package/src/templates/scripts/genesys/storageProvider.ts +182 -182
- package/src/templates/scripts/genesys/validate-prefabs.ts +138 -138
- package/src/templates/src/index.ts +22 -22
- package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/firstPerson/src/game.ts +39 -39
- package/src/templates/src/templates/firstPerson/src/player.ts +63 -63
- package/src/templates/src/templates/fps/assets/default.genesys-scene +9459 -9459
- package/src/templates/src/templates/fps/src/game.ts +39 -39
- package/src/templates/src/templates/fps/src/player.ts +69 -69
- package/src/templates/src/templates/fps/src/weapon.ts +54 -54
- package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/freeCamera/src/game.ts +39 -39
- package/src/templates/src/templates/freeCamera/src/player.ts +45 -45
- package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +121 -121
- package/src/templates/src/templates/sideScroller/src/const.ts +45 -45
- package/src/templates/src/templates/sideScroller/src/game.ts +122 -122
- package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -361
- package/src/templates/src/templates/sideScroller/src/player.ts +125 -125
- package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/thirdPerson/src/game.ts +39 -39
- package/src/templates/src/templates/thirdPerson/src/player.ts +61 -61
- package/src/templates/src/templates/vehicle/assets/default.genesys-scene +225 -225
- package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -145
- package/src/templates/src/templates/vehicle/src/game.ts +43 -43
- package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -191
- package/src/templates/src/templates/vehicle/src/player.ts +109 -109
- package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -266
- package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -101
- package/src/templates/src/templates/vr-game/assets/default.genesys-scene +246 -246
- package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -1
- package/src/templates/src/templates/vr-game/src/game.ts +66 -66
- package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -26
- package/src/templates/tsconfig.json +34 -34
- package/src/templates/vite.config.ts +52 -52
|
@@ -1,380 +1,380 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
import * as ENGINE from 'genesys.js';
|
|
5
|
-
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
-
|
|
7
|
-
import { BoundingBoxSchema, fetchBoundingBoxData } from '../calc-bounding-box.js';
|
|
8
|
-
import { ENGINE_PREFIX, JS_CLASSES_DIR_NAME, PROJECT_PREFIX, SCENE_EXTENSION } from '../const.js';
|
|
9
|
-
import { StorageProvider } from '../storageProvider.js';
|
|
10
|
-
|
|
11
|
-
import { populateClassesInfo } from './search-actors.js';
|
|
12
|
-
import { conditionallyRegisterGameClasses } from './utils.js';
|
|
13
|
-
import { isSubclass } from './utils.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// TODO: make it consistent to AssetType in genesys.ai
|
|
17
|
-
export enum AssetType {
|
|
18
|
-
Model = 'model',
|
|
19
|
-
Texture = 'texture',
|
|
20
|
-
HDRI = 'hdri',
|
|
21
|
-
Video = 'video',
|
|
22
|
-
Audio = 'audio',
|
|
23
|
-
Json = 'json',
|
|
24
|
-
Scene = 'scene',
|
|
25
|
-
Prefab = 'prefab',
|
|
26
|
-
Material = 'material',
|
|
27
|
-
SourceCode = 'sourcecode',
|
|
28
|
-
JsClass = 'jsclass',
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const modelTypes = ['.glb', '.gltf'];
|
|
32
|
-
|
|
33
|
-
export const assetDescriptions: Record<AssetType, string> = {
|
|
34
|
-
[AssetType.Model]: '3D model files',
|
|
35
|
-
[AssetType.Texture]: 'Texture files',
|
|
36
|
-
[AssetType.HDRI]: 'HDRI files',
|
|
37
|
-
[AssetType.Video]: 'Video files',
|
|
38
|
-
[AssetType.Audio]: 'Audio files',
|
|
39
|
-
[AssetType.Scene]: 'Scenes, also known as levels, are generally used to represent distinct, playable areas in the game.',
|
|
40
|
-
[AssetType.Prefab]: 'Prefabs are actors that are prebuilt with components and can be placed in the scene directly. They generally extend Javascript classes to achieve more visual complex behavior.',
|
|
41
|
-
[AssetType.Material]: 'Material files',
|
|
42
|
-
[AssetType.SourceCode]: 'Source code files, including TypeScript and JavaScript files.',
|
|
43
|
-
[AssetType.JsClass]: 'JavaScript classes, which are actors that are implemented in code (Javascript). They contain visual and audio components, logics, or anything as they are implemented in code. They are often used directly in the scene, they also be used as a prefab base. Always consider this when placing things in the scene.',
|
|
44
|
-
[AssetType.Json]: 'JSON files, which are used to store data in a structured format.',
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const assetTypeToExtensions: Record<AssetType, string[]> = {
|
|
48
|
-
[AssetType.Model]: modelTypes,
|
|
49
|
-
[AssetType.Texture]: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.ico', '.webp'],
|
|
50
|
-
[AssetType.HDRI]: ['.hdr', '.exr'],
|
|
51
|
-
[AssetType.Video]: ['.mp4', '.webm', '.mov', '.avi', '.mkv'],
|
|
52
|
-
[AssetType.Audio]: ['.mp3', '.wav', '.ogg', '.m4a', '.aac'],
|
|
53
|
-
[AssetType.Scene]: [SCENE_EXTENSION],
|
|
54
|
-
[AssetType.Prefab]: ['.prefab.json'],
|
|
55
|
-
[AssetType.Material]: ['.material.json'],
|
|
56
|
-
[AssetType.SourceCode]: ['.ts', '.js', '.tsx', '.jsx'],
|
|
57
|
-
[AssetType.JsClass]: [],
|
|
58
|
-
[AssetType.Json]: ['.json'],
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
interface AssetPopulationOptions {
|
|
62
|
-
peek?: boolean; // if true, only populate the basic metadata, such as name and description
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const findAssetType = (filePath: string) => {
|
|
66
|
-
if (filePath.includes(JS_CLASSES_DIR_NAME)) {
|
|
67
|
-
return AssetType.JsClass;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const extension = path.extname(filePath).toLowerCase();
|
|
71
|
-
for (const [assetType, extensions] of Object.entries(assetTypeToExtensions)) {
|
|
72
|
-
// make sure .json is checked last
|
|
73
|
-
if (assetType === AssetType.Json) {
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (extensions.includes(extension)) {
|
|
78
|
-
return assetType as AssetType;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (assetTypeToExtensions[AssetType.Json].includes(extension)) {
|
|
83
|
-
return AssetType.Json;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return undefined;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
export interface Metadata {
|
|
90
|
-
[key: string]: any;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface AssetsInfo {
|
|
94
|
-
metadataDescription: Record<string, any>;
|
|
95
|
-
assets: Record<string, Metadata>;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Recursively iterates a given directory and returns an array of all file paths
|
|
102
|
-
* that match the accepted types.
|
|
103
|
-
* @param dir The directory to iterate.
|
|
104
|
-
* @param acceptedTypes Array of accepted types.
|
|
105
|
-
* @returns An array of file paths.
|
|
106
|
-
*/
|
|
107
|
-
export async function searchForAssets(
|
|
108
|
-
dirs: string[],
|
|
109
|
-
acceptedTypes: AssetType[] = [],
|
|
110
|
-
searchKeywords: string[] = []
|
|
111
|
-
): Promise<AssetsInfo> {
|
|
112
|
-
for (const dir of dirs) {
|
|
113
|
-
if (!dir.startsWith(ENGINE.PROJECT_PATH_PREFIX) && !dir.startsWith(ENGINE.ENGINE_PATH_PREFIX)) {
|
|
114
|
-
throw new Error(`Directory ${dir} is not a valid project or engine directory`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// make sure the game is built and classes are registered
|
|
119
|
-
await conditionallyRegisterGameClasses();
|
|
120
|
-
|
|
121
|
-
const acceptedExtensions = acceptedTypes.map(type => assetTypeToExtensions[type]).flat();
|
|
122
|
-
const getAll = acceptedTypes.length === 0;
|
|
123
|
-
const files = dirs.map(dir => collectFiles(dir, { getAll, acceptedExtensions })).flat();
|
|
124
|
-
|
|
125
|
-
// handle js classes in a special way.
|
|
126
|
-
if (getAll || acceptedTypes.includes(AssetType.JsClass)) {
|
|
127
|
-
const jsClasses = dirs.map(dir => {
|
|
128
|
-
const prefix = dir.includes(PROJECT_PREFIX) ? ENGINE.Prefix.GAME : (dir.includes(ENGINE_PREFIX) ? ENGINE.Prefix.ENGINE : '');
|
|
129
|
-
const registeredClasses = ENGINE.ClassRegistry.getRegistry();
|
|
130
|
-
const actorClasses = Array.from(registeredClasses.entries()).filter(
|
|
131
|
-
([className, classCtor]) => className.startsWith(prefix) && isSubclass(classCtor, ENGINE.Actor) && !isSubclass(classCtor, ENGINE.BaseGameLoop));
|
|
132
|
-
return actorClasses.map(([key]) => `${JS_CLASSES_DIR_NAME}/${key}`);
|
|
133
|
-
});
|
|
134
|
-
files.push(...jsClasses.flat());
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// convert file paths to unix paths
|
|
138
|
-
const assetPaths: string[] = files.map(filePath => filePath.replace(/\\/g, '/'));
|
|
139
|
-
|
|
140
|
-
// only peeking for basic metadata when searching assets to reduce the amount of data transferred
|
|
141
|
-
const result: AssetsInfo = await populateAssets(assetPaths, { peek: true });
|
|
142
|
-
|
|
143
|
-
// filter assets as we rely on not only names, but the metadata as well, to filter them
|
|
144
|
-
filterAssets(result, searchKeywords);
|
|
145
|
-
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Gets the assets info with metadata for the given asset paths.
|
|
152
|
-
* @param assetPaths The paths of the assets to populate. It must be retrieved from `searchForAssets` function.
|
|
153
|
-
* @param options population options, such as `peek` to only populate basic metadata.
|
|
154
|
-
* @returns A promise that resolves to the assets info with metadata.
|
|
155
|
-
*/
|
|
156
|
-
export async function populateAssets(
|
|
157
|
-
assetPaths: string[],
|
|
158
|
-
options: AssetPopulationOptions
|
|
159
|
-
): Promise<AssetsInfo> {
|
|
160
|
-
|
|
161
|
-
const result: AssetsInfo = {
|
|
162
|
-
metadataDescription: {},
|
|
163
|
-
assets: assetPaths.reduce((acc, assetPath) => {
|
|
164
|
-
acc[assetPath] = {type: findAssetType(assetPath)};
|
|
165
|
-
return acc;
|
|
166
|
-
}, {} as Record<string, Metadata>)
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
await populateModels(result, options);
|
|
170
|
-
await populateJsClasses(result, options);
|
|
171
|
-
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
function collectFiles(
|
|
177
|
-
dir: string,
|
|
178
|
-
options: {
|
|
179
|
-
getAll: boolean;
|
|
180
|
-
acceptedExtensions: string[];
|
|
181
|
-
}
|
|
182
|
-
): string[] {
|
|
183
|
-
let results: string[] = [];
|
|
184
|
-
const storageProvider = new StorageProvider();
|
|
185
|
-
const actualDir = storageProvider.getFullPath(dir);
|
|
186
|
-
const list = fs.readdirSync(actualDir);
|
|
187
|
-
// Normalize accepted extensions to lower case for case-insensitive comparison
|
|
188
|
-
const normalizedExtensions = options.acceptedExtensions.map(ext => ext.toLowerCase());
|
|
189
|
-
list.forEach((file) => {
|
|
190
|
-
const filePath = path.join(dir, file);
|
|
191
|
-
const actualFilePath = storageProvider.getFullPath(filePath);
|
|
192
|
-
const stat = fs.statSync(actualFilePath);
|
|
193
|
-
if (stat && stat.isDirectory()) {
|
|
194
|
-
results = results.concat(collectFiles(filePath, options));
|
|
195
|
-
} else {
|
|
196
|
-
let shouldKeep = false;
|
|
197
|
-
if (options.getAll) {
|
|
198
|
-
shouldKeep = true;
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
const fileName = path.basename(filePath).toLowerCase();
|
|
202
|
-
if (normalizedExtensions.some((ext) => fileName.endsWith(ext))) {
|
|
203
|
-
shouldKeep = true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (shouldKeep) {
|
|
207
|
-
results.push(filePath);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
return results;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
function filterAssets(assets: AssetsInfo, searchKeywords: string[] = []) {
|
|
216
|
-
// if searchKeywords is not empty, filter the result
|
|
217
|
-
if (searchKeywords.length > 0) {
|
|
218
|
-
const rule = (filePath: string, metadata: any) => {
|
|
219
|
-
for (const keyword of searchKeywords) {
|
|
220
|
-
const kw = keyword.toLowerCase();
|
|
221
|
-
|
|
222
|
-
// if the file path contains any of the search keywords, return true
|
|
223
|
-
if (filePath.toLowerCase().includes(kw)) {
|
|
224
|
-
return true;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// if the file metadata values contains any of the search keywords, return true
|
|
228
|
-
if (Object.values(metadata).some((metadataValue: any) => {
|
|
229
|
-
return JSON.stringify(metadataValue).toLowerCase().includes(kw);
|
|
230
|
-
})) {
|
|
231
|
-
return true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
return false;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
assets.assets = Object.fromEntries(
|
|
238
|
-
Object.entries(assets.assets).filter(([filePath, metadata]) => rule(filePath, metadata))
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async function populateModels(assets: AssetsInfo, options: AssetPopulationOptions) {
|
|
244
|
-
if (Object.values(assets.assets).some(asset => asset.type === AssetType.Model)) {
|
|
245
|
-
await populateModelsMetadata(assets);
|
|
246
|
-
if (!options?.peek) {
|
|
247
|
-
await populateBoundingBoxes(assets);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
async function populateModelsMetadata(assets: AssetsInfo): Promise<void> {
|
|
253
|
-
// read manifest.json, and populate the result with the file details
|
|
254
|
-
const manifest: Record<string, Record<string, string>> = {};
|
|
255
|
-
const storageProvider = new StorageProvider();
|
|
256
|
-
for (const dir of [ENGINE.PROJECT_PATH_PREFIX, ENGINE.ENGINE_PATH_PREFIX]) {
|
|
257
|
-
const assetsManifest = path.join(dir, 'assets', 'manifest.json');
|
|
258
|
-
Object.assign(manifest, await storageProvider.downloadFileAsJson<any>(ENGINE.AssetPath.fromString(assetsManifest)));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
let metadataDescription: Record<string, any> = {};
|
|
262
|
-
if (manifest) {
|
|
263
|
-
// populate metadata description if it exists
|
|
264
|
-
metadataDescription = manifest['$metadata_description'] ?? {};
|
|
265
|
-
|
|
266
|
-
// populate the result with the file details
|
|
267
|
-
for (const [filePath, targetMetadata] of Object.entries(assets.assets)) {
|
|
268
|
-
const metadata = manifest[filePath];
|
|
269
|
-
if (metadata !== undefined && metadata !== null && metadata.constructor == Object) {
|
|
270
|
-
Object.assign(targetMetadata, metadata);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
assets.metadataDescription[AssetType.Model] = {
|
|
276
|
-
...assets.metadataDescription[AssetType.Model],
|
|
277
|
-
...metadataDescription,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async function populateBoundingBoxes(assets: AssetsInfo) {
|
|
282
|
-
const modelFiles = Object.keys(assets.assets).filter(filePath =>
|
|
283
|
-
modelTypes.includes(path.extname(filePath).toLowerCase())
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
// split model files into two groups - engine and project
|
|
287
|
-
const engineModelFiles = modelFiles.filter(filePath =>
|
|
288
|
-
filePath.startsWith(ENGINE.ENGINE_PATH_PREFIX)
|
|
289
|
-
);
|
|
290
|
-
const projectModelFiles = modelFiles.filter(filePath =>
|
|
291
|
-
filePath.startsWith(ENGINE.PROJECT_PATH_PREFIX)
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
const update = async (root: string, modelFiles: string[]) => {
|
|
295
|
-
const manifestFile = path.join(root, 'assets', 'bounding_box.json');
|
|
296
|
-
const storageProvider = new StorageProvider();
|
|
297
|
-
const gltfPaths: {[key: string]: string} = {};
|
|
298
|
-
for (const filePath of modelFiles) {
|
|
299
|
-
gltfPaths[filePath] = storageProvider.getFullPath(filePath);
|
|
300
|
-
}
|
|
301
|
-
const boundingBoxes = await fetchBoundingBoxData(storageProvider.getFullPath(manifestFile), gltfPaths);
|
|
302
|
-
for (const [filePath, boundingBox] of Object.entries(boundingBoxes)) {
|
|
303
|
-
assets.assets[filePath]['boundingBox'] = boundingBox;
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
if (engineModelFiles.length > 0) {
|
|
308
|
-
await update(ENGINE.ENGINE_PATH_PREFIX, engineModelFiles);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (projectModelFiles.length > 0) {
|
|
312
|
-
await update(ENGINE.PROJECT_PATH_PREFIX, projectModelFiles);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (engineModelFiles.length > 0 || projectModelFiles.length > 0) {
|
|
316
|
-
// also populate the metadata description for bounding box
|
|
317
|
-
const boundingBoxSchema = zodToJsonSchema(BoundingBoxSchema, 'BoundingBoxSchema');
|
|
318
|
-
const properties = (boundingBoxSchema as any).definitions.BoundingBoxSchema.properties;
|
|
319
|
-
const boundingBoxDescription: Record<string, string> = {};
|
|
320
|
-
for (const key in properties) {
|
|
321
|
-
if (properties[key].description) {
|
|
322
|
-
boundingBoxDescription[key] = properties[key].description;
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
const metadataDescription = {
|
|
326
|
-
boundingBox: {
|
|
327
|
-
description: 'The bounding box info of model assets',
|
|
328
|
-
properties: boundingBoxDescription,
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
assets.metadataDescription[AssetType.Model] = {
|
|
332
|
-
...assets.metadataDescription[AssetType.Model],
|
|
333
|
-
...metadataDescription
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
async function populateJsClasses(assets: AssetsInfo, options: AssetPopulationOptions): Promise<void> {
|
|
339
|
-
const jsClassNames: Record<string, string> = {};
|
|
340
|
-
for (const [filePath, metadata] of Object.entries(assets.assets)) {
|
|
341
|
-
if (filePath.includes(JS_CLASSES_DIR_NAME)) {
|
|
342
|
-
jsClassNames[filePath] = path.basename(filePath);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (Object.keys(jsClassNames).length === 0) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
const result = await populateClassesInfo({
|
|
350
|
-
classesToSearch: Object.values(jsClassNames),
|
|
351
|
-
includeConstructorParams: !options?.peek
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (result.actors
|
|
355
|
-
&& Object.keys(result.actors).length > 0
|
|
356
|
-
&& result.metadataDescription
|
|
357
|
-
&& Object.keys(result.metadataDescription).length > 0) {
|
|
358
|
-
|
|
359
|
-
assets.metadataDescription[AssetType.JsClass] = result.metadataDescription;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
for (const [filePath, metadata] of Object.entries(assets.assets)) {
|
|
363
|
-
if (filePath in jsClassNames) {
|
|
364
|
-
const className = jsClassNames[filePath];
|
|
365
|
-
const actorInfo = result.actors[className];
|
|
366
|
-
if (actorInfo) {
|
|
367
|
-
metadata.jsClassName = actorInfo.className;
|
|
368
|
-
if (!options?.peek) {
|
|
369
|
-
metadata.jsClassFilePath = actorInfo.filePath;
|
|
370
|
-
metadata.jsClassConstructorParams = actorInfo.constructorParams ?? [];
|
|
371
|
-
metadata.jsClassCanPopulateFromJson = actorInfo.canPopulateFromJson ?? false;
|
|
372
|
-
}
|
|
373
|
-
metadata.jsClassDescription = actorInfo.description ?? '';
|
|
374
|
-
} else {
|
|
375
|
-
console.warn(`Actor class ${className} not found in classes info for file ${filePath}`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import * as ENGINE from 'genesys.js';
|
|
5
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
6
|
+
|
|
7
|
+
import { BoundingBoxSchema, fetchBoundingBoxData } from '../calc-bounding-box.js';
|
|
8
|
+
import { ENGINE_PREFIX, JS_CLASSES_DIR_NAME, PROJECT_PREFIX, SCENE_EXTENSION } from '../const.js';
|
|
9
|
+
import { StorageProvider } from '../storageProvider.js';
|
|
10
|
+
|
|
11
|
+
import { populateClassesInfo } from './search-actors.js';
|
|
12
|
+
import { conditionallyRegisterGameClasses } from './utils.js';
|
|
13
|
+
import { isSubclass } from './utils.js';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// TODO: make it consistent to AssetType in genesys.ai
|
|
17
|
+
export enum AssetType {
|
|
18
|
+
Model = 'model',
|
|
19
|
+
Texture = 'texture',
|
|
20
|
+
HDRI = 'hdri',
|
|
21
|
+
Video = 'video',
|
|
22
|
+
Audio = 'audio',
|
|
23
|
+
Json = 'json',
|
|
24
|
+
Scene = 'scene',
|
|
25
|
+
Prefab = 'prefab',
|
|
26
|
+
Material = 'material',
|
|
27
|
+
SourceCode = 'sourcecode',
|
|
28
|
+
JsClass = 'jsclass',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const modelTypes = ['.glb', '.gltf'];
|
|
32
|
+
|
|
33
|
+
export const assetDescriptions: Record<AssetType, string> = {
|
|
34
|
+
[AssetType.Model]: '3D model files',
|
|
35
|
+
[AssetType.Texture]: 'Texture files',
|
|
36
|
+
[AssetType.HDRI]: 'HDRI files',
|
|
37
|
+
[AssetType.Video]: 'Video files',
|
|
38
|
+
[AssetType.Audio]: 'Audio files',
|
|
39
|
+
[AssetType.Scene]: 'Scenes, also known as levels, are generally used to represent distinct, playable areas in the game.',
|
|
40
|
+
[AssetType.Prefab]: 'Prefabs are actors that are prebuilt with components and can be placed in the scene directly. They generally extend Javascript classes to achieve more visual complex behavior.',
|
|
41
|
+
[AssetType.Material]: 'Material files',
|
|
42
|
+
[AssetType.SourceCode]: 'Source code files, including TypeScript and JavaScript files.',
|
|
43
|
+
[AssetType.JsClass]: 'JavaScript classes, which are actors that are implemented in code (Javascript). They contain visual and audio components, logics, or anything as they are implemented in code. They are often used directly in the scene, they also be used as a prefab base. Always consider this when placing things in the scene.',
|
|
44
|
+
[AssetType.Json]: 'JSON files, which are used to store data in a structured format.',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const assetTypeToExtensions: Record<AssetType, string[]> = {
|
|
48
|
+
[AssetType.Model]: modelTypes,
|
|
49
|
+
[AssetType.Texture]: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.ico', '.webp'],
|
|
50
|
+
[AssetType.HDRI]: ['.hdr', '.exr'],
|
|
51
|
+
[AssetType.Video]: ['.mp4', '.webm', '.mov', '.avi', '.mkv'],
|
|
52
|
+
[AssetType.Audio]: ['.mp3', '.wav', '.ogg', '.m4a', '.aac'],
|
|
53
|
+
[AssetType.Scene]: [SCENE_EXTENSION],
|
|
54
|
+
[AssetType.Prefab]: ['.prefab.json'],
|
|
55
|
+
[AssetType.Material]: ['.material.json'],
|
|
56
|
+
[AssetType.SourceCode]: ['.ts', '.js', '.tsx', '.jsx'],
|
|
57
|
+
[AssetType.JsClass]: [],
|
|
58
|
+
[AssetType.Json]: ['.json'],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
interface AssetPopulationOptions {
|
|
62
|
+
peek?: boolean; // if true, only populate the basic metadata, such as name and description
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const findAssetType = (filePath: string) => {
|
|
66
|
+
if (filePath.includes(JS_CLASSES_DIR_NAME)) {
|
|
67
|
+
return AssetType.JsClass;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
71
|
+
for (const [assetType, extensions] of Object.entries(assetTypeToExtensions)) {
|
|
72
|
+
// make sure .json is checked last
|
|
73
|
+
if (assetType === AssetType.Json) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (extensions.includes(extension)) {
|
|
78
|
+
return assetType as AssetType;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (assetTypeToExtensions[AssetType.Json].includes(extension)) {
|
|
83
|
+
return AssetType.Json;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export interface Metadata {
|
|
90
|
+
[key: string]: any;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface AssetsInfo {
|
|
94
|
+
metadataDescription: Record<string, any>;
|
|
95
|
+
assets: Record<string, Metadata>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Recursively iterates a given directory and returns an array of all file paths
|
|
102
|
+
* that match the accepted types.
|
|
103
|
+
* @param dir The directory to iterate.
|
|
104
|
+
* @param acceptedTypes Array of accepted types.
|
|
105
|
+
* @returns An array of file paths.
|
|
106
|
+
*/
|
|
107
|
+
export async function searchForAssets(
|
|
108
|
+
dirs: string[],
|
|
109
|
+
acceptedTypes: AssetType[] = [],
|
|
110
|
+
searchKeywords: string[] = []
|
|
111
|
+
): Promise<AssetsInfo> {
|
|
112
|
+
for (const dir of dirs) {
|
|
113
|
+
if (!dir.startsWith(ENGINE.PROJECT_PATH_PREFIX) && !dir.startsWith(ENGINE.ENGINE_PATH_PREFIX)) {
|
|
114
|
+
throw new Error(`Directory ${dir} is not a valid project or engine directory`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// make sure the game is built and classes are registered
|
|
119
|
+
await conditionallyRegisterGameClasses();
|
|
120
|
+
|
|
121
|
+
const acceptedExtensions = acceptedTypes.map(type => assetTypeToExtensions[type]).flat();
|
|
122
|
+
const getAll = acceptedTypes.length === 0;
|
|
123
|
+
const files = dirs.map(dir => collectFiles(dir, { getAll, acceptedExtensions })).flat();
|
|
124
|
+
|
|
125
|
+
// handle js classes in a special way.
|
|
126
|
+
if (getAll || acceptedTypes.includes(AssetType.JsClass)) {
|
|
127
|
+
const jsClasses = dirs.map(dir => {
|
|
128
|
+
const prefix = dir.includes(PROJECT_PREFIX) ? ENGINE.Prefix.GAME : (dir.includes(ENGINE_PREFIX) ? ENGINE.Prefix.ENGINE : '');
|
|
129
|
+
const registeredClasses = ENGINE.ClassRegistry.getRegistry();
|
|
130
|
+
const actorClasses = Array.from(registeredClasses.entries()).filter(
|
|
131
|
+
([className, classCtor]) => className.startsWith(prefix) && isSubclass(classCtor, ENGINE.Actor) && !isSubclass(classCtor, ENGINE.BaseGameLoop));
|
|
132
|
+
return actorClasses.map(([key]) => `${JS_CLASSES_DIR_NAME}/${key}`);
|
|
133
|
+
});
|
|
134
|
+
files.push(...jsClasses.flat());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// convert file paths to unix paths
|
|
138
|
+
const assetPaths: string[] = files.map(filePath => filePath.replace(/\\/g, '/'));
|
|
139
|
+
|
|
140
|
+
// only peeking for basic metadata when searching assets to reduce the amount of data transferred
|
|
141
|
+
const result: AssetsInfo = await populateAssets(assetPaths, { peek: true });
|
|
142
|
+
|
|
143
|
+
// filter assets as we rely on not only names, but the metadata as well, to filter them
|
|
144
|
+
filterAssets(result, searchKeywords);
|
|
145
|
+
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gets the assets info with metadata for the given asset paths.
|
|
152
|
+
* @param assetPaths The paths of the assets to populate. It must be retrieved from `searchForAssets` function.
|
|
153
|
+
* @param options population options, such as `peek` to only populate basic metadata.
|
|
154
|
+
* @returns A promise that resolves to the assets info with metadata.
|
|
155
|
+
*/
|
|
156
|
+
export async function populateAssets(
|
|
157
|
+
assetPaths: string[],
|
|
158
|
+
options: AssetPopulationOptions
|
|
159
|
+
): Promise<AssetsInfo> {
|
|
160
|
+
|
|
161
|
+
const result: AssetsInfo = {
|
|
162
|
+
metadataDescription: {},
|
|
163
|
+
assets: assetPaths.reduce((acc, assetPath) => {
|
|
164
|
+
acc[assetPath] = {type: findAssetType(assetPath)};
|
|
165
|
+
return acc;
|
|
166
|
+
}, {} as Record<string, Metadata>)
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
await populateModels(result, options);
|
|
170
|
+
await populateJsClasses(result, options);
|
|
171
|
+
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
function collectFiles(
|
|
177
|
+
dir: string,
|
|
178
|
+
options: {
|
|
179
|
+
getAll: boolean;
|
|
180
|
+
acceptedExtensions: string[];
|
|
181
|
+
}
|
|
182
|
+
): string[] {
|
|
183
|
+
let results: string[] = [];
|
|
184
|
+
const storageProvider = new StorageProvider();
|
|
185
|
+
const actualDir = storageProvider.getFullPath(dir);
|
|
186
|
+
const list = fs.readdirSync(actualDir);
|
|
187
|
+
// Normalize accepted extensions to lower case for case-insensitive comparison
|
|
188
|
+
const normalizedExtensions = options.acceptedExtensions.map(ext => ext.toLowerCase());
|
|
189
|
+
list.forEach((file) => {
|
|
190
|
+
const filePath = path.join(dir, file);
|
|
191
|
+
const actualFilePath = storageProvider.getFullPath(filePath);
|
|
192
|
+
const stat = fs.statSync(actualFilePath);
|
|
193
|
+
if (stat && stat.isDirectory()) {
|
|
194
|
+
results = results.concat(collectFiles(filePath, options));
|
|
195
|
+
} else {
|
|
196
|
+
let shouldKeep = false;
|
|
197
|
+
if (options.getAll) {
|
|
198
|
+
shouldKeep = true;
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
202
|
+
if (normalizedExtensions.some((ext) => fileName.endsWith(ext))) {
|
|
203
|
+
shouldKeep = true;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (shouldKeep) {
|
|
207
|
+
results.push(filePath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
return results;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
function filterAssets(assets: AssetsInfo, searchKeywords: string[] = []) {
|
|
216
|
+
// if searchKeywords is not empty, filter the result
|
|
217
|
+
if (searchKeywords.length > 0) {
|
|
218
|
+
const rule = (filePath: string, metadata: any) => {
|
|
219
|
+
for (const keyword of searchKeywords) {
|
|
220
|
+
const kw = keyword.toLowerCase();
|
|
221
|
+
|
|
222
|
+
// if the file path contains any of the search keywords, return true
|
|
223
|
+
if (filePath.toLowerCase().includes(kw)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// if the file metadata values contains any of the search keywords, return true
|
|
228
|
+
if (Object.values(metadata).some((metadataValue: any) => {
|
|
229
|
+
return JSON.stringify(metadataValue).toLowerCase().includes(kw);
|
|
230
|
+
})) {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return false;
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
assets.assets = Object.fromEntries(
|
|
238
|
+
Object.entries(assets.assets).filter(([filePath, metadata]) => rule(filePath, metadata))
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function populateModels(assets: AssetsInfo, options: AssetPopulationOptions) {
|
|
244
|
+
if (Object.values(assets.assets).some(asset => asset.type === AssetType.Model)) {
|
|
245
|
+
await populateModelsMetadata(assets);
|
|
246
|
+
if (!options?.peek) {
|
|
247
|
+
await populateBoundingBoxes(assets);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function populateModelsMetadata(assets: AssetsInfo): Promise<void> {
|
|
253
|
+
// read manifest.json, and populate the result with the file details
|
|
254
|
+
const manifest: Record<string, Record<string, string>> = {};
|
|
255
|
+
const storageProvider = new StorageProvider();
|
|
256
|
+
for (const dir of [ENGINE.PROJECT_PATH_PREFIX, ENGINE.ENGINE_PATH_PREFIX]) {
|
|
257
|
+
const assetsManifest = path.join(dir, 'assets', 'manifest.json');
|
|
258
|
+
Object.assign(manifest, await storageProvider.downloadFileAsJson<any>(ENGINE.AssetPath.fromString(assetsManifest)));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let metadataDescription: Record<string, any> = {};
|
|
262
|
+
if (manifest) {
|
|
263
|
+
// populate metadata description if it exists
|
|
264
|
+
metadataDescription = manifest['$metadata_description'] ?? {};
|
|
265
|
+
|
|
266
|
+
// populate the result with the file details
|
|
267
|
+
for (const [filePath, targetMetadata] of Object.entries(assets.assets)) {
|
|
268
|
+
const metadata = manifest[filePath];
|
|
269
|
+
if (metadata !== undefined && metadata !== null && metadata.constructor == Object) {
|
|
270
|
+
Object.assign(targetMetadata, metadata);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
assets.metadataDescription[AssetType.Model] = {
|
|
276
|
+
...assets.metadataDescription[AssetType.Model],
|
|
277
|
+
...metadataDescription,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function populateBoundingBoxes(assets: AssetsInfo) {
|
|
282
|
+
const modelFiles = Object.keys(assets.assets).filter(filePath =>
|
|
283
|
+
modelTypes.includes(path.extname(filePath).toLowerCase())
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// split model files into two groups - engine and project
|
|
287
|
+
const engineModelFiles = modelFiles.filter(filePath =>
|
|
288
|
+
filePath.startsWith(ENGINE.ENGINE_PATH_PREFIX)
|
|
289
|
+
);
|
|
290
|
+
const projectModelFiles = modelFiles.filter(filePath =>
|
|
291
|
+
filePath.startsWith(ENGINE.PROJECT_PATH_PREFIX)
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
const update = async (root: string, modelFiles: string[]) => {
|
|
295
|
+
const manifestFile = path.join(root, 'assets', 'bounding_box.json');
|
|
296
|
+
const storageProvider = new StorageProvider();
|
|
297
|
+
const gltfPaths: {[key: string]: string} = {};
|
|
298
|
+
for (const filePath of modelFiles) {
|
|
299
|
+
gltfPaths[filePath] = storageProvider.getFullPath(filePath);
|
|
300
|
+
}
|
|
301
|
+
const boundingBoxes = await fetchBoundingBoxData(storageProvider.getFullPath(manifestFile), gltfPaths);
|
|
302
|
+
for (const [filePath, boundingBox] of Object.entries(boundingBoxes)) {
|
|
303
|
+
assets.assets[filePath]['boundingBox'] = boundingBox;
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
if (engineModelFiles.length > 0) {
|
|
308
|
+
await update(ENGINE.ENGINE_PATH_PREFIX, engineModelFiles);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (projectModelFiles.length > 0) {
|
|
312
|
+
await update(ENGINE.PROJECT_PATH_PREFIX, projectModelFiles);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (engineModelFiles.length > 0 || projectModelFiles.length > 0) {
|
|
316
|
+
// also populate the metadata description for bounding box
|
|
317
|
+
const boundingBoxSchema = zodToJsonSchema(BoundingBoxSchema, 'BoundingBoxSchema');
|
|
318
|
+
const properties = (boundingBoxSchema as any).definitions.BoundingBoxSchema.properties;
|
|
319
|
+
const boundingBoxDescription: Record<string, string> = {};
|
|
320
|
+
for (const key in properties) {
|
|
321
|
+
if (properties[key].description) {
|
|
322
|
+
boundingBoxDescription[key] = properties[key].description;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const metadataDescription = {
|
|
326
|
+
boundingBox: {
|
|
327
|
+
description: 'The bounding box info of model assets',
|
|
328
|
+
properties: boundingBoxDescription,
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
assets.metadataDescription[AssetType.Model] = {
|
|
332
|
+
...assets.metadataDescription[AssetType.Model],
|
|
333
|
+
...metadataDescription
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function populateJsClasses(assets: AssetsInfo, options: AssetPopulationOptions): Promise<void> {
|
|
339
|
+
const jsClassNames: Record<string, string> = {};
|
|
340
|
+
for (const [filePath, metadata] of Object.entries(assets.assets)) {
|
|
341
|
+
if (filePath.includes(JS_CLASSES_DIR_NAME)) {
|
|
342
|
+
jsClassNames[filePath] = path.basename(filePath);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (Object.keys(jsClassNames).length === 0) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const result = await populateClassesInfo({
|
|
350
|
+
classesToSearch: Object.values(jsClassNames),
|
|
351
|
+
includeConstructorParams: !options?.peek
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (result.actors
|
|
355
|
+
&& Object.keys(result.actors).length > 0
|
|
356
|
+
&& result.metadataDescription
|
|
357
|
+
&& Object.keys(result.metadataDescription).length > 0) {
|
|
358
|
+
|
|
359
|
+
assets.metadataDescription[AssetType.JsClass] = result.metadataDescription;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
for (const [filePath, metadata] of Object.entries(assets.assets)) {
|
|
363
|
+
if (filePath in jsClassNames) {
|
|
364
|
+
const className = jsClassNames[filePath];
|
|
365
|
+
const actorInfo = result.actors[className];
|
|
366
|
+
if (actorInfo) {
|
|
367
|
+
metadata.jsClassName = actorInfo.className;
|
|
368
|
+
if (!options?.peek) {
|
|
369
|
+
metadata.jsClassFilePath = actorInfo.filePath;
|
|
370
|
+
metadata.jsClassConstructorParams = actorInfo.constructorParams ?? [];
|
|
371
|
+
metadata.jsClassCanPopulateFromJson = actorInfo.canPopulateFromJson ?? false;
|
|
372
|
+
}
|
|
373
|
+
metadata.jsClassDescription = actorInfo.description ?? '';
|
|
374
|
+
} else {
|
|
375
|
+
console.warn(`Actor class ${className} not found in classes info for file ${filePath}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|