@directivegames/genesys.sdk 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +60 -0
  2. package/dist/src/asset-pack/eslint.config.js +43 -0
  3. package/dist/src/asset-pack/scripts/postinstall.js +64 -0
  4. package/dist/src/asset-pack/src/index.js +1 -0
  5. package/dist/src/core/cli.js +306 -0
  6. package/dist/src/core/common.js +324 -0
  7. package/dist/src/core/index.js +6 -0
  8. package/dist/src/core/tools/build-project.js +450 -0
  9. package/dist/src/core/tools/index.js +2 -0
  10. package/dist/src/core/tools/new-asset-pack.js +150 -0
  11. package/dist/src/core/tools/new-project.js +292 -0
  12. package/dist/src/core/types.js +1 -0
  13. package/dist/src/dependencies.js +82 -0
  14. package/dist/src/electron/IpcSerializableError.js +38 -0
  15. package/dist/src/electron/api.js +7 -0
  16. package/dist/src/electron/backend/actions.js +56 -0
  17. package/dist/src/electron/backend/handler.js +441 -0
  18. package/dist/src/electron/backend/logging.js +41 -0
  19. package/dist/src/electron/backend/main.js +315 -0
  20. package/dist/src/electron/backend/menu.js +208 -0
  21. package/dist/src/electron/backend/state.js +201 -0
  22. package/dist/src/electron/backend/tools/const.js +9 -0
  23. package/dist/src/electron/backend/tools/file-server.js +383 -0
  24. package/dist/src/electron/backend/tools/open-project.js +261 -0
  25. package/dist/src/electron/backend/window.js +161 -0
  26. package/dist/src/templates/eslint.config.js +43 -0
  27. package/dist/src/templates/scripts/genesys/build-project.js +42 -0
  28. package/dist/src/templates/scripts/genesys/calc-bounding-box.js +205 -0
  29. package/dist/src/templates/scripts/genesys/common.js +36 -0
  30. package/dist/src/templates/scripts/genesys/const.js +9 -0
  31. package/dist/src/templates/scripts/genesys/dev/dump-default-scene.js +8 -0
  32. package/dist/src/templates/scripts/genesys/dev/generate-manifest.js +116 -0
  33. package/dist/src/templates/scripts/genesys/dev/launcher.js +39 -0
  34. package/dist/src/templates/scripts/genesys/dev/storage-provider.js +188 -0
  35. package/dist/src/templates/scripts/genesys/dev/update-template-scenes.js +67 -0
  36. package/dist/src/templates/scripts/genesys/doc-server.js +12 -0
  37. package/dist/src/templates/scripts/genesys/genesys-mcp.js +413 -0
  38. package/dist/src/templates/scripts/genesys/mcp/doc-tools.js +70 -0
  39. package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +123 -0
  40. package/dist/src/templates/scripts/genesys/mcp/editor-tools.js +51 -0
  41. package/dist/src/templates/scripts/genesys/mcp/get-scene-state.js +26 -0
  42. package/dist/src/templates/scripts/genesys/mcp/run-subprocess.js +23 -0
  43. package/dist/src/templates/scripts/genesys/mcp/search-actors.js +703 -0
  44. package/dist/src/templates/scripts/genesys/mcp/search-assets.js +296 -0
  45. package/dist/src/templates/scripts/genesys/mcp/utils.js +234 -0
  46. package/dist/src/templates/scripts/genesys/misc.js +32 -0
  47. package/dist/src/templates/scripts/genesys/mock.js +5 -0
  48. package/dist/src/templates/scripts/genesys/place-actors.js +112 -0
  49. package/dist/src/templates/scripts/genesys/post-install.js +25 -0
  50. package/dist/src/templates/scripts/genesys/remove-engine-comments.js +113 -0
  51. package/dist/src/templates/scripts/genesys/storageProvider.js +146 -0
  52. package/dist/src/templates/scripts/genesys/validate-prefabs.js +115 -0
  53. package/dist/src/templates/src/index.js +20 -0
  54. package/dist/src/templates/src/templates/firstPerson/src/auto-imports.js +1 -0
  55. package/dist/src/templates/src/templates/firstPerson/src/game.js +30 -0
  56. package/dist/src/templates/src/templates/firstPerson/src/player.js +60 -0
  57. package/dist/src/templates/src/templates/fps/src/auto-imports.js +1 -0
  58. package/dist/src/templates/src/templates/fps/src/game.js +30 -0
  59. package/dist/src/templates/src/templates/fps/src/player.js +64 -0
  60. package/dist/src/templates/src/templates/fps/src/weapon.js +62 -0
  61. package/dist/src/templates/src/templates/freeCamera/src/auto-imports.js +1 -0
  62. package/dist/src/templates/src/templates/freeCamera/src/game.js +30 -0
  63. package/dist/src/templates/src/templates/freeCamera/src/player.js +43 -0
  64. package/dist/src/templates/src/templates/sideScroller/src/auto-imports.js +1 -0
  65. package/dist/src/templates/src/templates/sideScroller/src/const.js +43 -0
  66. package/dist/src/templates/src/templates/sideScroller/src/game.js +103 -0
  67. package/dist/src/templates/src/templates/sideScroller/src/level-generator.js +249 -0
  68. package/dist/src/templates/src/templates/sideScroller/src/player.js +105 -0
  69. package/dist/src/templates/src/templates/thirdPerson/src/auto-imports.js +1 -0
  70. package/dist/src/templates/src/templates/thirdPerson/src/game.js +30 -0
  71. package/dist/src/templates/src/templates/thirdPerson/src/player.js +63 -0
  72. package/dist/src/templates/src/templates/vehicle/src/auto-imports.js +1 -0
  73. package/dist/src/templates/src/templates/vehicle/src/base-vehicle.js +122 -0
  74. package/dist/src/templates/src/templates/vehicle/src/game.js +33 -0
  75. package/dist/src/templates/src/templates/vehicle/src/mesh-vehicle.js +189 -0
  76. package/dist/src/templates/src/templates/vehicle/src/player.js +102 -0
  77. package/dist/src/templates/src/templates/vehicle/src/primitive-vehicle.js +259 -0
  78. package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +100 -0
  79. package/dist/src/templates/src/templates/vr-game/src/auto-imports.js +1 -0
  80. package/dist/src/templates/src/templates/vr-game/src/game.js +55 -0
  81. package/dist/src/templates/src/templates/vr-game/src/sample-vr-actor.js +29 -0
  82. package/dist/src/templates/vite.config.js +46 -0
  83. package/package.json +176 -0
  84. package/scripts/post-install.ts +143 -0
  85. package/src/asset-pack/.gitattributes +89 -0
  86. package/src/asset-pack/eslint.config.js +45 -0
  87. package/src/asset-pack/gitignore +11 -0
  88. package/src/asset-pack/scripts/postinstall.ts +81 -0
  89. package/src/asset-pack/src/index.ts +0 -0
  90. package/src/asset-pack/tsconfig.json +34 -0
  91. package/src/templates/.cursor/mcp.json +20 -0
  92. package/src/templates/.cursorignore +2 -0
  93. package/src/templates/.gitattributes +89 -0
  94. package/src/templates/.vscode/settings.json +6 -0
  95. package/src/templates/AGENTS.md +86 -0
  96. package/src/templates/CLAUDE.md +1 -0
  97. package/src/templates/README.md +24 -0
  98. package/src/templates/eslint.config.js +45 -0
  99. package/src/templates/gitignore +11 -0
  100. package/src/templates/index.html +34 -0
  101. package/src/templates/pnpm-lock.yaml +3676 -0
  102. package/src/templates/scripts/genesys/build-project.ts +51 -0
  103. package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -0
  104. package/src/templates/scripts/genesys/common.ts +46 -0
  105. package/src/templates/scripts/genesys/const.ts +9 -0
  106. package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -0
  107. package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -0
  108. package/src/templates/scripts/genesys/dev/launcher.ts +46 -0
  109. package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -0
  110. package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -0
  111. package/src/templates/scripts/genesys/doc-server.ts +16 -0
  112. package/src/templates/scripts/genesys/genesys-mcp.ts +526 -0
  113. package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -0
  114. package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -0
  115. package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -0
  116. package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -0
  117. package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -0
  118. package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -0
  119. package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -0
  120. package/src/templates/scripts/genesys/mcp/utils.ts +281 -0
  121. package/src/templates/scripts/genesys/misc.ts +42 -0
  122. package/src/templates/scripts/genesys/mock.ts +6 -0
  123. package/src/templates/scripts/genesys/place-actors.ts +179 -0
  124. package/src/templates/scripts/genesys/post-install.ts +30 -0
  125. package/src/templates/scripts/genesys/prefab.schema.json +85 -0
  126. package/src/templates/scripts/genesys/remove-engine-comments.ts +135 -0
  127. package/src/templates/scripts/genesys/run-mcp-inspector.bat +5 -0
  128. package/src/templates/scripts/genesys/storageProvider.ts +182 -0
  129. package/src/templates/scripts/genesys/validate-prefabs.ts +138 -0
  130. package/src/templates/src/index.ts +22 -0
  131. package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +166 -0
  132. package/src/templates/src/templates/firstPerson/src/auto-imports.ts +0 -0
  133. package/src/templates/src/templates/firstPerson/src/game.ts +39 -0
  134. package/src/templates/src/templates/firstPerson/src/player.ts +63 -0
  135. package/src/templates/src/templates/fps/assets/default.genesys-scene +9460 -0
  136. package/src/templates/src/templates/fps/assets/models/SM_Beam_400.glb +0 -0
  137. package/src/templates/src/templates/fps/assets/models/SM_ChamferCube.glb +0 -0
  138. package/src/templates/src/templates/fps/assets/models/SM_Floor_Thick_400x400.glb +0 -0
  139. package/src/templates/src/templates/fps/assets/models/SM_Floor_Thick_400x400_Orange.glb +0 -0
  140. package/src/templates/src/templates/fps/assets/models/SM_Floor_Thin_400x400.glb +0 -0
  141. package/src/templates/src/templates/fps/assets/models/SM_Floor_Thin_400x400_Orange.glb +0 -0
  142. package/src/templates/src/templates/fps/assets/models/SM_Ramp_400x400.glb +0 -0
  143. package/src/templates/src/templates/fps/assets/models/SM_Rifle.glb +0 -0
  144. package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x200.glb +0 -0
  145. package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x200_Orange.glb +0 -0
  146. package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x400.glb +0 -0
  147. package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x400_Orange.glb +0 -0
  148. package/src/templates/src/templates/fps/src/auto-imports.ts +0 -0
  149. package/src/templates/src/templates/fps/src/game.ts +39 -0
  150. package/src/templates/src/templates/fps/src/player.ts +69 -0
  151. package/src/templates/src/templates/fps/src/weapon.ts +54 -0
  152. package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +166 -0
  153. package/src/templates/src/templates/freeCamera/src/auto-imports.ts +0 -0
  154. package/src/templates/src/templates/freeCamera/src/game.ts +39 -0
  155. package/src/templates/src/templates/freeCamera/src/player.ts +45 -0
  156. package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +122 -0
  157. package/src/templates/src/templates/sideScroller/src/auto-imports.ts +0 -0
  158. package/src/templates/src/templates/sideScroller/src/const.ts +46 -0
  159. package/src/templates/src/templates/sideScroller/src/game.ts +122 -0
  160. package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -0
  161. package/src/templates/src/templates/sideScroller/src/player.ts +125 -0
  162. package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +166 -0
  163. package/src/templates/src/templates/thirdPerson/src/auto-imports.ts +0 -0
  164. package/src/templates/src/templates/thirdPerson/src/game.ts +39 -0
  165. package/src/templates/src/templates/thirdPerson/src/player.ts +61 -0
  166. package/src/templates/src/templates/vehicle/assets/default.genesys-scene +226 -0
  167. package/src/templates/src/templates/vehicle/assets/models/cyberTruck/chassis.glb +0 -0
  168. package/src/templates/src/templates/vehicle/assets/models/cyberTruck/wheel.glb +0 -0
  169. package/src/templates/src/templates/vehicle/src/auto-imports.ts +0 -0
  170. package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -0
  171. package/src/templates/src/templates/vehicle/src/game.ts +43 -0
  172. package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -0
  173. package/src/templates/src/templates/vehicle/src/player.ts +109 -0
  174. package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -0
  175. package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -0
  176. package/src/templates/src/templates/vr-game/assets/default.genesys-scene +247 -0
  177. package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -0
  178. package/src/templates/src/templates/vr-game/src/game.ts +66 -0
  179. package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -0
  180. package/src/templates/tsconfig.json +35 -0
  181. package/src/templates/vite.config.ts +52 -0
@@ -0,0 +1,296 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as ENGINE from 'genesys.js';
4
+ import { zodToJsonSchema } from 'zod-to-json-schema';
5
+ import { BoundingBoxSchema, fetchBoundingBoxData } from '../calc-bounding-box.js';
6
+ import { ENGINE_PREFIX, JS_CLASSES_DIR_NAME, PROJECT_PREFIX, SCENE_EXTENSION } from '../const.js';
7
+ import { StorageProvider } from '../storageProvider.js';
8
+ import { populateClassesInfo } from './search-actors.js';
9
+ import { conditionallyRegisterGameClasses } from './utils.js';
10
+ import { isSubclass } from './utils.js';
11
+ // TODO: make it consistent to AssetType in genesys.ai
12
+ export var AssetType;
13
+ (function (AssetType) {
14
+ AssetType["Model"] = "model";
15
+ AssetType["Texture"] = "texture";
16
+ AssetType["HDRI"] = "hdri";
17
+ AssetType["Video"] = "video";
18
+ AssetType["Audio"] = "audio";
19
+ AssetType["Json"] = "json";
20
+ AssetType["Scene"] = "scene";
21
+ AssetType["Prefab"] = "prefab";
22
+ AssetType["Material"] = "material";
23
+ AssetType["SourceCode"] = "sourcecode";
24
+ AssetType["JsClass"] = "jsclass";
25
+ })(AssetType || (AssetType = {}));
26
+ const modelTypes = ['.glb', '.gltf'];
27
+ export const assetDescriptions = {
28
+ [AssetType.Model]: '3D model files',
29
+ [AssetType.Texture]: 'Texture files',
30
+ [AssetType.HDRI]: 'HDRI files',
31
+ [AssetType.Video]: 'Video files',
32
+ [AssetType.Audio]: 'Audio files',
33
+ [AssetType.Scene]: 'Scenes, also known as levels, are generally used to represent distinct, playable areas in the game.',
34
+ [AssetType.Prefab]: 'Prefabs are actors that are prebuilt with components and can be placed in the scene directly. They generally extend Javascript classes to achieve more visual complex behavior.',
35
+ [AssetType.Material]: 'Material files',
36
+ [AssetType.SourceCode]: 'Source code files, including TypeScript and JavaScript files.',
37
+ [AssetType.JsClass]: 'JavaScript classes, which are actors that are implemented in code (Javascript). They contain visual and audio components, logics, or anything as they are implemented in code. They are often used directly in the scene, they also be used as a prefab base. Always consider this when placing things in the scene.',
38
+ [AssetType.Json]: 'JSON files, which are used to store data in a structured format.',
39
+ };
40
+ const assetTypeToExtensions = {
41
+ [AssetType.Model]: modelTypes,
42
+ [AssetType.Texture]: ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.ico', '.webp'],
43
+ [AssetType.HDRI]: ['.hdr', '.exr'],
44
+ [AssetType.Video]: ['.mp4', '.webm', '.mov', '.avi', '.mkv'],
45
+ [AssetType.Audio]: ['.mp3', '.wav', '.ogg', '.m4a', '.aac'],
46
+ [AssetType.Scene]: [SCENE_EXTENSION],
47
+ [AssetType.Prefab]: ['.prefab.json'],
48
+ [AssetType.Material]: ['.material.json'],
49
+ [AssetType.SourceCode]: ['.ts', '.js', '.tsx', '.jsx'],
50
+ [AssetType.JsClass]: [],
51
+ [AssetType.Json]: ['.json'],
52
+ };
53
+ const findAssetType = (filePath) => {
54
+ if (filePath.includes(JS_CLASSES_DIR_NAME)) {
55
+ return AssetType.JsClass;
56
+ }
57
+ const extension = path.extname(filePath).toLowerCase();
58
+ for (const [assetType, extensions] of Object.entries(assetTypeToExtensions)) {
59
+ // make sure .json is checked last
60
+ if (assetType === AssetType.Json) {
61
+ continue;
62
+ }
63
+ if (extensions.includes(extension)) {
64
+ return assetType;
65
+ }
66
+ }
67
+ if (assetTypeToExtensions[AssetType.Json].includes(extension)) {
68
+ return AssetType.Json;
69
+ }
70
+ return undefined;
71
+ };
72
+ /**
73
+ * Recursively iterates a given directory and returns an array of all file paths
74
+ * that match the accepted types.
75
+ * @param dir The directory to iterate.
76
+ * @param acceptedTypes Array of accepted types.
77
+ * @returns An array of file paths.
78
+ */
79
+ export async function searchForAssets(dirs, acceptedTypes = [], searchKeywords = []) {
80
+ for (const dir of dirs) {
81
+ if (!dir.startsWith(ENGINE.PROJECT_PATH_PREFIX) && !dir.startsWith(ENGINE.ENGINE_PATH_PREFIX)) {
82
+ throw new Error(`Directory ${dir} is not a valid project or engine directory`);
83
+ }
84
+ }
85
+ // make sure the game is built and classes are registered
86
+ await conditionallyRegisterGameClasses();
87
+ const acceptedExtensions = acceptedTypes.map(type => assetTypeToExtensions[type]).flat();
88
+ const getAll = acceptedTypes.length === 0;
89
+ const files = dirs.map(dir => collectFiles(dir, { getAll, acceptedExtensions })).flat();
90
+ // handle js classes in a special way.
91
+ if (getAll || acceptedTypes.includes(AssetType.JsClass)) {
92
+ const jsClasses = dirs.map(dir => {
93
+ const prefix = dir.includes(PROJECT_PREFIX) ? ENGINE.Prefix.GAME : (dir.includes(ENGINE_PREFIX) ? ENGINE.Prefix.ENGINE : '');
94
+ const registeredClasses = ENGINE.ClassRegistry.getRegistry();
95
+ const actorClasses = Array.from(registeredClasses.entries()).filter(([className, classCtor]) => className.startsWith(prefix) && isSubclass(classCtor, ENGINE.Actor) && !isSubclass(classCtor, ENGINE.BaseGameLoop));
96
+ return actorClasses.map(([key]) => `${JS_CLASSES_DIR_NAME}/${key}`);
97
+ });
98
+ files.push(...jsClasses.flat());
99
+ }
100
+ // convert file paths to unix paths
101
+ const assetPaths = files.map(filePath => filePath.replace(/\\/g, '/'));
102
+ // only peeking for basic metadata when searching assets to reduce the amount of data transferred
103
+ const result = await populateAssets(assetPaths, { peek: true });
104
+ // filter assets as we rely on not only names, but the metadata as well, to filter them
105
+ filterAssets(result, searchKeywords);
106
+ return result;
107
+ }
108
+ /**
109
+ * Gets the assets info with metadata for the given asset paths.
110
+ * @param assetPaths The paths of the assets to populate. It must be retrieved from `searchForAssets` function.
111
+ * @param options population options, such as `peek` to only populate basic metadata.
112
+ * @returns A promise that resolves to the assets info with metadata.
113
+ */
114
+ export async function populateAssets(assetPaths, options) {
115
+ const result = {
116
+ metadataDescription: {},
117
+ assets: assetPaths.reduce((acc, assetPath) => {
118
+ acc[assetPath] = { type: findAssetType(assetPath) };
119
+ return acc;
120
+ }, {})
121
+ };
122
+ await populateModels(result, options);
123
+ await populateJsClasses(result, options);
124
+ return result;
125
+ }
126
+ function collectFiles(dir, options) {
127
+ let results = [];
128
+ const storageProvider = new StorageProvider();
129
+ const actualDir = storageProvider.getFullPath(dir);
130
+ const list = fs.readdirSync(actualDir);
131
+ // Normalize accepted extensions to lower case for case-insensitive comparison
132
+ const normalizedExtensions = options.acceptedExtensions.map(ext => ext.toLowerCase());
133
+ list.forEach((file) => {
134
+ const filePath = path.join(dir, file);
135
+ const actualFilePath = storageProvider.getFullPath(filePath);
136
+ const stat = fs.statSync(actualFilePath);
137
+ if (stat && stat.isDirectory()) {
138
+ results = results.concat(collectFiles(filePath, options));
139
+ }
140
+ else {
141
+ let shouldKeep = false;
142
+ if (options.getAll) {
143
+ shouldKeep = true;
144
+ }
145
+ else {
146
+ const fileName = path.basename(filePath).toLowerCase();
147
+ if (normalizedExtensions.some((ext) => fileName.endsWith(ext))) {
148
+ shouldKeep = true;
149
+ }
150
+ }
151
+ if (shouldKeep) {
152
+ results.push(filePath);
153
+ }
154
+ }
155
+ });
156
+ return results;
157
+ }
158
+ function filterAssets(assets, searchKeywords = []) {
159
+ // if searchKeywords is not empty, filter the result
160
+ if (searchKeywords.length > 0) {
161
+ const rule = (filePath, metadata) => {
162
+ for (const keyword of searchKeywords) {
163
+ const kw = keyword.toLowerCase();
164
+ // if the file path contains any of the search keywords, return true
165
+ if (filePath.toLowerCase().includes(kw)) {
166
+ return true;
167
+ }
168
+ // if the file metadata values contains any of the search keywords, return true
169
+ if (Object.values(metadata).some((metadataValue) => {
170
+ return JSON.stringify(metadataValue).toLowerCase().includes(kw);
171
+ })) {
172
+ return true;
173
+ }
174
+ }
175
+ return false;
176
+ };
177
+ assets.assets = Object.fromEntries(Object.entries(assets.assets).filter(([filePath, metadata]) => rule(filePath, metadata)));
178
+ }
179
+ }
180
+ async function populateModels(assets, options) {
181
+ if (Object.values(assets.assets).some(asset => asset.type === AssetType.Model)) {
182
+ await populateModelsMetadata(assets);
183
+ if (!options?.peek) {
184
+ await populateBoundingBoxes(assets);
185
+ }
186
+ }
187
+ }
188
+ async function populateModelsMetadata(assets) {
189
+ // read manifest.json, and populate the result with the file details
190
+ const manifest = {};
191
+ const storageProvider = new StorageProvider();
192
+ for (const dir of [ENGINE.PROJECT_PATH_PREFIX, ENGINE.ENGINE_PATH_PREFIX]) {
193
+ const assetsManifest = path.join(dir, 'assets', 'manifest.json');
194
+ Object.assign(manifest, await storageProvider.downloadFileAsJson(ENGINE.AssetPath.fromString(assetsManifest)));
195
+ }
196
+ let metadataDescription = {};
197
+ if (manifest) {
198
+ // populate metadata description if it exists
199
+ metadataDescription = manifest['$metadata_description'] ?? {};
200
+ // populate the result with the file details
201
+ for (const [filePath, targetMetadata] of Object.entries(assets.assets)) {
202
+ const metadata = manifest[filePath];
203
+ if (metadata !== undefined && metadata !== null && metadata.constructor == Object) {
204
+ Object.assign(targetMetadata, metadata);
205
+ }
206
+ }
207
+ }
208
+ assets.metadataDescription[AssetType.Model] = {
209
+ ...assets.metadataDescription[AssetType.Model],
210
+ ...metadataDescription,
211
+ };
212
+ }
213
+ async function populateBoundingBoxes(assets) {
214
+ const modelFiles = Object.keys(assets.assets).filter(filePath => modelTypes.includes(path.extname(filePath).toLowerCase()));
215
+ // split model files into two groups - engine and project
216
+ const engineModelFiles = modelFiles.filter(filePath => filePath.startsWith(ENGINE.ENGINE_PATH_PREFIX));
217
+ const projectModelFiles = modelFiles.filter(filePath => filePath.startsWith(ENGINE.PROJECT_PATH_PREFIX));
218
+ const update = async (root, modelFiles) => {
219
+ const manifestFile = path.join(root, 'assets', 'bounding_box.json');
220
+ const storageProvider = new StorageProvider();
221
+ const gltfPaths = {};
222
+ for (const filePath of modelFiles) {
223
+ gltfPaths[filePath] = storageProvider.getFullPath(filePath);
224
+ }
225
+ const boundingBoxes = await fetchBoundingBoxData(storageProvider.getFullPath(manifestFile), gltfPaths);
226
+ for (const [filePath, boundingBox] of Object.entries(boundingBoxes)) {
227
+ assets.assets[filePath]['boundingBox'] = boundingBox;
228
+ }
229
+ };
230
+ if (engineModelFiles.length > 0) {
231
+ await update(ENGINE.ENGINE_PATH_PREFIX, engineModelFiles);
232
+ }
233
+ if (projectModelFiles.length > 0) {
234
+ await update(ENGINE.PROJECT_PATH_PREFIX, projectModelFiles);
235
+ }
236
+ if (engineModelFiles.length > 0 || projectModelFiles.length > 0) {
237
+ // also populate the metadata description for bounding box
238
+ const boundingBoxSchema = zodToJsonSchema(BoundingBoxSchema, 'BoundingBoxSchema');
239
+ const properties = boundingBoxSchema.definitions.BoundingBoxSchema.properties;
240
+ const boundingBoxDescription = {};
241
+ for (const key in properties) {
242
+ if (properties[key].description) {
243
+ boundingBoxDescription[key] = properties[key].description;
244
+ }
245
+ }
246
+ const metadataDescription = {
247
+ boundingBox: {
248
+ description: 'The bounding box info of model assets',
249
+ properties: boundingBoxDescription,
250
+ }
251
+ };
252
+ assets.metadataDescription[AssetType.Model] = {
253
+ ...assets.metadataDescription[AssetType.Model],
254
+ ...metadataDescription
255
+ };
256
+ }
257
+ }
258
+ async function populateJsClasses(assets, options) {
259
+ const jsClassNames = {};
260
+ for (const [filePath, metadata] of Object.entries(assets.assets)) {
261
+ if (filePath.includes(JS_CLASSES_DIR_NAME)) {
262
+ jsClassNames[filePath] = path.basename(filePath);
263
+ }
264
+ }
265
+ if (Object.keys(jsClassNames).length === 0) {
266
+ return;
267
+ }
268
+ const result = await populateClassesInfo({
269
+ classesToSearch: Object.values(jsClassNames),
270
+ includeConstructorParams: !options?.peek
271
+ });
272
+ if (result.actors
273
+ && Object.keys(result.actors).length > 0
274
+ && result.metadataDescription
275
+ && Object.keys(result.metadataDescription).length > 0) {
276
+ assets.metadataDescription[AssetType.JsClass] = result.metadataDescription;
277
+ }
278
+ for (const [filePath, metadata] of Object.entries(assets.assets)) {
279
+ if (filePath in jsClassNames) {
280
+ const className = jsClassNames[filePath];
281
+ const actorInfo = result.actors[className];
282
+ if (actorInfo) {
283
+ metadata.jsClassName = actorInfo.className;
284
+ if (!options?.peek) {
285
+ metadata.jsClassFilePath = actorInfo.filePath;
286
+ metadata.jsClassConstructorParams = actorInfo.constructorParams ?? [];
287
+ metadata.jsClassCanPopulateFromJson = actorInfo.canPopulateFromJson ?? false;
288
+ }
289
+ metadata.jsClassDescription = actorInfo.description ?? '';
290
+ }
291
+ else {
292
+ console.warn(`Actor class ${className} not found in classes info for file ${filePath}`);
293
+ }
294
+ }
295
+ }
296
+ }
@@ -0,0 +1,234 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import * as ENGINE from 'genesys.js';
4
+ import * as THREE from 'three';
5
+ import { isDev } from '../common.js';
6
+ import { mockBrowserEnvironment } from '../mock.js';
7
+ import { getResolvedPath, StorageProvider } from '../storageProvider.js';
8
+ const fileServerPort = !isDev ? 4000 : 4001;
9
+ mockBrowserEnvironment();
10
+ class ResourceManagerSkippingLoadingGLTF extends ENGINE.ResourceManager {
11
+ async loadModel(path) {
12
+ return null;
13
+ }
14
+ }
15
+ export const fixUpClassName = (className) => {
16
+ const allClasses = ENGINE.ClassRegistry.getRegistry();
17
+ if (allClasses.has(className)) {
18
+ return className;
19
+ }
20
+ const gameClassName = ENGINE.Prefix.GAME + className;
21
+ if (allClasses.has(gameClassName)) {
22
+ return gameClassName;
23
+ }
24
+ const engineClassName = ENGINE.Prefix.ENGINE + className;
25
+ if (allClasses.has(engineClassName)) {
26
+ return engineClassName;
27
+ }
28
+ throw new Error(`Class ${className} not found`);
29
+ };
30
+ export async function loadWorld(scenePath, options = {}) {
31
+ const storageProvider = new StorageProvider();
32
+ const cleanUp = ENGINE.projectContext({ project: 'local-project', storageProvider: storageProvider });
33
+ const sceneFile = scenePath;
34
+ let world = null;
35
+ let originalResourceManager = null;
36
+ if (!options.skipLoadingGLTF) {
37
+ originalResourceManager = ENGINE.resourceManager;
38
+ ENGINE.setResourceManager(new ResourceManagerSkippingLoadingGLTF());
39
+ }
40
+ try {
41
+ world = new ENGINE.World(defaultWorldOptions);
42
+ const worldData = await storageProvider.downloadFileAsJson(ENGINE.AssetPath.fromString(sceneFile));
43
+ // wait for all gltf mesh components to load, should probably move this into engine.
44
+ const actors = world.getActors(ENGINE.Actor);
45
+ const promises = actors.map(actor => actor.waitForComponentsToLoad());
46
+ await Promise.all(promises);
47
+ await ENGINE.WorldSerializer.loadWorld(world, worldData);
48
+ }
49
+ catch (error) {
50
+ cleanUp();
51
+ throw new Error(`Failed to load world from ${sceneFile}: ${error instanceof Error ? error.message : error}`);
52
+ }
53
+ finally {
54
+ if (originalResourceManager) {
55
+ ENGINE.setResourceManager(originalResourceManager);
56
+ }
57
+ }
58
+ return {
59
+ world: world,
60
+ [Symbol.dispose]() {
61
+ if (!options.readonly && world) {
62
+ const worldData = world.asExportedObject();
63
+ fs.writeFileSync(storageProvider.getFullPath(sceneFile), JSON.stringify(worldData, null, 2));
64
+ }
65
+ cleanUp();
66
+ }
67
+ };
68
+ }
69
+ export const defaultWorldOptions = {
70
+ rendererDomElement: document.createElement('div'),
71
+ gameContainer: document.createElement('div'),
72
+ backgroundColor: 0x2E2E2E,
73
+ physicsOptions: {
74
+ engine: ENGINE.PhysicsEngine.Rapier,
75
+ gravity: ENGINE.MathHelpers.makeVector({ up: -9.81 }),
76
+ },
77
+ navigationOptions: {
78
+ engine: ENGINE.NavigationEngine.RecastNavigation,
79
+ },
80
+ useManifold: true
81
+ };
82
+ export function mcpLogger(server) {
83
+ const originalConsoleLog = console.log;
84
+ const originalConsoleWarn = console.warn;
85
+ const originalConsoleError = console.error;
86
+ // it seems sendLoggingMessage isn't picked up by cursor as of 2025 July 17th,
87
+ // The log can't be found in any of the output channels, or in cursor logs
88
+ // for now we log to stderr, which is supported by MCP
89
+ // https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
90
+ const sendLogToClient = false;
91
+ console.log = (...args) => {
92
+ originalConsoleError('[MCP Info]: ', ...args);
93
+ if (sendLogToClient) {
94
+ server.server.sendLoggingMessage({ level: 'info', data: args.join(' ') });
95
+ }
96
+ };
97
+ console.warn = (...args) => {
98
+ originalConsoleError('[MCP Warning]: ', ...args);
99
+ if (sendLogToClient) {
100
+ server.server.sendLoggingMessage({ level: 'warning', data: args.join(' ') });
101
+ }
102
+ };
103
+ console.error = (...args) => {
104
+ originalConsoleError('[MCP Error]: ', ...args);
105
+ if (sendLogToClient) {
106
+ server.server.sendLoggingMessage({ level: 'error', data: args.join(' ') });
107
+ }
108
+ };
109
+ return {
110
+ [Symbol.dispose]() {
111
+ console.log = originalConsoleLog;
112
+ console.warn = originalConsoleWarn;
113
+ console.error = originalConsoleError;
114
+ }
115
+ };
116
+ }
117
+ export function isSubclass(child, parent) {
118
+ if (typeof child !== 'function' || typeof parent !== 'function')
119
+ return false;
120
+ let proto = child;
121
+ while (proto) {
122
+ if (proto === parent)
123
+ return true;
124
+ proto = Object.getPrototypeOf(proto);
125
+ }
126
+ return false;
127
+ }
128
+ async function buildProject() {
129
+ const url = `http://localhost:${fileServerPort}/api/build-project`;
130
+ const options = {
131
+ method: 'POST'
132
+ };
133
+ const response = await fetch(url, options);
134
+ if (!response.ok) {
135
+ throw new Error(`Failed to rebuild game.js: ${response.statusText}`);
136
+ }
137
+ const result = await response.json();
138
+ return result;
139
+ }
140
+ export async function registerGameClasses() {
141
+ const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
142
+ const storageProvider = new StorageProvider();
143
+ storageProvider.resolvePath(bundlePath);
144
+ try {
145
+ const buildProjectResult = await buildProject();
146
+ const bundleFullPath = getResolvedPath(bundlePath);
147
+ if (!fs.existsSync(bundleFullPath)) {
148
+ throw new Error(`bundle file not found at ${bundleFullPath}, please make sure build project is successful, buildProject result: ${JSON.stringify(buildProjectResult)}`);
149
+ }
150
+ const bundleText = fs.readFileSync(bundleFullPath, 'utf8');
151
+ const injectedDependencies = (mod) => {
152
+ if (mod === 'genesys.js')
153
+ return ENGINE;
154
+ if (mod === 'three')
155
+ return THREE;
156
+ throw new Error(`Unknown module: ${mod}`);
157
+ };
158
+ ENGINE.ClassRegistry.clearGameClasses();
159
+ // Create a module-like object to simulate CommonJS environment
160
+ const moduleObj = { exports: {} };
161
+ const run = new Function('require', 'module', bundleText);
162
+ run(injectedDependencies, moduleObj);
163
+ // Apply any exports to the global scope if needed
164
+ if (moduleObj.exports && typeof moduleObj.exports === 'object') {
165
+ Object.assign(window, moduleObj.exports);
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.error('Error registering game classes', error);
170
+ }
171
+ }
172
+ export function isClassRegistered(className) {
173
+ return ENGINE.ClassRegistry.getRegistry().has(className);
174
+ }
175
+ export async function registerGameClassesIfAnyNotRegistered(classNamesToCheck) {
176
+ const validNames = classNamesToCheck.filter((className) => className.startsWith(ENGINE.Prefix.GAME) || className.startsWith(ENGINE.Prefix.ENGINE));
177
+ if (validNames.some(className => !isClassRegistered(className))) {
178
+ await registerGameClasses();
179
+ }
180
+ }
181
+ export async function conditionallyRegisterGameClasses() {
182
+ const storageProvider = new StorageProvider();
183
+ const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
184
+ storageProvider.resolvePath(bundlePath);
185
+ const srcDir = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, 'src'));
186
+ storageProvider.resolvePath(srcDir);
187
+ // Check if .dist/game.js exists
188
+ if (!fs.existsSync(bundlePath.getResolvedPath())) {
189
+ // If game.js doesn't exist, we need to register classes
190
+ await registerGameClasses();
191
+ return;
192
+ }
193
+ const bundleFileStats = fs.statSync(getResolvedPath(bundlePath));
194
+ const bundleFileTimestamp = bundleFileStats.mtime.getTime();
195
+ console.log('bundleFileTimestamp:', new Date(bundleFileTimestamp));
196
+ // Check if src directory exists
197
+ if (!fs.existsSync(getResolvedPath(srcDir))) {
198
+ console.log('No src directory, no need to register classes');
199
+ return; // No src directory, nothing to check
200
+ }
201
+ // Recursively find all js, jsx, ts, tsx files in src directory
202
+ const sourceFiles = findSourceFiles(getResolvedPath(srcDir));
203
+ // Check if any source file is newer than game.js
204
+ for (const sourceFile of sourceFiles) {
205
+ const sourceStats = fs.statSync(sourceFile);
206
+ const sourceTime = sourceStats.mtime.getTime();
207
+ if (sourceTime > bundleFileTimestamp) {
208
+ // Found a newer source file, register classes and return
209
+ console.log(`Found a newer source file, ${sourceFile}, timestamp ${new Date(sourceTime)}, registering classes`);
210
+ await registerGameClasses();
211
+ return;
212
+ }
213
+ }
214
+ console.log('No newer source files found, no need to register classes');
215
+ }
216
+ function findSourceFiles(dir) {
217
+ const sourceFiles = [];
218
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
219
+ for (const entry of entries) {
220
+ const fullPath = path.join(dir, entry.name);
221
+ if (entry.isDirectory()) {
222
+ // Recursively search subdirectories
223
+ sourceFiles.push(...findSourceFiles(fullPath));
224
+ }
225
+ else if (entry.isFile()) {
226
+ // Check if file has one of the target extensions
227
+ const ext = path.extname(entry.name).toLowerCase();
228
+ if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
229
+ sourceFiles.push(fullPath);
230
+ }
231
+ }
232
+ }
233
+ return sourceFiles;
234
+ }
@@ -0,0 +1,32 @@
1
+ import path from 'path';
2
+ import * as ENGINE from 'genesys.js';
3
+ import { getProjectRoot } from './common.js';
4
+ import { isSubclass } from './mcp/utils.js';
5
+ import { fixUpClassName, registerGameClasses } from './mcp/utils.js';
6
+ import { StorageProvider } from './storageProvider.js';
7
+ export async function generateCode(className, filePath, baseClassName) {
8
+ try {
9
+ baseClassName = fixUpClassName(baseClassName);
10
+ }
11
+ catch (error) {
12
+ // if the base class name is not found, register all game classes and try again
13
+ await registerGameClasses();
14
+ baseClassName = fixUpClassName(baseClassName);
15
+ }
16
+ using context = ENGINE.scopedProjectContext({ project: 'mcp-project', storageProvider: new StorageProvider() });
17
+ const testIsSubclass = (childName, parent) => {
18
+ const child = ENGINE.ClassRegistry.getRegistry().get(childName);
19
+ if (!child) {
20
+ return false;
21
+ }
22
+ return isSubclass(child, parent);
23
+ };
24
+ const fullFilePath = path.isAbsolute(filePath) ? filePath : path.join(getProjectRoot(), filePath);
25
+ let fileGenerated = false;
26
+ const isSubclassOfActor = testIsSubclass(baseClassName, ENGINE.Actor);
27
+ if (isSubclassOfActor) {
28
+ await ENGINE.WorldCommands.generateActorTemplateFile(className, fullFilePath, baseClassName);
29
+ fileGenerated = true;
30
+ }
31
+ return fileGenerated;
32
+ }
@@ -0,0 +1,5 @@
1
+ import { mockBrowserEnvironment as engineMock } from 'genesys.js';
2
+ import { JSDOM } from 'jsdom';
3
+ export function mockBrowserEnvironment() {
4
+ engineMock(JSDOM);
5
+ }
@@ -0,0 +1,112 @@
1
+ import * as ENGINE from 'genesys.js';
2
+ import * as THREE from 'three';
3
+ import { ThreeEulerSchema, ThreeVector3Schema } from './mcp/search-actors.js';
4
+ import { loadWorld, registerGameClassesIfAnyNotRegistered } from './mcp/utils.js';
5
+ import { mockBrowserEnvironment } from './mock.js';
6
+ import '../src/game.js';
7
+ mockBrowserEnvironment();
8
+ function convertConstructorParams(value) {
9
+ // If not an object, return as is
10
+ if (!value || typeof value !== 'object') {
11
+ return value;
12
+ }
13
+ // Check if it's a Vector3
14
+ const vector3Result = ThreeVector3Schema.safeParse(value);
15
+ if (vector3Result.success) {
16
+ return new THREE.Vector3(vector3Result.data.x, vector3Result.data.y, vector3Result.data.z);
17
+ }
18
+ // Check if it's an Euler
19
+ const eulerResult = ThreeEulerSchema.safeParse(value);
20
+ if (eulerResult.success) {
21
+ return new THREE.Euler(eulerResult.data.x, eulerResult.data.y, eulerResult.data.z);
22
+ }
23
+ // Handle arrays
24
+ if (Array.isArray(value)) {
25
+ return value.map(convertConstructorParams);
26
+ }
27
+ // Handle objects
28
+ const result = {};
29
+ for (const [key, val] of Object.entries(value)) {
30
+ result[key] = convertConstructorParams(val);
31
+ }
32
+ return result;
33
+ }
34
+ export async function placePrimitive(args) {
35
+ using worldResource = await loadWorld(args.sceneName, { skipLoadingGLTF: true });
36
+ const actors = ENGINE.WorldCommands.placePrimitives({
37
+ world: worldResource.world,
38
+ primitiveActors: args.primitiveActors,
39
+ });
40
+ return actors.map(actor => actor.uuid);
41
+ }
42
+ export async function removeActors(args) {
43
+ using worldResource = await loadWorld(args.sceneName, { skipLoadingGLTF: true });
44
+ ENGINE.WorldCommands.removeActorsByUuids({
45
+ world: worldResource.world,
46
+ actorIds: args.actorIds,
47
+ });
48
+ }
49
+ export async function addGltf(args) {
50
+ using worldResource = await loadWorld(args.sceneName, { skipLoadingGLTF: true });
51
+ const actors = await ENGINE.WorldCommands.placeGltfs({
52
+ world: worldResource.world,
53
+ gltfs: args.gltfs
54
+ });
55
+ return actors.map(actor => actor.uuid);
56
+ }
57
+ export async function placePrefab(args) {
58
+ using worldResource = await loadWorld(args.sceneName, { skipLoadingGLTF: true });
59
+ const actors = await ENGINE.WorldCommands.placePrefabs({
60
+ world: worldResource.world,
61
+ prefabs: args.prefabs
62
+ });
63
+ return actors.map(actor => actor.uuid);
64
+ }
65
+ export async function placeJsClassActor(args) {
66
+ await registerGameClassesIfAnyNotRegistered(args.jsClasses.map(jsClass => jsClass.className));
67
+ using worldResource = await loadWorld(args.sceneName, { skipLoadingGLTF: true });
68
+ const actors = [];
69
+ for (const { className, constructorParams, actorInfo } of args.jsClasses) {
70
+ try {
71
+ // Convert constructor parameters if they exist
72
+ const convertedParams = constructorParams ? convertConstructorParams(constructorParams) : [];
73
+ const actor = ENGINE.ClassRegistry.constructObject(className, false, ...convertedParams);
74
+ if (actorInfo) {
75
+ Object.assign(actor.editorData, actorInfo);
76
+ }
77
+ actors.push(actor);
78
+ }
79
+ catch (e) {
80
+ console.error(`Error constructing object ${className}`, e);
81
+ }
82
+ }
83
+ worldResource.world.addActors(...actors);
84
+ return actors.map(actor => actor.uuid);
85
+ }
86
+ export async function updateActors(args) {
87
+ const readonly = args.actorsToUpdate.length == 0;
88
+ using worldResource = await loadWorld(args.sceneName, { readonly, skipLoadingGLTF: true });
89
+ let count = 0;
90
+ for (const { uuid, transform, actorInfo } of args.actorsToUpdate) {
91
+ const actor = worldResource.world.getActorByUuid(uuid);
92
+ if (!actor) {
93
+ continue;
94
+ }
95
+ if (transform) {
96
+ if (transform.position) {
97
+ actor.setWorldPosition(transform.position);
98
+ }
99
+ if (transform.rotation) {
100
+ actor.setWorldRotation(transform.rotation);
101
+ }
102
+ if (transform.scale) {
103
+ actor.setWorldScale(transform.scale);
104
+ }
105
+ if (actorInfo) {
106
+ Object.assign(actor.editorData, actorInfo);
107
+ }
108
+ }
109
+ count += 1;
110
+ }
111
+ return count;
112
+ }