unreal-engine-mcp-server 0.2.1

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 (155) hide show
  1. package/.dockerignore +57 -0
  2. package/.env.production +25 -0
  3. package/.eslintrc.json +54 -0
  4. package/.github/workflows/publish-mcp.yml +75 -0
  5. package/Dockerfile +54 -0
  6. package/LICENSE +21 -0
  7. package/Public/icon.png +0 -0
  8. package/README.md +209 -0
  9. package/claude_desktop_config_example.json +13 -0
  10. package/dist/cli.d.ts +3 -0
  11. package/dist/cli.js +7 -0
  12. package/dist/index.d.ts +31 -0
  13. package/dist/index.js +484 -0
  14. package/dist/prompts/index.d.ts +14 -0
  15. package/dist/prompts/index.js +38 -0
  16. package/dist/python-utils.d.ts +29 -0
  17. package/dist/python-utils.js +54 -0
  18. package/dist/resources/actors.d.ts +13 -0
  19. package/dist/resources/actors.js +83 -0
  20. package/dist/resources/assets.d.ts +23 -0
  21. package/dist/resources/assets.js +245 -0
  22. package/dist/resources/levels.d.ts +17 -0
  23. package/dist/resources/levels.js +94 -0
  24. package/dist/tools/actors.d.ts +51 -0
  25. package/dist/tools/actors.js +459 -0
  26. package/dist/tools/animation.d.ts +196 -0
  27. package/dist/tools/animation.js +579 -0
  28. package/dist/tools/assets.d.ts +21 -0
  29. package/dist/tools/assets.js +304 -0
  30. package/dist/tools/audio.d.ts +170 -0
  31. package/dist/tools/audio.js +416 -0
  32. package/dist/tools/blueprint.d.ts +144 -0
  33. package/dist/tools/blueprint.js +652 -0
  34. package/dist/tools/build_environment_advanced.d.ts +66 -0
  35. package/dist/tools/build_environment_advanced.js +484 -0
  36. package/dist/tools/consolidated-tool-definitions.d.ts +2598 -0
  37. package/dist/tools/consolidated-tool-definitions.js +607 -0
  38. package/dist/tools/consolidated-tool-handlers.d.ts +2 -0
  39. package/dist/tools/consolidated-tool-handlers.js +1050 -0
  40. package/dist/tools/debug.d.ts +185 -0
  41. package/dist/tools/debug.js +265 -0
  42. package/dist/tools/editor.d.ts +88 -0
  43. package/dist/tools/editor.js +365 -0
  44. package/dist/tools/engine.d.ts +30 -0
  45. package/dist/tools/engine.js +36 -0
  46. package/dist/tools/foliage.d.ts +155 -0
  47. package/dist/tools/foliage.js +525 -0
  48. package/dist/tools/introspection.d.ts +98 -0
  49. package/dist/tools/introspection.js +683 -0
  50. package/dist/tools/landscape.d.ts +158 -0
  51. package/dist/tools/landscape.js +375 -0
  52. package/dist/tools/level.d.ts +110 -0
  53. package/dist/tools/level.js +362 -0
  54. package/dist/tools/lighting.d.ts +159 -0
  55. package/dist/tools/lighting.js +1179 -0
  56. package/dist/tools/materials.d.ts +34 -0
  57. package/dist/tools/materials.js +146 -0
  58. package/dist/tools/niagara.d.ts +145 -0
  59. package/dist/tools/niagara.js +289 -0
  60. package/dist/tools/performance.d.ts +163 -0
  61. package/dist/tools/performance.js +412 -0
  62. package/dist/tools/physics.d.ts +189 -0
  63. package/dist/tools/physics.js +784 -0
  64. package/dist/tools/rc.d.ts +110 -0
  65. package/dist/tools/rc.js +363 -0
  66. package/dist/tools/sequence.d.ts +112 -0
  67. package/dist/tools/sequence.js +675 -0
  68. package/dist/tools/tool-definitions.d.ts +4919 -0
  69. package/dist/tools/tool-definitions.js +891 -0
  70. package/dist/tools/tool-handlers.d.ts +47 -0
  71. package/dist/tools/tool-handlers.js +830 -0
  72. package/dist/tools/ui.d.ts +171 -0
  73. package/dist/tools/ui.js +337 -0
  74. package/dist/tools/visual.d.ts +29 -0
  75. package/dist/tools/visual.js +67 -0
  76. package/dist/types/env.d.ts +10 -0
  77. package/dist/types/env.js +18 -0
  78. package/dist/types/index.d.ts +323 -0
  79. package/dist/types/index.js +28 -0
  80. package/dist/types/tool-types.d.ts +274 -0
  81. package/dist/types/tool-types.js +13 -0
  82. package/dist/unreal-bridge.d.ts +126 -0
  83. package/dist/unreal-bridge.js +992 -0
  84. package/dist/utils/cache-manager.d.ts +64 -0
  85. package/dist/utils/cache-manager.js +176 -0
  86. package/dist/utils/error-handler.d.ts +66 -0
  87. package/dist/utils/error-handler.js +243 -0
  88. package/dist/utils/errors.d.ts +133 -0
  89. package/dist/utils/errors.js +256 -0
  90. package/dist/utils/http.d.ts +26 -0
  91. package/dist/utils/http.js +135 -0
  92. package/dist/utils/logger.d.ts +12 -0
  93. package/dist/utils/logger.js +32 -0
  94. package/dist/utils/normalize.d.ts +17 -0
  95. package/dist/utils/normalize.js +49 -0
  96. package/dist/utils/response-validator.d.ts +34 -0
  97. package/dist/utils/response-validator.js +121 -0
  98. package/dist/utils/safe-json.d.ts +4 -0
  99. package/dist/utils/safe-json.js +97 -0
  100. package/dist/utils/stdio-redirect.d.ts +2 -0
  101. package/dist/utils/stdio-redirect.js +20 -0
  102. package/dist/utils/validation.d.ts +50 -0
  103. package/dist/utils/validation.js +173 -0
  104. package/mcp-config-example.json +14 -0
  105. package/package.json +63 -0
  106. package/server.json +60 -0
  107. package/src/cli.ts +7 -0
  108. package/src/index.ts +543 -0
  109. package/src/prompts/index.ts +51 -0
  110. package/src/python/editor_compat.py +181 -0
  111. package/src/python-utils.ts +57 -0
  112. package/src/resources/actors.ts +92 -0
  113. package/src/resources/assets.ts +251 -0
  114. package/src/resources/levels.ts +83 -0
  115. package/src/tools/actors.ts +480 -0
  116. package/src/tools/animation.ts +713 -0
  117. package/src/tools/assets.ts +305 -0
  118. package/src/tools/audio.ts +548 -0
  119. package/src/tools/blueprint.ts +736 -0
  120. package/src/tools/build_environment_advanced.ts +526 -0
  121. package/src/tools/consolidated-tool-definitions.ts +619 -0
  122. package/src/tools/consolidated-tool-handlers.ts +1093 -0
  123. package/src/tools/debug.ts +368 -0
  124. package/src/tools/editor.ts +360 -0
  125. package/src/tools/engine.ts +32 -0
  126. package/src/tools/foliage.ts +652 -0
  127. package/src/tools/introspection.ts +778 -0
  128. package/src/tools/landscape.ts +523 -0
  129. package/src/tools/level.ts +410 -0
  130. package/src/tools/lighting.ts +1316 -0
  131. package/src/tools/materials.ts +148 -0
  132. package/src/tools/niagara.ts +312 -0
  133. package/src/tools/performance.ts +549 -0
  134. package/src/tools/physics.ts +924 -0
  135. package/src/tools/rc.ts +437 -0
  136. package/src/tools/sequence.ts +791 -0
  137. package/src/tools/tool-definitions.ts +907 -0
  138. package/src/tools/tool-handlers.ts +941 -0
  139. package/src/tools/ui.ts +499 -0
  140. package/src/tools/visual.ts +60 -0
  141. package/src/types/env.ts +27 -0
  142. package/src/types/index.ts +414 -0
  143. package/src/types/tool-types.ts +343 -0
  144. package/src/unreal-bridge.ts +1118 -0
  145. package/src/utils/cache-manager.ts +213 -0
  146. package/src/utils/error-handler.ts +320 -0
  147. package/src/utils/errors.ts +312 -0
  148. package/src/utils/http.ts +184 -0
  149. package/src/utils/logger.ts +30 -0
  150. package/src/utils/normalize.ts +54 -0
  151. package/src/utils/response-validator.ts +145 -0
  152. package/src/utils/safe-json.ts +112 -0
  153. package/src/utils/stdio-redirect.ts +18 -0
  154. package/src/utils/validation.ts +212 -0
  155. package/tsconfig.json +33 -0
@@ -0,0 +1,181 @@
1
+ """
2
+ Compatibility module for handling deprecated Unreal Engine Python API calls.
3
+ Provides wrapper functions that use the newer recommended APIs where available.
4
+ """
5
+
6
+ import unreal
7
+
8
+ def get_editor_world():
9
+ """
10
+ Get the current editor world using the recommended API.
11
+ """
12
+ try:
13
+ # Use new recommended API
14
+ if hasattr(unreal, 'UnrealEditorSubsystem'):
15
+ subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
16
+ if hasattr(subsystem, 'get_editor_world'):
17
+ return subsystem.get_editor_world()
18
+ except:
19
+ pass
20
+
21
+ return None
22
+
23
+ def get_all_level_actors():
24
+ """
25
+ Get all actors in the current level using the recommended API.
26
+ """
27
+ try:
28
+ # Use new recommended API - EditorActorSubsystem
29
+ if hasattr(unreal, 'EditorActorSubsystem'):
30
+ subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
31
+ if hasattr(subsystem, 'get_all_level_actors'):
32
+ return subsystem.get_all_level_actors()
33
+ except:
34
+ pass
35
+
36
+ return []
37
+
38
+ def spawn_actor_from_class(actor_class, location, rotation):
39
+ """
40
+ Spawn an actor in the level using the recommended API.
41
+ """
42
+ try:
43
+ # Use new recommended API
44
+ if hasattr(unreal, 'EditorActorSubsystem'):
45
+ subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
46
+ if hasattr(subsystem, 'spawn_actor_from_class'):
47
+ return subsystem.spawn_actor_from_class(actor_class, location, rotation)
48
+ except:
49
+ pass
50
+
51
+ return None
52
+
53
+ def destroy_actor(actor):
54
+ """
55
+ Destroy an actor in the level using the recommended API.
56
+ """
57
+ try:
58
+ # Use new recommended API
59
+ if hasattr(unreal, 'EditorActorSubsystem'):
60
+ subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
61
+ if hasattr(subsystem, 'destroy_actor'):
62
+ return subsystem.destroy_actor(actor)
63
+ except:
64
+ pass
65
+
66
+ return False
67
+
68
+ def save_current_level():
69
+ """
70
+ Save the current level using the recommended API.
71
+ """
72
+ try:
73
+ # Use new recommended API - LevelEditorSubsystem
74
+ if hasattr(unreal, 'LevelEditorSubsystem'):
75
+ subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)
76
+ if hasattr(subsystem, 'save_current_level'):
77
+ return subsystem.save_current_level()
78
+ except:
79
+ pass
80
+
81
+ return False
82
+
83
+ def get_level_viewport_camera_info():
84
+ """
85
+ Get level viewport camera information using the recommended API.
86
+ """
87
+ try:
88
+ # Use new recommended API
89
+ if hasattr(unreal, 'UnrealEditorSubsystem'):
90
+ subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)
91
+ if hasattr(subsystem, 'get_level_viewport_camera_info'):
92
+ return subsystem.get_level_viewport_camera_info()
93
+ except:
94
+ pass
95
+
96
+ return None
97
+
98
+ # Blueprint compatibility functions
99
+ def get_blueprint_generated_class(blueprint):
100
+ """
101
+ Get the generated class from a blueprint using the recommended API.
102
+ """
103
+ try:
104
+ # Try using BlueprintEditorLibrary
105
+ if hasattr(unreal, 'BlueprintEditorLibrary'):
106
+ return unreal.BlueprintEditorLibrary.generated_class(blueprint)
107
+ except:
108
+ pass
109
+
110
+ # Try getting it as a property (might not work)
111
+ try:
112
+ return blueprint.get_editor_property('generated_class')
113
+ except:
114
+ pass
115
+
116
+ return None
117
+
118
+ def get_blueprint_parent_class(blueprint):
119
+ """
120
+ Get the parent class from a blueprint.
121
+ """
122
+ try:
123
+ # Try getting it as a property
124
+ return blueprint.get_editor_property('parent_class')
125
+ except:
126
+ pass
127
+
128
+ # Default to Actor for most blueprints
129
+ return unreal.Actor if hasattr(unreal, 'Actor') else None
130
+
131
+ def get_class_name(unreal_class):
132
+ """
133
+ Get the name of an Unreal class object safely.
134
+ """
135
+ if not unreal_class:
136
+ return "None"
137
+
138
+ # Try various methods to get the class name
139
+ try:
140
+ if hasattr(unreal_class, '__name__'):
141
+ return unreal_class.__name__
142
+ except:
143
+ pass
144
+
145
+ try:
146
+ if hasattr(unreal_class, 'get_name'):
147
+ # get_name() is a method on instances, not classes
148
+ # So we need to be careful here
149
+ return str(unreal_class).split('.')[-1].replace("'", "").replace('>', '')
150
+ except:
151
+ pass
152
+
153
+ # Fallback to string representation
154
+ class_str = str(unreal_class)
155
+ if '.' in class_str:
156
+ return class_str.split('.')[-1].replace("'", "").replace('>', '')
157
+
158
+ return class_str
159
+
160
+ def ensure_kismet_system_library():
161
+ """
162
+ Try to import KismetSystemLibrary if available.
163
+ Note: This library might not be available in all UE versions or configurations.
164
+ """
165
+ try:
166
+ # KismetSystemLibrary is part of the BlueprintGraph module
167
+ # It might not be exposed to Python in all versions
168
+ if hasattr(unreal, 'KismetSystemLibrary'):
169
+ return unreal.KismetSystemLibrary
170
+
171
+ # Try alternate import methods
172
+ import importlib
173
+ try:
174
+ kismet = importlib.import_module('unreal.KismetSystemLibrary')
175
+ return kismet
176
+ except:
177
+ pass
178
+ except:
179
+ pass
180
+
181
+ return None
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Python Utilities for Modern Unreal Engine API
3
+ *
4
+ * This module provides Python code snippets that use the modern
5
+ * Unreal Engine Python API instead of deprecated functions.
6
+ */
7
+
8
+ export class PythonUtils {
9
+ /**
10
+ * Get all actors in the level using modern API
11
+ * @returns Python code to get all level actors
12
+ */
13
+ static getAllLevelActors(): string {
14
+ return `
15
+ # Use modern EditorActorSubsystem instead of deprecated EditorLevelLibrary
16
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
17
+ actors = editor_actor_subsystem.get_all_level_actors()
18
+ `.trim();
19
+ }
20
+
21
+ /**
22
+ * Get selected actors using modern API
23
+ * @returns Python code to get selected actors
24
+ */
25
+ static getSelectedActors(): string {
26
+ return `
27
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
28
+ selected_actors = editor_actor_subsystem.get_selected_level_actors()
29
+ `.trim();
30
+ }
31
+
32
+ /**
33
+ * Spawn actor from class using modern API
34
+ * @returns Python code to spawn actor
35
+ */
36
+ static spawnActorFromClass(): string {
37
+ return `
38
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
39
+ # Use spawn_actor_from_class for spawning
40
+ `.trim();
41
+ }
42
+
43
+ /**
44
+ * Get a safe way to access actors
45
+ * @returns Python code with modern API
46
+ */
47
+ static getSafeActorAccess(): string {
48
+ return `
49
+ # Use modern API
50
+ try:
51
+ editor_actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
52
+ actors = editor_actor_subsystem.get_all_level_actors() if editor_actor_subsystem else []
53
+ except:
54
+ actors = []
55
+ `.trim();
56
+ }
57
+ }
@@ -0,0 +1,92 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+
3
+ interface CacheEntry {
4
+ data: any;
5
+ timestamp: number;
6
+ }
7
+
8
+ export class ActorResources {
9
+ private cache = new Map<string, CacheEntry>();
10
+ private readonly CACHE_TTL_MS = 5000; // 5 seconds cache for actors (they change more frequently)
11
+
12
+ constructor(private bridge: UnrealBridge) {}
13
+
14
+ private getFromCache(key: string): any | null {
15
+ const entry = this.cache.get(key);
16
+ if (entry && (Date.now() - entry.timestamp) < this.CACHE_TTL_MS) {
17
+ return entry.data;
18
+ }
19
+ this.cache.delete(key);
20
+ return null;
21
+ }
22
+
23
+ private setCache(key: string, data: any): void {
24
+ this.cache.set(key, { data, timestamp: Date.now() });
25
+ }
26
+
27
+ async listActors() {
28
+ // Check cache first
29
+ const cached = this.getFromCache('listActors');
30
+ if (cached !== null) {
31
+ return cached;
32
+ }
33
+
34
+ // Use Python to get actors via EditorActorSubsystem
35
+ try {
36
+ const pythonCode = `
37
+ import unreal
38
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
39
+ actors = actor_subsystem.get_all_level_actors()
40
+ actor_list = []
41
+ for actor in actors:
42
+ if actor:
43
+ actor_list.append({
44
+ 'name': actor.get_name(),
45
+ 'class': actor.get_class().get_name(),
46
+ 'path': actor.get_path_name()
47
+ })
48
+ print(f"Found {len(actor_list)} actors")
49
+ `.trim();
50
+
51
+ const result = await this.bridge.executePython(pythonCode);
52
+ this.setCache('listActors', result);
53
+ return result;
54
+ } catch (err) {
55
+ return { error: `Failed to list actors: ${err}` };
56
+ }
57
+ }
58
+
59
+ async getActorByName(actorName: string) {
60
+ // GetActorOfClass expects a class, not a name. Use Python to find by name
61
+ try {
62
+ const pythonCode = `
63
+ import unreal
64
+ actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
65
+ actors = actor_subsystem.get_all_level_actors()
66
+ for actor in actors:
67
+ if actor and actor.get_name() == "${actorName}":
68
+ print(f"Found actor: {actor.get_path_name()}")
69
+ break
70
+ else:
71
+ print(f"Actor not found: ${actorName}")
72
+ `.trim();
73
+
74
+ const result = await this.bridge.executePython(pythonCode);
75
+ return result;
76
+ } catch (err) {
77
+ return { error: `Failed to get actor: ${err}` };
78
+ }
79
+ }
80
+
81
+ async getActorTransform(actorPath: string) {
82
+ try {
83
+ const res = await this.bridge.httpCall('/remote/object/property', 'GET', {
84
+ objectPath: actorPath,
85
+ propertyName: 'ActorTransform'
86
+ });
87
+ return res;
88
+ } catch (err) {
89
+ return { error: `Failed to get transform: ${err}` };
90
+ }
91
+ }
92
+ }
@@ -0,0 +1,251 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+
3
+ export class AssetResources {
4
+ constructor(private bridge: UnrealBridge) {}
5
+
6
+ // Simple in-memory cache for asset listing
7
+ private cache = new Map<string, { timestamp: number; data: any }>();
8
+ private get ttlMs(): number { return Number(process.env.ASSET_LIST_TTL_MS || 10000); }
9
+ private makeKey(dir: string, recursive: boolean, page?: number) {
10
+ return page !== undefined ? `${dir}::${recursive ? 1 : 0}::${page}` : `${dir}::${recursive ? 1 : 0}`;
11
+ }
12
+
13
+ async list(dir = '/Game', recursive = false, limit = 50) {
14
+ // ALWAYS use non-recursive listing to show only immediate children
15
+ // This prevents timeouts and makes navigation clearer
16
+ recursive = false; // Force non-recursive
17
+
18
+ // Cache fast-path
19
+ try {
20
+ const key = this.makeKey(dir, false);
21
+ const entry = this.cache.get(key);
22
+ const now = Date.now();
23
+ if (entry && (now - entry.timestamp) < this.ttlMs) {
24
+ return entry.data;
25
+ }
26
+ } catch {}
27
+
28
+ // Check if bridge is connected
29
+ if (!this.bridge.isConnected) {
30
+ return {
31
+ assets: [],
32
+ warning: 'Unreal Engine is not connected. Please ensure Unreal Engine is running with Remote Control enabled.',
33
+ connectionStatus: 'disconnected'
34
+ };
35
+ }
36
+
37
+ // Always use directory-only listing (immediate children)
38
+ return this.listDirectoryOnly(dir, false, limit);
39
+ // End of list method - all logic is now in listDirectoryOnly
40
+ }
41
+
42
+ /**
43
+ * List assets with pagination support
44
+ * @param dir Directory to list assets from
45
+ * @param page Page number (0-based)
46
+ * @param pageSize Number of assets per page (max 50 to avoid socket failures)
47
+ */
48
+ async listPaged(dir = '/Game', page = 0, pageSize = 30, recursive = false) {
49
+ // Ensure pageSize doesn't exceed safe limit
50
+ const safePageSize = Math.min(pageSize, 50);
51
+ const offset = page * safePageSize;
52
+
53
+ // Check cache for this specific page
54
+ const cacheKey = this.makeKey(dir, recursive, page);
55
+ const cached = this.cache.get(cacheKey);
56
+ if (cached && (Date.now() - cached.timestamp) < this.ttlMs) {
57
+ return cached.data;
58
+ }
59
+
60
+ if (!this.bridge.isConnected) {
61
+ return {
62
+ assets: [],
63
+ page,
64
+ pageSize: safePageSize,
65
+ warning: 'Unreal Engine is not connected.',
66
+ connectionStatus: 'disconnected'
67
+ };
68
+ }
69
+
70
+ try {
71
+ // Use search API with pagination
72
+ // Use the same directory listing approach but with pagination
73
+ const allAssets = await this.listDirectoryOnly(dir, false, 1000);
74
+
75
+ // Paginate the results
76
+ const start = offset;
77
+ const end = offset + safePageSize;
78
+ const pagedAssets = allAssets.assets ? allAssets.assets.slice(start, end) : [];
79
+
80
+ const result = {
81
+ assets: pagedAssets,
82
+ page,
83
+ pageSize: safePageSize,
84
+ count: pagedAssets.length,
85
+ totalCount: allAssets.assets ? allAssets.assets.length : 0,
86
+ hasMore: end < (allAssets.assets ? allAssets.assets.length : 0),
87
+ method: 'directory_listing_paged'
88
+ };
89
+
90
+ this.cache.set(cacheKey, { timestamp: Date.now(), data: result });
91
+ return result;
92
+ } catch (err: any) {
93
+ console.warn(`Asset listing page ${page} failed:`, err.message);
94
+ }
95
+
96
+ return {
97
+ assets: [],
98
+ page,
99
+ pageSize: safePageSize,
100
+ error: 'Failed to fetch page'
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Directory-based listing for paths with too many assets
106
+ * Shows only immediate children (folders and files) to avoid timeouts
107
+ */
108
+ private async listDirectoryOnly(dir: string, recursive: boolean, limit: number) {
109
+ // Always return only immediate children to avoid timeout and improve navigation
110
+ try {
111
+ const py = `
112
+ import unreal
113
+ import json
114
+
115
+ _dir = r"${dir}"
116
+
117
+ try:
118
+ # ALWAYS non-recursive - get only immediate children
119
+ all_paths = unreal.EditorAssetLibrary.list_assets(_dir, False, False)
120
+
121
+ # Organize into immediate children only
122
+ immediate_folders = set()
123
+ immediate_assets = []
124
+
125
+ for path in all_paths:
126
+ # Remove the base directory to get relative path
127
+ relative = path.replace(_dir, '').strip('/')
128
+ if not relative:
129
+ continue
130
+
131
+ # Split to check depth
132
+ parts = relative.split('/')
133
+
134
+ if len(parts) == 1:
135
+ # This is an immediate child asset
136
+ immediate_assets.append(path)
137
+ elif len(parts) > 1:
138
+ # This indicates a subfolder exists
139
+ immediate_folders.add(parts[0])
140
+
141
+ result = []
142
+
143
+ # Add folders first
144
+ for folder in sorted(immediate_folders):
145
+ result.append({
146
+ 'n': folder,
147
+ 'p': _dir + '/' + folder,
148
+ 'c': 'Folder',
149
+ 'isFolder': True
150
+ })
151
+
152
+ # Add immediate assets (limit to prevent socket issues)
153
+ for asset_path in immediate_assets[:min(${limit}, len(immediate_assets))]:
154
+ name = asset_path.split('/')[-1].split('.')[0]
155
+ result.append({
156
+ 'n': name,
157
+ 'p': asset_path,
158
+ 'c': 'Asset'
159
+ })
160
+
161
+ # Always showing immediate children only
162
+ note = f'Showing immediate children of {_dir} ({len(immediate_folders)} folders, {len(immediate_assets)} files)'
163
+
164
+ print("RESULT:" + json.dumps({
165
+ 'success': True,
166
+ 'assets': result,
167
+ 'count': len(result),
168
+ 'folders': len(immediate_folders),
169
+ 'files': len(immediate_assets),
170
+ 'note': note
171
+ }))
172
+ except Exception as e:
173
+ print("RESULT:" + json.dumps({'success': False, 'error': str(e), 'assets': []}))
174
+ `.trim();
175
+
176
+ const resp = await this.bridge.executePython(py);
177
+ let output = '';
178
+ if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
179
+ output = resp.LogOutput.map((l: any) => l.Output || '').join('');
180
+ } else if (typeof resp === 'string') {
181
+ output = resp;
182
+ } else {
183
+ output = JSON.stringify(resp);
184
+ }
185
+
186
+ const m = output.match(/RESULT:({.*})/);
187
+ if (m) {
188
+ try {
189
+ const parsed = JSON.parse(m[1]);
190
+ if (parsed.success) {
191
+ // Transform to standard format
192
+ const assets = parsed.assets.map((a: any) => ({
193
+ Name: a.n,
194
+ Path: a.p,
195
+ Class: a.c,
196
+ isFolder: a.isFolder || false
197
+ }));
198
+
199
+ return {
200
+ assets,
201
+ count: parsed.count,
202
+ folders: parsed.folders,
203
+ files: parsed.files,
204
+ note: parsed.note,
205
+ method: 'directory_listing'
206
+ };
207
+ }
208
+ } catch {}
209
+ }
210
+ } catch (err: any) {
211
+ console.warn('Engine asset listing failed:', err.message);
212
+ }
213
+
214
+ // Fallback: return empty with explanation
215
+ return {
216
+ assets: [],
217
+ warning: 'Directory contains too many assets. Showing immediate children only.',
218
+ suggestion: 'Navigate to specific subdirectories for detailed listings.',
219
+ method: 'directory_timeout_fallback'
220
+ };
221
+ }
222
+
223
+ async find(assetPath: string) {
224
+ // Guard against invalid paths (trailing slash, empty, whitespace)
225
+ if (!assetPath || typeof assetPath !== 'string' || assetPath.trim() === '' || assetPath.endsWith('/')) {
226
+ return false;
227
+ }
228
+
229
+ const py = `
230
+ import unreal
231
+ apath = r"${assetPath}"
232
+ try:
233
+ exists = unreal.EditorAssetLibrary.does_asset_exist(apath)
234
+ print("RESULT:{'success': True, 'exists': %s}" % ('True' if exists else 'False'))
235
+ except Exception as e:
236
+ print("RESULT:{'success': False, 'error': '" + str(e) + "'}")
237
+ `.trim();
238
+ const resp = await this.bridge.executePython(py);
239
+ let output = '';
240
+ if (resp?.LogOutput && Array.isArray(resp.LogOutput)) output = resp.LogOutput.map((l: any) => l.Output || '').join('');
241
+ else if (typeof resp === 'string') output = resp; else output = JSON.stringify(resp);
242
+ const m = output.match(/RESULT:({.*})/);
243
+ if (m) {
244
+ try {
245
+ const parsed = JSON.parse(m[1].replace(/'/g, '"'));
246
+ if (parsed.success) return !!parsed.exists;
247
+ } catch {}
248
+ }
249
+ return false;
250
+ }
251
+ }
@@ -0,0 +1,83 @@
1
+ import { UnrealBridge } from '../unreal-bridge.js';
2
+
3
+ export class LevelResources {
4
+ constructor(private bridge: UnrealBridge) {}
5
+
6
+ async getCurrentLevel() {
7
+ // Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary
8
+ try {
9
+ const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n name = world.get_name() if world else \'None\'\n path = world.get_path_name() if world else \'None\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'name\': name, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
10
+ const resp: any = await this.bridge.executePython(py);
11
+ // Handle LogOutput format from executePython
12
+ let out = '';
13
+ if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
14
+ out = resp.LogOutput.map((log: any) => log.Output || '').join('');
15
+ } else if (typeof resp === 'string') {
16
+ out = resp;
17
+ } else {
18
+ out = JSON.stringify(resp);
19
+ }
20
+ const m = out.match(/RESULT:({.*})/);
21
+ if (m) {
22
+ const parsed = JSON.parse(m[1]);
23
+ if (parsed.success) return parsed;
24
+ }
25
+ // If Python failed, return error
26
+ return { error: 'Failed to get current level', success: false };
27
+ } catch (err) {
28
+ return { error: `Failed to get current level: ${err}`, success: false };
29
+ }
30
+ }
31
+
32
+ async getLevelName() {
33
+ // Return camera/world info via Python first
34
+ try {
35
+ const py = '\nimport unreal, json\ntry:\n # Use UnrealEditorSubsystem instead of deprecated EditorLevelLibrary\n editor_subsys = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem)\n world = editor_subsys.get_editor_world()\n path = world.get_path_name() if world else \'\'\n print(\'RESULT:\' + json.dumps({\'success\': True, \'path\': path}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
36
+ const resp: any = await this.bridge.executePython(py);
37
+ // Handle LogOutput format from executePython
38
+ let out = '';
39
+ if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
40
+ out = resp.LogOutput.map((log: any) => log.Output || '').join('');
41
+ } else if (typeof resp === 'string') {
42
+ out = resp;
43
+ } else {
44
+ out = JSON.stringify(resp);
45
+ }
46
+ const m = out.match(/RESULT:({.*})/);
47
+ if (m) {
48
+ const parsed = JSON.parse(m[1]);
49
+ if (parsed.success) return parsed;
50
+ }
51
+ // If Python failed, return error
52
+ return { error: 'Failed to get level name', success: false };
53
+ } catch (err) {
54
+ return { error: `Failed to get level name: ${err}`, success: false };
55
+ }
56
+ }
57
+
58
+ async saveCurrentLevel() {
59
+ // Prefer Python save (or LevelEditorSubsystem) then fallback
60
+ try {
61
+ const py = '\nimport unreal, json\ntry:\n les = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem)\n if les: les.save_current_level()\n else: unreal.EditorLevelLibrary.save_current_level()\n print(\'RESULT:\' + json.dumps({\'success\': True}))\nexcept Exception as e:\n print(\'RESULT:\' + json.dumps({\'success\': False, \'error\': str(e)}))\n'.trim();
62
+ const resp: any = await this.bridge.executePython(py);
63
+ // Handle LogOutput format from executePython
64
+ let out = '';
65
+ if (resp?.LogOutput && Array.isArray(resp.LogOutput)) {
66
+ out = resp.LogOutput.map((log: any) => log.Output || '').join('');
67
+ } else if (typeof resp === 'string') {
68
+ out = resp;
69
+ } else {
70
+ out = JSON.stringify(resp);
71
+ }
72
+ const m = out.match(/RESULT:({.*})/);
73
+ if (m) {
74
+ const parsed = JSON.parse(m[1]);
75
+ if (parsed.success) return { success: true, message: 'Level saved' };
76
+ }
77
+ // If Python failed, return error
78
+ return { error: 'Failed to save level', success: false };
79
+ } catch (err) {
80
+ return { error: `Failed to save level: ${err}`, success: false };
81
+ }
82
+ }
83
+ }