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.
Files changed (124) hide show
  1. package/README.md +729 -0
  2. package/dist/build/error-parser.js +51 -0
  3. package/dist/build/fix-suggester.js +84 -0
  4. package/dist/build/ubt-runner.js +146 -0
  5. package/dist/cli.js +13 -0
  6. package/dist/config.js +8 -0
  7. package/dist/docs/data/ue57-api.js +228 -0
  8. package/dist/docs/doc-index.js +110 -0
  9. package/dist/docs/types.js +4 -0
  10. package/dist/generators/class-generator.js +363 -0
  11. package/dist/generators/file-modifier.js +276 -0
  12. package/dist/generators/uht-validator.js +177 -0
  13. package/dist/index.js +89 -0
  14. package/dist/parsers/cpp-class-index.js +230 -0
  15. package/dist/parsers/cpp-parser.js +369 -0
  16. package/dist/parsers/ini-parser.js +216 -0
  17. package/dist/parsers/uproject-parser.js +130 -0
  18. package/dist/plugin-bridge/client.js +217 -0
  19. package/dist/plugin-bridge/protocol.js +6 -0
  20. package/dist/plugin-bridge/retry.js +23 -0
  21. package/dist/setup.js +209 -0
  22. package/dist/tools/ai-systems/index.js +247 -0
  23. package/dist/tools/ai-systems/types.js +4 -0
  24. package/dist/tools/animation/index.js +241 -0
  25. package/dist/tools/animation/types.js +4 -0
  26. package/dist/tools/audio/index.js +204 -0
  27. package/dist/tools/audio/types.js +4 -0
  28. package/dist/tools/blueprint/index.js +495 -0
  29. package/dist/tools/blueprint/types.js +4 -0
  30. package/dist/tools/build/index.js +163 -0
  31. package/dist/tools/chaos/index.js +230 -0
  32. package/dist/tools/chaos/types.js +4 -0
  33. package/dist/tools/collision-physics/index.js +211 -0
  34. package/dist/tools/config/index.js +288 -0
  35. package/dist/tools/cpp/index.js +305 -0
  36. package/dist/tools/docs/index.js +251 -0
  37. package/dist/tools/editor/index.js +242 -0
  38. package/dist/tools/gas/index.js +222 -0
  39. package/dist/tools/gas/types.js +5 -0
  40. package/dist/tools/import-export/index.js +218 -0
  41. package/dist/tools/input/index.js +146 -0
  42. package/dist/tools/known-issues/index.js +88 -0
  43. package/dist/tools/known-issues/middleware.js +55 -0
  44. package/dist/tools/known-issues/store.js +125 -0
  45. package/dist/tools/livelink/index.js +203 -0
  46. package/dist/tools/livelink/types.js +4 -0
  47. package/dist/tools/material/index.js +190 -0
  48. package/dist/tools/motion-design/index.js +251 -0
  49. package/dist/tools/motion-design/types.js +6 -0
  50. package/dist/tools/movie-render/index.js +220 -0
  51. package/dist/tools/networking/index.js +149 -0
  52. package/dist/tools/pcg/index.js +164 -0
  53. package/dist/tools/selection/index.js +180 -0
  54. package/dist/tools/sequencer/index.js +218 -0
  55. package/dist/tools/validation/index.js +183 -0
  56. package/dist/tools/validation/types.js +4 -0
  57. package/dist/tools/viewport/index.js +310 -0
  58. package/dist/tools/worldpartition/index.js +226 -0
  59. package/dist/tools/worldpartition/types.js +4 -0
  60. package/dist/utils/execFileNoThrow.js +40 -0
  61. package/dist/utils/logger.js +27 -0
  62. package/dist/utils/path-guard.js +26 -0
  63. package/package.json +40 -0
  64. package/unreal-plugin/MCPBridge/MCPBridge.uplugin +29 -0
  65. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/MCPBridgeEditor.Build.cs +68 -0
  66. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.cpp +919 -0
  67. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAICommands.h +23 -0
  68. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.cpp +415 -0
  69. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPActorCommands.h +16 -0
  70. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.cpp +653 -0
  71. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAnimationCommands.h +24 -0
  72. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.cpp +290 -0
  73. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAssetCommands.h +17 -0
  74. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.cpp +624 -0
  75. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPAudioCommands.h +22 -0
  76. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.cpp +616 -0
  77. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintHandlers.h +25 -0
  78. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.cpp +744 -0
  79. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBlueprintWriteHandlers.h +24 -0
  80. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeEditor.cpp +23 -0
  81. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.cpp +149 -0
  82. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPBridgeSubsystem.h +38 -0
  83. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.cpp +771 -0
  84. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPChaosCommands.h +22 -0
  85. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.cpp +749 -0
  86. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPCollisionPhysicsCommands.h +22 -0
  87. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.cpp +172 -0
  88. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPEditorStateCommands.h +16 -0
  89. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.cpp +715 -0
  90. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPGASCommands.h +22 -0
  91. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.cpp +679 -0
  92. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPImportExportCommands.h +22 -0
  93. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.cpp +381 -0
  94. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPInputHandlers.h +24 -0
  95. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.cpp +504 -0
  96. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPLiveLinkCommands.h +22 -0
  97. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.cpp +511 -0
  98. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMaterialCommands.h +22 -0
  99. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.cpp +1110 -0
  100. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMotionDesignCommands.h +28 -0
  101. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.cpp +590 -0
  102. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPMovieRenderCommands.h +16 -0
  103. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.cpp +482 -0
  104. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPNetworkingCommands.h +16 -0
  105. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.cpp +338 -0
  106. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPPieCommands.h +16 -0
  107. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.cpp +677 -0
  108. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSelectionCommands.h +22 -0
  109. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.cpp +721 -0
  110. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPSequencerCommands.h +16 -0
  111. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.cpp +368 -0
  112. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPValidationCommands.h +22 -0
  113. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.cpp +1208 -0
  114. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPViewportCommands.h +29 -0
  115. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.cpp +822 -0
  116. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Private/MCPWorldPartitionCommands.h +23 -0
  117. package/unreal-plugin/MCPBridge/Source/MCPBridgeEditor/Public/MCPBridgeEditor.h +14 -0
  118. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/MCPBridgeRuntime.Build.cs +28 -0
  119. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPBridgeRuntime.cpp +22 -0
  120. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPCommandRouter.cpp +118 -0
  121. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Private/MCPTcpServer.cpp +196 -0
  122. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPBridgeRuntime.h +15 -0
  123. package/unreal-plugin/MCPBridge/Source/MCPBridgeRuntime/Public/MCPCommandRouter.h +55 -0
  124. 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
+ }