@directivegames/genesys.sdk 3.2.2 → 3.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -60
- package/dist/src/core/cli.js +22 -22
- package/dist/src/templates/scripts/genesys/genesys-mcp.js +25 -25
- package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +4 -4
- package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +30 -30
- package/package.json +176 -176
- package/scripts/post-install.ts +143 -143
- package/src/asset-pack/.gitattributes +88 -88
- package/src/asset-pack/eslint.config.js +45 -45
- package/src/asset-pack/gitignore +11 -11
- package/src/asset-pack/scripts/postinstall.ts +81 -81
- package/src/asset-pack/tsconfig.json +33 -33
- package/src/templates/.cursor/mcp.json +20 -20
- package/src/templates/.cursorignore +2 -2
- package/src/templates/.gitattributes +88 -88
- package/src/templates/.vscode/settings.json +6 -6
- package/src/templates/AGENTS.md +86 -86
- package/src/templates/README.md +24 -24
- package/src/templates/eslint.config.js +45 -45
- package/src/templates/gitignore +11 -11
- package/src/templates/index.html +34 -34
- package/src/templates/pnpm-lock.yaml +3676 -3676
- package/src/templates/scripts/genesys/build-project.ts +51 -51
- package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -272
- package/src/templates/scripts/genesys/common.ts +46 -46
- package/src/templates/scripts/genesys/const.ts +9 -9
- package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -11
- package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -146
- package/src/templates/scripts/genesys/dev/launcher.ts +46 -46
- package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -229
- package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -84
- package/src/templates/scripts/genesys/doc-server.ts +16 -16
- package/src/templates/scripts/genesys/genesys-mcp.ts +526 -526
- package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -86
- package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -151
- package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -73
- package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -35
- package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -30
- package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -858
- package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -380
- package/src/templates/scripts/genesys/mcp/utils.ts +281 -281
- package/src/templates/scripts/genesys/misc.ts +42 -42
- package/src/templates/scripts/genesys/mock.ts +6 -6
- package/src/templates/scripts/genesys/place-actors.ts +179 -179
- package/src/templates/scripts/genesys/post-install.ts +30 -30
- package/src/templates/scripts/genesys/prefab.schema.json +84 -84
- package/src/templates/scripts/genesys/remove-engine-comments.ts +134 -134
- package/src/templates/scripts/genesys/run-mcp-inspector.bat +4 -4
- package/src/templates/scripts/genesys/storageProvider.ts +182 -182
- package/src/templates/scripts/genesys/validate-prefabs.ts +138 -138
- package/src/templates/src/index.ts +22 -22
- package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/firstPerson/src/game.ts +39 -39
- package/src/templates/src/templates/firstPerson/src/player.ts +63 -63
- package/src/templates/src/templates/fps/assets/default.genesys-scene +9459 -9459
- package/src/templates/src/templates/fps/src/game.ts +39 -39
- package/src/templates/src/templates/fps/src/player.ts +69 -69
- package/src/templates/src/templates/fps/src/weapon.ts +54 -54
- package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/freeCamera/src/game.ts +39 -39
- package/src/templates/src/templates/freeCamera/src/player.ts +45 -45
- package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +121 -121
- package/src/templates/src/templates/sideScroller/src/const.ts +45 -45
- package/src/templates/src/templates/sideScroller/src/game.ts +122 -122
- package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -361
- package/src/templates/src/templates/sideScroller/src/player.ts +125 -125
- package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +165 -165
- package/src/templates/src/templates/thirdPerson/src/game.ts +39 -39
- package/src/templates/src/templates/thirdPerson/src/player.ts +61 -61
- package/src/templates/src/templates/vehicle/assets/default.genesys-scene +225 -225
- package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -145
- package/src/templates/src/templates/vehicle/src/game.ts +43 -43
- package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -191
- package/src/templates/src/templates/vehicle/src/player.ts +109 -109
- package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -266
- package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -101
- package/src/templates/src/templates/vr-game/assets/default.genesys-scene +246 -246
- package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -1
- package/src/templates/src/templates/vr-game/src/game.ts +66 -66
- package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -26
- package/src/templates/tsconfig.json +34 -34
- package/src/templates/vite.config.ts +52 -52
|
@@ -1,281 +1,281 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import * as ENGINE from 'genesys.js';
|
|
5
|
-
import * as THREE from 'three';
|
|
6
|
-
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
7
|
-
|
|
8
|
-
import { isDev } from '../common.js';
|
|
9
|
-
import { mockBrowserEnvironment } from '../mock.js';
|
|
10
|
-
import { getResolvedPath, StorageProvider } from '../storageProvider.js';
|
|
11
|
-
|
|
12
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
-
|
|
14
|
-
const fileServerPort = !isDev ? 4000 : 4001;
|
|
15
|
-
|
|
16
|
-
mockBrowserEnvironment();
|
|
17
|
-
|
|
18
|
-
class ResourceManagerSkippingLoadingGLTF extends ENGINE.ResourceManager {
|
|
19
|
-
public override async loadModel(path: ENGINE.AssetPath): Promise<GLTF | null> {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export const fixUpClassName = (className: string) => {
|
|
25
|
-
const allClasses = ENGINE.ClassRegistry.getRegistry();
|
|
26
|
-
|
|
27
|
-
if (allClasses.has(className)) {
|
|
28
|
-
return className;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const gameClassName = ENGINE.Prefix.GAME + className;
|
|
32
|
-
if (allClasses.has(gameClassName)) {
|
|
33
|
-
return gameClassName;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const engineClassName = ENGINE.Prefix.ENGINE + className;
|
|
37
|
-
if (allClasses.has(engineClassName)) {
|
|
38
|
-
return engineClassName;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
throw new Error(`Class ${className} not found`);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export interface LoadWorldOptions {
|
|
45
|
-
readonly?: boolean;
|
|
46
|
-
skipLoadingGLTF?: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function loadWorld(scenePath: string, options: LoadWorldOptions = {}) {
|
|
50
|
-
const storageProvider = new StorageProvider();
|
|
51
|
-
const cleanUp = ENGINE.projectContext({ project: 'local-project', storageProvider: storageProvider });
|
|
52
|
-
|
|
53
|
-
const sceneFile = scenePath;
|
|
54
|
-
|
|
55
|
-
let world: ENGINE.World | null = null;
|
|
56
|
-
|
|
57
|
-
let originalResourceManager = null;
|
|
58
|
-
if (!options.skipLoadingGLTF) {
|
|
59
|
-
originalResourceManager = ENGINE.resourceManager;
|
|
60
|
-
ENGINE.setResourceManager(new ResourceManagerSkippingLoadingGLTF());
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
world = new ENGINE.World(defaultWorldOptions);
|
|
65
|
-
const worldData = await storageProvider.downloadFileAsJson<any>(ENGINE.AssetPath.fromString(sceneFile));
|
|
66
|
-
|
|
67
|
-
// wait for all gltf mesh components to load, should probably move this into engine.
|
|
68
|
-
const actors = world.getActors(ENGINE.Actor);
|
|
69
|
-
const promises = actors.map(actor => actor.waitForComponentsToLoad());
|
|
70
|
-
await Promise.all(promises);
|
|
71
|
-
|
|
72
|
-
await ENGINE.WorldSerializer.loadWorld(world, worldData);
|
|
73
|
-
} catch (error) {
|
|
74
|
-
cleanUp();
|
|
75
|
-
throw new Error(`Failed to load world from ${sceneFile}: ${error instanceof Error ? error.message : error}`);
|
|
76
|
-
} finally {
|
|
77
|
-
if (originalResourceManager) {
|
|
78
|
-
ENGINE.setResourceManager(originalResourceManager);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
world: world,
|
|
84
|
-
[Symbol.dispose]() {
|
|
85
|
-
if (!options.readonly && world) {
|
|
86
|
-
const worldData = world.asExportedObject();
|
|
87
|
-
fs.writeFileSync(storageProvider.getFullPath(sceneFile), JSON.stringify(worldData, null, 2));
|
|
88
|
-
}
|
|
89
|
-
cleanUp();
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export const defaultWorldOptions = {
|
|
95
|
-
rendererDomElement: document.createElement('div'),
|
|
96
|
-
gameContainer: document.createElement('div'),
|
|
97
|
-
backgroundColor: 0x2E2E2E,
|
|
98
|
-
physicsOptions: {
|
|
99
|
-
engine: ENGINE.PhysicsEngine.Rapier,
|
|
100
|
-
gravity: ENGINE.MathHelpers.makeVector({ up: -9.81 }),
|
|
101
|
-
},
|
|
102
|
-
navigationOptions: {
|
|
103
|
-
engine: ENGINE.NavigationEngine.RecastNavigation,
|
|
104
|
-
},
|
|
105
|
-
useManifold: true
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
export function mcpLogger(server: McpServer): Disposable {
|
|
109
|
-
const originalConsoleLog = console.log;
|
|
110
|
-
const originalConsoleWarn = console.warn;
|
|
111
|
-
const originalConsoleError = console.error;
|
|
112
|
-
|
|
113
|
-
// it seems sendLoggingMessage isn't picked up by cursor as of 2025 July 17th,
|
|
114
|
-
// The log can't be found in any of the output channels, or in cursor logs
|
|
115
|
-
// for now we log to stderr, which is supported by MCP
|
|
116
|
-
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
117
|
-
const sendLogToClient = false;
|
|
118
|
-
|
|
119
|
-
console.log = (...args: any[]) => {
|
|
120
|
-
originalConsoleError('[MCP Info]: ', ...args);
|
|
121
|
-
if (sendLogToClient) {
|
|
122
|
-
server.server.sendLoggingMessage({ level: 'info', data: args.join(' ') });
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
console.warn = (...args: any[]) => {
|
|
126
|
-
originalConsoleError('[MCP Warning]: ', ...args);
|
|
127
|
-
if (sendLogToClient) {
|
|
128
|
-
server.server.sendLoggingMessage({ level: 'warning', data: args.join(' ') });
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
console.error = (...args: any[]) => {
|
|
132
|
-
originalConsoleError('[MCP Error]: ', ...args);
|
|
133
|
-
if (sendLogToClient) {
|
|
134
|
-
server.server.sendLoggingMessage({ level: 'error', data: args.join(' ') });
|
|
135
|
-
}
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
[Symbol.dispose]() {
|
|
140
|
-
console.log = originalConsoleLog;
|
|
141
|
-
console.warn = originalConsoleWarn;
|
|
142
|
-
console.error = originalConsoleError;
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function isSubclass(child: Function | null | undefined, parent: Function): boolean {
|
|
148
|
-
if (typeof child !== 'function' || typeof parent !== 'function') return false;
|
|
149
|
-
let proto: any = child;
|
|
150
|
-
while (proto) {
|
|
151
|
-
if (proto === parent) return true;
|
|
152
|
-
proto = Object.getPrototypeOf(proto);
|
|
153
|
-
}
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
async function buildProject() {
|
|
158
|
-
const url = `http://localhost:${fileServerPort}/api/build-project`;
|
|
159
|
-
const options = {
|
|
160
|
-
method: 'POST'
|
|
161
|
-
};
|
|
162
|
-
const response = await fetch(url, options);
|
|
163
|
-
if (!response.ok) {
|
|
164
|
-
throw new Error(`Failed to rebuild game.js: ${response.statusText}`);
|
|
165
|
-
}
|
|
166
|
-
const result = await response.json();
|
|
167
|
-
return result;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export async function registerGameClasses(): Promise<void> {
|
|
171
|
-
const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
|
|
172
|
-
const storageProvider = new StorageProvider();
|
|
173
|
-
storageProvider.resolvePath(bundlePath);
|
|
174
|
-
try {
|
|
175
|
-
const buildProjectResult = await buildProject();
|
|
176
|
-
|
|
177
|
-
const bundleFullPath = getResolvedPath(bundlePath);
|
|
178
|
-
if (!fs.existsSync(bundleFullPath)) {
|
|
179
|
-
throw new Error(`bundle file not found at ${bundleFullPath}, please make sure build project is successful, buildProject result: ${JSON.stringify(buildProjectResult)}`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const bundleText = fs.readFileSync(bundleFullPath, 'utf8');
|
|
183
|
-
const injectedDependencies = (mod: string) => {
|
|
184
|
-
if (mod === 'genesys.js') return ENGINE;
|
|
185
|
-
if (mod === 'three') return THREE;
|
|
186
|
-
throw new Error(`Unknown module: ${mod}`);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
ENGINE.ClassRegistry.clearGameClasses();
|
|
190
|
-
// Create a module-like object to simulate CommonJS environment
|
|
191
|
-
const moduleObj = { exports: {} };
|
|
192
|
-
const run = new Function('require', 'module', bundleText);
|
|
193
|
-
run(injectedDependencies, moduleObj);
|
|
194
|
-
|
|
195
|
-
// Apply any exports to the global scope if needed
|
|
196
|
-
if (moduleObj.exports && typeof moduleObj.exports === 'object') {
|
|
197
|
-
Object.assign(window, moduleObj.exports);
|
|
198
|
-
}
|
|
199
|
-
} catch (error) {
|
|
200
|
-
console.error('Error registering game classes', error);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function isClassRegistered(className: string): boolean {
|
|
205
|
-
return ENGINE.ClassRegistry.getRegistry().has(className);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export async function registerGameClassesIfAnyNotRegistered(classNamesToCheck: string[]): Promise<void> {
|
|
209
|
-
const validNames = classNamesToCheck.filter((className) => className.startsWith(ENGINE.Prefix.GAME) || className.startsWith(ENGINE.Prefix.ENGINE));
|
|
210
|
-
if (validNames.some(className => !isClassRegistered(className))) {
|
|
211
|
-
await registerGameClasses();
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export async function conditionallyRegisterGameClasses(): Promise<void> {
|
|
216
|
-
const storageProvider = new StorageProvider();
|
|
217
|
-
|
|
218
|
-
const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
|
|
219
|
-
storageProvider.resolvePath(bundlePath);
|
|
220
|
-
|
|
221
|
-
const srcDir = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, 'src'));
|
|
222
|
-
storageProvider.resolvePath(srcDir);
|
|
223
|
-
|
|
224
|
-
// Check if .dist/game.js exists
|
|
225
|
-
if (!fs.existsSync(bundlePath.getResolvedPath())) {
|
|
226
|
-
// If game.js doesn't exist, we need to register classes
|
|
227
|
-
await registerGameClasses();
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const bundleFileStats = fs.statSync(getResolvedPath(bundlePath));
|
|
232
|
-
const bundleFileTimestamp = bundleFileStats.mtime.getTime();
|
|
233
|
-
console.log('bundleFileTimestamp:', new Date(bundleFileTimestamp));
|
|
234
|
-
|
|
235
|
-
// Check if src directory exists
|
|
236
|
-
if (!fs.existsSync(getResolvedPath(srcDir))) {
|
|
237
|
-
console.log('No src directory, no need to register classes');
|
|
238
|
-
return; // No src directory, nothing to check
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Recursively find all js, jsx, ts, tsx files in src directory
|
|
242
|
-
const sourceFiles = findSourceFiles(getResolvedPath(srcDir));
|
|
243
|
-
|
|
244
|
-
// Check if any source file is newer than game.js
|
|
245
|
-
for (const sourceFile of sourceFiles) {
|
|
246
|
-
const sourceStats = fs.statSync(sourceFile);
|
|
247
|
-
const sourceTime = sourceStats.mtime.getTime();
|
|
248
|
-
|
|
249
|
-
if (sourceTime > bundleFileTimestamp) {
|
|
250
|
-
// Found a newer source file, register classes and return
|
|
251
|
-
console.log(`Found a newer source file, ${sourceFile}, timestamp ${new Date(sourceTime)}, registering classes`);
|
|
252
|
-
await registerGameClasses();
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
console.log('No newer source files found, no need to register classes');
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function findSourceFiles(dir: string): string[] {
|
|
261
|
-
const sourceFiles: string[] = [];
|
|
262
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
263
|
-
|
|
264
|
-
for (const entry of entries) {
|
|
265
|
-
const fullPath = path.join(dir, entry.name);
|
|
266
|
-
|
|
267
|
-
if (entry.isDirectory()) {
|
|
268
|
-
// Recursively search subdirectories
|
|
269
|
-
sourceFiles.push(...findSourceFiles(fullPath));
|
|
270
|
-
} else if (entry.isFile()) {
|
|
271
|
-
// Check if file has one of the target extensions
|
|
272
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
273
|
-
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
274
|
-
sourceFiles.push(fullPath);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return sourceFiles;
|
|
280
|
-
}
|
|
281
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import * as ENGINE from 'genesys.js';
|
|
5
|
+
import * as THREE from 'three';
|
|
6
|
+
import { type GLTF } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
|
7
|
+
|
|
8
|
+
import { isDev } from '../common.js';
|
|
9
|
+
import { mockBrowserEnvironment } from '../mock.js';
|
|
10
|
+
import { getResolvedPath, StorageProvider } from '../storageProvider.js';
|
|
11
|
+
|
|
12
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
+
|
|
14
|
+
const fileServerPort = !isDev ? 4000 : 4001;
|
|
15
|
+
|
|
16
|
+
mockBrowserEnvironment();
|
|
17
|
+
|
|
18
|
+
class ResourceManagerSkippingLoadingGLTF extends ENGINE.ResourceManager {
|
|
19
|
+
public override async loadModel(path: ENGINE.AssetPath): Promise<GLTF | null> {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const fixUpClassName = (className: string) => {
|
|
25
|
+
const allClasses = ENGINE.ClassRegistry.getRegistry();
|
|
26
|
+
|
|
27
|
+
if (allClasses.has(className)) {
|
|
28
|
+
return className;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const gameClassName = ENGINE.Prefix.GAME + className;
|
|
32
|
+
if (allClasses.has(gameClassName)) {
|
|
33
|
+
return gameClassName;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const engineClassName = ENGINE.Prefix.ENGINE + className;
|
|
37
|
+
if (allClasses.has(engineClassName)) {
|
|
38
|
+
return engineClassName;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
throw new Error(`Class ${className} not found`);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface LoadWorldOptions {
|
|
45
|
+
readonly?: boolean;
|
|
46
|
+
skipLoadingGLTF?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function loadWorld(scenePath: string, options: LoadWorldOptions = {}) {
|
|
50
|
+
const storageProvider = new StorageProvider();
|
|
51
|
+
const cleanUp = ENGINE.projectContext({ project: 'local-project', storageProvider: storageProvider });
|
|
52
|
+
|
|
53
|
+
const sceneFile = scenePath;
|
|
54
|
+
|
|
55
|
+
let world: ENGINE.World | null = null;
|
|
56
|
+
|
|
57
|
+
let originalResourceManager = null;
|
|
58
|
+
if (!options.skipLoadingGLTF) {
|
|
59
|
+
originalResourceManager = ENGINE.resourceManager;
|
|
60
|
+
ENGINE.setResourceManager(new ResourceManagerSkippingLoadingGLTF());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
world = new ENGINE.World(defaultWorldOptions);
|
|
65
|
+
const worldData = await storageProvider.downloadFileAsJson<any>(ENGINE.AssetPath.fromString(sceneFile));
|
|
66
|
+
|
|
67
|
+
// wait for all gltf mesh components to load, should probably move this into engine.
|
|
68
|
+
const actors = world.getActors(ENGINE.Actor);
|
|
69
|
+
const promises = actors.map(actor => actor.waitForComponentsToLoad());
|
|
70
|
+
await Promise.all(promises);
|
|
71
|
+
|
|
72
|
+
await ENGINE.WorldSerializer.loadWorld(world, worldData);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
cleanUp();
|
|
75
|
+
throw new Error(`Failed to load world from ${sceneFile}: ${error instanceof Error ? error.message : error}`);
|
|
76
|
+
} finally {
|
|
77
|
+
if (originalResourceManager) {
|
|
78
|
+
ENGINE.setResourceManager(originalResourceManager);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
world: world,
|
|
84
|
+
[Symbol.dispose]() {
|
|
85
|
+
if (!options.readonly && world) {
|
|
86
|
+
const worldData = world.asExportedObject();
|
|
87
|
+
fs.writeFileSync(storageProvider.getFullPath(sceneFile), JSON.stringify(worldData, null, 2));
|
|
88
|
+
}
|
|
89
|
+
cleanUp();
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const defaultWorldOptions = {
|
|
95
|
+
rendererDomElement: document.createElement('div'),
|
|
96
|
+
gameContainer: document.createElement('div'),
|
|
97
|
+
backgroundColor: 0x2E2E2E,
|
|
98
|
+
physicsOptions: {
|
|
99
|
+
engine: ENGINE.PhysicsEngine.Rapier,
|
|
100
|
+
gravity: ENGINE.MathHelpers.makeVector({ up: -9.81 }),
|
|
101
|
+
},
|
|
102
|
+
navigationOptions: {
|
|
103
|
+
engine: ENGINE.NavigationEngine.RecastNavigation,
|
|
104
|
+
},
|
|
105
|
+
useManifold: true
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function mcpLogger(server: McpServer): Disposable {
|
|
109
|
+
const originalConsoleLog = console.log;
|
|
110
|
+
const originalConsoleWarn = console.warn;
|
|
111
|
+
const originalConsoleError = console.error;
|
|
112
|
+
|
|
113
|
+
// it seems sendLoggingMessage isn't picked up by cursor as of 2025 July 17th,
|
|
114
|
+
// The log can't be found in any of the output channels, or in cursor logs
|
|
115
|
+
// for now we log to stderr, which is supported by MCP
|
|
116
|
+
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
117
|
+
const sendLogToClient = false;
|
|
118
|
+
|
|
119
|
+
console.log = (...args: any[]) => {
|
|
120
|
+
originalConsoleError('[MCP Info]: ', ...args);
|
|
121
|
+
if (sendLogToClient) {
|
|
122
|
+
server.server.sendLoggingMessage({ level: 'info', data: args.join(' ') });
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
console.warn = (...args: any[]) => {
|
|
126
|
+
originalConsoleError('[MCP Warning]: ', ...args);
|
|
127
|
+
if (sendLogToClient) {
|
|
128
|
+
server.server.sendLoggingMessage({ level: 'warning', data: args.join(' ') });
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
console.error = (...args: any[]) => {
|
|
132
|
+
originalConsoleError('[MCP Error]: ', ...args);
|
|
133
|
+
if (sendLogToClient) {
|
|
134
|
+
server.server.sendLoggingMessage({ level: 'error', data: args.join(' ') });
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
[Symbol.dispose]() {
|
|
140
|
+
console.log = originalConsoleLog;
|
|
141
|
+
console.warn = originalConsoleWarn;
|
|
142
|
+
console.error = originalConsoleError;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isSubclass(child: Function | null | undefined, parent: Function): boolean {
|
|
148
|
+
if (typeof child !== 'function' || typeof parent !== 'function') return false;
|
|
149
|
+
let proto: any = child;
|
|
150
|
+
while (proto) {
|
|
151
|
+
if (proto === parent) return true;
|
|
152
|
+
proto = Object.getPrototypeOf(proto);
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function buildProject() {
|
|
158
|
+
const url = `http://localhost:${fileServerPort}/api/build-project`;
|
|
159
|
+
const options = {
|
|
160
|
+
method: 'POST'
|
|
161
|
+
};
|
|
162
|
+
const response = await fetch(url, options);
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Failed to rebuild game.js: ${response.statusText}`);
|
|
165
|
+
}
|
|
166
|
+
const result = await response.json();
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function registerGameClasses(): Promise<void> {
|
|
171
|
+
const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
|
|
172
|
+
const storageProvider = new StorageProvider();
|
|
173
|
+
storageProvider.resolvePath(bundlePath);
|
|
174
|
+
try {
|
|
175
|
+
const buildProjectResult = await buildProject();
|
|
176
|
+
|
|
177
|
+
const bundleFullPath = getResolvedPath(bundlePath);
|
|
178
|
+
if (!fs.existsSync(bundleFullPath)) {
|
|
179
|
+
throw new Error(`bundle file not found at ${bundleFullPath}, please make sure build project is successful, buildProject result: ${JSON.stringify(buildProjectResult)}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const bundleText = fs.readFileSync(bundleFullPath, 'utf8');
|
|
183
|
+
const injectedDependencies = (mod: string) => {
|
|
184
|
+
if (mod === 'genesys.js') return ENGINE;
|
|
185
|
+
if (mod === 'three') return THREE;
|
|
186
|
+
throw new Error(`Unknown module: ${mod}`);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
ENGINE.ClassRegistry.clearGameClasses();
|
|
190
|
+
// Create a module-like object to simulate CommonJS environment
|
|
191
|
+
const moduleObj = { exports: {} };
|
|
192
|
+
const run = new Function('require', 'module', bundleText);
|
|
193
|
+
run(injectedDependencies, moduleObj);
|
|
194
|
+
|
|
195
|
+
// Apply any exports to the global scope if needed
|
|
196
|
+
if (moduleObj.exports && typeof moduleObj.exports === 'object') {
|
|
197
|
+
Object.assign(window, moduleObj.exports);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Error registering game classes', error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function isClassRegistered(className: string): boolean {
|
|
205
|
+
return ENGINE.ClassRegistry.getRegistry().has(className);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function registerGameClassesIfAnyNotRegistered(classNamesToCheck: string[]): Promise<void> {
|
|
209
|
+
const validNames = classNamesToCheck.filter((className) => className.startsWith(ENGINE.Prefix.GAME) || className.startsWith(ENGINE.Prefix.ENGINE));
|
|
210
|
+
if (validNames.some(className => !isClassRegistered(className))) {
|
|
211
|
+
await registerGameClasses();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export async function conditionallyRegisterGameClasses(): Promise<void> {
|
|
216
|
+
const storageProvider = new StorageProvider();
|
|
217
|
+
|
|
218
|
+
const bundlePath = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, '.dist', 'game.js'));
|
|
219
|
+
storageProvider.resolvePath(bundlePath);
|
|
220
|
+
|
|
221
|
+
const srcDir = ENGINE.AssetPath.fromString(ENGINE.AssetPath.join(ENGINE.PROJECT_PATH_PREFIX, 'src'));
|
|
222
|
+
storageProvider.resolvePath(srcDir);
|
|
223
|
+
|
|
224
|
+
// Check if .dist/game.js exists
|
|
225
|
+
if (!fs.existsSync(bundlePath.getResolvedPath())) {
|
|
226
|
+
// If game.js doesn't exist, we need to register classes
|
|
227
|
+
await registerGameClasses();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const bundleFileStats = fs.statSync(getResolvedPath(bundlePath));
|
|
232
|
+
const bundleFileTimestamp = bundleFileStats.mtime.getTime();
|
|
233
|
+
console.log('bundleFileTimestamp:', new Date(bundleFileTimestamp));
|
|
234
|
+
|
|
235
|
+
// Check if src directory exists
|
|
236
|
+
if (!fs.existsSync(getResolvedPath(srcDir))) {
|
|
237
|
+
console.log('No src directory, no need to register classes');
|
|
238
|
+
return; // No src directory, nothing to check
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Recursively find all js, jsx, ts, tsx files in src directory
|
|
242
|
+
const sourceFiles = findSourceFiles(getResolvedPath(srcDir));
|
|
243
|
+
|
|
244
|
+
// Check if any source file is newer than game.js
|
|
245
|
+
for (const sourceFile of sourceFiles) {
|
|
246
|
+
const sourceStats = fs.statSync(sourceFile);
|
|
247
|
+
const sourceTime = sourceStats.mtime.getTime();
|
|
248
|
+
|
|
249
|
+
if (sourceTime > bundleFileTimestamp) {
|
|
250
|
+
// Found a newer source file, register classes and return
|
|
251
|
+
console.log(`Found a newer source file, ${sourceFile}, timestamp ${new Date(sourceTime)}, registering classes`);
|
|
252
|
+
await registerGameClasses();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
console.log('No newer source files found, no need to register classes');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findSourceFiles(dir: string): string[] {
|
|
261
|
+
const sourceFiles: string[] = [];
|
|
262
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
263
|
+
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
const fullPath = path.join(dir, entry.name);
|
|
266
|
+
|
|
267
|
+
if (entry.isDirectory()) {
|
|
268
|
+
// Recursively search subdirectories
|
|
269
|
+
sourceFiles.push(...findSourceFiles(fullPath));
|
|
270
|
+
} else if (entry.isFile()) {
|
|
271
|
+
// Check if file has one of the target extensions
|
|
272
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
273
|
+
if (['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
|
|
274
|
+
sourceFiles.push(fullPath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return sourceFiles;
|
|
280
|
+
}
|
|
281
|
+
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
|
|
3
|
-
import * as ENGINE from 'genesys.js';
|
|
4
|
-
|
|
5
|
-
import { getProjectRoot } from './common.js';
|
|
6
|
-
import { isSubclass } from './mcp/utils.js';
|
|
7
|
-
import { fixUpClassName, registerGameClasses } from './mcp/utils.js';
|
|
8
|
-
import { StorageProvider } from './storageProvider.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export async function generateCode(className: string, filePath: string, baseClassName: string): Promise<boolean> {
|
|
12
|
-
try {
|
|
13
|
-
baseClassName = fixUpClassName(baseClassName);
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
// if the base class name is not found, register all game classes and try again
|
|
17
|
-
await registerGameClasses();
|
|
18
|
-
baseClassName = fixUpClassName(baseClassName);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
using context = ENGINE.scopedProjectContext({project: 'mcp-project', storageProvider: new StorageProvider()});
|
|
22
|
-
|
|
23
|
-
const testIsSubclass = (childName: string, parent: Function) => {
|
|
24
|
-
const child = ENGINE.ClassRegistry.getRegistry().get(childName);
|
|
25
|
-
if (!child) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
return isSubclass(child, parent);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const fullFilePath = path.isAbsolute(filePath) ? filePath : path.join(getProjectRoot(), filePath);
|
|
32
|
-
|
|
33
|
-
let fileGenerated = false;
|
|
34
|
-
const isSubclassOfActor = testIsSubclass(baseClassName, ENGINE.Actor);
|
|
35
|
-
if (isSubclassOfActor) {
|
|
36
|
-
await ENGINE.WorldCommands.generateActorTemplateFile(className, fullFilePath, baseClassName);
|
|
37
|
-
fileGenerated = true;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return fileGenerated;
|
|
41
|
-
}
|
|
42
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import * as ENGINE from 'genesys.js';
|
|
4
|
+
|
|
5
|
+
import { getProjectRoot } from './common.js';
|
|
6
|
+
import { isSubclass } from './mcp/utils.js';
|
|
7
|
+
import { fixUpClassName, registerGameClasses } from './mcp/utils.js';
|
|
8
|
+
import { StorageProvider } from './storageProvider.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export async function generateCode(className: string, filePath: string, baseClassName: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
baseClassName = fixUpClassName(baseClassName);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
// if the base class name is not found, register all game classes and try again
|
|
17
|
+
await registerGameClasses();
|
|
18
|
+
baseClassName = fixUpClassName(baseClassName);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
using context = ENGINE.scopedProjectContext({project: 'mcp-project', storageProvider: new StorageProvider()});
|
|
22
|
+
|
|
23
|
+
const testIsSubclass = (childName: string, parent: Function) => {
|
|
24
|
+
const child = ENGINE.ClassRegistry.getRegistry().get(childName);
|
|
25
|
+
if (!child) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return isSubclass(child, parent);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const fullFilePath = path.isAbsolute(filePath) ? filePath : path.join(getProjectRoot(), filePath);
|
|
32
|
+
|
|
33
|
+
let fileGenerated = false;
|
|
34
|
+
const isSubclassOfActor = testIsSubclass(baseClassName, ENGINE.Actor);
|
|
35
|
+
if (isSubclassOfActor) {
|
|
36
|
+
await ENGINE.WorldCommands.generateActorTemplateFile(className, fullFilePath, baseClassName);
|
|
37
|
+
fileGenerated = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return fileGenerated;
|
|
41
|
+
}
|
|
42
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { mockBrowserEnvironment as engineMock } from 'genesys.js';
|
|
2
|
-
import { JSDOM } from 'jsdom';
|
|
3
|
-
|
|
4
|
-
export function mockBrowserEnvironment() {
|
|
5
|
-
engineMock(JSDOM);
|
|
6
|
-
}
|
|
1
|
+
import { mockBrowserEnvironment as engineMock } from 'genesys.js';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
|
|
4
|
+
export function mockBrowserEnvironment() {
|
|
5
|
+
engineMock(JSDOM);
|
|
6
|
+
}
|