@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.
- package/README.md +60 -0
- package/dist/src/asset-pack/eslint.config.js +43 -0
- package/dist/src/asset-pack/scripts/postinstall.js +64 -0
- package/dist/src/asset-pack/src/index.js +1 -0
- package/dist/src/core/cli.js +306 -0
- package/dist/src/core/common.js +324 -0
- package/dist/src/core/index.js +6 -0
- package/dist/src/core/tools/build-project.js +450 -0
- package/dist/src/core/tools/index.js +2 -0
- package/dist/src/core/tools/new-asset-pack.js +150 -0
- package/dist/src/core/tools/new-project.js +292 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/dependencies.js +82 -0
- package/dist/src/electron/IpcSerializableError.js +38 -0
- package/dist/src/electron/api.js +7 -0
- package/dist/src/electron/backend/actions.js +56 -0
- package/dist/src/electron/backend/handler.js +441 -0
- package/dist/src/electron/backend/logging.js +41 -0
- package/dist/src/electron/backend/main.js +315 -0
- package/dist/src/electron/backend/menu.js +208 -0
- package/dist/src/electron/backend/state.js +201 -0
- package/dist/src/electron/backend/tools/const.js +9 -0
- package/dist/src/electron/backend/tools/file-server.js +383 -0
- package/dist/src/electron/backend/tools/open-project.js +261 -0
- package/dist/src/electron/backend/window.js +161 -0
- package/dist/src/templates/eslint.config.js +43 -0
- package/dist/src/templates/scripts/genesys/build-project.js +42 -0
- package/dist/src/templates/scripts/genesys/calc-bounding-box.js +205 -0
- package/dist/src/templates/scripts/genesys/common.js +36 -0
- package/dist/src/templates/scripts/genesys/const.js +9 -0
- package/dist/src/templates/scripts/genesys/dev/dump-default-scene.js +8 -0
- package/dist/src/templates/scripts/genesys/dev/generate-manifest.js +116 -0
- package/dist/src/templates/scripts/genesys/dev/launcher.js +39 -0
- package/dist/src/templates/scripts/genesys/dev/storage-provider.js +188 -0
- package/dist/src/templates/scripts/genesys/dev/update-template-scenes.js +67 -0
- package/dist/src/templates/scripts/genesys/doc-server.js +12 -0
- package/dist/src/templates/scripts/genesys/genesys-mcp.js +413 -0
- package/dist/src/templates/scripts/genesys/mcp/doc-tools.js +70 -0
- package/dist/src/templates/scripts/genesys/mcp/editor-functions.js +123 -0
- package/dist/src/templates/scripts/genesys/mcp/editor-tools.js +51 -0
- package/dist/src/templates/scripts/genesys/mcp/get-scene-state.js +26 -0
- package/dist/src/templates/scripts/genesys/mcp/run-subprocess.js +23 -0
- package/dist/src/templates/scripts/genesys/mcp/search-actors.js +703 -0
- package/dist/src/templates/scripts/genesys/mcp/search-assets.js +296 -0
- package/dist/src/templates/scripts/genesys/mcp/utils.js +234 -0
- package/dist/src/templates/scripts/genesys/misc.js +32 -0
- package/dist/src/templates/scripts/genesys/mock.js +5 -0
- package/dist/src/templates/scripts/genesys/place-actors.js +112 -0
- package/dist/src/templates/scripts/genesys/post-install.js +25 -0
- package/dist/src/templates/scripts/genesys/remove-engine-comments.js +113 -0
- package/dist/src/templates/scripts/genesys/storageProvider.js +146 -0
- package/dist/src/templates/scripts/genesys/validate-prefabs.js +115 -0
- package/dist/src/templates/src/index.js +20 -0
- package/dist/src/templates/src/templates/firstPerson/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/firstPerson/src/game.js +30 -0
- package/dist/src/templates/src/templates/firstPerson/src/player.js +60 -0
- package/dist/src/templates/src/templates/fps/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/fps/src/game.js +30 -0
- package/dist/src/templates/src/templates/fps/src/player.js +64 -0
- package/dist/src/templates/src/templates/fps/src/weapon.js +62 -0
- package/dist/src/templates/src/templates/freeCamera/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/freeCamera/src/game.js +30 -0
- package/dist/src/templates/src/templates/freeCamera/src/player.js +43 -0
- package/dist/src/templates/src/templates/sideScroller/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/sideScroller/src/const.js +43 -0
- package/dist/src/templates/src/templates/sideScroller/src/game.js +103 -0
- package/dist/src/templates/src/templates/sideScroller/src/level-generator.js +249 -0
- package/dist/src/templates/src/templates/sideScroller/src/player.js +105 -0
- package/dist/src/templates/src/templates/thirdPerson/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/thirdPerson/src/game.js +30 -0
- package/dist/src/templates/src/templates/thirdPerson/src/player.js +63 -0
- package/dist/src/templates/src/templates/vehicle/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/vehicle/src/base-vehicle.js +122 -0
- package/dist/src/templates/src/templates/vehicle/src/game.js +33 -0
- package/dist/src/templates/src/templates/vehicle/src/mesh-vehicle.js +189 -0
- package/dist/src/templates/src/templates/vehicle/src/player.js +102 -0
- package/dist/src/templates/src/templates/vehicle/src/primitive-vehicle.js +259 -0
- package/dist/src/templates/src/templates/vehicle/src/ui-hints.js +100 -0
- package/dist/src/templates/src/templates/vr-game/src/auto-imports.js +1 -0
- package/dist/src/templates/src/templates/vr-game/src/game.js +55 -0
- package/dist/src/templates/src/templates/vr-game/src/sample-vr-actor.js +29 -0
- package/dist/src/templates/vite.config.js +46 -0
- package/package.json +176 -0
- package/scripts/post-install.ts +143 -0
- package/src/asset-pack/.gitattributes +89 -0
- package/src/asset-pack/eslint.config.js +45 -0
- package/src/asset-pack/gitignore +11 -0
- package/src/asset-pack/scripts/postinstall.ts +81 -0
- package/src/asset-pack/src/index.ts +0 -0
- package/src/asset-pack/tsconfig.json +34 -0
- package/src/templates/.cursor/mcp.json +20 -0
- package/src/templates/.cursorignore +2 -0
- package/src/templates/.gitattributes +89 -0
- package/src/templates/.vscode/settings.json +6 -0
- package/src/templates/AGENTS.md +86 -0
- package/src/templates/CLAUDE.md +1 -0
- package/src/templates/README.md +24 -0
- package/src/templates/eslint.config.js +45 -0
- package/src/templates/gitignore +11 -0
- package/src/templates/index.html +34 -0
- package/src/templates/pnpm-lock.yaml +3676 -0
- package/src/templates/scripts/genesys/build-project.ts +51 -0
- package/src/templates/scripts/genesys/calc-bounding-box.ts +272 -0
- package/src/templates/scripts/genesys/common.ts +46 -0
- package/src/templates/scripts/genesys/const.ts +9 -0
- package/src/templates/scripts/genesys/dev/dump-default-scene.ts +11 -0
- package/src/templates/scripts/genesys/dev/generate-manifest.ts +146 -0
- package/src/templates/scripts/genesys/dev/launcher.ts +46 -0
- package/src/templates/scripts/genesys/dev/storage-provider.ts +229 -0
- package/src/templates/scripts/genesys/dev/update-template-scenes.ts +84 -0
- package/src/templates/scripts/genesys/doc-server.ts +16 -0
- package/src/templates/scripts/genesys/genesys-mcp.ts +526 -0
- package/src/templates/scripts/genesys/mcp/doc-tools.ts +86 -0
- package/src/templates/scripts/genesys/mcp/editor-functions.ts +151 -0
- package/src/templates/scripts/genesys/mcp/editor-tools.ts +73 -0
- package/src/templates/scripts/genesys/mcp/get-scene-state.ts +35 -0
- package/src/templates/scripts/genesys/mcp/run-subprocess.ts +30 -0
- package/src/templates/scripts/genesys/mcp/search-actors.ts +858 -0
- package/src/templates/scripts/genesys/mcp/search-assets.ts +380 -0
- package/src/templates/scripts/genesys/mcp/utils.ts +281 -0
- package/src/templates/scripts/genesys/misc.ts +42 -0
- package/src/templates/scripts/genesys/mock.ts +6 -0
- package/src/templates/scripts/genesys/place-actors.ts +179 -0
- package/src/templates/scripts/genesys/post-install.ts +30 -0
- package/src/templates/scripts/genesys/prefab.schema.json +85 -0
- package/src/templates/scripts/genesys/remove-engine-comments.ts +135 -0
- package/src/templates/scripts/genesys/run-mcp-inspector.bat +5 -0
- package/src/templates/scripts/genesys/storageProvider.ts +182 -0
- package/src/templates/scripts/genesys/validate-prefabs.ts +138 -0
- package/src/templates/src/index.ts +22 -0
- package/src/templates/src/templates/firstPerson/assets/default.genesys-scene +166 -0
- package/src/templates/src/templates/firstPerson/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/firstPerson/src/game.ts +39 -0
- package/src/templates/src/templates/firstPerson/src/player.ts +63 -0
- package/src/templates/src/templates/fps/assets/default.genesys-scene +9460 -0
- package/src/templates/src/templates/fps/assets/models/SM_Beam_400.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_ChamferCube.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Floor_Thick_400x400.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Floor_Thick_400x400_Orange.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Floor_Thin_400x400.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Floor_Thin_400x400_Orange.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Ramp_400x400.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Rifle.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x200.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x200_Orange.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x400.glb +0 -0
- package/src/templates/src/templates/fps/assets/models/SM_Wall_Thin_400x400_Orange.glb +0 -0
- package/src/templates/src/templates/fps/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/fps/src/game.ts +39 -0
- package/src/templates/src/templates/fps/src/player.ts +69 -0
- package/src/templates/src/templates/fps/src/weapon.ts +54 -0
- package/src/templates/src/templates/freeCamera/assets/default.genesys-scene +166 -0
- package/src/templates/src/templates/freeCamera/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/freeCamera/src/game.ts +39 -0
- package/src/templates/src/templates/freeCamera/src/player.ts +45 -0
- package/src/templates/src/templates/sideScroller/assets/default.genesys-scene +122 -0
- package/src/templates/src/templates/sideScroller/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/sideScroller/src/const.ts +46 -0
- package/src/templates/src/templates/sideScroller/src/game.ts +122 -0
- package/src/templates/src/templates/sideScroller/src/level-generator.ts +361 -0
- package/src/templates/src/templates/sideScroller/src/player.ts +125 -0
- package/src/templates/src/templates/thirdPerson/assets/default.genesys-scene +166 -0
- package/src/templates/src/templates/thirdPerson/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/thirdPerson/src/game.ts +39 -0
- package/src/templates/src/templates/thirdPerson/src/player.ts +61 -0
- package/src/templates/src/templates/vehicle/assets/default.genesys-scene +226 -0
- package/src/templates/src/templates/vehicle/assets/models/cyberTruck/chassis.glb +0 -0
- package/src/templates/src/templates/vehicle/assets/models/cyberTruck/wheel.glb +0 -0
- package/src/templates/src/templates/vehicle/src/auto-imports.ts +0 -0
- package/src/templates/src/templates/vehicle/src/base-vehicle.ts +145 -0
- package/src/templates/src/templates/vehicle/src/game.ts +43 -0
- package/src/templates/src/templates/vehicle/src/mesh-vehicle.ts +191 -0
- package/src/templates/src/templates/vehicle/src/player.ts +109 -0
- package/src/templates/src/templates/vehicle/src/primitive-vehicle.ts +266 -0
- package/src/templates/src/templates/vehicle/src/ui-hints.ts +101 -0
- package/src/templates/src/templates/vr-game/assets/default.genesys-scene +247 -0
- package/src/templates/src/templates/vr-game/src/auto-imports.ts +1 -0
- package/src/templates/src/templates/vr-game/src/game.ts +66 -0
- package/src/templates/src/templates/vr-game/src/sample-vr-actor.ts +26 -0
- package/src/templates/tsconfig.json +35 -0
- package/src/templates/vite.config.ts +52 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import Store from 'electron-store';
|
|
2
|
+
import upath from 'upath';
|
|
3
|
+
import { AppStateKey } from '../api.js';
|
|
4
|
+
// Default state values
|
|
5
|
+
const defaultState = {
|
|
6
|
+
project: {
|
|
7
|
+
lastOpenedDirectory: undefined,
|
|
8
|
+
lastParentDirectory: undefined,
|
|
9
|
+
},
|
|
10
|
+
projects: {},
|
|
11
|
+
window: {
|
|
12
|
+
state: undefined,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
// Create the store instance with proper typing
|
|
16
|
+
const store = new Store({
|
|
17
|
+
defaults: defaultState,
|
|
18
|
+
name: 'genesys-config',
|
|
19
|
+
});
|
|
20
|
+
// State management class
|
|
21
|
+
export class AppStateManager {
|
|
22
|
+
static instance;
|
|
23
|
+
store;
|
|
24
|
+
callbacks = new Set();
|
|
25
|
+
constructor() {
|
|
26
|
+
this.store = store;
|
|
27
|
+
}
|
|
28
|
+
static getInstance() {
|
|
29
|
+
if (!AppStateManager.instance) {
|
|
30
|
+
AppStateManager.instance = new AppStateManager();
|
|
31
|
+
}
|
|
32
|
+
return AppStateManager.instance;
|
|
33
|
+
}
|
|
34
|
+
getState(key) {
|
|
35
|
+
return this.store.get(key);
|
|
36
|
+
}
|
|
37
|
+
setState(key, value) {
|
|
38
|
+
const oldValue = this.store.get(key);
|
|
39
|
+
if (value === undefined || value === null) {
|
|
40
|
+
this.store.delete(key);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
this.store.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
// Notify all callbacks about the state change
|
|
46
|
+
this.callbacks.forEach(callback => {
|
|
47
|
+
try {
|
|
48
|
+
callback(key, oldValue, value);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('Error in state change callback:', error);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Register a callback to be called when state changes
|
|
57
|
+
* @param callback Function to call when state changes
|
|
58
|
+
* @returns Function to unregister the callback
|
|
59
|
+
*/
|
|
60
|
+
onStateChange(callback) {
|
|
61
|
+
this.callbacks.add(callback);
|
|
62
|
+
// Return unsubscribe function
|
|
63
|
+
return () => {
|
|
64
|
+
this.callbacks.delete(callback);
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Remove all state change callbacks
|
|
69
|
+
*/
|
|
70
|
+
clearCallbacks() {
|
|
71
|
+
this.callbacks.clear();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a project path for consistent storage and comparison
|
|
75
|
+
* Handles both path separators and case sensitivity on Windows
|
|
76
|
+
* @param projectPath The path to normalize
|
|
77
|
+
* @returns Normalized path
|
|
78
|
+
*/
|
|
79
|
+
normalizeProjectPath(projectPath) {
|
|
80
|
+
// First normalize path separators
|
|
81
|
+
let normalized = upath.normalize(projectPath);
|
|
82
|
+
// On Windows, paths are case-insensitive, so normalize to lowercase for comparison
|
|
83
|
+
// but preserve the original casing for display by using a consistent casing strategy
|
|
84
|
+
if (process.platform === 'win32') {
|
|
85
|
+
// Convert to lowercase for consistent comparison, but keep original structure
|
|
86
|
+
normalized = normalized.toLowerCase();
|
|
87
|
+
}
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Update the last opened time for a project
|
|
92
|
+
* @param projectPath The path to the project
|
|
93
|
+
* @param timestamp Optional timestamp, defaults to current time
|
|
94
|
+
*/
|
|
95
|
+
updateProjectLastOpened(projectPath, timestamp = Date.now()) {
|
|
96
|
+
// Normalize and handle case-insensitive duplicates on Windows
|
|
97
|
+
const normalizedPath = this.normalizeProjectPath(projectPath);
|
|
98
|
+
const currentProjects = this.getState(AppStateKey.Projects) ?? {};
|
|
99
|
+
// Clean up and normalize all existing project paths
|
|
100
|
+
const cleanedProjects = {};
|
|
101
|
+
const pathMap = new Map();
|
|
102
|
+
// Process existing projects: normalize all paths and keep latest timestamp for duplicates
|
|
103
|
+
for (const [path, existingTimestamp] of Object.entries(currentProjects)) {
|
|
104
|
+
const normalizedExistingPath = this.normalizeProjectPath(path);
|
|
105
|
+
const timestampValue = existingTimestamp;
|
|
106
|
+
const existingMappedTimestamp = pathMap.get(normalizedExistingPath);
|
|
107
|
+
// Keep the latest timestamp if we find duplicates
|
|
108
|
+
if (!existingMappedTimestamp || timestampValue > existingMappedTimestamp) {
|
|
109
|
+
pathMap.set(normalizedExistingPath, timestampValue);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Add/update the current project
|
|
113
|
+
pathMap.set(normalizedPath, timestamp);
|
|
114
|
+
// Build final cleaned projects object with normalized paths
|
|
115
|
+
for (const [path, ts] of pathMap) {
|
|
116
|
+
cleanedProjects[path] = ts;
|
|
117
|
+
}
|
|
118
|
+
console.log(`[state] 📁 Updated project tracking for: ${normalizedPath}`);
|
|
119
|
+
// Log cleanup if paths were normalized or duplicates removed
|
|
120
|
+
const originalCount = Object.keys(currentProjects).length;
|
|
121
|
+
const cleanedCount = Object.keys(cleanedProjects).length;
|
|
122
|
+
if (originalCount !== cleanedCount) {
|
|
123
|
+
console.log(`[state] 🧹 Normalized paths and cleaned up ${originalCount - cleanedCount} duplicate entries`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
// Check if any paths were actually normalized
|
|
127
|
+
const pathsNormalized = Object.keys(currentProjects).some(path => path !== this.normalizeProjectPath(path));
|
|
128
|
+
if (pathsNormalized) {
|
|
129
|
+
console.log('[state] 🧹 Normalized project paths for consistency');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.setState(AppStateKey.Projects, cleanedProjects);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get the last opened time for a project
|
|
136
|
+
* @param projectPath The path to the project
|
|
137
|
+
* @returns The timestamp or undefined if not found
|
|
138
|
+
*/
|
|
139
|
+
getProjectLastOpened(projectPath) {
|
|
140
|
+
const normalizedPath = this.normalizeProjectPath(projectPath);
|
|
141
|
+
const projects = this.getState(AppStateKey.Projects) ?? {};
|
|
142
|
+
return projects[normalizedPath];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get all projects data (path -> timestamp mapping)
|
|
146
|
+
*/
|
|
147
|
+
getAllProjects() {
|
|
148
|
+
return this.getState(AppStateKey.Projects) ?? {};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Clean up duplicate projects in the existing state
|
|
152
|
+
* This can be called to fix any existing duplicates
|
|
153
|
+
*/
|
|
154
|
+
cleanupDuplicateProjects() {
|
|
155
|
+
const currentProjects = this.getState(AppStateKey.Projects) ?? {};
|
|
156
|
+
const pathMap = new Map();
|
|
157
|
+
// Process existing projects: normalize all paths and keep latest timestamp for duplicates
|
|
158
|
+
for (const [path, timestamp] of Object.entries(currentProjects)) {
|
|
159
|
+
const normalizedPath = this.normalizeProjectPath(path);
|
|
160
|
+
const timestampValue = timestamp;
|
|
161
|
+
const existingTimestamp = pathMap.get(normalizedPath);
|
|
162
|
+
// Keep the latest timestamp if we find duplicates
|
|
163
|
+
if (!existingTimestamp || timestampValue > existingTimestamp) {
|
|
164
|
+
pathMap.set(normalizedPath, timestampValue);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Build cleaned projects object
|
|
168
|
+
const cleanedProjects = {};
|
|
169
|
+
for (const [path, ts] of pathMap) {
|
|
170
|
+
cleanedProjects[path] = ts;
|
|
171
|
+
}
|
|
172
|
+
const originalCount = Object.keys(currentProjects).length;
|
|
173
|
+
const cleanedCount = Object.keys(cleanedProjects).length;
|
|
174
|
+
if (originalCount !== cleanedCount) {
|
|
175
|
+
console.log(`[state] 🧹 Cleaned up ${originalCount - cleanedCount} duplicate project entries`);
|
|
176
|
+
this.setState(AppStateKey.Projects, cleanedProjects);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.log('[state] ✅ No duplicate projects found');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Remove a project from the projects list
|
|
184
|
+
* @param projectPath The path to the project to remove
|
|
185
|
+
*/
|
|
186
|
+
removeProject(projectPath) {
|
|
187
|
+
const normalizedPath = this.normalizeProjectPath(projectPath);
|
|
188
|
+
const currentProjects = this.getState(AppStateKey.Projects) ?? {};
|
|
189
|
+
// Remove both the normalized path and any potential duplicates
|
|
190
|
+
const cleanedProjects = {};
|
|
191
|
+
for (const [path, timestamp] of Object.entries(currentProjects)) {
|
|
192
|
+
if (this.normalizeProjectPath(path) !== normalizedPath) {
|
|
193
|
+
cleanedProjects[this.normalizeProjectPath(path)] = timestamp;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
console.log(`[state] 🗑️ Removed project: ${normalizedPath}`);
|
|
197
|
+
this.setState(AppStateKey.Projects, cleanedProjects);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Export singleton instance
|
|
201
|
+
export const appState = AppStateManager.getInstance();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import isDev from 'electron-is-dev';
|
|
3
|
+
import * as ENGINE from 'genesys.js';
|
|
4
|
+
// MUST be kept in sync with https://github.com/directivegames/genesys.ai/blob/develop/src/const.ts
|
|
5
|
+
export const GENESYS_URL = 'https://web--genesys-ai.us-central1.hosted.app/dashboard';
|
|
6
|
+
export const GENESYS_LOCAL_URL = 'http://localhost:3000/dashboard';
|
|
7
|
+
export const DefaultGameFilePath = path.join('src', ENGINE.DEFAULT_GAME_NAME);
|
|
8
|
+
export const ReleaseUrl = 'https://github.com/directivegames/genesys.sdk-release/releases';
|
|
9
|
+
export const DefaultFileServerPort = !isDev ? 4000 : 4001;
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
import cors from 'cors';
|
|
5
|
+
import { dialog, app as electronApp } from 'electron';
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { minimatch } from 'minimatch';
|
|
8
|
+
import multer from 'multer';
|
|
9
|
+
import { WebSocketServer } from 'ws';
|
|
10
|
+
import { runCommand } from '../../../core/common.js';
|
|
11
|
+
import { IgnoredFiles } from '../../../core/index.js';
|
|
12
|
+
import { buildProject } from '../../../core/tools/build-project.js';
|
|
13
|
+
import { getAppInfo } from '../handler.js';
|
|
14
|
+
import { logger } from '../logging.js';
|
|
15
|
+
import { DefaultFileServerPort } from './const.js';
|
|
16
|
+
const hiddenFiles = [
|
|
17
|
+
...IgnoredFiles,
|
|
18
|
+
'.cursor',
|
|
19
|
+
'.engine',
|
|
20
|
+
'.idea',
|
|
21
|
+
'.dist',
|
|
22
|
+
'dist',
|
|
23
|
+
'.editor',
|
|
24
|
+
'node_modules',
|
|
25
|
+
'scripts',
|
|
26
|
+
'.gitignore',
|
|
27
|
+
'.git',
|
|
28
|
+
'package.json',
|
|
29
|
+
'package-lock.json',
|
|
30
|
+
'pnpm-lock.yaml',
|
|
31
|
+
'tsconfig.json',
|
|
32
|
+
'*.code-workspace',
|
|
33
|
+
'.placeholder',
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* Check if a filename matches any of the hidden file patterns
|
|
37
|
+
*/
|
|
38
|
+
function isHiddenFile(filename) {
|
|
39
|
+
return hiddenFiles.some(pattern => {
|
|
40
|
+
// If pattern contains wildcards, use minimatch for glob matching
|
|
41
|
+
if (pattern.includes('*') || pattern.includes('?') || pattern.includes('[')) {
|
|
42
|
+
return minimatch(filename, pattern);
|
|
43
|
+
}
|
|
44
|
+
// Otherwise, use exact string matching
|
|
45
|
+
return filename === pattern;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Check if a file path should be ignored by the watcher
|
|
50
|
+
* This handles both files and directories
|
|
51
|
+
*/
|
|
52
|
+
function shouldIgnorePath(filePath, rootDir) {
|
|
53
|
+
const relativePath = path.relative(rootDir, filePath);
|
|
54
|
+
const pathParts = relativePath.split(path.sep);
|
|
55
|
+
// Check if any part of the path matches hidden patterns
|
|
56
|
+
return pathParts.some(part => isHiddenFile(part)) ||
|
|
57
|
+
isHiddenFile(path.basename(filePath));
|
|
58
|
+
}
|
|
59
|
+
class FileServer {
|
|
60
|
+
server = null;
|
|
61
|
+
wsServer = null;
|
|
62
|
+
port = DefaultFileServerPort;
|
|
63
|
+
isRunning = false;
|
|
64
|
+
connections = new Set(); // Track all open connections
|
|
65
|
+
constructor() { }
|
|
66
|
+
createApp(rootDir) {
|
|
67
|
+
const app = express();
|
|
68
|
+
app.use(cors());
|
|
69
|
+
app.use(express.json());
|
|
70
|
+
app.use(express.static(rootDir, {
|
|
71
|
+
dotfiles: 'allow'
|
|
72
|
+
}));
|
|
73
|
+
const storage = multer.diskStorage({
|
|
74
|
+
destination: (req, file, cb) => {
|
|
75
|
+
const uploadPath = req.headers['x-upload-path'] || '';
|
|
76
|
+
const absPath = path.join(rootDir, uploadPath);
|
|
77
|
+
if (!fs.existsSync(absPath)) {
|
|
78
|
+
fs.mkdirSync(absPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
cb(null, absPath);
|
|
81
|
+
},
|
|
82
|
+
filename: (req, file, cb) => {
|
|
83
|
+
cb(null, file.originalname);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
const upload = multer({ storage });
|
|
87
|
+
app.get('', (req, res) => {
|
|
88
|
+
res.json(getAppInfo());
|
|
89
|
+
});
|
|
90
|
+
app.get('/api/files', (req, res) => {
|
|
91
|
+
const absPath = path.join(rootDir, req.query.path || '');
|
|
92
|
+
const recursive = req.query.recursive === 'true';
|
|
93
|
+
const directories = [];
|
|
94
|
+
const files = [];
|
|
95
|
+
if (fs.existsSync(absPath)) {
|
|
96
|
+
if (recursive) {
|
|
97
|
+
const walkDir = (currentPath, relativePath = '') => {
|
|
98
|
+
const items = fs.readdirSync(currentPath).filter(file => !isHiddenFile(file));
|
|
99
|
+
items.forEach(item => {
|
|
100
|
+
const itemPath = path.join(currentPath, item);
|
|
101
|
+
const itemRelativePath = path.join(relativePath, item);
|
|
102
|
+
const stats = fs.statSync(itemPath);
|
|
103
|
+
const normalizedPath = itemRelativePath.replace(/\\/g, '/');
|
|
104
|
+
const itemInfo = {
|
|
105
|
+
name: item,
|
|
106
|
+
path: normalizedPath,
|
|
107
|
+
absolutePath: itemPath,
|
|
108
|
+
size: stats.size,
|
|
109
|
+
modifiedTime: stats.mtime
|
|
110
|
+
};
|
|
111
|
+
if (stats.isDirectory()) {
|
|
112
|
+
directories.push(itemInfo);
|
|
113
|
+
walkDir(itemPath, itemRelativePath);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
files.push(itemInfo);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
walkDir(absPath, path.relative(rootDir, absPath));
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const items = fs.readdirSync(absPath).filter(file => !isHiddenFile(file));
|
|
124
|
+
items.forEach(item => {
|
|
125
|
+
const filePath = path.join(absPath, item);
|
|
126
|
+
const stats = fs.statSync(filePath);
|
|
127
|
+
const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
128
|
+
const itemInfo = {
|
|
129
|
+
name: item,
|
|
130
|
+
path: relativePath,
|
|
131
|
+
absolutePath: filePath,
|
|
132
|
+
size: stats.size,
|
|
133
|
+
modifiedTime: stats.mtime
|
|
134
|
+
};
|
|
135
|
+
if (stats.isDirectory()) {
|
|
136
|
+
directories.push(itemInfo);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
files.push(itemInfo);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
res.json({
|
|
145
|
+
directories,
|
|
146
|
+
files
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
app.post('/api/files', (req, res) => {
|
|
150
|
+
if (!req.body.path) {
|
|
151
|
+
res.status(400).json({ error: 'File path is required' });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const absPath = path.join(rootDir, req.body.path);
|
|
155
|
+
const dirPath = path.dirname(absPath);
|
|
156
|
+
if (!fs.existsSync(dirPath)) {
|
|
157
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
158
|
+
}
|
|
159
|
+
fs.writeFileSync(absPath, req.body.content ?? '');
|
|
160
|
+
res.json({ success: true, path: req.body.path });
|
|
161
|
+
logger.verbose(`File updated: ${absPath}`);
|
|
162
|
+
});
|
|
163
|
+
app.post('/api/files/upload', upload.single('file'), (req, res) => {
|
|
164
|
+
if (!req.file) {
|
|
165
|
+
res.status(400).json({ error: 'No file uploaded' });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const relativePath = path.join(req.headers['x-upload-path'] || '', req.file.originalname)
|
|
169
|
+
.replace(/\\/g, '/');
|
|
170
|
+
const absPath = path.join(rootDir, relativePath);
|
|
171
|
+
res.json({
|
|
172
|
+
success: true,
|
|
173
|
+
filename: req.file.originalname,
|
|
174
|
+
path: relativePath
|
|
175
|
+
});
|
|
176
|
+
logger.verbose(`File uploaded: ${absPath}`);
|
|
177
|
+
});
|
|
178
|
+
app.post('/api/build-project', async (req, res) => {
|
|
179
|
+
try {
|
|
180
|
+
const projectPath = req.body?.projectPath ?? rootDir;
|
|
181
|
+
const runTsc = req.body?.runTsc ?? false;
|
|
182
|
+
const handler = {
|
|
183
|
+
ui: {
|
|
184
|
+
showLoadingOverlay: async () => { },
|
|
185
|
+
showErrorDialog: async (title, message) => {
|
|
186
|
+
dialog.showErrorBox(title, message);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
os: {
|
|
190
|
+
openPath: async () => { }
|
|
191
|
+
},
|
|
192
|
+
app: {
|
|
193
|
+
isPackaged: electronApp.isPackaged,
|
|
194
|
+
resourcesPath: process.resourcesPath
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const result = await buildProject(projectPath, runTsc, logger, handler);
|
|
198
|
+
if (result.success) {
|
|
199
|
+
res.json({
|
|
200
|
+
success: result.success,
|
|
201
|
+
message: result.message,
|
|
202
|
+
error: result.error
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
res.status(500).json({
|
|
207
|
+
success: result.success,
|
|
208
|
+
message: result.message,
|
|
209
|
+
error: result.error
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
res.status(500).json({ error: error.message });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
app.post('/api/exec', (req, res) => {
|
|
218
|
+
if (!req.body.command) {
|
|
219
|
+
res.status(400).json({ error: 'No command provided' });
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
logger.log(`Executing command: ${req.body.command}`);
|
|
223
|
+
try {
|
|
224
|
+
runCommand(req.body.command, null, logger);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
res.status(500).json({ error: error.message });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
res.json({
|
|
231
|
+
success: true,
|
|
232
|
+
command: req.body.command
|
|
233
|
+
});
|
|
234
|
+
logger.log(`Command executed: ${req.body.command}`);
|
|
235
|
+
});
|
|
236
|
+
app.delete('/api/files', (req, res) => {
|
|
237
|
+
if (!req.query.path) {
|
|
238
|
+
res.status(400).json({ error: 'Path is required' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
const absPath = path.join(rootDir, req.query.path);
|
|
242
|
+
if (!fs.existsSync(absPath)) {
|
|
243
|
+
res.status(404).json({ error: 'File or directory not found' });
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const stats = fs.statSync(absPath);
|
|
247
|
+
if (stats.isDirectory()) {
|
|
248
|
+
fs.rmdirSync(absPath, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
fs.unlinkSync(absPath);
|
|
252
|
+
}
|
|
253
|
+
res.json({ success: true, path: req.query.path });
|
|
254
|
+
logger.log(`File deleted: ${absPath}`);
|
|
255
|
+
});
|
|
256
|
+
return app;
|
|
257
|
+
}
|
|
258
|
+
createWsServer(rootDir) {
|
|
259
|
+
if (!this.server) {
|
|
260
|
+
throw new Error('Server must be initialized before creating WebSocket server');
|
|
261
|
+
}
|
|
262
|
+
const wss = new WebSocketServer({ server: this.server });
|
|
263
|
+
function broadcastChange(type, filePath, action) {
|
|
264
|
+
const payload = JSON.stringify({
|
|
265
|
+
type,
|
|
266
|
+
path: filePath,
|
|
267
|
+
action
|
|
268
|
+
});
|
|
269
|
+
wss.clients.forEach(client => {
|
|
270
|
+
if (client.readyState === 1) { // 1 = OPEN
|
|
271
|
+
client.send(payload);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Watch for file/folder changes using chokidar
|
|
276
|
+
const watcher = chokidar.watch(rootDir, {
|
|
277
|
+
persistent: true,
|
|
278
|
+
ignoreInitial: true,
|
|
279
|
+
depth: 99,
|
|
280
|
+
ignored: (filePath) => shouldIgnorePath(filePath, rootDir),
|
|
281
|
+
});
|
|
282
|
+
watcher
|
|
283
|
+
.on('add', filePath => {
|
|
284
|
+
const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
285
|
+
logger.verbose(`[watcher] add: ${rel}`);
|
|
286
|
+
broadcastChange('file', rel, 'created');
|
|
287
|
+
})
|
|
288
|
+
.on('change', filePath => {
|
|
289
|
+
const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
290
|
+
logger.verbose(`[watcher] change: ${rel}`);
|
|
291
|
+
broadcastChange('file', rel, 'modified');
|
|
292
|
+
})
|
|
293
|
+
.on('unlink', filePath => {
|
|
294
|
+
const rel = path.relative(rootDir, filePath).replace(/\\/g, '/');
|
|
295
|
+
logger.verbose(`[watcher] unlink: ${rel}`);
|
|
296
|
+
broadcastChange('file', rel, 'deleted');
|
|
297
|
+
})
|
|
298
|
+
.on('addDir', dirPath => {
|
|
299
|
+
const rel = path.relative(rootDir, dirPath).replace(/\\/g, '/');
|
|
300
|
+
logger.verbose(`[watcher] addDir: ${rel}`);
|
|
301
|
+
broadcastChange('folder', rel, 'created');
|
|
302
|
+
})
|
|
303
|
+
.on('unlinkDir', dirPath => {
|
|
304
|
+
const rel = path.relative(rootDir, dirPath).replace(/\\/g, '/');
|
|
305
|
+
logger.verbose(`[watcher] unlinkDir: ${rel}`);
|
|
306
|
+
broadcastChange('folder', rel, 'deleted');
|
|
307
|
+
});
|
|
308
|
+
wss.on('connection', ws => {
|
|
309
|
+
logger.log('WebSocket client connected');
|
|
310
|
+
ws.on('close', () => {
|
|
311
|
+
logger.log('WebSocket client disconnected');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
return wss;
|
|
315
|
+
}
|
|
316
|
+
async start(port, rootDir) {
|
|
317
|
+
if (this.isRunning) {
|
|
318
|
+
await this.stop();
|
|
319
|
+
}
|
|
320
|
+
this.port = port;
|
|
321
|
+
return new Promise((resolve, reject) => {
|
|
322
|
+
try {
|
|
323
|
+
const app = this.createApp(rootDir);
|
|
324
|
+
this.server = app.listen(this.port, '0.0.0.0', () => {
|
|
325
|
+
this.isRunning = true;
|
|
326
|
+
logger.log(`File server started on port ${this.port} at ${rootDir}`);
|
|
327
|
+
resolve();
|
|
328
|
+
});
|
|
329
|
+
// Track all open TCP connections!
|
|
330
|
+
this.server.on('connection', (conn) => {
|
|
331
|
+
this.connections.add(conn);
|
|
332
|
+
conn.on('close', () => this.connections.delete(conn));
|
|
333
|
+
});
|
|
334
|
+
this.server.on('error', (err) => {
|
|
335
|
+
this.isRunning = false;
|
|
336
|
+
reject(err);
|
|
337
|
+
});
|
|
338
|
+
this.wsServer = this.createWsServer(rootDir);
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
reject(error);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async stop() {
|
|
346
|
+
if (!this.isRunning || !this.server) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
return new Promise((resolve, reject) => {
|
|
350
|
+
try {
|
|
351
|
+
logger.log('Stopping file server...');
|
|
352
|
+
// Close WebSocket server if it exists
|
|
353
|
+
if (this.wsServer) {
|
|
354
|
+
this.wsServer.close();
|
|
355
|
+
this.wsServer = null;
|
|
356
|
+
logger.log('WebSocket server stopped');
|
|
357
|
+
}
|
|
358
|
+
this.server.close(() => {
|
|
359
|
+
this.isRunning = false;
|
|
360
|
+
this.server = null;
|
|
361
|
+
logger.log('File server stopped');
|
|
362
|
+
resolve();
|
|
363
|
+
});
|
|
364
|
+
logger.log(`Destroying ${this.connections.size} connections.`);
|
|
365
|
+
for (const conn of this.connections) {
|
|
366
|
+
conn.destroy();
|
|
367
|
+
}
|
|
368
|
+
this.connections.clear();
|
|
369
|
+
}
|
|
370
|
+
catch (error) {
|
|
371
|
+
logger.error('Failed to stop file server:', error);
|
|
372
|
+
reject(error);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
isServerRunning() {
|
|
377
|
+
return this.isRunning;
|
|
378
|
+
}
|
|
379
|
+
getPort() {
|
|
380
|
+
return this.port;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
export const fileServer = new FileServer();
|