ultimate-unreal-engine-mcp 0.1.0
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 +729 -0
- package/dist/build/error-parser.js +51 -0
- package/dist/build/fix-suggester.js +84 -0
- package/dist/build/ubt-runner.js +146 -0
- package/dist/cli.js +13 -0
- package/dist/config.js +8 -0
- package/dist/docs/data/ue57-api.js +228 -0
- package/dist/docs/doc-index.js +110 -0
- package/dist/docs/types.js +4 -0
- package/dist/generators/class-generator.js +363 -0
- package/dist/generators/file-modifier.js +276 -0
- package/dist/generators/uht-validator.js +177 -0
- package/dist/index.js +89 -0
- package/dist/parsers/cpp-class-index.js +230 -0
- package/dist/parsers/cpp-parser.js +369 -0
- package/dist/parsers/ini-parser.js +216 -0
- package/dist/parsers/uproject-parser.js +130 -0
- package/dist/plugin-bridge/client.js +217 -0
- package/dist/plugin-bridge/protocol.js +6 -0
- package/dist/plugin-bridge/retry.js +23 -0
- package/dist/setup.js +209 -0
- package/dist/tools/ai-systems/index.js +247 -0
- package/dist/tools/ai-systems/types.js +4 -0
- package/dist/tools/animation/index.js +241 -0
- package/dist/tools/animation/types.js +4 -0
- package/dist/tools/audio/index.js +204 -0
- package/dist/tools/audio/types.js +4 -0
- package/dist/tools/blueprint/index.js +495 -0
- package/dist/tools/blueprint/types.js +4 -0
- package/dist/tools/build/index.js +163 -0
- package/dist/tools/chaos/index.js +230 -0
- package/dist/tools/chaos/types.js +4 -0
- package/dist/tools/collision-physics/index.js +211 -0
- package/dist/tools/config/index.js +288 -0
- package/dist/tools/cpp/index.js +305 -0
- package/dist/tools/docs/index.js +251 -0
- package/dist/tools/editor/index.js +242 -0
- package/dist/tools/gas/index.js +222 -0
- package/dist/tools/gas/types.js +5 -0
- package/dist/tools/import-export/index.js +218 -0
- package/dist/tools/input/index.js +146 -0
- package/dist/tools/known-issues/index.js +88 -0
- package/dist/tools/known-issues/middleware.js +55 -0
- package/dist/tools/known-issues/store.js +125 -0
- package/dist/tools/livelink/index.js +203 -0
- package/dist/tools/livelink/types.js +4 -0
- package/dist/tools/material/index.js +190 -0
- package/dist/tools/motion-design/index.js +251 -0
- package/dist/tools/motion-design/types.js +6 -0
- package/dist/tools/movie-render/index.js +220 -0
- package/dist/tools/networking/index.js +149 -0
- package/dist/tools/pcg/index.js +164 -0
- package/dist/tools/selection/index.js +180 -0
- package/dist/tools/sequencer/index.js +218 -0
- package/dist/tools/validation/index.js +183 -0
- package/dist/tools/validation/types.js +4 -0
- package/dist/tools/viewport/index.js +310 -0
- package/dist/tools/worldpartition/index.js +226 -0
- package/dist/tools/worldpartition/types.js +4 -0
- package/dist/utils/execFileNoThrow.js +40 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/path-guard.js +26 -0
- package/package.json +40 -0
- package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
- package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPTcpServer.h +59 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/parsers/cpp-class-index.ts
|
|
2
|
+
// Project-wide class scanner and query engine for UE C++ headers.
|
|
3
|
+
//
|
|
4
|
+
// Scans Source/**/*.h files, parses each with cpp-parser.ts, and provides:
|
|
5
|
+
// - buildIndex(projectRoot): scans disk with mtime caching
|
|
6
|
+
// - buildIndexFromMap(files): in-memory variant for testing (no fs I/O)
|
|
7
|
+
// - CppClassIndex: query methods (getClassHierarchy, getIncludes, findClassFile)
|
|
8
|
+
// - clearIndexCache(): reset mtime cache between test runs
|
|
9
|
+
//
|
|
10
|
+
// Design rules (Phase 4 / CONTEXT.md):
|
|
11
|
+
// - No console.log — all log output via console.error only
|
|
12
|
+
// - T-04-04 mitigated: cycle detection via visited Set in getClassHierarchy
|
|
13
|
+
// - T-04-05: mtime caching keeps repeated calls O(1) per unchanged file
|
|
14
|
+
// - buildIndex() returns IndexBuildResult discriminated union — never throws
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { parseCppFile } from './cpp-parser.js';
|
|
18
|
+
const mtimeCache = new Map();
|
|
19
|
+
/**
|
|
20
|
+
* Clears the mtime cache. Used in tests to ensure isolation between runs.
|
|
21
|
+
*/
|
|
22
|
+
export function clearIndexCache() {
|
|
23
|
+
mtimeCache.clear();
|
|
24
|
+
}
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Internal: build CppClassIndex from a Map<className, ClassIndexEntry>
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
function makeCppClassIndex(entries) {
|
|
29
|
+
return {
|
|
30
|
+
entries,
|
|
31
|
+
/**
|
|
32
|
+
* Returns the inheritance chain starting from className.
|
|
33
|
+
* Stops when parent is not in index or a cycle is detected.
|
|
34
|
+
* T-04-04: visited Set prevents infinite loop on circular inheritance.
|
|
35
|
+
*
|
|
36
|
+
* Rule: always add the starting class. For subsequent parents, only add
|
|
37
|
+
* them if they exist in the index (stop without adding unknown parents).
|
|
38
|
+
* Exception for cycle detection: if a class appears in visited, stop
|
|
39
|
+
* without adding it again.
|
|
40
|
+
*/
|
|
41
|
+
getClassHierarchy(className) {
|
|
42
|
+
const chain = [];
|
|
43
|
+
const visited = new Set();
|
|
44
|
+
// Always add the starting class regardless of whether it's in the index
|
|
45
|
+
chain.push(className);
|
|
46
|
+
visited.add(className);
|
|
47
|
+
// Look up the entry for the starting class
|
|
48
|
+
let entry = entries.get(className);
|
|
49
|
+
if (!entry || !entry.parentClass) {
|
|
50
|
+
return chain;
|
|
51
|
+
}
|
|
52
|
+
let current = entry.parentClass;
|
|
53
|
+
while (true) {
|
|
54
|
+
if (visited.has(current)) {
|
|
55
|
+
// Cycle detected — stop WITHOUT adding the duplicate
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
const currentEntry = entries.get(current);
|
|
59
|
+
if (!currentEntry) {
|
|
60
|
+
// Parent not in index — stop WITHOUT adding it to chain
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
// Parent is in index — add it and continue traversal
|
|
64
|
+
chain.push(current);
|
|
65
|
+
visited.add(current);
|
|
66
|
+
if (!currentEntry.parentClass) {
|
|
67
|
+
// No further parent — chain ends here
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
current = currentEntry.parentClass;
|
|
71
|
+
}
|
|
72
|
+
return chain;
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* Returns the #include list for the entry's header.
|
|
76
|
+
* Returns [] if className is not in the index.
|
|
77
|
+
*/
|
|
78
|
+
getIncludes(className) {
|
|
79
|
+
const entry = entries.get(className);
|
|
80
|
+
return entry ? entry.includes : [];
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Returns header and source paths for a class.
|
|
84
|
+
* Returns null if className is not in the index.
|
|
85
|
+
*/
|
|
86
|
+
findClassFile(className) {
|
|
87
|
+
const entry = entries.get(className);
|
|
88
|
+
if (!entry)
|
|
89
|
+
return null;
|
|
90
|
+
return { header: entry.headerPath, source: entry.sourcePath };
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Internal: parse a single header content and produce ClassIndexEntry[]
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
function buildEntriesFromContent(headerPath, content, sourcePath) {
|
|
98
|
+
const parseResult = parseCppFile(content);
|
|
99
|
+
if (!parseResult.success) {
|
|
100
|
+
return { entries: [], errorCount: 1 };
|
|
101
|
+
}
|
|
102
|
+
const { classes, includes } = parseResult.data;
|
|
103
|
+
const entries = classes.map((cls) => ({
|
|
104
|
+
className: cls.name,
|
|
105
|
+
headerPath,
|
|
106
|
+
sourcePath: sourcePath(cls.name),
|
|
107
|
+
parentClass: cls.parentClass,
|
|
108
|
+
includes,
|
|
109
|
+
specifiers: cls.specifiers,
|
|
110
|
+
}));
|
|
111
|
+
return { entries, errorCount: 0 };
|
|
112
|
+
}
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// buildIndexFromMap — in-memory test variant (no fs I/O)
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
/**
|
|
117
|
+
* Builds a CppClassIndex from an in-memory map of { absoluteFilePath: fileContent }.
|
|
118
|
+
* Does NOT touch the filesystem — intended for testing.
|
|
119
|
+
* No mtime caching in this variant (always parses fresh).
|
|
120
|
+
*/
|
|
121
|
+
export async function buildIndexFromMap(files) {
|
|
122
|
+
const allEntries = new Map();
|
|
123
|
+
let fileCount = 0;
|
|
124
|
+
let errorCount = 0;
|
|
125
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
126
|
+
fileCount++;
|
|
127
|
+
// Derive the directory for potential .cpp companion lookup
|
|
128
|
+
const dir = path.dirname(filePath);
|
|
129
|
+
const { entries, errorCount: fileErrors } = buildEntriesFromContent(filePath, content, (className) => {
|
|
130
|
+
// In-memory: check if a .cpp file exists in the provided map
|
|
131
|
+
const candidateCpp = path.join(dir, `${className}.cpp`);
|
|
132
|
+
return files[candidateCpp] !== undefined ? candidateCpp : null;
|
|
133
|
+
});
|
|
134
|
+
errorCount += fileErrors;
|
|
135
|
+
for (const entry of entries) {
|
|
136
|
+
allEntries.set(entry.className, entry);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
success: true,
|
|
141
|
+
index: makeCppClassIndex(allEntries),
|
|
142
|
+
fileCount,
|
|
143
|
+
errorCount,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// buildIndex — real filesystem variant with mtime caching
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
/**
|
|
150
|
+
* Scans {projectRoot}/Source/**\/*.h, parses each header, and builds an index.
|
|
151
|
+
*
|
|
152
|
+
* Returns IndexBuildResult — never throws.
|
|
153
|
+
* - success=true even if individual headers fail (errorCount tracks failures)
|
|
154
|
+
* - success=false only if the Source/ directory itself cannot be read
|
|
155
|
+
*
|
|
156
|
+
* Mtime caching: parsed results are cached per absolute path. Re-scanning an
|
|
157
|
+
* unchanged file (same mtime) reuses cached entries without re-parsing.
|
|
158
|
+
*/
|
|
159
|
+
export async function buildIndex(projectRoot) {
|
|
160
|
+
const sourceDir = path.join(projectRoot, 'Source');
|
|
161
|
+
let files;
|
|
162
|
+
try {
|
|
163
|
+
const allEntries = await fs.promises.readdir(sourceDir, { recursive: true });
|
|
164
|
+
files = allEntries
|
|
165
|
+
.filter((f) => typeof f === 'string' && f.endsWith('.h'))
|
|
166
|
+
.map((f) => path.join(sourceDir, f));
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
170
|
+
return { success: false, error: `Cannot read Source/ directory: ${msg}` };
|
|
171
|
+
}
|
|
172
|
+
const allEntries = new Map();
|
|
173
|
+
let fileCount = 0;
|
|
174
|
+
let errorCount = 0;
|
|
175
|
+
for (const headerPath of files) {
|
|
176
|
+
fileCount++;
|
|
177
|
+
// Stat for mtime caching
|
|
178
|
+
let mtime;
|
|
179
|
+
try {
|
|
180
|
+
const stat = await fs.promises.stat(headerPath);
|
|
181
|
+
mtime = stat.mtimeMs;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Cannot stat — skip with error
|
|
185
|
+
errorCount++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const cached = mtimeCache.get(headerPath);
|
|
189
|
+
let entries;
|
|
190
|
+
if (cached && cached.mtime === mtime) {
|
|
191
|
+
// Cache hit — reuse without re-parsing
|
|
192
|
+
entries = cached.entries;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
// Cache miss — read and parse
|
|
196
|
+
let content;
|
|
197
|
+
try {
|
|
198
|
+
content = await fs.promises.readFile(headerPath, 'utf-8');
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
errorCount++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const dir = path.dirname(headerPath);
|
|
205
|
+
const result = buildEntriesFromContent(headerPath, content, (className) => {
|
|
206
|
+
const candidateCpp = path.join(dir, `${className}.cpp`);
|
|
207
|
+
try {
|
|
208
|
+
fs.accessSync(candidateCpp);
|
|
209
|
+
return candidateCpp;
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
errorCount += result.errorCount;
|
|
216
|
+
entries = result.entries;
|
|
217
|
+
// Update cache
|
|
218
|
+
mtimeCache.set(headerPath, { mtime, entries });
|
|
219
|
+
}
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
allEntries.set(entry.className, entry);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
success: true,
|
|
226
|
+
index: makeCppClassIndex(allEntries),
|
|
227
|
+
fileCount,
|
|
228
|
+
errorCount,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// src/parsers/cpp-parser.ts
|
|
2
|
+
// Purpose-built TypeScript regex parser for UE C++ macro grammar.
|
|
3
|
+
//
|
|
4
|
+
// Parses a small, well-defined subset of C++ that UHT (Unreal Header Tool) emits:
|
|
5
|
+
// - UCLASS / UPROPERTY / UFUNCTION macros with specifiers and meta=(...)
|
|
6
|
+
// - Inheritance declarations: class AMyActor : public AActor
|
|
7
|
+
// - #include directives (both quoted and angled)
|
|
8
|
+
//
|
|
9
|
+
// This is NOT a general C++ parser — it targets UE macro patterns only.
|
|
10
|
+
// Design rules (Phase 4 context):
|
|
11
|
+
// - Pure string processing — no fs imports, no eval, never throws
|
|
12
|
+
// - Returns discriminated union ParseResult<T>
|
|
13
|
+
// - All output goes to console.error (never console.log)
|
|
14
|
+
// - T-04-02 mitigation: balanced-paren collector capped at 500 lines
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Internal helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const INCLUDE_RE = /^\s*#include\s+["<]([^">]+)[">]/;
|
|
19
|
+
const MACRO_START_RE = /^\s*(UCLASS|UPROPERTY|UFUNCTION)\s*\(/;
|
|
20
|
+
/**
|
|
21
|
+
* Collects a balanced-parenthesis macro argument string starting from the
|
|
22
|
+
* opening paren found at `startLine`. The `lines` array must have the opening
|
|
23
|
+
* paren on lines[startLine]. Returns the inner content (excluding outer parens)
|
|
24
|
+
* and the index of the line containing the matching closing paren.
|
|
25
|
+
*
|
|
26
|
+
* T-04-02: Capped at 500 lines to prevent runaway loops on pathological input.
|
|
27
|
+
*/
|
|
28
|
+
function collectMacroArgs(lines, startLine) {
|
|
29
|
+
let depth = 0;
|
|
30
|
+
let collecting = false;
|
|
31
|
+
const parts = [];
|
|
32
|
+
const MAX_LINES = 500;
|
|
33
|
+
for (let i = startLine; i < lines.length && i - startLine <= MAX_LINES; i++) {
|
|
34
|
+
const line = lines[i];
|
|
35
|
+
for (let j = 0; j < line.length; j++) {
|
|
36
|
+
const ch = line[j];
|
|
37
|
+
if (ch === '(') {
|
|
38
|
+
depth++;
|
|
39
|
+
collecting = true;
|
|
40
|
+
}
|
|
41
|
+
else if (ch === ')') {
|
|
42
|
+
depth--;
|
|
43
|
+
if (depth === 0 && collecting) {
|
|
44
|
+
// Capture everything collected so far as the full arg string
|
|
45
|
+
parts.push(line.slice(0, j));
|
|
46
|
+
// Remove the opening paren from the first segment
|
|
47
|
+
const joined = parts.join('\n');
|
|
48
|
+
// Strip everything up to and including the first '('
|
|
49
|
+
const openIdx = joined.indexOf('(');
|
|
50
|
+
const inner = openIdx >= 0 ? joined.slice(openIdx + 1) : joined;
|
|
51
|
+
return { args: inner.trim(), endLine: i };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (collecting) {
|
|
56
|
+
parts.push(line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Unclosed paren or exceeded 500 lines — return what we have
|
|
60
|
+
const joined = parts.join('\n');
|
|
61
|
+
const openIdx = joined.indexOf('(');
|
|
62
|
+
const inner = openIdx >= 0 ? joined.slice(openIdx + 1) : joined;
|
|
63
|
+
return { args: inner.trim(), endLine: Math.min(startLine + MAX_LINES, lines.length - 1) };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parses a meta=(...) block from specifier text.
|
|
67
|
+
* Example: 'meta=(ClampMin="0", ClampMax="100")' → { ClampMin: "0", ClampMax: "100" }
|
|
68
|
+
*/
|
|
69
|
+
function parseMeta(metaBlock) {
|
|
70
|
+
const result = {};
|
|
71
|
+
// metaBlock is the content inside the outer parens, e.g. 'ClampMin="0", ClampMax="100"'
|
|
72
|
+
const inner = metaBlock.replace(/^\s*meta\s*=\s*\(/, '').replace(/\)\s*$/, '').trim();
|
|
73
|
+
if (!inner)
|
|
74
|
+
return result;
|
|
75
|
+
// Split on commas that are not inside nested parens or quotes
|
|
76
|
+
const pairs = splitTopLevel(inner, ',');
|
|
77
|
+
for (const pair of pairs) {
|
|
78
|
+
const eqIdx = pair.indexOf('=');
|
|
79
|
+
if (eqIdx < 0)
|
|
80
|
+
continue;
|
|
81
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
82
|
+
const val = pair.slice(eqIdx + 1).trim().replace(/^"(.*)"$/, '$1');
|
|
83
|
+
if (key)
|
|
84
|
+
result[key] = val;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Splits a string on a delimiter, but only at the top level
|
|
90
|
+
* (not inside nested parentheses or double-quoted strings).
|
|
91
|
+
*/
|
|
92
|
+
function splitTopLevel(text, delimiter) {
|
|
93
|
+
const parts = [];
|
|
94
|
+
let depth = 0;
|
|
95
|
+
let inString = false;
|
|
96
|
+
let current = '';
|
|
97
|
+
for (let i = 0; i < text.length; i++) {
|
|
98
|
+
const ch = text[i];
|
|
99
|
+
if (ch === '"' && text[i - 1] !== '\\') {
|
|
100
|
+
inString = !inString;
|
|
101
|
+
current += ch;
|
|
102
|
+
}
|
|
103
|
+
else if (!inString && ch === '(') {
|
|
104
|
+
depth++;
|
|
105
|
+
current += ch;
|
|
106
|
+
}
|
|
107
|
+
else if (!inString && ch === ')') {
|
|
108
|
+
depth--;
|
|
109
|
+
current += ch;
|
|
110
|
+
}
|
|
111
|
+
else if (!inString && depth === 0 && text.slice(i, i + delimiter.length) === delimiter) {
|
|
112
|
+
parts.push(current.trim());
|
|
113
|
+
current = '';
|
|
114
|
+
i += delimiter.length - 1;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
current += ch;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (current.trim())
|
|
121
|
+
parts.push(current.trim());
|
|
122
|
+
return parts;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parses the args string from inside a UCLASS/UPROPERTY/UFUNCTION macro.
|
|
126
|
+
* Returns { specifiers, meta }.
|
|
127
|
+
* meta=(...) is extracted and NOT included in specifiers.
|
|
128
|
+
*/
|
|
129
|
+
function parseSpecifiersAndMeta(args) {
|
|
130
|
+
if (!args.trim())
|
|
131
|
+
return { specifiers: [], meta: {} };
|
|
132
|
+
const topLevel = splitTopLevel(args, ',');
|
|
133
|
+
const specifiers = [];
|
|
134
|
+
let meta = {};
|
|
135
|
+
for (const item of topLevel) {
|
|
136
|
+
const trimmed = item.trim();
|
|
137
|
+
if (!trimmed)
|
|
138
|
+
continue;
|
|
139
|
+
if (/^meta\s*=\s*\(/.test(trimmed)) {
|
|
140
|
+
meta = parseMeta(trimmed);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
specifiers.push(trimmed);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { specifiers, meta };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Finds the next non-blank line index after `fromLine`.
|
|
150
|
+
* Returns -1 if none found.
|
|
151
|
+
*/
|
|
152
|
+
function nextNonBlankLine(lines, fromLine) {
|
|
153
|
+
for (let i = fromLine + 1; i < lines.length; i++) {
|
|
154
|
+
if (lines[i].trim().length > 0)
|
|
155
|
+
return i;
|
|
156
|
+
}
|
|
157
|
+
return -1;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Parses a property declaration line like:
|
|
161
|
+
* "float Health;"
|
|
162
|
+
* "AActor* TargetActor;"
|
|
163
|
+
* "TArray<FHitResult> HitResults;"
|
|
164
|
+
* Returns { type, name } or null if unparseable.
|
|
165
|
+
*/
|
|
166
|
+
function parsePropertyDecl(declLine) {
|
|
167
|
+
const trimmed = declLine.trim();
|
|
168
|
+
// Remove trailing semicolons, UPROPERTY that might bleed in, etc.
|
|
169
|
+
const cleaned = trimmed.replace(/;.*$/, '').trim();
|
|
170
|
+
if (!cleaned)
|
|
171
|
+
return null;
|
|
172
|
+
// Handle template types: TArray<FHitResult> HitResults
|
|
173
|
+
// Strategy: find the last space-separated token as name, everything before as type
|
|
174
|
+
// But we must handle TArray<A, B> with inner commas
|
|
175
|
+
// Walk backwards through space-separated tokens respecting angle brackets
|
|
176
|
+
// Find last word boundary at depth 0 (not inside <> or ())
|
|
177
|
+
let depth = 0;
|
|
178
|
+
let nameStart = -1;
|
|
179
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
180
|
+
const ch = cleaned[i];
|
|
181
|
+
if (ch === '>' || ch === ')')
|
|
182
|
+
depth++;
|
|
183
|
+
else if (ch === '<' || ch === '(')
|
|
184
|
+
depth--;
|
|
185
|
+
else if (depth === 0 && ch === ' ') {
|
|
186
|
+
nameStart = i + 1;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (nameStart < 0)
|
|
191
|
+
return null;
|
|
192
|
+
const name = cleaned.slice(nameStart).trim();
|
|
193
|
+
const type = cleaned.slice(0, nameStart).trim();
|
|
194
|
+
if (!name || !type)
|
|
195
|
+
return null;
|
|
196
|
+
// Validate name is an identifier (word chars only)
|
|
197
|
+
if (!/^\w+$/.test(name))
|
|
198
|
+
return null;
|
|
199
|
+
return { type, name };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Parses a function declaration line like:
|
|
203
|
+
* "void Attack();"
|
|
204
|
+
* "void ServerFire(float Damage);"
|
|
205
|
+
* "FMyStruct GetStuff();"
|
|
206
|
+
* Returns { returnType, name } or null if unparseable.
|
|
207
|
+
*/
|
|
208
|
+
function parseFunctionDecl(declLine) {
|
|
209
|
+
const trimmed = declLine.trim();
|
|
210
|
+
// Match: returnType functionName(...)
|
|
211
|
+
// The function name is the last word before '('
|
|
212
|
+
const funcMatch = trimmed.match(/^(.*?)\s+(\w+)\s*\(/);
|
|
213
|
+
if (!funcMatch)
|
|
214
|
+
return null;
|
|
215
|
+
const returnType = funcMatch[1].trim();
|
|
216
|
+
const name = funcMatch[2].trim();
|
|
217
|
+
if (!returnType || !name)
|
|
218
|
+
return null;
|
|
219
|
+
return { returnType, name };
|
|
220
|
+
}
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Main parse function
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
/**
|
|
225
|
+
* Parses the content of a UE C++ header file.
|
|
226
|
+
*
|
|
227
|
+
* Returns ParseResult<ParsedCppFile> — never throws.
|
|
228
|
+
* T-04-01: Pure string processing, no eval/exec.
|
|
229
|
+
*/
|
|
230
|
+
export function parseCppFile(content) {
|
|
231
|
+
try {
|
|
232
|
+
// Guard against null/undefined input
|
|
233
|
+
if (content === null || content === undefined) {
|
|
234
|
+
return { success: true, data: { classes: [], includes: [], rawContent: '' } };
|
|
235
|
+
}
|
|
236
|
+
const rawContent = String(content);
|
|
237
|
+
if (!rawContent.trim()) {
|
|
238
|
+
return { success: true, data: { classes: [], includes: [], rawContent } };
|
|
239
|
+
}
|
|
240
|
+
const lines = rawContent.split('\n');
|
|
241
|
+
const includes = [];
|
|
242
|
+
let classes = [];
|
|
243
|
+
let activeClass = null;
|
|
244
|
+
let pendingMacro = null;
|
|
245
|
+
let i = 0;
|
|
246
|
+
while (i < lines.length) {
|
|
247
|
+
const line = lines[i];
|
|
248
|
+
const lineNum = i + 1; // 1-based
|
|
249
|
+
// ---- #include extraction ----
|
|
250
|
+
const includeMatch = line.match(INCLUDE_RE);
|
|
251
|
+
if (includeMatch) {
|
|
252
|
+
let path = includeMatch[1];
|
|
253
|
+
// For angle-bracket includes, the regex captures inside the brackets.
|
|
254
|
+
// Re-check if it was angled: look at the original line
|
|
255
|
+
if (line.trim().startsWith('#include <')) {
|
|
256
|
+
path = '<' + path + '>';
|
|
257
|
+
}
|
|
258
|
+
includes.push(path);
|
|
259
|
+
i++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// ---- UCLASS / UPROPERTY / UFUNCTION detection ----
|
|
263
|
+
const macroMatch = line.match(MACRO_START_RE);
|
|
264
|
+
if (macroMatch) {
|
|
265
|
+
const macroKind = macroMatch[1];
|
|
266
|
+
const { args, endLine } = collectMacroArgs(lines, i);
|
|
267
|
+
const { specifiers, meta } = parseSpecifiersAndMeta(args);
|
|
268
|
+
pendingMacro = { kind: macroKind, specifiers, meta, line: lineNum };
|
|
269
|
+
i = endLine + 1;
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// ---- Resolve pending UCLASS: look for class declaration ----
|
|
273
|
+
if (pendingMacro?.kind === 'UCLASS') {
|
|
274
|
+
// Look for: class NAME : public PARENT or class NAME
|
|
275
|
+
const classMatch = line.match(/\bclass\s+(?:\w+\s+)?(\w+)\s*(?::\s*public\s+(\w+))?/);
|
|
276
|
+
if (classMatch && !line.trim().startsWith('//')) {
|
|
277
|
+
const className = classMatch[1];
|
|
278
|
+
const parentClass = classMatch[2] ?? '';
|
|
279
|
+
const newClass = {
|
|
280
|
+
name: className,
|
|
281
|
+
parentClass,
|
|
282
|
+
specifiers: pendingMacro.specifiers,
|
|
283
|
+
meta: pendingMacro.meta,
|
|
284
|
+
properties: [],
|
|
285
|
+
functions: [],
|
|
286
|
+
line: lineNum,
|
|
287
|
+
};
|
|
288
|
+
// Push previous class if still open
|
|
289
|
+
if (activeClass) {
|
|
290
|
+
classes.push(activeClass.decl);
|
|
291
|
+
}
|
|
292
|
+
activeClass = { decl: newClass, braceDepth: 0, started: false };
|
|
293
|
+
pendingMacro = null;
|
|
294
|
+
i++;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// ---- Track brace depth to know when we're inside a class body ----
|
|
299
|
+
if (activeClass) {
|
|
300
|
+
for (const ch of line) {
|
|
301
|
+
if (ch === '{') {
|
|
302
|
+
activeClass.braceDepth++;
|
|
303
|
+
activeClass.started = true;
|
|
304
|
+
}
|
|
305
|
+
else if (ch === '}') {
|
|
306
|
+
activeClass.braceDepth--;
|
|
307
|
+
if (activeClass.started && activeClass.braceDepth <= 0) {
|
|
308
|
+
// Class body closed
|
|
309
|
+
classes.push(activeClass.decl);
|
|
310
|
+
activeClass = null;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// ---- Resolve pending UPROPERTY / UFUNCTION ----
|
|
317
|
+
if (pendingMacro?.kind === 'UPROPERTY' || pendingMacro?.kind === 'UFUNCTION') {
|
|
318
|
+
const trimmed = line.trim();
|
|
319
|
+
// Skip blank lines, GENERATED_BODY(), braces
|
|
320
|
+
if (trimmed.length === 0 ||
|
|
321
|
+
trimmed.startsWith('GENERATED_BODY') ||
|
|
322
|
+
trimmed === '{' ||
|
|
323
|
+
trimmed === '}') {
|
|
324
|
+
i++;
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (pendingMacro.kind === 'UPROPERTY') {
|
|
328
|
+
const parsed = parsePropertyDecl(line);
|
|
329
|
+
if (parsed && activeClass) {
|
|
330
|
+
activeClass.decl.properties.push({
|
|
331
|
+
name: parsed.name,
|
|
332
|
+
type: parsed.type,
|
|
333
|
+
specifiers: pendingMacro.specifiers,
|
|
334
|
+
meta: pendingMacro.meta,
|
|
335
|
+
line: pendingMacro.line,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
pendingMacro = null;
|
|
339
|
+
}
|
|
340
|
+
else if (pendingMacro.kind === 'UFUNCTION') {
|
|
341
|
+
const parsed = parseFunctionDecl(line);
|
|
342
|
+
if (parsed && activeClass) {
|
|
343
|
+
activeClass.decl.functions.push({
|
|
344
|
+
name: parsed.name,
|
|
345
|
+
returnType: parsed.returnType,
|
|
346
|
+
specifiers: pendingMacro.specifiers,
|
|
347
|
+
meta: pendingMacro.meta,
|
|
348
|
+
line: pendingMacro.line,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
pendingMacro = null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
i++;
|
|
355
|
+
}
|
|
356
|
+
// Flush any still-open class
|
|
357
|
+
if (activeClass) {
|
|
358
|
+
classes.push(activeClass.decl);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
success: true,
|
|
362
|
+
data: { classes, includes, rawContent },
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
367
|
+
return { success: false, error: `Parser internal error: ${msg}` };
|
|
368
|
+
}
|
|
369
|
+
}
|