@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,858 +1,858 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
|
|
4
|
-
import * as ENGINE from 'genesys.js';
|
|
5
|
-
import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from 'ts-morph';
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
8
|
-
|
|
9
|
-
import { StorageProvider } from '../storageProvider.js';
|
|
10
|
-
|
|
11
|
-
import { isSubclass } from './utils.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import type { ClassDeclaration, ParameterDeclaration, SourceFile} from 'ts-morph';
|
|
15
|
-
|
|
16
|
-
const PropertyInfoSchema: z.ZodType<{
|
|
17
|
-
type: string;
|
|
18
|
-
description?: string;
|
|
19
|
-
canPopulateFromJson?: boolean;
|
|
20
|
-
optional: boolean;
|
|
21
|
-
properties?: Record<string, any>;
|
|
22
|
-
}> = z.object({
|
|
23
|
-
type: z.string().describe('Type of the property, e.g. "string", "number", "THREE.Vector3", "object"'),
|
|
24
|
-
description: z.string().optional().describe('Description of the property, extracted from JSDoc if available'),
|
|
25
|
-
canPopulateFromJson: z.boolean().optional().describe('Whether this property can be populated from JSON values'),
|
|
26
|
-
optional: z.boolean().describe('Whether the property is optional or not'),
|
|
27
|
-
properties: z.lazy(() => z.record(PropertyInfoSchema)).optional().describe('If the property is an object, this contains its properties recursively'),
|
|
28
|
-
}).describe('Represents information about a property within a parameter');
|
|
29
|
-
|
|
30
|
-
export type PropertyInfo = z.infer<typeof PropertyInfoSchema>;
|
|
31
|
-
|
|
32
|
-
const ParameterInfoSchema: z.ZodType<{
|
|
33
|
-
paramName: string;
|
|
34
|
-
type: string;
|
|
35
|
-
description?: string;
|
|
36
|
-
canPopulateFromJson?: boolean;
|
|
37
|
-
properties?: Record<string, PropertyInfo>;
|
|
38
|
-
optional: boolean;
|
|
39
|
-
}> = z.object({
|
|
40
|
-
paramName: z.string().describe('Name of the constructor parameter'),
|
|
41
|
-
type: z.string().describe('Type of the parameter, e.g. "string", "number", "THREE.Vector3", "object"'),
|
|
42
|
-
description: z.string().optional().describe('Description of the parameter, extracted from JSDoc if available'),
|
|
43
|
-
canPopulateFromJson: z.boolean().optional().describe('Whether this parameter can be populated from JSON values'),
|
|
44
|
-
properties: z.record(PropertyInfoSchema).optional().describe('If the parameter is an object, this contains its properties recursively'),
|
|
45
|
-
optional: z.boolean().describe('Whether the parameter is optional or not'),
|
|
46
|
-
}).describe('Represents information about a constructor parameter');
|
|
47
|
-
|
|
48
|
-
export type ParameterInfo = z.infer<typeof ParameterInfoSchema>;
|
|
49
|
-
|
|
50
|
-
const ConstructorParametersSchema = z.array(ParameterInfoSchema).describe('Constructor parameters as an array preserving order');
|
|
51
|
-
|
|
52
|
-
export type ConstructorParameters = z.infer<typeof ConstructorParametersSchema>;
|
|
53
|
-
|
|
54
|
-
const ActorInfoSchema = z.object({
|
|
55
|
-
className: z.string().describe('Fully qualified name of the class including ENGINE. or GAME. prefix'),
|
|
56
|
-
filePath: z.string().describe('Path to the source file containing this class'),
|
|
57
|
-
baseClasses: z.array(z.string()).describe('Array of parent class names this actor extends from'),
|
|
58
|
-
constructorParams: ConstructorParametersSchema.optional().describe('Array of constructor parameters in declaration order'),
|
|
59
|
-
canPopulateFromJson: z.boolean().optional().describe('Whether this actor can be instantiated purely from JSON data (all required parameters are JSON-serializable)'),
|
|
60
|
-
description: z.string().optional().describe('Human-readable description of the actor class and its purpose')
|
|
61
|
-
}).describe('Complete information about an actor class');
|
|
62
|
-
|
|
63
|
-
export type ActorInfo = z.infer<typeof ActorInfoSchema>;
|
|
64
|
-
|
|
65
|
-
export const ThreeVector3Schema = z.object({
|
|
66
|
-
type: z.literal('THREE.Vector3').describe('The type of the object, must be "THREE.Vector3"'),
|
|
67
|
-
x: z.number().describe('The x component of the vector'),
|
|
68
|
-
y: z.number().describe('The y component of the vector'),
|
|
69
|
-
z: z.number().describe('The z component of the vector'),
|
|
70
|
-
}).describe('Represents a THREE.Vector3 object');
|
|
71
|
-
|
|
72
|
-
export const ThreeEulerSchema = z.object({
|
|
73
|
-
type: z.literal('THREE.Euler').describe('The type of the object, must be "THREE.Euler"'),
|
|
74
|
-
x: z.number().describe('The x component of the euler, pitch, in radians'),
|
|
75
|
-
y: z.number().describe('The y component of the euler, yaw, in radians'),
|
|
76
|
-
z: z.number().describe('The z component of the euler, roll, in radians'),
|
|
77
|
-
}).describe('Represents a THREE.Euler object');
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Result of searching for actors
|
|
82
|
-
*/
|
|
83
|
-
export interface ActorsSearchResult {
|
|
84
|
-
metadataDescription: Record<string, any>;
|
|
85
|
-
actors: Record<string, ActorInfo>; // className -> ActorInfo
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Configuration options for actor search
|
|
90
|
-
*/
|
|
91
|
-
export interface ActorSearchOptions {
|
|
92
|
-
classesToSearch?: string[]; // Specific classes to search for, if empty all actors will be returned
|
|
93
|
-
includeConstructorParams?: boolean; // Whether to include constructor parameters in the result
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Creates metadata description for the search results by recursively extracting descriptions from Zod schemas
|
|
98
|
-
*/
|
|
99
|
-
function createMetadataDescription(): Record<string, any> {
|
|
100
|
-
function extractSchemaDescriptions(schema: any): any {
|
|
101
|
-
// Handle missing or invalid schema
|
|
102
|
-
if (!schema) return {};
|
|
103
|
-
|
|
104
|
-
const result: Record<string, any> = {};
|
|
105
|
-
|
|
106
|
-
// Add schema's own description if present
|
|
107
|
-
if (schema.description) {
|
|
108
|
-
result.description = schema.description;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Handle object with properties
|
|
112
|
-
if (schema.type === 'object' && schema.properties) {
|
|
113
|
-
result.fields = {};
|
|
114
|
-
for (const [key, prop] of Object.entries<any>(schema.properties)) {
|
|
115
|
-
result.fields[key] = extractSchemaDescriptions(prop);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Handle arrays
|
|
120
|
-
if (schema.type === 'array' && schema.items) {
|
|
121
|
-
const items = extractSchemaDescriptions(schema.items);
|
|
122
|
-
if (items && Object.keys(items).length > 0) {
|
|
123
|
-
result.items = items;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Handle references
|
|
128
|
-
if (schema.$ref && schema.$ref.startsWith('#/definitions/')) {
|
|
129
|
-
const refName = schema.$ref.replace('#/definitions/', '');
|
|
130
|
-
const refSchema = definitions[refName];
|
|
131
|
-
if (refSchema) {
|
|
132
|
-
Object.assign(result, extractSchemaDescriptions(refSchema));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Handle anyOf/oneOf (union types)
|
|
137
|
-
if (schema.anyOf ?? schema.oneOf) {
|
|
138
|
-
const variants = schema.anyOf ?? schema.oneOf;
|
|
139
|
-
result.variants = variants.map((variant: any) => extractSchemaDescriptions(variant));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Convert Zod schema to JSON schema
|
|
146
|
-
const actorInfoSchema = zodToJsonSchema(ActorInfoSchema, 'ActorInfoSchema');
|
|
147
|
-
|
|
148
|
-
// Get all definitions from the schema
|
|
149
|
-
const definitions = (actorInfoSchema as any).definitions ?? {};
|
|
150
|
-
|
|
151
|
-
const result = extractSchemaDescriptions(actorInfoSchema).fields;
|
|
152
|
-
|
|
153
|
-
const vector3Schema = zodToJsonSchema(ThreeVector3Schema, 'ThreeVector3Schema');
|
|
154
|
-
const eulerSchema = zodToJsonSchema(ThreeEulerSchema, 'ThreeEulerSchema');
|
|
155
|
-
const vector3Example: z.infer<typeof ThreeVector3Schema> = {
|
|
156
|
-
type: 'THREE.Vector3',
|
|
157
|
-
x: 1,
|
|
158
|
-
y: 2,
|
|
159
|
-
z: 3,
|
|
160
|
-
};
|
|
161
|
-
const eulerExample: z.infer<typeof ThreeEulerSchema> = {
|
|
162
|
-
type: 'THREE.Euler',
|
|
163
|
-
x: 0,
|
|
164
|
-
y: 1.57,
|
|
165
|
-
z: -3.14,
|
|
166
|
-
};
|
|
167
|
-
result.specialTypes = {
|
|
168
|
-
description: 'Here shows how some special types should be populated from JSON values',
|
|
169
|
-
'THREE.Vector3': { ...extractSchemaDescriptions(vector3Schema.definitions?.ThreeVector3Schema ?? {}), example: vector3Example },
|
|
170
|
-
'THREE.Euler': { ...extractSchemaDescriptions(eulerSchema.definitions?.ThreeEulerSchema ?? {}), example: eulerExample },
|
|
171
|
-
};
|
|
172
|
-
return result;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Creates a ts-morph project with proper TypeScript configuration
|
|
177
|
-
*/
|
|
178
|
-
function createTsMorphProject(storageProvider: StorageProvider): Project {
|
|
179
|
-
try {
|
|
180
|
-
// Try to use the project's tsconfig.json
|
|
181
|
-
const tsconfigPath = storageProvider.getFullPath('@project/tsconfig.json');
|
|
182
|
-
|
|
183
|
-
const project = new Project({
|
|
184
|
-
tsConfigFilePath: fs.existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
185
|
-
useInMemoryFileSystem: false,
|
|
186
|
-
skipAddingFilesFromTsConfig: true,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
console.log('✅ ts-morph project created successfully');
|
|
190
|
-
return project;
|
|
191
|
-
} catch (error) {
|
|
192
|
-
console.warn('⚠️ Failed to create ts-morph project with tsconfig, using default configuration');
|
|
193
|
-
console.warn('Error:', error instanceof Error ? error.message : String(error));
|
|
194
|
-
|
|
195
|
-
// Fallback to basic configuration
|
|
196
|
-
return new Project({
|
|
197
|
-
useInMemoryFileSystem: false,
|
|
198
|
-
compilerOptions: {
|
|
199
|
-
target: ScriptTarget.ES2020,
|
|
200
|
-
module: ModuleKind.ESNext,
|
|
201
|
-
moduleResolution: ModuleResolutionKind.NodeJs,
|
|
202
|
-
strict: true,
|
|
203
|
-
esModuleInterop: true,
|
|
204
|
-
skipLibCheck: true,
|
|
205
|
-
forceConsistentCasingInFileNames: true,
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* Extracts enum values from a union type
|
|
213
|
-
*/
|
|
214
|
-
function extractEnumValues(type: any): string[] {
|
|
215
|
-
try {
|
|
216
|
-
if (type.isUnion()) {
|
|
217
|
-
const enumValues: string[] = [];
|
|
218
|
-
const unionTypes = type.getUnionTypes();
|
|
219
|
-
|
|
220
|
-
for (const unionType of unionTypes) {
|
|
221
|
-
if (unionType.isStringLiteral()) {
|
|
222
|
-
const literalValue = unionType.getLiteralValue();
|
|
223
|
-
if (typeof literalValue === 'string') {
|
|
224
|
-
enumValues.push(literalValue);
|
|
225
|
-
}
|
|
226
|
-
} else if (unionType.isNumberLiteral()) {
|
|
227
|
-
const literalValue = unionType.getLiteralValue();
|
|
228
|
-
if (typeof literalValue === 'number') {
|
|
229
|
-
enumValues.push(literalValue.toString());
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
return enumValues;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
return [];
|
|
238
|
-
} catch (error) {
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Extracts properties from an object type using ts-morph Type analysis
|
|
245
|
-
*/
|
|
246
|
-
function extractObjectProperties(type: any, visited: Set<string> = new Set()): Record<string, PropertyInfo> {
|
|
247
|
-
try {
|
|
248
|
-
const properties: Record<string, PropertyInfo> = {};
|
|
249
|
-
const typeString = type.getText();
|
|
250
|
-
|
|
251
|
-
// Prevent infinite recursion
|
|
252
|
-
if (visited.has(typeString)) {
|
|
253
|
-
return {};
|
|
254
|
-
}
|
|
255
|
-
visited.add(typeString);
|
|
256
|
-
|
|
257
|
-
// Handle union types - extract properties from the non-undefined part
|
|
258
|
-
if (type.isUnion()) {
|
|
259
|
-
const unionTypes = type.getUnionTypes();
|
|
260
|
-
for (const unionType of unionTypes) {
|
|
261
|
-
// Skip undefined types
|
|
262
|
-
if (unionType.isUndefined()) {
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
// If we find an object type in the union, extract its properties
|
|
266
|
-
const unionProperties = unionType.getApparentProperties();
|
|
267
|
-
if (unionProperties && unionProperties.length > 0) {
|
|
268
|
-
return extractObjectProperties(unionType, visited);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Get all properties of the type (includes inherited properties)
|
|
274
|
-
const typeProperties = type.getApparentProperties();
|
|
275
|
-
|
|
276
|
-
for (const prop of typeProperties) {
|
|
277
|
-
const propName = prop.getName();
|
|
278
|
-
|
|
279
|
-
// Skip internal/private properties
|
|
280
|
-
if (propName.startsWith('_') || propName.startsWith('__')) {
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const propDeclaration = prop.getValueDeclaration();
|
|
285
|
-
if (!propDeclaration) continue;
|
|
286
|
-
|
|
287
|
-
// Get property type
|
|
288
|
-
const propType = prop.getTypeAtLocation(propDeclaration);
|
|
289
|
-
const propTypeText = propType.getText(propDeclaration);
|
|
290
|
-
|
|
291
|
-
// Try to extract JSDoc description
|
|
292
|
-
let description: string | undefined;
|
|
293
|
-
if (propDeclaration && 'getJsDocs' in propDeclaration) {
|
|
294
|
-
const jsDocs = (propDeclaration as any).getJsDocs();
|
|
295
|
-
if (jsDocs.length > 0) {
|
|
296
|
-
description = jsDocs[0].getDescription();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// Check if this is an enum/union type
|
|
301
|
-
const enumValues = extractEnumValues(propType);
|
|
302
|
-
let finalType = normalizeTypeText(propTypeText);
|
|
303
|
-
|
|
304
|
-
// Add enum values to the cleaned description
|
|
305
|
-
let finalDescription = description;
|
|
306
|
-
if (enumValues.length > 0) {
|
|
307
|
-
const enumDescription = `Possible values: ${enumValues.join(', ')}`;
|
|
308
|
-
finalDescription = finalDescription ? `${finalDescription}. ${enumDescription}` : enumDescription;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Determine if this type can be populated from JSON
|
|
312
|
-
const jsonSerializable = canPopulateFromJson(propTypeText, propType);
|
|
313
|
-
|
|
314
|
-
// Determine if this property has nested properties
|
|
315
|
-
const paramInfo: PropertyInfo = {
|
|
316
|
-
type: shouldTreatAsObject(propTypeText, propType) ? 'object' : finalType,
|
|
317
|
-
description: finalDescription,
|
|
318
|
-
canPopulateFromJson: jsonSerializable,
|
|
319
|
-
optional: prop.isOptional()
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
// If it's an object type, recursively extract properties
|
|
323
|
-
if (shouldTreatAsObject(propTypeText, propType)) {
|
|
324
|
-
paramInfo.properties = extractObjectProperties(propType, visited);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
properties[propName] = paramInfo;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
return properties;
|
|
331
|
-
} catch (error) {
|
|
332
|
-
console.warn('Failed to extract object properties:', error instanceof Error ? error.message : String(error));
|
|
333
|
-
return {};
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Extracts properties from an options object type using ts-morph Type analysis
|
|
339
|
-
*/
|
|
340
|
-
function extractOptionsProperties(param: ParameterDeclaration): Record<string, PropertyInfo> {
|
|
341
|
-
try {
|
|
342
|
-
const type = param.getType();
|
|
343
|
-
return extractObjectProperties(type);
|
|
344
|
-
} catch (error) {
|
|
345
|
-
console.warn(`Failed to extract properties for parameter ${param.getName()}:`, error instanceof Error ? error.message : String(error));
|
|
346
|
-
return {};
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Determines if a type should be treated as an object with properties
|
|
352
|
-
*/
|
|
353
|
-
function shouldTreatAsObject(typeText: string, tsType?: any): boolean {
|
|
354
|
-
// Skip primitive types (but not when they appear in object literals)
|
|
355
|
-
if (!typeText.includes('{') && ['string', 'number', 'boolean'].some(t => typeText === t || typeText === `${t} | undefined`)) {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
// Skip THREE.Vector3 and THREE.Euler - these are handled specially
|
|
360
|
-
if (typeText.includes('Vector3') || typeText.includes('Euler')) return false;
|
|
361
|
-
|
|
362
|
-
// Skip ENGINE.* and GAME.* classes - these are complex types
|
|
363
|
-
if (typeText.includes('ENGINE.') || typeText.includes('GAME.')) return false;
|
|
364
|
-
|
|
365
|
-
// Skip function types
|
|
366
|
-
if (typeText.includes('=>') || typeText.includes('Function') || typeText.includes('()')) return false;
|
|
367
|
-
|
|
368
|
-
// If we have TypeScript type information, use it
|
|
369
|
-
if (tsType) {
|
|
370
|
-
try {
|
|
371
|
-
// Handle union types - check if any union member is an interface-like type
|
|
372
|
-
if (tsType.isUnion()) {
|
|
373
|
-
const unionTypes = tsType.getUnionTypes();
|
|
374
|
-
for (const unionType of unionTypes) {
|
|
375
|
-
// Skip undefined types
|
|
376
|
-
if (unionType.isUndefined()) {
|
|
377
|
-
continue;
|
|
378
|
-
}
|
|
379
|
-
// Check if this union member is an interface-like type
|
|
380
|
-
if (isInterfaceLikeType(unionType)) {
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Check if the type is an interface-like type
|
|
387
|
-
if (isInterfaceLikeType(tsType)) {
|
|
388
|
-
return true;
|
|
389
|
-
}
|
|
390
|
-
} catch (error) {
|
|
391
|
-
// If we can't get properties, fall back to string matching
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Treat object literals as objects (inline interfaces)
|
|
396
|
-
if (typeText.includes('{')) {
|
|
397
|
-
return true;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
return false;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Checks if a type is interface-like (has properties but is not a function or complex class)
|
|
405
|
-
*/
|
|
406
|
-
function isInterfaceLikeType(type: any): boolean {
|
|
407
|
-
try {
|
|
408
|
-
const properties = type.getApparentProperties();
|
|
409
|
-
if (!properties || properties.length === 0) {
|
|
410
|
-
return false;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
// Check if any property is a function - if so, this might be a class or complex type
|
|
414
|
-
for (const prop of properties) {
|
|
415
|
-
try {
|
|
416
|
-
const propType = prop.getTypeAtLocation(prop.getValueDeclaration());
|
|
417
|
-
const propTypeText = propType.getText();
|
|
418
|
-
|
|
419
|
-
// If we find function properties, this is likely a class or complex type
|
|
420
|
-
if (propTypeText.includes('=>') || propTypeText.includes('Function') || propTypeText.includes('()')) {
|
|
421
|
-
// Allow if it's clearly a callback/event handler (common in options)
|
|
422
|
-
const propName = prop.getName();
|
|
423
|
-
if (propName.toLowerCase().includes('callback') ||
|
|
424
|
-
propName.toLowerCase().includes('handler') ||
|
|
425
|
-
propName.toLowerCase().includes('listener') ||
|
|
426
|
-
propName.toLowerCase().startsWith('on')) {
|
|
427
|
-
continue; // Allow these function properties
|
|
428
|
-
}
|
|
429
|
-
return false; // Skip types with non-callback function properties
|
|
430
|
-
}
|
|
431
|
-
} catch (error) {
|
|
432
|
-
// If we can't analyze a property, continue
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
return true; // It has properties and they're not complex functions
|
|
438
|
-
} catch (error) {
|
|
439
|
-
return false;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Determines if a type can be populated from JSON values
|
|
445
|
-
*/
|
|
446
|
-
function canPopulateFromJson(typeText: string, tsType?: any, visited: Set<string> = new Set()): boolean {
|
|
447
|
-
// Handle undefined types - they can be omitted in JSON
|
|
448
|
-
if (typeText.includes('undefined')) {
|
|
449
|
-
// Extract the non-undefined part
|
|
450
|
-
const nonUndefPart = typeText.replace(/\s*\|\s*undefined/g, '').trim();
|
|
451
|
-
if (nonUndefPart) {
|
|
452
|
-
// Create a new visited set for the non-undefined part to avoid false recursion detection
|
|
453
|
-
const newVisited = new Set(visited);
|
|
454
|
-
newVisited.delete(typeText); // Remove the original union type
|
|
455
|
-
return canPopulateFromJson(nonUndefPart, tsType, newVisited);
|
|
456
|
-
}
|
|
457
|
-
return true; // Pure undefined can be omitted
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Prevent infinite recursion
|
|
461
|
-
if (visited.has(typeText)) {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
visited.add(typeText);
|
|
465
|
-
|
|
466
|
-
// Primitive types can be populated
|
|
467
|
-
if (['string', 'number', 'boolean'].some(t => typeText === t || typeText.startsWith(t))) {
|
|
468
|
-
return true;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// THREE.Vector3 and THREE.Euler can be populated as [x,y,z] arrays
|
|
472
|
-
if (typeText.includes('Vector3') || typeText.includes('Euler')) {
|
|
473
|
-
return true;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// ENGINE.* and GAME.* classes cannot be populated (complex runtime objects)
|
|
477
|
-
if (typeText.includes('ENGINE.') || typeText.includes('GAME.')) {
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Function types cannot be populated
|
|
482
|
-
if (typeText.includes('=>') || typeText.includes('Function') || typeText.includes('()')) {
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Object literals can be populated (inline object types)
|
|
487
|
-
if (typeText.includes('{')) {
|
|
488
|
-
return true; // Inline object types can be populated
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Array types can be populated if their element type can be populated
|
|
492
|
-
if (typeText.includes('[]')) {
|
|
493
|
-
const elementType = typeText.replace('[]', '').trim();
|
|
494
|
-
return canPopulateFromJson(elementType, undefined, visited);
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Check TypeScript type information
|
|
498
|
-
if (tsType) {
|
|
499
|
-
try {
|
|
500
|
-
const enumValues = extractEnumValues(tsType);
|
|
501
|
-
if (enumValues.length > 0) {
|
|
502
|
-
return true; // Enums can be populated
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Handle union types
|
|
506
|
-
if (tsType.isUnion()) {
|
|
507
|
-
const unionTypes = tsType.getUnionTypes();
|
|
508
|
-
// Check if any non-undefined union member can be populated
|
|
509
|
-
for (const unionType of unionTypes) {
|
|
510
|
-
if (unionType.isUndefined()) {
|
|
511
|
-
continue;
|
|
512
|
-
}
|
|
513
|
-
const unionTypeText = unionType.getText();
|
|
514
|
-
if (canPopulateFromJson(unionTypeText, unionType, visited)) {
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// Check if this is an object type with properties
|
|
522
|
-
if (isInterfaceLikeType(tsType)) {
|
|
523
|
-
return canObjectBePopulatedFromJson(tsType, visited);
|
|
524
|
-
}
|
|
525
|
-
} catch (error) {
|
|
526
|
-
// If type analysis fails, fall back to string matching
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
// Default to false for unknown types
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Checks if an object type can be populated from JSON by checking if all required properties can be populated
|
|
536
|
-
*/
|
|
537
|
-
function canObjectBePopulatedFromJson(tsType: any, visited: Set<string> = new Set()): boolean {
|
|
538
|
-
try {
|
|
539
|
-
const properties = tsType.getApparentProperties();
|
|
540
|
-
if (!properties || properties.length === 0) {
|
|
541
|
-
return true; // Empty object can be populated
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
// Check all properties
|
|
545
|
-
for (const prop of properties) {
|
|
546
|
-
const propName = prop.getName();
|
|
547
|
-
|
|
548
|
-
// Skip internal/private properties
|
|
549
|
-
if (propName.startsWith('_') || propName.startsWith('__')) {
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const propDeclaration = prop.getValueDeclaration();
|
|
554
|
-
if (!propDeclaration) continue;
|
|
555
|
-
|
|
556
|
-
// Check if property is required
|
|
557
|
-
const isOptional = prop.isOptional();
|
|
558
|
-
|
|
559
|
-
// If it's required, it must be populatable
|
|
560
|
-
if (!isOptional) {
|
|
561
|
-
const propType = prop.getTypeAtLocation(propDeclaration);
|
|
562
|
-
const propTypeText = propType.getText(propDeclaration);
|
|
563
|
-
|
|
564
|
-
if (!canPopulateFromJson(propTypeText, propType, visited)) {
|
|
565
|
-
return false; // Required property cannot be populated
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
// Note: We don't need to check optional properties - they can be omitted
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
return true; // All required properties can be populated (or all properties are optional)
|
|
572
|
-
} catch (error) {
|
|
573
|
-
return false; // If analysis fails, assume it cannot be populated
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Normalizes type text for display
|
|
579
|
-
*/
|
|
580
|
-
function normalizeTypeText(typeText: string): string {
|
|
581
|
-
if (typeText.includes('Vector3')) return 'THREE.Vector3';
|
|
582
|
-
if (typeText.includes('Euler')) return 'THREE.Euler';
|
|
583
|
-
return typeText;
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Extracts JSDoc description for a parameter
|
|
588
|
-
*/
|
|
589
|
-
function extractParamJSDoc(param: ParameterDeclaration): string | undefined {
|
|
590
|
-
// Get JSDoc tags from the parent constructor
|
|
591
|
-
const constructor = param.getParent();
|
|
592
|
-
if (!constructor || !('getJsDocs' in constructor)) return undefined;
|
|
593
|
-
|
|
594
|
-
try {
|
|
595
|
-
const jsDocs = (constructor as any).getJsDocs();
|
|
596
|
-
if (!jsDocs || jsDocs.length === 0) return undefined;
|
|
597
|
-
|
|
598
|
-
const paramName = param.getName();
|
|
599
|
-
for (const jsDoc of jsDocs) {
|
|
600
|
-
const tags = jsDoc.getTags();
|
|
601
|
-
for (const tag of tags) {
|
|
602
|
-
if (tag.getTagName() === 'param' && tag.getComment()?.includes(paramName)) {
|
|
603
|
-
return tag.getComment();
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
} catch (error) {
|
|
608
|
-
// If JSDoc extraction fails, silently continue
|
|
609
|
-
return undefined;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
return undefined;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Analyzes a constructor parameter to extract detailed information
|
|
617
|
-
*/
|
|
618
|
-
function analyzeParameter(param: ParameterDeclaration): ParameterInfo {
|
|
619
|
-
const tsType = param.getType();
|
|
620
|
-
const type = tsType.getText(param);
|
|
621
|
-
const normalizedType = normalizeTypeText(type);
|
|
622
|
-
const paramName = param.getName();
|
|
623
|
-
|
|
624
|
-
// Extract JSDoc description
|
|
625
|
-
let description = extractParamJSDoc(param);
|
|
626
|
-
|
|
627
|
-
// Check if this is an enum/union type
|
|
628
|
-
const enumValues = extractEnumValues(tsType);
|
|
629
|
-
|
|
630
|
-
// Add enum values to the cleaned description
|
|
631
|
-
let finalDescription = description;
|
|
632
|
-
if (enumValues.length > 0) {
|
|
633
|
-
const enumDescription = `Possible values: ${enumValues.join(', ')}`;
|
|
634
|
-
finalDescription = finalDescription ? `${finalDescription}. ${enumDescription}` : enumDescription;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Determine if this type can be populated from JSON
|
|
638
|
-
const jsonSerializable = canPopulateFromJson(type, tsType);
|
|
639
|
-
|
|
640
|
-
const paramInfo: ParameterInfo = {
|
|
641
|
-
paramName,
|
|
642
|
-
type: shouldTreatAsObject(type, tsType) ? 'object' : normalizedType,
|
|
643
|
-
description: finalDescription,
|
|
644
|
-
canPopulateFromJson: jsonSerializable,
|
|
645
|
-
optional: param.isOptional()
|
|
646
|
-
};
|
|
647
|
-
|
|
648
|
-
// Extract properties if this is an options object
|
|
649
|
-
if (shouldTreatAsObject(type, tsType)) {
|
|
650
|
-
paramInfo.properties = extractOptionsProperties(param);
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return paramInfo;
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Determines if an actor class can be populated from JSON based on its constructor parameters
|
|
658
|
-
*/
|
|
659
|
-
function canActorBePopulatedFromJson(constructorParams: ConstructorParameters): boolean {
|
|
660
|
-
// If there are no constructor parameters, it can be populated
|
|
661
|
-
if (!constructorParams || constructorParams.length === 0) {
|
|
662
|
-
return true;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Check each parameter
|
|
666
|
-
for (const paramInfo of constructorParams) {
|
|
667
|
-
// If any required parameter cannot be populated from JSON, the actor cannot be populated
|
|
668
|
-
if (paramInfo.canPopulateFromJson === false) {
|
|
669
|
-
// Check if this parameter is optional (has undefined in type)
|
|
670
|
-
const isOptional = paramInfo.type.includes('undefined');
|
|
671
|
-
if (!isOptional) {
|
|
672
|
-
return false; // Required parameter cannot be populated from JSON
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
return true; // All required parameters can be populated from JSON
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* Extracts constructor parameters from a class declaration
|
|
682
|
-
*/
|
|
683
|
-
function extractConstructorParams(classDecl: ClassDeclaration): ConstructorParameters {
|
|
684
|
-
const constructors = classDecl.getConstructors();
|
|
685
|
-
if (constructors.length === 0) return [];
|
|
686
|
-
|
|
687
|
-
// Take the first constructor (could be enhanced to handle overloads)
|
|
688
|
-
const constructor = constructors[0];
|
|
689
|
-
|
|
690
|
-
return constructor.getParameters().map(param => analyzeParameter(param));
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/**
|
|
694
|
-
* Analyzes a class declaration to extract actor information
|
|
695
|
-
*/
|
|
696
|
-
function analyzeActorClassBasics(
|
|
697
|
-
sourceFile: SourceFile,
|
|
698
|
-
classDeclaration: ClassDeclaration,
|
|
699
|
-
classPrefix: ENGINE.Prefix
|
|
700
|
-
): ActorInfo | null {
|
|
701
|
-
const className = classDeclaration.getName();
|
|
702
|
-
if (!className) return null;
|
|
703
|
-
|
|
704
|
-
const prefixedClassName = classPrefix + className;
|
|
705
|
-
const classConstructor = ENGINE.ClassRegistry.getRegistry().get(prefixedClassName);
|
|
706
|
-
if (!isSubclass(classConstructor, ENGINE.Actor)) return null;
|
|
707
|
-
|
|
708
|
-
// Extract basic information
|
|
709
|
-
const filePath = sourceFile.getFilePath();
|
|
710
|
-
const heritage = classDeclaration.getExtends();
|
|
711
|
-
const parentClassName = heritage?.getExpression().getText() ?? '';
|
|
712
|
-
|
|
713
|
-
// For now, create a simple base classes array
|
|
714
|
-
const baseClasses: string[] = [];
|
|
715
|
-
if (parentClassName) {
|
|
716
|
-
baseClasses.push(parentClassName);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Extract JSDoc description if available
|
|
720
|
-
const jsDocs = classDeclaration.getJsDocs();
|
|
721
|
-
const description = jsDocs.length > 0 ? jsDocs[0].getDescription() : undefined;
|
|
722
|
-
|
|
723
|
-
// Extract constructor parameters
|
|
724
|
-
// const constructorParams = extractConstructorParams(classDeclaration);
|
|
725
|
-
|
|
726
|
-
return {
|
|
727
|
-
className: prefixedClassName,
|
|
728
|
-
filePath,
|
|
729
|
-
baseClasses,
|
|
730
|
-
// constructorParams,
|
|
731
|
-
description,
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* Recursively searches a directory for TypeScript files
|
|
737
|
-
*/
|
|
738
|
-
function collectTypeScriptFiles(
|
|
739
|
-
dir: string,
|
|
740
|
-
storageProvider: StorageProvider
|
|
741
|
-
): string[] {
|
|
742
|
-
let results: string[] = [];
|
|
743
|
-
const actualDir = storageProvider.getFullPath(dir);
|
|
744
|
-
|
|
745
|
-
if (!fs.existsSync(actualDir)) {
|
|
746
|
-
console.warn(`Directory does not exist: ${actualDir}`);
|
|
747
|
-
return results;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const list = fs.readdirSync(actualDir);
|
|
751
|
-
|
|
752
|
-
list.forEach((file) => {
|
|
753
|
-
const filePath = path.join(dir, file);
|
|
754
|
-
const actualFilePath = storageProvider.getFullPath(filePath);
|
|
755
|
-
const stat = fs.statSync(actualFilePath);
|
|
756
|
-
|
|
757
|
-
if (stat && stat.isDirectory() && file !== 'node_modules' && !file.startsWith('.')) {
|
|
758
|
-
// Recursively search subdirectories
|
|
759
|
-
results = results.concat(collectTypeScriptFiles(filePath, storageProvider));
|
|
760
|
-
} else if (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts') && !file.endsWith('.spec.ts')) {
|
|
761
|
-
// Include TypeScript files but exclude declaration files
|
|
762
|
-
results.push(filePath);
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
return results;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Main function to search for actor classes
|
|
771
|
-
*/
|
|
772
|
-
export async function populateClassesInfo(
|
|
773
|
-
options: ActorSearchOptions = {}
|
|
774
|
-
): Promise<ActorsSearchResult> {
|
|
775
|
-
const {
|
|
776
|
-
classesToSearch = [],
|
|
777
|
-
includeConstructorParams = false
|
|
778
|
-
} = options;
|
|
779
|
-
|
|
780
|
-
const dirs = [];
|
|
781
|
-
if (Object.values(classesToSearch).some(className => className.startsWith(ENGINE.Prefix.ENGINE))) {
|
|
782
|
-
dirs.push(ENGINE.ENGINE_PATH_PREFIX);
|
|
783
|
-
}
|
|
784
|
-
if (Object.values(classesToSearch).some(className => className.startsWith(ENGINE.Prefix.GAME))) {
|
|
785
|
-
dirs.push(ENGINE.PROJECT_PATH_PREFIX);
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
const storageProvider = new StorageProvider();
|
|
789
|
-
|
|
790
|
-
// Collect TypeScript files
|
|
791
|
-
const files: string[] = [];
|
|
792
|
-
for (const dir of dirs) {
|
|
793
|
-
const dirFiles = collectTypeScriptFiles(dir, storageProvider);
|
|
794
|
-
files.push(...dirFiles);
|
|
795
|
-
console.log(`Found ${dirFiles.length} TypeScript files in ${dir}`);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
console.log(`Total TypeScript files found: ${files.length}`);
|
|
799
|
-
|
|
800
|
-
// Create ts-morph project
|
|
801
|
-
const project = createTsMorphProject(storageProvider);
|
|
802
|
-
|
|
803
|
-
// Add files to ts-morph project (limit for performance)
|
|
804
|
-
const filesToAnalyze = files;
|
|
805
|
-
console.log(`Analyzing ${filesToAnalyze.length} files with ts-morph...`);
|
|
806
|
-
|
|
807
|
-
for (const filePath of filesToAnalyze) {
|
|
808
|
-
try {
|
|
809
|
-
const actualPath = storageProvider.getFullPath(filePath);
|
|
810
|
-
if (fs.existsSync(actualPath)) {
|
|
811
|
-
project.addSourceFileAtPath(actualPath);
|
|
812
|
-
}
|
|
813
|
-
} catch (error) {
|
|
814
|
-
console.warn(`Failed to add file ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const result: ActorsSearchResult = {
|
|
819
|
-
metadataDescription: includeConstructorParams ? createMetadataDescription() : {},
|
|
820
|
-
actors: {}
|
|
821
|
-
};
|
|
822
|
-
|
|
823
|
-
// Analyze each source file for actor classes
|
|
824
|
-
let totalClasses = 0;
|
|
825
|
-
let actorClasses = 0;
|
|
826
|
-
|
|
827
|
-
const engineFullPath = storageProvider.getFullPath(ENGINE.ENGINE_PATH_PREFIX);
|
|
828
|
-
|
|
829
|
-
for (const sourceFile of project.getSourceFiles()) {
|
|
830
|
-
const isEngineClass = sourceFile.getFilePath().startsWith(engineFullPath);
|
|
831
|
-
const classPrefix = isEngineClass ? ENGINE.Prefix.ENGINE : ENGINE.Prefix.GAME;
|
|
832
|
-
const classes = sourceFile.getClasses();
|
|
833
|
-
totalClasses += classes.length;
|
|
834
|
-
|
|
835
|
-
for (const classDecl of classes) {
|
|
836
|
-
const actorInfo = analyzeActorClassBasics(sourceFile, classDecl, classPrefix);
|
|
837
|
-
if (actorInfo) {
|
|
838
|
-
actorClasses++;
|
|
839
|
-
console.log(`Found Actor-derived class: ${actorInfo.className} in ${path.basename(actorInfo.filePath)}`);
|
|
840
|
-
|
|
841
|
-
if (includeConstructorParams) {
|
|
842
|
-
actorInfo.constructorParams = extractConstructorParams(classDecl);
|
|
843
|
-
// Determine if the entire actor can be populated from JSON
|
|
844
|
-
actorInfo.canPopulateFromJson = canActorBePopulatedFromJson(actorInfo.constructorParams);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (!classesToSearch || classesToSearch.length === 0 || classesToSearch.includes(actorInfo.className)) {
|
|
848
|
-
result.actors[actorInfo.className] = actorInfo;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
console.log(`✅ Analysis complete: ${totalClasses} total classes, ${actorClasses} Actor-derived classes found`);
|
|
855
|
-
|
|
856
|
-
return result;
|
|
857
|
-
}
|
|
858
|
-
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
import * as ENGINE from 'genesys.js';
|
|
5
|
+
import { ModuleKind, ModuleResolutionKind, Project, ScriptTarget } from 'ts-morph';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
8
|
+
|
|
9
|
+
import { StorageProvider } from '../storageProvider.js';
|
|
10
|
+
|
|
11
|
+
import { isSubclass } from './utils.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import type { ClassDeclaration, ParameterDeclaration, SourceFile} from 'ts-morph';
|
|
15
|
+
|
|
16
|
+
const PropertyInfoSchema: z.ZodType<{
|
|
17
|
+
type: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
canPopulateFromJson?: boolean;
|
|
20
|
+
optional: boolean;
|
|
21
|
+
properties?: Record<string, any>;
|
|
22
|
+
}> = z.object({
|
|
23
|
+
type: z.string().describe('Type of the property, e.g. "string", "number", "THREE.Vector3", "object"'),
|
|
24
|
+
description: z.string().optional().describe('Description of the property, extracted from JSDoc if available'),
|
|
25
|
+
canPopulateFromJson: z.boolean().optional().describe('Whether this property can be populated from JSON values'),
|
|
26
|
+
optional: z.boolean().describe('Whether the property is optional or not'),
|
|
27
|
+
properties: z.lazy(() => z.record(PropertyInfoSchema)).optional().describe('If the property is an object, this contains its properties recursively'),
|
|
28
|
+
}).describe('Represents information about a property within a parameter');
|
|
29
|
+
|
|
30
|
+
export type PropertyInfo = z.infer<typeof PropertyInfoSchema>;
|
|
31
|
+
|
|
32
|
+
const ParameterInfoSchema: z.ZodType<{
|
|
33
|
+
paramName: string;
|
|
34
|
+
type: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
canPopulateFromJson?: boolean;
|
|
37
|
+
properties?: Record<string, PropertyInfo>;
|
|
38
|
+
optional: boolean;
|
|
39
|
+
}> = z.object({
|
|
40
|
+
paramName: z.string().describe('Name of the constructor parameter'),
|
|
41
|
+
type: z.string().describe('Type of the parameter, e.g. "string", "number", "THREE.Vector3", "object"'),
|
|
42
|
+
description: z.string().optional().describe('Description of the parameter, extracted from JSDoc if available'),
|
|
43
|
+
canPopulateFromJson: z.boolean().optional().describe('Whether this parameter can be populated from JSON values'),
|
|
44
|
+
properties: z.record(PropertyInfoSchema).optional().describe('If the parameter is an object, this contains its properties recursively'),
|
|
45
|
+
optional: z.boolean().describe('Whether the parameter is optional or not'),
|
|
46
|
+
}).describe('Represents information about a constructor parameter');
|
|
47
|
+
|
|
48
|
+
export type ParameterInfo = z.infer<typeof ParameterInfoSchema>;
|
|
49
|
+
|
|
50
|
+
const ConstructorParametersSchema = z.array(ParameterInfoSchema).describe('Constructor parameters as an array preserving order');
|
|
51
|
+
|
|
52
|
+
export type ConstructorParameters = z.infer<typeof ConstructorParametersSchema>;
|
|
53
|
+
|
|
54
|
+
const ActorInfoSchema = z.object({
|
|
55
|
+
className: z.string().describe('Fully qualified name of the class including ENGINE. or GAME. prefix'),
|
|
56
|
+
filePath: z.string().describe('Path to the source file containing this class'),
|
|
57
|
+
baseClasses: z.array(z.string()).describe('Array of parent class names this actor extends from'),
|
|
58
|
+
constructorParams: ConstructorParametersSchema.optional().describe('Array of constructor parameters in declaration order'),
|
|
59
|
+
canPopulateFromJson: z.boolean().optional().describe('Whether this actor can be instantiated purely from JSON data (all required parameters are JSON-serializable)'),
|
|
60
|
+
description: z.string().optional().describe('Human-readable description of the actor class and its purpose')
|
|
61
|
+
}).describe('Complete information about an actor class');
|
|
62
|
+
|
|
63
|
+
export type ActorInfo = z.infer<typeof ActorInfoSchema>;
|
|
64
|
+
|
|
65
|
+
export const ThreeVector3Schema = z.object({
|
|
66
|
+
type: z.literal('THREE.Vector3').describe('The type of the object, must be "THREE.Vector3"'),
|
|
67
|
+
x: z.number().describe('The x component of the vector'),
|
|
68
|
+
y: z.number().describe('The y component of the vector'),
|
|
69
|
+
z: z.number().describe('The z component of the vector'),
|
|
70
|
+
}).describe('Represents a THREE.Vector3 object');
|
|
71
|
+
|
|
72
|
+
export const ThreeEulerSchema = z.object({
|
|
73
|
+
type: z.literal('THREE.Euler').describe('The type of the object, must be "THREE.Euler"'),
|
|
74
|
+
x: z.number().describe('The x component of the euler, pitch, in radians'),
|
|
75
|
+
y: z.number().describe('The y component of the euler, yaw, in radians'),
|
|
76
|
+
z: z.number().describe('The z component of the euler, roll, in radians'),
|
|
77
|
+
}).describe('Represents a THREE.Euler object');
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Result of searching for actors
|
|
82
|
+
*/
|
|
83
|
+
export interface ActorsSearchResult {
|
|
84
|
+
metadataDescription: Record<string, any>;
|
|
85
|
+
actors: Record<string, ActorInfo>; // className -> ActorInfo
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Configuration options for actor search
|
|
90
|
+
*/
|
|
91
|
+
export interface ActorSearchOptions {
|
|
92
|
+
classesToSearch?: string[]; // Specific classes to search for, if empty all actors will be returned
|
|
93
|
+
includeConstructorParams?: boolean; // Whether to include constructor parameters in the result
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates metadata description for the search results by recursively extracting descriptions from Zod schemas
|
|
98
|
+
*/
|
|
99
|
+
function createMetadataDescription(): Record<string, any> {
|
|
100
|
+
function extractSchemaDescriptions(schema: any): any {
|
|
101
|
+
// Handle missing or invalid schema
|
|
102
|
+
if (!schema) return {};
|
|
103
|
+
|
|
104
|
+
const result: Record<string, any> = {};
|
|
105
|
+
|
|
106
|
+
// Add schema's own description if present
|
|
107
|
+
if (schema.description) {
|
|
108
|
+
result.description = schema.description;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Handle object with properties
|
|
112
|
+
if (schema.type === 'object' && schema.properties) {
|
|
113
|
+
result.fields = {};
|
|
114
|
+
for (const [key, prop] of Object.entries<any>(schema.properties)) {
|
|
115
|
+
result.fields[key] = extractSchemaDescriptions(prop);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Handle arrays
|
|
120
|
+
if (schema.type === 'array' && schema.items) {
|
|
121
|
+
const items = extractSchemaDescriptions(schema.items);
|
|
122
|
+
if (items && Object.keys(items).length > 0) {
|
|
123
|
+
result.items = items;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Handle references
|
|
128
|
+
if (schema.$ref && schema.$ref.startsWith('#/definitions/')) {
|
|
129
|
+
const refName = schema.$ref.replace('#/definitions/', '');
|
|
130
|
+
const refSchema = definitions[refName];
|
|
131
|
+
if (refSchema) {
|
|
132
|
+
Object.assign(result, extractSchemaDescriptions(refSchema));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle anyOf/oneOf (union types)
|
|
137
|
+
if (schema.anyOf ?? schema.oneOf) {
|
|
138
|
+
const variants = schema.anyOf ?? schema.oneOf;
|
|
139
|
+
result.variants = variants.map((variant: any) => extractSchemaDescriptions(variant));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Convert Zod schema to JSON schema
|
|
146
|
+
const actorInfoSchema = zodToJsonSchema(ActorInfoSchema, 'ActorInfoSchema');
|
|
147
|
+
|
|
148
|
+
// Get all definitions from the schema
|
|
149
|
+
const definitions = (actorInfoSchema as any).definitions ?? {};
|
|
150
|
+
|
|
151
|
+
const result = extractSchemaDescriptions(actorInfoSchema).fields;
|
|
152
|
+
|
|
153
|
+
const vector3Schema = zodToJsonSchema(ThreeVector3Schema, 'ThreeVector3Schema');
|
|
154
|
+
const eulerSchema = zodToJsonSchema(ThreeEulerSchema, 'ThreeEulerSchema');
|
|
155
|
+
const vector3Example: z.infer<typeof ThreeVector3Schema> = {
|
|
156
|
+
type: 'THREE.Vector3',
|
|
157
|
+
x: 1,
|
|
158
|
+
y: 2,
|
|
159
|
+
z: 3,
|
|
160
|
+
};
|
|
161
|
+
const eulerExample: z.infer<typeof ThreeEulerSchema> = {
|
|
162
|
+
type: 'THREE.Euler',
|
|
163
|
+
x: 0,
|
|
164
|
+
y: 1.57,
|
|
165
|
+
z: -3.14,
|
|
166
|
+
};
|
|
167
|
+
result.specialTypes = {
|
|
168
|
+
description: 'Here shows how some special types should be populated from JSON values',
|
|
169
|
+
'THREE.Vector3': { ...extractSchemaDescriptions(vector3Schema.definitions?.ThreeVector3Schema ?? {}), example: vector3Example },
|
|
170
|
+
'THREE.Euler': { ...extractSchemaDescriptions(eulerSchema.definitions?.ThreeEulerSchema ?? {}), example: eulerExample },
|
|
171
|
+
};
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Creates a ts-morph project with proper TypeScript configuration
|
|
177
|
+
*/
|
|
178
|
+
function createTsMorphProject(storageProvider: StorageProvider): Project {
|
|
179
|
+
try {
|
|
180
|
+
// Try to use the project's tsconfig.json
|
|
181
|
+
const tsconfigPath = storageProvider.getFullPath('@project/tsconfig.json');
|
|
182
|
+
|
|
183
|
+
const project = new Project({
|
|
184
|
+
tsConfigFilePath: fs.existsSync(tsconfigPath) ? tsconfigPath : undefined,
|
|
185
|
+
useInMemoryFileSystem: false,
|
|
186
|
+
skipAddingFilesFromTsConfig: true,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
console.log('✅ ts-morph project created successfully');
|
|
190
|
+
return project;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.warn('⚠️ Failed to create ts-morph project with tsconfig, using default configuration');
|
|
193
|
+
console.warn('Error:', error instanceof Error ? error.message : String(error));
|
|
194
|
+
|
|
195
|
+
// Fallback to basic configuration
|
|
196
|
+
return new Project({
|
|
197
|
+
useInMemoryFileSystem: false,
|
|
198
|
+
compilerOptions: {
|
|
199
|
+
target: ScriptTarget.ES2020,
|
|
200
|
+
module: ModuleKind.ESNext,
|
|
201
|
+
moduleResolution: ModuleResolutionKind.NodeJs,
|
|
202
|
+
strict: true,
|
|
203
|
+
esModuleInterop: true,
|
|
204
|
+
skipLibCheck: true,
|
|
205
|
+
forceConsistentCasingInFileNames: true,
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Extracts enum values from a union type
|
|
213
|
+
*/
|
|
214
|
+
function extractEnumValues(type: any): string[] {
|
|
215
|
+
try {
|
|
216
|
+
if (type.isUnion()) {
|
|
217
|
+
const enumValues: string[] = [];
|
|
218
|
+
const unionTypes = type.getUnionTypes();
|
|
219
|
+
|
|
220
|
+
for (const unionType of unionTypes) {
|
|
221
|
+
if (unionType.isStringLiteral()) {
|
|
222
|
+
const literalValue = unionType.getLiteralValue();
|
|
223
|
+
if (typeof literalValue === 'string') {
|
|
224
|
+
enumValues.push(literalValue);
|
|
225
|
+
}
|
|
226
|
+
} else if (unionType.isNumberLiteral()) {
|
|
227
|
+
const literalValue = unionType.getLiteralValue();
|
|
228
|
+
if (typeof literalValue === 'number') {
|
|
229
|
+
enumValues.push(literalValue.toString());
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return enumValues;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return [];
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return [];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Extracts properties from an object type using ts-morph Type analysis
|
|
245
|
+
*/
|
|
246
|
+
function extractObjectProperties(type: any, visited: Set<string> = new Set()): Record<string, PropertyInfo> {
|
|
247
|
+
try {
|
|
248
|
+
const properties: Record<string, PropertyInfo> = {};
|
|
249
|
+
const typeString = type.getText();
|
|
250
|
+
|
|
251
|
+
// Prevent infinite recursion
|
|
252
|
+
if (visited.has(typeString)) {
|
|
253
|
+
return {};
|
|
254
|
+
}
|
|
255
|
+
visited.add(typeString);
|
|
256
|
+
|
|
257
|
+
// Handle union types - extract properties from the non-undefined part
|
|
258
|
+
if (type.isUnion()) {
|
|
259
|
+
const unionTypes = type.getUnionTypes();
|
|
260
|
+
for (const unionType of unionTypes) {
|
|
261
|
+
// Skip undefined types
|
|
262
|
+
if (unionType.isUndefined()) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
// If we find an object type in the union, extract its properties
|
|
266
|
+
const unionProperties = unionType.getApparentProperties();
|
|
267
|
+
if (unionProperties && unionProperties.length > 0) {
|
|
268
|
+
return extractObjectProperties(unionType, visited);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get all properties of the type (includes inherited properties)
|
|
274
|
+
const typeProperties = type.getApparentProperties();
|
|
275
|
+
|
|
276
|
+
for (const prop of typeProperties) {
|
|
277
|
+
const propName = prop.getName();
|
|
278
|
+
|
|
279
|
+
// Skip internal/private properties
|
|
280
|
+
if (propName.startsWith('_') || propName.startsWith('__')) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const propDeclaration = prop.getValueDeclaration();
|
|
285
|
+
if (!propDeclaration) continue;
|
|
286
|
+
|
|
287
|
+
// Get property type
|
|
288
|
+
const propType = prop.getTypeAtLocation(propDeclaration);
|
|
289
|
+
const propTypeText = propType.getText(propDeclaration);
|
|
290
|
+
|
|
291
|
+
// Try to extract JSDoc description
|
|
292
|
+
let description: string | undefined;
|
|
293
|
+
if (propDeclaration && 'getJsDocs' in propDeclaration) {
|
|
294
|
+
const jsDocs = (propDeclaration as any).getJsDocs();
|
|
295
|
+
if (jsDocs.length > 0) {
|
|
296
|
+
description = jsDocs[0].getDescription();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check if this is an enum/union type
|
|
301
|
+
const enumValues = extractEnumValues(propType);
|
|
302
|
+
let finalType = normalizeTypeText(propTypeText);
|
|
303
|
+
|
|
304
|
+
// Add enum values to the cleaned description
|
|
305
|
+
let finalDescription = description;
|
|
306
|
+
if (enumValues.length > 0) {
|
|
307
|
+
const enumDescription = `Possible values: ${enumValues.join(', ')}`;
|
|
308
|
+
finalDescription = finalDescription ? `${finalDescription}. ${enumDescription}` : enumDescription;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Determine if this type can be populated from JSON
|
|
312
|
+
const jsonSerializable = canPopulateFromJson(propTypeText, propType);
|
|
313
|
+
|
|
314
|
+
// Determine if this property has nested properties
|
|
315
|
+
const paramInfo: PropertyInfo = {
|
|
316
|
+
type: shouldTreatAsObject(propTypeText, propType) ? 'object' : finalType,
|
|
317
|
+
description: finalDescription,
|
|
318
|
+
canPopulateFromJson: jsonSerializable,
|
|
319
|
+
optional: prop.isOptional()
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// If it's an object type, recursively extract properties
|
|
323
|
+
if (shouldTreatAsObject(propTypeText, propType)) {
|
|
324
|
+
paramInfo.properties = extractObjectProperties(propType, visited);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
properties[propName] = paramInfo;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return properties;
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.warn('Failed to extract object properties:', error instanceof Error ? error.message : String(error));
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Extracts properties from an options object type using ts-morph Type analysis
|
|
339
|
+
*/
|
|
340
|
+
function extractOptionsProperties(param: ParameterDeclaration): Record<string, PropertyInfo> {
|
|
341
|
+
try {
|
|
342
|
+
const type = param.getType();
|
|
343
|
+
return extractObjectProperties(type);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
console.warn(`Failed to extract properties for parameter ${param.getName()}:`, error instanceof Error ? error.message : String(error));
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Determines if a type should be treated as an object with properties
|
|
352
|
+
*/
|
|
353
|
+
function shouldTreatAsObject(typeText: string, tsType?: any): boolean {
|
|
354
|
+
// Skip primitive types (but not when they appear in object literals)
|
|
355
|
+
if (!typeText.includes('{') && ['string', 'number', 'boolean'].some(t => typeText === t || typeText === `${t} | undefined`)) {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Skip THREE.Vector3 and THREE.Euler - these are handled specially
|
|
360
|
+
if (typeText.includes('Vector3') || typeText.includes('Euler')) return false;
|
|
361
|
+
|
|
362
|
+
// Skip ENGINE.* and GAME.* classes - these are complex types
|
|
363
|
+
if (typeText.includes('ENGINE.') || typeText.includes('GAME.')) return false;
|
|
364
|
+
|
|
365
|
+
// Skip function types
|
|
366
|
+
if (typeText.includes('=>') || typeText.includes('Function') || typeText.includes('()')) return false;
|
|
367
|
+
|
|
368
|
+
// If we have TypeScript type information, use it
|
|
369
|
+
if (tsType) {
|
|
370
|
+
try {
|
|
371
|
+
// Handle union types - check if any union member is an interface-like type
|
|
372
|
+
if (tsType.isUnion()) {
|
|
373
|
+
const unionTypes = tsType.getUnionTypes();
|
|
374
|
+
for (const unionType of unionTypes) {
|
|
375
|
+
// Skip undefined types
|
|
376
|
+
if (unionType.isUndefined()) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
// Check if this union member is an interface-like type
|
|
380
|
+
if (isInterfaceLikeType(unionType)) {
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check if the type is an interface-like type
|
|
387
|
+
if (isInterfaceLikeType(tsType)) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
} catch (error) {
|
|
391
|
+
// If we can't get properties, fall back to string matching
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Treat object literals as objects (inline interfaces)
|
|
396
|
+
if (typeText.includes('{')) {
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Checks if a type is interface-like (has properties but is not a function or complex class)
|
|
405
|
+
*/
|
|
406
|
+
function isInterfaceLikeType(type: any): boolean {
|
|
407
|
+
try {
|
|
408
|
+
const properties = type.getApparentProperties();
|
|
409
|
+
if (!properties || properties.length === 0) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check if any property is a function - if so, this might be a class or complex type
|
|
414
|
+
for (const prop of properties) {
|
|
415
|
+
try {
|
|
416
|
+
const propType = prop.getTypeAtLocation(prop.getValueDeclaration());
|
|
417
|
+
const propTypeText = propType.getText();
|
|
418
|
+
|
|
419
|
+
// If we find function properties, this is likely a class or complex type
|
|
420
|
+
if (propTypeText.includes('=>') || propTypeText.includes('Function') || propTypeText.includes('()')) {
|
|
421
|
+
// Allow if it's clearly a callback/event handler (common in options)
|
|
422
|
+
const propName = prop.getName();
|
|
423
|
+
if (propName.toLowerCase().includes('callback') ||
|
|
424
|
+
propName.toLowerCase().includes('handler') ||
|
|
425
|
+
propName.toLowerCase().includes('listener') ||
|
|
426
|
+
propName.toLowerCase().startsWith('on')) {
|
|
427
|
+
continue; // Allow these function properties
|
|
428
|
+
}
|
|
429
|
+
return false; // Skip types with non-callback function properties
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
// If we can't analyze a property, continue
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return true; // It has properties and they're not complex functions
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Determines if a type can be populated from JSON values
|
|
445
|
+
*/
|
|
446
|
+
function canPopulateFromJson(typeText: string, tsType?: any, visited: Set<string> = new Set()): boolean {
|
|
447
|
+
// Handle undefined types - they can be omitted in JSON
|
|
448
|
+
if (typeText.includes('undefined')) {
|
|
449
|
+
// Extract the non-undefined part
|
|
450
|
+
const nonUndefPart = typeText.replace(/\s*\|\s*undefined/g, '').trim();
|
|
451
|
+
if (nonUndefPart) {
|
|
452
|
+
// Create a new visited set for the non-undefined part to avoid false recursion detection
|
|
453
|
+
const newVisited = new Set(visited);
|
|
454
|
+
newVisited.delete(typeText); // Remove the original union type
|
|
455
|
+
return canPopulateFromJson(nonUndefPart, tsType, newVisited);
|
|
456
|
+
}
|
|
457
|
+
return true; // Pure undefined can be omitted
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Prevent infinite recursion
|
|
461
|
+
if (visited.has(typeText)) {
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
464
|
+
visited.add(typeText);
|
|
465
|
+
|
|
466
|
+
// Primitive types can be populated
|
|
467
|
+
if (['string', 'number', 'boolean'].some(t => typeText === t || typeText.startsWith(t))) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// THREE.Vector3 and THREE.Euler can be populated as [x,y,z] arrays
|
|
472
|
+
if (typeText.includes('Vector3') || typeText.includes('Euler')) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ENGINE.* and GAME.* classes cannot be populated (complex runtime objects)
|
|
477
|
+
if (typeText.includes('ENGINE.') || typeText.includes('GAME.')) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Function types cannot be populated
|
|
482
|
+
if (typeText.includes('=>') || typeText.includes('Function') || typeText.includes('()')) {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Object literals can be populated (inline object types)
|
|
487
|
+
if (typeText.includes('{')) {
|
|
488
|
+
return true; // Inline object types can be populated
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Array types can be populated if their element type can be populated
|
|
492
|
+
if (typeText.includes('[]')) {
|
|
493
|
+
const elementType = typeText.replace('[]', '').trim();
|
|
494
|
+
return canPopulateFromJson(elementType, undefined, visited);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Check TypeScript type information
|
|
498
|
+
if (tsType) {
|
|
499
|
+
try {
|
|
500
|
+
const enumValues = extractEnumValues(tsType);
|
|
501
|
+
if (enumValues.length > 0) {
|
|
502
|
+
return true; // Enums can be populated
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Handle union types
|
|
506
|
+
if (tsType.isUnion()) {
|
|
507
|
+
const unionTypes = tsType.getUnionTypes();
|
|
508
|
+
// Check if any non-undefined union member can be populated
|
|
509
|
+
for (const unionType of unionTypes) {
|
|
510
|
+
if (unionType.isUndefined()) {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const unionTypeText = unionType.getText();
|
|
514
|
+
if (canPopulateFromJson(unionTypeText, unionType, visited)) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check if this is an object type with properties
|
|
522
|
+
if (isInterfaceLikeType(tsType)) {
|
|
523
|
+
return canObjectBePopulatedFromJson(tsType, visited);
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
// If type analysis fails, fall back to string matching
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Default to false for unknown types
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Checks if an object type can be populated from JSON by checking if all required properties can be populated
|
|
536
|
+
*/
|
|
537
|
+
function canObjectBePopulatedFromJson(tsType: any, visited: Set<string> = new Set()): boolean {
|
|
538
|
+
try {
|
|
539
|
+
const properties = tsType.getApparentProperties();
|
|
540
|
+
if (!properties || properties.length === 0) {
|
|
541
|
+
return true; // Empty object can be populated
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Check all properties
|
|
545
|
+
for (const prop of properties) {
|
|
546
|
+
const propName = prop.getName();
|
|
547
|
+
|
|
548
|
+
// Skip internal/private properties
|
|
549
|
+
if (propName.startsWith('_') || propName.startsWith('__')) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const propDeclaration = prop.getValueDeclaration();
|
|
554
|
+
if (!propDeclaration) continue;
|
|
555
|
+
|
|
556
|
+
// Check if property is required
|
|
557
|
+
const isOptional = prop.isOptional();
|
|
558
|
+
|
|
559
|
+
// If it's required, it must be populatable
|
|
560
|
+
if (!isOptional) {
|
|
561
|
+
const propType = prop.getTypeAtLocation(propDeclaration);
|
|
562
|
+
const propTypeText = propType.getText(propDeclaration);
|
|
563
|
+
|
|
564
|
+
if (!canPopulateFromJson(propTypeText, propType, visited)) {
|
|
565
|
+
return false; // Required property cannot be populated
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Note: We don't need to check optional properties - they can be omitted
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return true; // All required properties can be populated (or all properties are optional)
|
|
572
|
+
} catch (error) {
|
|
573
|
+
return false; // If analysis fails, assume it cannot be populated
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Normalizes type text for display
|
|
579
|
+
*/
|
|
580
|
+
function normalizeTypeText(typeText: string): string {
|
|
581
|
+
if (typeText.includes('Vector3')) return 'THREE.Vector3';
|
|
582
|
+
if (typeText.includes('Euler')) return 'THREE.Euler';
|
|
583
|
+
return typeText;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Extracts JSDoc description for a parameter
|
|
588
|
+
*/
|
|
589
|
+
function extractParamJSDoc(param: ParameterDeclaration): string | undefined {
|
|
590
|
+
// Get JSDoc tags from the parent constructor
|
|
591
|
+
const constructor = param.getParent();
|
|
592
|
+
if (!constructor || !('getJsDocs' in constructor)) return undefined;
|
|
593
|
+
|
|
594
|
+
try {
|
|
595
|
+
const jsDocs = (constructor as any).getJsDocs();
|
|
596
|
+
if (!jsDocs || jsDocs.length === 0) return undefined;
|
|
597
|
+
|
|
598
|
+
const paramName = param.getName();
|
|
599
|
+
for (const jsDoc of jsDocs) {
|
|
600
|
+
const tags = jsDoc.getTags();
|
|
601
|
+
for (const tag of tags) {
|
|
602
|
+
if (tag.getTagName() === 'param' && tag.getComment()?.includes(paramName)) {
|
|
603
|
+
return tag.getComment();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
// If JSDoc extraction fails, silently continue
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Analyzes a constructor parameter to extract detailed information
|
|
617
|
+
*/
|
|
618
|
+
function analyzeParameter(param: ParameterDeclaration): ParameterInfo {
|
|
619
|
+
const tsType = param.getType();
|
|
620
|
+
const type = tsType.getText(param);
|
|
621
|
+
const normalizedType = normalizeTypeText(type);
|
|
622
|
+
const paramName = param.getName();
|
|
623
|
+
|
|
624
|
+
// Extract JSDoc description
|
|
625
|
+
let description = extractParamJSDoc(param);
|
|
626
|
+
|
|
627
|
+
// Check if this is an enum/union type
|
|
628
|
+
const enumValues = extractEnumValues(tsType);
|
|
629
|
+
|
|
630
|
+
// Add enum values to the cleaned description
|
|
631
|
+
let finalDescription = description;
|
|
632
|
+
if (enumValues.length > 0) {
|
|
633
|
+
const enumDescription = `Possible values: ${enumValues.join(', ')}`;
|
|
634
|
+
finalDescription = finalDescription ? `${finalDescription}. ${enumDescription}` : enumDescription;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Determine if this type can be populated from JSON
|
|
638
|
+
const jsonSerializable = canPopulateFromJson(type, tsType);
|
|
639
|
+
|
|
640
|
+
const paramInfo: ParameterInfo = {
|
|
641
|
+
paramName,
|
|
642
|
+
type: shouldTreatAsObject(type, tsType) ? 'object' : normalizedType,
|
|
643
|
+
description: finalDescription,
|
|
644
|
+
canPopulateFromJson: jsonSerializable,
|
|
645
|
+
optional: param.isOptional()
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// Extract properties if this is an options object
|
|
649
|
+
if (shouldTreatAsObject(type, tsType)) {
|
|
650
|
+
paramInfo.properties = extractOptionsProperties(param);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return paramInfo;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Determines if an actor class can be populated from JSON based on its constructor parameters
|
|
658
|
+
*/
|
|
659
|
+
function canActorBePopulatedFromJson(constructorParams: ConstructorParameters): boolean {
|
|
660
|
+
// If there are no constructor parameters, it can be populated
|
|
661
|
+
if (!constructorParams || constructorParams.length === 0) {
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Check each parameter
|
|
666
|
+
for (const paramInfo of constructorParams) {
|
|
667
|
+
// If any required parameter cannot be populated from JSON, the actor cannot be populated
|
|
668
|
+
if (paramInfo.canPopulateFromJson === false) {
|
|
669
|
+
// Check if this parameter is optional (has undefined in type)
|
|
670
|
+
const isOptional = paramInfo.type.includes('undefined');
|
|
671
|
+
if (!isOptional) {
|
|
672
|
+
return false; // Required parameter cannot be populated from JSON
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return true; // All required parameters can be populated from JSON
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Extracts constructor parameters from a class declaration
|
|
682
|
+
*/
|
|
683
|
+
function extractConstructorParams(classDecl: ClassDeclaration): ConstructorParameters {
|
|
684
|
+
const constructors = classDecl.getConstructors();
|
|
685
|
+
if (constructors.length === 0) return [];
|
|
686
|
+
|
|
687
|
+
// Take the first constructor (could be enhanced to handle overloads)
|
|
688
|
+
const constructor = constructors[0];
|
|
689
|
+
|
|
690
|
+
return constructor.getParameters().map(param => analyzeParameter(param));
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Analyzes a class declaration to extract actor information
|
|
695
|
+
*/
|
|
696
|
+
function analyzeActorClassBasics(
|
|
697
|
+
sourceFile: SourceFile,
|
|
698
|
+
classDeclaration: ClassDeclaration,
|
|
699
|
+
classPrefix: ENGINE.Prefix
|
|
700
|
+
): ActorInfo | null {
|
|
701
|
+
const className = classDeclaration.getName();
|
|
702
|
+
if (!className) return null;
|
|
703
|
+
|
|
704
|
+
const prefixedClassName = classPrefix + className;
|
|
705
|
+
const classConstructor = ENGINE.ClassRegistry.getRegistry().get(prefixedClassName);
|
|
706
|
+
if (!isSubclass(classConstructor, ENGINE.Actor)) return null;
|
|
707
|
+
|
|
708
|
+
// Extract basic information
|
|
709
|
+
const filePath = sourceFile.getFilePath();
|
|
710
|
+
const heritage = classDeclaration.getExtends();
|
|
711
|
+
const parentClassName = heritage?.getExpression().getText() ?? '';
|
|
712
|
+
|
|
713
|
+
// For now, create a simple base classes array
|
|
714
|
+
const baseClasses: string[] = [];
|
|
715
|
+
if (parentClassName) {
|
|
716
|
+
baseClasses.push(parentClassName);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Extract JSDoc description if available
|
|
720
|
+
const jsDocs = classDeclaration.getJsDocs();
|
|
721
|
+
const description = jsDocs.length > 0 ? jsDocs[0].getDescription() : undefined;
|
|
722
|
+
|
|
723
|
+
// Extract constructor parameters
|
|
724
|
+
// const constructorParams = extractConstructorParams(classDeclaration);
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
className: prefixedClassName,
|
|
728
|
+
filePath,
|
|
729
|
+
baseClasses,
|
|
730
|
+
// constructorParams,
|
|
731
|
+
description,
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Recursively searches a directory for TypeScript files
|
|
737
|
+
*/
|
|
738
|
+
function collectTypeScriptFiles(
|
|
739
|
+
dir: string,
|
|
740
|
+
storageProvider: StorageProvider
|
|
741
|
+
): string[] {
|
|
742
|
+
let results: string[] = [];
|
|
743
|
+
const actualDir = storageProvider.getFullPath(dir);
|
|
744
|
+
|
|
745
|
+
if (!fs.existsSync(actualDir)) {
|
|
746
|
+
console.warn(`Directory does not exist: ${actualDir}`);
|
|
747
|
+
return results;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const list = fs.readdirSync(actualDir);
|
|
751
|
+
|
|
752
|
+
list.forEach((file) => {
|
|
753
|
+
const filePath = path.join(dir, file);
|
|
754
|
+
const actualFilePath = storageProvider.getFullPath(filePath);
|
|
755
|
+
const stat = fs.statSync(actualFilePath);
|
|
756
|
+
|
|
757
|
+
if (stat && stat.isDirectory() && file !== 'node_modules' && !file.startsWith('.')) {
|
|
758
|
+
// Recursively search subdirectories
|
|
759
|
+
results = results.concat(collectTypeScriptFiles(filePath, storageProvider));
|
|
760
|
+
} else if (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts') && !file.endsWith('.spec.ts')) {
|
|
761
|
+
// Include TypeScript files but exclude declaration files
|
|
762
|
+
results.push(filePath);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
return results;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/**
|
|
770
|
+
* Main function to search for actor classes
|
|
771
|
+
*/
|
|
772
|
+
export async function populateClassesInfo(
|
|
773
|
+
options: ActorSearchOptions = {}
|
|
774
|
+
): Promise<ActorsSearchResult> {
|
|
775
|
+
const {
|
|
776
|
+
classesToSearch = [],
|
|
777
|
+
includeConstructorParams = false
|
|
778
|
+
} = options;
|
|
779
|
+
|
|
780
|
+
const dirs = [];
|
|
781
|
+
if (Object.values(classesToSearch).some(className => className.startsWith(ENGINE.Prefix.ENGINE))) {
|
|
782
|
+
dirs.push(ENGINE.ENGINE_PATH_PREFIX);
|
|
783
|
+
}
|
|
784
|
+
if (Object.values(classesToSearch).some(className => className.startsWith(ENGINE.Prefix.GAME))) {
|
|
785
|
+
dirs.push(ENGINE.PROJECT_PATH_PREFIX);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const storageProvider = new StorageProvider();
|
|
789
|
+
|
|
790
|
+
// Collect TypeScript files
|
|
791
|
+
const files: string[] = [];
|
|
792
|
+
for (const dir of dirs) {
|
|
793
|
+
const dirFiles = collectTypeScriptFiles(dir, storageProvider);
|
|
794
|
+
files.push(...dirFiles);
|
|
795
|
+
console.log(`Found ${dirFiles.length} TypeScript files in ${dir}`);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
console.log(`Total TypeScript files found: ${files.length}`);
|
|
799
|
+
|
|
800
|
+
// Create ts-morph project
|
|
801
|
+
const project = createTsMorphProject(storageProvider);
|
|
802
|
+
|
|
803
|
+
// Add files to ts-morph project (limit for performance)
|
|
804
|
+
const filesToAnalyze = files;
|
|
805
|
+
console.log(`Analyzing ${filesToAnalyze.length} files with ts-morph...`);
|
|
806
|
+
|
|
807
|
+
for (const filePath of filesToAnalyze) {
|
|
808
|
+
try {
|
|
809
|
+
const actualPath = storageProvider.getFullPath(filePath);
|
|
810
|
+
if (fs.existsSync(actualPath)) {
|
|
811
|
+
project.addSourceFileAtPath(actualPath);
|
|
812
|
+
}
|
|
813
|
+
} catch (error) {
|
|
814
|
+
console.warn(`Failed to add file ${filePath}:`, error instanceof Error ? error.message : String(error));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const result: ActorsSearchResult = {
|
|
819
|
+
metadataDescription: includeConstructorParams ? createMetadataDescription() : {},
|
|
820
|
+
actors: {}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// Analyze each source file for actor classes
|
|
824
|
+
let totalClasses = 0;
|
|
825
|
+
let actorClasses = 0;
|
|
826
|
+
|
|
827
|
+
const engineFullPath = storageProvider.getFullPath(ENGINE.ENGINE_PATH_PREFIX);
|
|
828
|
+
|
|
829
|
+
for (const sourceFile of project.getSourceFiles()) {
|
|
830
|
+
const isEngineClass = sourceFile.getFilePath().startsWith(engineFullPath);
|
|
831
|
+
const classPrefix = isEngineClass ? ENGINE.Prefix.ENGINE : ENGINE.Prefix.GAME;
|
|
832
|
+
const classes = sourceFile.getClasses();
|
|
833
|
+
totalClasses += classes.length;
|
|
834
|
+
|
|
835
|
+
for (const classDecl of classes) {
|
|
836
|
+
const actorInfo = analyzeActorClassBasics(sourceFile, classDecl, classPrefix);
|
|
837
|
+
if (actorInfo) {
|
|
838
|
+
actorClasses++;
|
|
839
|
+
console.log(`Found Actor-derived class: ${actorInfo.className} in ${path.basename(actorInfo.filePath)}`);
|
|
840
|
+
|
|
841
|
+
if (includeConstructorParams) {
|
|
842
|
+
actorInfo.constructorParams = extractConstructorParams(classDecl);
|
|
843
|
+
// Determine if the entire actor can be populated from JSON
|
|
844
|
+
actorInfo.canPopulateFromJson = canActorBePopulatedFromJson(actorInfo.constructorParams);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (!classesToSearch || classesToSearch.length === 0 || classesToSearch.includes(actorInfo.className)) {
|
|
848
|
+
result.actors[actorInfo.className] = actorInfo;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
console.log(`✅ Analysis complete: ${totalClasses} total classes, ${actorClasses} Actor-derived classes found`);
|
|
855
|
+
|
|
856
|
+
return result;
|
|
857
|
+
}
|
|
858
|
+
|