@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.
Files changed (82) hide show
  1. package/README.md +60 -60
  2. package/dist/src/core/cli.js +22 -22
  3. package/dist/src/templates/scripts/genesys/genesys-mcp.js +25 -25
  4. package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +4 -4
  5. package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +30 -30
  6. package/package.json +176 -176
  7. package/scripts/post-install.ts +143 -143
  8. package/src/asset-pack/.gitattributes +88 -88
  9. package/src/asset-pack/eslint.config.js +45 -45
  10. package/src/asset-pack/gitignore +11 -11
  11. package/src/asset-pack/scripts/postinstall.ts +81 -81
  12. package/src/asset-pack/tsconfig.json +33 -33
  13. package/src/templates/.cursor/mcp.json +20 -20
  14. package/src/templates/.cursorignore +2 -2
  15. package/src/templates/.gitattributes +88 -88
  16. package/src/templates/.vscode/settings.json +6 -6
  17. package/src/templates/AGENTS.md +86 -86
  18. package/src/templates/README.md +24 -24
  19. package/src/templates/eslint.config.js +45 -45
  20. package/src/templates/gitignore +11 -11
  21. package/src/templates/index.html +34 -34
  22. package/src/templates/pnpm-lock.yaml +3676 -3676
  23. package/src/templates/scripts/genesys/build-project.ts +51 -51
  24. package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -272
  25. package/src/templates/scripts/genesys/common.ts +46 -46
  26. package/src/templates/scripts/genesys/const.ts +9 -9
  27. package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -11
  28. package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -146
  29. package/src/templates/scripts/genesys/dev/launcher.ts +46 -46
  30. package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -229
  31. package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -84
  32. package/src/templates/scripts/genesys/doc-server.ts +16 -16
  33. package/src/templates/scripts/genesys/genesys-mcp.ts +526 -526
  34. package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -86
  35. package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -151
  36. package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -73
  37. package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -35
  38. package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -30
  39. package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -858
  40. package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -380
  41. package/src/templates/scripts/genesys/mcp/utils.ts +281 -281
  42. package/src/templates/scripts/genesys/misc.ts +42 -42
  43. package/src/templates/scripts/genesys/mock.ts +6 -6
  44. package/src/templates/scripts/genesys/place-actors.ts +179 -179
  45. package/src/templates/scripts/genesys/post-install.ts +30 -30
  46. package/src/templates/scripts/genesys/prefab.schema.json +84 -84
  47. package/src/templates/scripts/genesys/remove-engine-comments.ts +134 -134
  48. package/src/templates/scripts/genesys/run-mcp-inspector.bat +4 -4
  49. package/src/templates/scripts/genesys/storageProvider.ts +182 -182
  50. package/src/templates/scripts/genesys/validate-prefabs.ts +138 -138
  51. package/src/templates/src/index.ts +22 -22
  52. package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +165 -165
  53. package/src/templates/src/templates/firstPerson/src/game.ts +39 -39
  54. package/src/templates/src/templates/firstPerson/src/player.ts +63 -63
  55. package/src/templates/src/templates/fps/assets/default.genesys-scene +9459 -9459
  56. package/src/templates/src/templates/fps/src/game.ts +39 -39
  57. package/src/templates/src/templates/fps/src/player.ts +69 -69
  58. package/src/templates/src/templates/fps/src/weapon.ts +54 -54
  59. package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +165 -165
  60. package/src/templates/src/templates/freeCamera/src/game.ts +39 -39
  61. package/src/templates/src/templates/freeCamera/src/player.ts +45 -45
  62. package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +121 -121
  63. package/src/templates/src/templates/sideScroller/src/const.ts +45 -45
  64. package/src/templates/src/templates/sideScroller/src/game.ts +122 -122
  65. package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -361
  66. package/src/templates/src/templates/sideScroller/src/player.ts +125 -125
  67. package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +165 -165
  68. package/src/templates/src/templates/thirdPerson/src/game.ts +39 -39
  69. package/src/templates/src/templates/thirdPerson/src/player.ts +61 -61
  70. package/src/templates/src/templates/vehicle/assets/default.genesys-scene +225 -225
  71. package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -145
  72. package/src/templates/src/templates/vehicle/src/game.ts +43 -43
  73. package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -191
  74. package/src/templates/src/templates/vehicle/src/player.ts +109 -109
  75. package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -266
  76. package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -101
  77. package/src/templates/src/templates/vr-game/assets/default.genesys-scene +246 -246
  78. package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -1
  79. package/src/templates/src/templates/vr-game/src/game.ts +66 -66
  80. package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -26
  81. package/src/templates/tsconfig.json +34 -34
  82. 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
+