@aeonsparx/pi-unreal 0.0.0-stage → 1.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/CHANGELOG.md ADDED
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version numbers follow
4
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Every version bump in `package.json`
5
+ adds an entry here; release notes stay auto-generated from commits.
6
+
7
+ Rule for bump class (repo policy): new tools, new parameters and new answer types are **minor**;
8
+ **patch** is fixes with the same surface; **major** is a breaking surface change — a removed tool,
9
+ a renamed parameter, or answer text an existing consumer parses.
10
+
11
+ ## [Unreleased]
12
+
13
+ ## [1.1.0] - 2026-10-28
14
+
15
+ Closes the field findings in `docs/field-findings-2026-10-27.md` (measured on a UE 5.8.3 installed
16
+ engine against a project with 25 enabled plugin references).
17
+
18
+ ### Added
19
+
20
+ - `unreal_index` `action: "headers"` — a path-only header index answering "does this header exist in
21
+ this engine version?": exact path, owning module, owning plugin, `EnabledByDefault`, and whether
22
+ the `.uproject` references it. Invented names come back as *not a file path* plus the closest real
23
+ file names, labelled as files and never as types. Never built implicitly; keyed by its own root set
24
+ (`lib/store.ts` `buildHeaderIndex`, `lookupHeaderPaths`, `suggestHeaderPaths`; `lib/tools.ts`
25
+ `headerReport`, `headerLookup`).
26
+ - Engine-scope indexes include the project's enabled plugin roots, and `engineAreas` accepts engine
27
+ plugin dials: `["Plugins"]` (every `Engine/Plugins` source tree, ~2.8x the `Runtime` scan) or
28
+ `["Plugins/Animation/PhysicsControl"]` (one plugin).
29
+ - Every answer states what it scanned. A 0-row symbol answer names the index keys, the root kinds
30
+ (`279 engine-source root(s), 62 engine-plugin root(s)`), the plugins the project does not reference
31
+ whose names match the query, and the exact next call (`lib/tools.ts` `zeroResultNotes`,
32
+ `indexCoverageNote`, `rootKindsNote`). `unreal_index` `action: "status"` reports the same per index,
33
+ plus the header indexes.
34
+ - `unreal_context` `module: "X"` reports per-target availability: whether the target's module graph
35
+ contains it, its module directory, the declared module `Type`, its owning plugin with
36
+ `EnabledByDefault`, and the `.uproject` reference status as separate facts
37
+ (`lib/tools.ts` `moduleAvailability`).
38
+ - `unreal_symbol` exposes `substring: true` (the substring pass already existed in `lookupSymbol`;
39
+ it was unreachable from the tool).
40
+ - `unreal_project_inspect` / `unreal_plugin_inspect` name the modules a resolved plugin reference
41
+ buys — `Mover: resolved ← Engine · modules Mover(Runtime), MoverCVDData(Runtime),
42
+ MoverEditor(UncookedOnly)` — and list engine plugins available with **no** `.uproject` reference
43
+ (`EnabledByDefault: true`), separated from the references.
44
+
45
+ ### Changed
46
+
47
+ - Index format v6 (`INDEX_VERSION = 6`): `meta.rootKinds` + `meta.areas`. Engine-scope index keys
48
+ change because engine roots now include enabled plugin roots, so v5 engine indexes are not reused
49
+ (one rebuild per machine; project indexes are unaffected).
50
+ - Definitions-mode module attribution names modules after their `*.Build.cs` file, not the directory
51
+ leaf: `Engine/Plugins/Animation/PoseSearch/Source/Runtime` is module `PoseSearch`, not `Runtime`.
52
+ Multi-`Build.cs` directories are disambiguated by the `.uplugin` module names
53
+ (`store.ts` `moduleFromBuildCs`, `buildCsNames`, `pickModuleName`; `assembleRoots` root modules).
54
+ This is what makes `unreal_symbol module: "PoseSearch"` answer in `definitions` mode.
55
+ - Index writes and whole-file passes are streamed: `buildIndex` sorts definition rows per 40 000-row
56
+ shard and k-way merges them into `defs.tsv`, and the linear passes (substring lookup,
57
+ symbols-in-file, module-scope recovery) read the TSV in 4 MB chunks. The whole-Plugins engine scope
58
+ is 1.24 M rows / ~250 MB and previously died in the allocator at 1.91 GB
59
+ (`ReachJSValueFormalMemoryCommit Allocation failed: out of memory`).
60
+ - `unreal_context` `file:` derives scope from path shape (`Engine/Plugins/...` is engine scope), and
61
+ a miss falls back to the header index: "this file exists, in plugin X, `EnabledByDefault` false,
62
+ not referenced by your `.uproject`, here is the call that indexes it" — instead of "not in index".
63
+ - `unreal_index` `action: "compiledb"` with `scope: "project+engine"` now includes `Engine/Plugins`
64
+ sources, where plugin `.cpp` bodies live.
65
+
66
+ ### Fixed
67
+
68
+ - `engineAreas` schema wording: `["Runtime"]` is the engine's own gameplay code, not every gameplay
69
+ type — plugin-shipped types were unreachable and a 0 read as "the engine has no such type".
70
+ - `test/live-check.ts` no longer defaults to a machine-local `.uproject` path (it resolved to
71
+ nothing on CI) and prints a usage line instead.
72
+
73
+ ### Documentation
74
+
75
+ - `README.md` and `docs/internals.md`: engine/plugin coverage and its measured cost, the header index
76
+ and the path-vs-symbol distinction, "an empty clangd answer is evidence", and the asset boundary —
77
+ pi-unreal answers C++ declaration/module/include/dependency facts and does not answer UENUM index
78
+ order, UStruct field types, UCLASS/CDO values or Blueprint graphs (that needs the `unreal-engine`
79
+ editor MCP with the editor running).
80
+
81
+ ## [1.0.0]
82
+
83
+ - Initial release: `unreal_resolve_engine`, `unreal_project_inspect`, `unreal_plugin_inspect`,
84
+ `unreal_symbol`, `unreal_symbol_list`, `unreal_context`, `unreal_index` (compiler-free symbol
85
+ index, UnrealBuildTool Query facts, clangd compile-database enrichment).
86
+
87
+ [Unreleased]: https://github.com/Aeonsparx-Interactive/pi-unreal/commits/main
88
+ [1.1.0]: https://github.com/Aeonsparx-Interactive/pi-unreal/releases/tag/v1.1.0
89
+ [1.0.0]: https://github.com/Aeonsparx-Interactive/pi-unreal/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aeonsparx Interactive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,200 @@
1
- # Temporary Holding Version
1
+ # pi-unreal
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Unreal Engine awareness for the Pi coding agent.**
4
+
5
+ Open a UE project in a `pi` session and the agent stops guessing. It knows which engine
6
+ installation that `.uproject` really resolves to, what the `.uproject` / `.uplugin` files actually
7
+ say, which plugins exist on disk, and where a C++ symbol is declared — with the file, the line and
8
+ the owning module. No compiler, no Visual Studio, no Rider, no clangd required.
9
+
10
+ It is **read-only**: it never writes to your project, never touches the Windows registry, never
11
+ launches the editor, and blocks three kinds of edit that damage Unreal work.
12
+
13
+ ## Usage
14
+
15
+ ### Requirements
16
+
17
+ - **Windows**, with **PowerShell 7** (`pwsh`) for registry-based engine discovery. Without it the
18
+ extension still works: resolution falls back to `UNREAL_ENGINE` / `UE_ROOT` /
19
+ `PI_UNREAL_ENGINE_ROOT`, the Launcher's install list, and a scan of
20
+ `C:/Program Files/Epic Games`.
21
+ - An Unreal Engine installation the project can resolve to (Launcher install or source build).
22
+ Descriptor field handling is transcribed from UE 5.8.
23
+ - Nothing else. No compiler, no IDE language service. clangd is optional enrichment for
24
+ `compiledb` and the `clangd --check` acceptance step.
25
+
26
+
27
+ ### Installation
28
+
29
+ ```bash
30
+ pi install npm:@aeonsparx/pi-unreal # optionally pin to a specific version, reproducible
31
+ pi install git:github.com/Aeonsparx-Interactive/pi-unreal@v1.1 # follows patch releases on the v1.1 line
32
+ ```
33
+
34
+ The `npm:` route installs from the registry and pins an exact version. The `git:` route clones the
35
+ repository and checks out the ref: a line alias (`@v1.1`) is a moving tag that patch releases
36
+ force-move, so `pi` picks up patches on update; pin `@v1.1.0` for a fixed snapshot. Loading straight
37
+ from a clone still works and needs no install: `pi -e ./extensions/unreal/index.ts`.
38
+
39
+ ## What you get in a session
40
+
41
+ **Context is detected for you.** When the session starts in a directory that contains a
42
+ `.uproject` — the working directory itself, or any of up to 3 parent directories — one note is
43
+ shown before the first answer: the resolved engine root and version, installed vs source build,
44
+ the project's modules, the number of declared plugin references, and the conventions below. Ask
45
+ for a re-check any time with `/unreal`.
46
+
47
+ **Ask in plain English.** "Which engine is this project on?", "Is MassGameplay available to this
48
+ project?", "Where is `FLpMovementDirectionThresholds` defined?", "What do I need to link to use
49
+ `UAnimInstance`?" The agent picks the matching tool:
50
+
51
+ | Tool | Question it answers |
52
+ | --- | --- |
53
+ | `unreal_resolve_engine` | "Which engine does this `.uproject` use — and how do you know?" |
54
+ | `unreal_project_inspect` | Modules, target platforms, and every plugin reference resolved against the plugins that exist on disk |
55
+ | `unreal_plugin_inspect` | One `.uplugin` as the engine parses it: modules, loading phases, version fields, plugin dependencies |
56
+ | `unreal_symbol` | "Where is this class / struct / function / `UPROPERTY` / delegate declared?" One line per hit: kind, file:line, module |
57
+ | `unreal_symbol_list` | "What does this project define?" Names by prefix — `ULp`, `ANiMy`, `UAnim` |
58
+ | `unreal_context` | "What does this file/module include, define, and depend on?" Include paths, dependency lists, the real `WITH_*` defines; for a module, also whether the target's module graph contains it and how the `.uproject` treats its plugin |
59
+ | `unreal_index` | Builds and reports the source index, the path-only **header index** ("does this header exist in this engine version?"), and the clangd compile database on request |
60
+
61
+ **Slash commands you can type yourself:**
62
+
63
+ | Command | Result |
64
+ | --- | --- |
65
+ | `/unreal [project]` | Engine, modules and plugin-reference report for this directory |
66
+ | `/unreal-engines` | Every engine installation this machine can prove, and how each was found |
67
+ | `/unreal-symbol <symbol>` | Definition lookup, project + plugins, then engine if an engine index exists |
68
+ | `/unreal-module <Module>` | Module directory, public/private dependency lists, defines file, C++ standard, SDK |
69
+ | `/unreal-cache stats\|clear` | Inspect or clear the cached indexes |
70
+
71
+ Example — `/unreal`:
72
+
73
+ ```text
74
+ project C:/Work/Lp/Lp.uproject
75
+ engine: C:/Program Files/Epic Games/UE_5.8
76
+ version 5.8.3 | installed build | association "5.8" (version) via dir-scan
77
+ Build.version: C:/Program Files/Epic Games/UE_5.8/Engine/Build/Build.version
78
+ modules: Lp (Runtime), LpEditor (Editor)
79
+ plugins declared: 25
80
+ plugin index: 897 .uplugin (cache hit)
81
+ plugin references:
82
+ Mover: resolved ← Engine · modules Mover(Runtime), MoverCVDData(Runtime), MoverEditor(UncookedOnly)
83
+ MassGameplay: resolved ← Engine · modules MassEntity(Runtime), MassCore(Runtime), MassSimulation(Runtime), …
84
+ SomeLocalPlugin: resolved ← Project · modules SomeLocalPlugin(Runtime)
85
+ RetiredPlugin: missing
86
+ totals: 22 resolved, 3 missing of 25 references
87
+ engine plugins available with NO .uproject reference (EnabledByDefault true): GameplayCameras, IKRig
88
+ ```
89
+
90
+ Example — `unreal_symbol` for an engine type (engine index already built):
91
+
92
+ ```text
93
+ unreal_symbol 'AActor' scope=engine: 1 definition(s)
94
+ class AActor C:/Program Files/Epic Games/UE_5.8/Engine/Source/Runtime/Engine/Classes/GameFramework/Actor.h:282 [Engine · engine]
95
+ ```
96
+
97
+ That is a definition line, not a grep dump: grepping a hot engine class name returns ~176 files of
98
+ noise, which is exactly what burns a session's context.
99
+
100
+ Example — `unreal_index` `action: "headers"`, the question a symbol index cannot answer:
101
+
102
+ ```text
103
+ unreal_index headers → hdr-UE_5.8-1a40c1c3d516
104
+ 37,788 header paths from 1,934 roots (1,655 engine-plugin root(s), 279 engine-source root(s))
105
+ 'MoverState.h': does NOT exist as a file path in this engine version
106
+ closest real file names (files, not types):
107
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MoverTypes.h [Mover]
108
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MoverSimulationTypes.h [Mover]
109
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MovementMode.h [Mover]
110
+ Engine/Plugins/Experimental/Mover/Source/Mover/Public/MovementModeStateMachine.h [Mover]
111
+ ```
112
+
113
+ `MoverState.h` is the kind of name an agent invents. The answer says it is not a file, lists real
114
+ files, and never presents a file name as a type. The same call on
115
+ `PhysicsControlComponent.h` returns the path plus `plugin PhysicsControl [Engine] · EnabledByDefault
116
+ false · .uproject reference NO` — the file exists, the project does not reference the plugin, and
117
+ those are different facts.
118
+
119
+ Example — the coverage lines a 0-row answer prints (nothing here says "the engine has no such type"):
120
+
121
+ ```text
122
+ unreal_symbol 'PhysicsControlComponent' scope=engine: 0 definition(s)
123
+ · no definitions matched 'PhysicsControlComponent' in scope 'engine'
124
+ · scanned engine index idx-engine-defs-b8d82464af4a: 279 engine-source root(s), 62 engine-plugin root(s)
125
+ · engine plugins on disk: 897, referenced by this .uproject: 25; the engine index scans referenced plugins only
126
+ · a plugin the project does not reference is not in any index root: pass engineAreas ["Plugins"] (…2.8× the Runtime scan) or ["Plugins/Animation/PhysicsControl"]
127
+ · next: unreal_symbol query 'PhysicsControlComponent' scope "all" prefix true, or unreal_index action "headers" name "PhysicsControlComponent.h"
128
+ ```
129
+
130
+ ## Guardrails (automatic, nothing to enable)
131
+
132
+ | Situation | What happens |
133
+ | --- | --- |
134
+ | An edit targeting `*.uasset` / `*.umap` | **Blocked** — those are cooked binary assets; author them in the running editor |
135
+ | A C++ edit while `UnrealEditor` is running | **Warning** — freshly compiled C++ will not load into a running editor; close it, then build |
136
+ | `clang-format` aimed at a `*.cs` file | **Blocked** — clang-format applies the C# rule and can empty the file |
137
+
138
+ ## Costs: what is fast, and what is opt-in
139
+
140
+ | Operation | Time |
141
+ | --- | --- |
142
+ | Cached lookup (symbol, project facts) | ~0.3 s |
143
+ | First project/plugin index (≈2,400 files) | seconds |
144
+ | Engine `Runtime` index — cold | ~1 minute |
145
+ | Engine `Runtime` index — warm | ~6 s |
146
+ | UnrealBuildTool Query facts | ~7 s, then cached |
147
+ | clangd compile database (`unreal_index` → `compiledb`) | minutes, ~1 GB for the engine scope |
148
+
149
+ Because of that model:
150
+
151
+ - Symbol lookups never stall. If a question needs engine sources and no engine index exists, the
152
+ agent reports the cost and how to pay it, and asks you. Say **yes** once per machine —
153
+ `/unreal-cache stats` shows what you have. Engine scans default to `engineAreas: ["Runtime"]`,
154
+ which is the engine's **own** gameplay code (`AActor`, `UAnimInstance`, `USceneComponent`);
155
+ `Editor` and `Developer` are opt-in and roughly triple the scan. An engine-scope index always
156
+ includes the plugin roots your `.uproject` enables, so `UMoverComponent` and the PoseSearch types
157
+ are in it. Types shipped by plugins you do **not** reference (`PhysicsControl`, `BlendStack`,
158
+ `StateTree`, `GameplayAbilities`) need `engineAreas: ["Plugins"]` — every `Engine/Plugins`
159
+ source tree, ~2.8× the `Runtime` scan — or `engineAreas: ["Plugins/Animation/PhysicsControl"]`
160
+ for one plugin.
161
+ - "Does this header exist in this engine version?" is a path question, not a symbol question:
162
+ `unreal_index` with `action: "headers"` (and `name: "MoverState.h"`) answers it in seconds from a
163
+ path walk, and says which plugin owns a header it finds, whether that plugin is
164
+ `EnabledByDefault`, and whether your `.uproject` references it. Invented names come back as
165
+ *not a file path* plus the closest real **file** names — never as types.
166
+ - Ask for `unreal_index` with `action: "build"`, `scope: "engine"` when you want engine-wide
167
+ navigation, and `action: "compiledb"` when you want clangd's references/call hierarchy.
168
+ - After a branch switch, a rebase, adding a plugin, or an engine version change, say "rebuild the
169
+ Unreal index" (that is `refresh: true`).
170
+ - Everything lives in `~/.pi/agent/unreal-cache` (override with `$PI_UNREAL_CACHE_DIR`) — never
171
+ inside your project tree or your engine installation. The compile database encodes this
172
+ machine's absolute paths, so it is machine-local by design.
173
+
174
+ ## Troubleshooting
175
+
176
+ | Report | What to do |
177
+ | --- | --- |
178
+ | `engine: AMBIGUOUS — more than one valid installation matches` | Two roots match the association. Pass the one you want as `engineRootHint` (e.g. `engineRootHint: "C:/Program Files/Epic Games/UE_5.8"`). |
179
+ | `engine: UNRESOLVED` with rejected candidates | Read the reasons — a stale registry key, a missing `Build.version`, a version mismatch. The report lists remediation steps; the extension never writes registry keys for you. |
180
+ | `no .uproject within 4 levels above …` | The search walks up from the working directory. Pass the project explicitly: `project: "C:/Work/Lp/Lp.uproject"` (a directory containing one works too). |
181
+ | `unreal_symbol` finds nothing in scope `engine` | Read the coverage lines the answer prints: they name the root kinds that were scanned. A plugin your `.uproject` does not reference is in no index root — `engineAreas: ["Plugins"]` (or one plugin subtree), then rebuild. |
182
+ | `unreal_symbol` finds nothing and you are not sure the name exists | `unreal_index` → `action: "headers"`, `name: "ThatName.h"`: a seconds-long path answer with the closest real file names. |
183
+ | Results look stale after a rebase or engine update | Re-run with `refresh: true`, or `/unreal-cache clear`. |
184
+ | `no plugin named "X" in the index` | That reference is genuinely not on disk. `/unreal` shows which roots were scanned. |
185
+
186
+ ### What it deliberately does not do
187
+
188
+ - It does not install, register, migrate, or "fix" an engine, and never writes registry keys.
189
+ - It does not build, cook, package, launch the editor, or edit `.ini` files.
190
+ - It does not hand-edit assets, and blocks attempts to.
191
+ - Symbol lookup reports **declarations**. Type resolution, overloads, call sites, inheritance and
192
+ diagnostics are clangd's job when clangd is installed. An empty clangd answer is evidence: on an
193
+ installed engine the compile database has no engine translation units, so "clangd found nothing"
194
+ in engine code is a property of the setup, not a bug this package fixes.
195
+ - A 0-row answer is a statement about **the roots that were scanned**, never about the engine. The
196
+ answer names its index keys and root kinds; `unreal_index` → `action: "status"` prints the same.
197
+ - pi-unreal answers C++ **declaration, module, include-path and dependency** facts. It does not
198
+ answer asset questions — UENUM index order, UStruct field types, UCLASS/CDO instance values,
199
+ Blueprint graphs. Those need the `unreal-engine` editor MCP with the editor running.
200
+ - `.ini` provenance, live-coding state, and asset metadata are out of scope for v1.
@@ -0,0 +1,245 @@
1
+ # pi-unreal internals
2
+
3
+ Design and implementation notes: how the tools derive their answers, and the measured numbers behind
4
+ the cost model. Read the README for day-to-day usage; this file is the engine-facing detail that
5
+ the README deliberately leaves out.
6
+
7
+ ## Symbol index (no compiler, no IDE)
8
+
9
+ `unreal_symbol`, `unreal_context`, and `unreal_index` answer navigation questions from two
10
+ compiler-free sources:
11
+
12
+ 1. **UnrealBuildTool Query** (`-Mode=Query`: `TargetDetails`, `TargetModuleReferences`) — the
13
+ engine's own record of targets, modules, module dependencies, include paths, and forced
14
+ includes. Measured on UE 5.8: 6.0 s for 1,079 modules, 2.1 s for the dependency graph, and it
15
+ needs no compiler. That is why MSVC-only machines (Visual Studio, Rider) are first class here:
16
+ Visual Studio's `Browse.VC.db` is encrypted with no public API, Rider's resolver is in-process
17
+ through RiderLink, and both derive their context from the same UBT rules this layer reads.
18
+ 2. **A regex definition scanner** over the project, its enabled plugins, and the engine areas its
19
+ modules actually depend on — `class`/`struct`/`enum`/`union`, `UFUNCTION`, `UPROPERTY`,
20
+ delegates, aliases, with `Owner:Member` qualified names and forward declarations marked. In an
21
+ installed engine "engine areas" is not where the gameplay types are: `Engine/Source/Runtime` is the
22
+ engine's own code, and plugin-shipped types live under `Engine/Plugins/<Category>/<Plugin>/Source/`,
23
+ so an engine-scope index takes its plugin roots from the `.uproject` (plus an explicit
24
+ `engineAreas: ["Plugins"]` dial for the rest).
25
+
26
+ Artifacts live in `$PI_UNREAL_CACHE_DIR/indexes/<key>/` (`meta.json`, `defs.tsv`, `files.tsv`,
27
+ plus `includes.tsv` in `full` mode): plain TSV — greppable, inspectable, shared across sessions
28
+ and projects — and never inside the project tree or the engine installation.
29
+
30
+ Writes and whole-file passes are **streamed**, because the whole-Plugins engine index is 1.24 M rows
31
+ (~250 MB of TSV): `buildIndex` sorts rows per 40 000-row shard and k-way merges them into `defs.tsv`
32
+ (peak memory is one shard, output is identical to a single sorted pass), and the linear passes
33
+ (substring lookup, symbols-in-file, module scope recovery) read the TSV in 4 MB chunks and stop when
34
+ the answer is full. An earlier build accumulated every row in an array and died in the allocator at
35
+ 1.91 GB (`ReachJSValueFormalMemoryCommit Allocation failed: out of memory`) — which is the failure
36
+ mode of "just load the index" at engine scale.
37
+
38
+ Measured on this box (UE 5.8.3 installed engine; Lp project with 25 enabled plugins):
39
+
40
+ | Scope | Files | Definitions | Cold build |
41
+ | --- | --- | --- | --- |
42
+ | Project + plugins | 2,405 | 38,901 | 6.7 s |
43
+ | Engine `Runtime` (shipped default: 279 module roots) | 18,566 | 436,729 | 57.6 s cold, 5.7 s warm |
44
+ | Engine `Runtime` + 62 enabled plugin roots | 20,966 | 476,327 | 122 s cold, 0.4 s warm |
45
+ | Engine `Runtime` + every `Engine/Plugins` source | 71,004 | 1,240,246 | ~2 min cold (1,996 roots: 279 engine-source + 1,717 engine-plugin) |
46
+ | Engine `Runtime/Core` + `Runtime/CoreUObject` | 3,094 | 70,548 | 16 s |
47
+ | Header index (paths only, engine `Runtime` + all `Engine/Plugins`) | 37,788 headers | — | 4.5 s cold, 0.5 s warm |
48
+ | Cached lookup | — | — | ~0.3 s |
49
+
50
+ Cost discipline is the design. A lookup builds the project index (seconds) and uses an engine
51
+ index only if one exists; `scope: "engine"` on a cold machine returns
52
+ `engine index not built — build it with unreal_index (action: "build", scope: "engine")` instead of
53
+ spending minutes on a scan nobody asked for. `unreal_index` is the explicit, paid path.
54
+
55
+ Ranking inside `unreal_symbol`: exact (0) → qualified member (1) → prefix (2) → substring (3).
56
+ Forward declarations rank last, so `unreal_symbol AActor` returns
57
+ `Engine/Source/Runtime/Engine/Classes/GameFramework/Actor.h:282` (the `UCLASS` declaration), not
58
+ the `class AActor;` line in `PhysicsCollision.h`.
59
+ `unreal_symbol LpGameModeBase` finds `ALpGameModeBase` (the UE prefix is added for you); `unreal_symbol FLpMovementDirectionThresholds:F` finds a single-letter `UPROPERTY` field; `unreal_context LpCharacterTypes.h` resolves a bare file name through the index. Engine areas are derived from module
60
+ dependencies (`.uproject` modules → recursive `PublicDependencyModuleNames` → plugin-supplied
61
+ modules → recursive `PublicDependencyModuleNames` → plugin-supplied
62
+ modules → `Engine/Source/<area>/*`), so the index contains what this project can compile against,
63
+ not the whole engine. An **engine-scope** index is `Engine/Source/<area>` roots *plus the plugin
64
+ roots the `.uproject` enables* — in UE 5.8 most gameplay types ship as plugins (`Mover`,
65
+ `PoseSearch`, `SmartObjects`, `BlendStack`, `StateTree`, `GameplayAbilities`), and an index of
66
+ `Engine/Source/Runtime` alone reports 0 rows for `UMoverComponent` while the project index already
67
+ contains it. Engine plugins the project does **not** reference are in no root: `engineAreas:
68
+ ["Plugins"]` adds every `Engine/Plugins` source tree (~2.8× the `Runtime` scan),
69
+ `["Plugins/Animation/PhysicsControl"]` adds one.
70
+
71
+ Every answer states what it scanned: index key, root kinds (`279 engine-source root(s),
72
+ 62 engine-plugin root(s)`), and the `engineAreas` it was built with. A 0-row answer is a statement
73
+ about those roots, never about the engine, and it prints the exact next call.
74
+
75
+ ## Header index (`unreal_index` action `headers`)
76
+
77
+ "Does this header exist in this engine version?" is a path question that a definitions index cannot
78
+ answer: absence of a symbol row is not absence of a file, and the index roots exclude every plugin
79
+ the project does not enable. `buildHeaderIndex` walks file **paths** only — no content read — over
80
+ the same root assembly (`Engine/Source/<area>`, `Engine/Plugins/...`, project sources), writes
81
+ `headers.tsv` (`file \t module \t kind`) beside the symbol indexes, and answers exists / exact path
82
+ / owning module / owning plugin / `EnabledByDefault` / referenced-in-`.uproject`. Measured: 37,788
83
+ header paths in 4.5 s on a UE 5.8 install (`Runtime` + all of `Engine/Plugins`).
84
+
85
+ It is never built implicitly (a path answer must not cost a scan nobody asked for), its key encodes
86
+ its own root set, and when the name does not exist the answer lists the closest real **file** names
87
+ labelled as files. That is the discipline behind it: an invented name like `MoverState.h` must not
88
+ become a confident type suggestion — the old workflow produced `Mover/Public/InputContainerStruct.h`
89
+ (a real file, a bogus answer) for `FCharacterDefaultInputs`, and the types
90
+ `FPoseSearchBlendPose(History)` do not exist in 5.8 at all.
91
+
92
+ One engine fact worth knowing: UBT Query reports `"defines": []` for every module. The effective
93
+ preprocessor set lives in a forced include —
94
+ `Intermediate/Build/<Platform>/<Config>/<Target>/Development/<Module>/Definitions.<Module>.h` —
95
+ and `unreal_context` reads that file instead of guessing `WITH_*` values. Query is also never
96
+ run with `-NoIntellisense`: that flag produces a syntactically valid skeleton with an empty
97
+ `moduleToCompileSettings`, and `payloadOk()` rejects it.
98
+
99
+ ## clangd enrichment (`unreal_index` action `compiledb`)
100
+
101
+ The definition index is the compiler-free path. When you want clangd's semantic power (references,
102
+ call hierarchy, diagnostics), the database it needs comes from the engine: `UnrealBuildTool
103
+ -Modes=GenerateClangDatabase`, normalized in the pi-unreal cache.
104
+
105
+ The UnrealBuildTool invocation is the part nobody documents, so it is encoded in one place
106
+ (`lib/compiledb.ts`):
107
+
108
+ - The target is **three positional tokens** — `Win64 Development LpEditor`. `TargetDescriptor` only
109
+ inspects arguments that do not start with `-`, and its `+` split requires every token to be the
110
+ same kind, so `-Target=LpEditor` fails with `No platforms specified for target` and
111
+ `Win64+Development+LpEditor` fails with `The platform name Development is not a valid platform
112
+ name`. `-Platform=`/`-Configuration=` set `BuildConfiguration` only and produce a duplicate
113
+ platform error next to the positional tokens.
114
+ - `-OutputDir=` must already exist (UnrealBuildTool throws
115
+ `DirectoryNotFoundException: Could not find a part of the path ...\compile_commands.json`), and
116
+ the mode's own `-NoPCH -NoVFS -Compiler=Clang` additions mean the emitted driver is
117
+ `clang-cl.exe`, not `cl.exe`.
118
+ - `-Include=` rules match nothing useful (measured: `Source/Lp`, `*/Source/Lp/*`,
119
+ `C:/Users/.../Source/Lp/**` all yield an empty database), so scoping happens after generation.
120
+
121
+ Normalization is Epic's `stripPostPch.py` recipe, ported: expand the per-module `@file.rsp`
122
+ response files, drop `/Yu* /Fm* /Fe* /Fp*` and everything after `/link`, drop PCH translation
123
+ units (`SharedPCH.*.h.cpp`, `PCH.*.h.cpp`) and header entries, then keep the requested scope. The
124
+ database is written to `$PI_UNREAL_CACHE_DIR/compiledb/<target>.<platform>.<config>/` — it encodes
125
+ this machine's absolute paths and toolchain version, so it is a machine artifact, never a repo one.
126
+
127
+ `check: "<file>"` runs `clangd --check` against the result, which is the acceptance test. Measured
128
+ on Lp (UE 5.8.3, installed engine, VS 18 LLVM clangd): UnrealBuildTool 1.5-1.7 s, 4 entries,
129
+ 4 response files expanded, 0 switches dropped; `clangd --check` built a 62 MB preamble and
130
+ reported **`All checks completed, 0 errors`** in ~2.6 s.
131
+
132
+ The honest limits of that 4-entry number: an installed (precompiled) engine contributes no compile
133
+ actions, so the database covers the project's own translation units, not `Engine/Source`. Engine
134
+ files still work under clangd because the project's `.clangd` supplies the include paths — exactly
135
+ what Epic's Unreal-clangd VS Code extension does (its generated `Lp.0.rsp` contains only `/FI`
136
+ plus 523 `/I` lines, and `compile_commands.json` there has 6 project entries and no engine entry).
137
+ Re-run with `refresh: true` after changing `Build.cs` files, adding modules, or switching engine
138
+ version.
139
+
140
+ ## How engine resolution works
141
+
142
+ Mirrors `FDesktopPlatformWindows::GetAvailableEngineDescriptions`
143
+ (`Engine/Source/Developer/DesktopPlatform/Private/DesktopPlatformBase.cpp:459-511`) and
144
+ `VerifyEngineGuid` (`DesktopPlatformWindows.cpp:615-664`):
145
+
146
+ | `.uproject` association | Source of truth |
147
+ | --- | --- |
148
+ | engine-root path | used directly, validated |
149
+ | `5.8` (Launcher install) | `HKLM:\SOFTWARE\EpicGames\Unreal Engine\<ver>` → `InstalledDirectory`, then `<installDirDefault>\UE_<ver>` |
150
+ | `{GUID}` (source build) | `HKCU:\SOFTWARE\Epic Games\Unreal Engine\Builds` |
151
+ | empty | walk up 12 levels looking for `Engine/Config/BaseEngine.ini` |
152
+
153
+ Below the association: `LauncherInstalled.dat`, `HKLM` versions, `UNREAL_ENGINE` / `UE_ROOT` /
154
+ `PI_UNREAL_ENGINE_ROOT`, and a scan of `C:/Program Files/Epic Games` for `UE_*`. Explicit
155
+ `engineRootHint` outranks every registry source.
156
+
157
+ Every candidate is validated before use — `Engine/Binaries` + `Engine/Build`
158
+ (`DesktopPlatformBase.cpp:401-421`) **and** `Engine/Build/Build.version` matching the
159
+ association. Stale registry keys and env pointers are reported as rejected candidates with the
160
+ reason, never silently dropped. One survivor → `resolved`; several → `ambiguous`; none →
161
+ `unresolved` + remediation. Nothing is ever written back to the registry.
162
+
163
+ Registry reads run through one memoized `pwsh -NoProfile` snapshot (8 s timeout). If `pwsh` is
164
+ missing the snapshot degrades to `available: false` and the resolver continues with
165
+ file-based sources — the extension never fails at import time.
166
+
167
+ ## Plugin discovery
168
+
169
+ Mirrors `UnrealBuildTool.System.ReadAvailablePlugins` (`System/Plugins.cs:322`):
170
+ Engine `Engine/Plugins` → Project `Plugins/` → Mod `Mods/` → `AdditionalPluginDirectories`,
171
+ with later scans shadowing earlier ones on name collision. `Binaries/`, `Intermediate/`,
172
+ `Saved/`, `.git/`, `ThirdParty/` are skipped. Reference semantics mirror `PluginManager`:
173
+ `Enabled` / `Optional` / `Activate` (`PluginReferenceDescriptor.cpp:132-172`); an engine-version
174
+ mismatch is a warning, not an error (`PluginManager.cpp:2690-2712`).
175
+
176
+ Indexes are cached on disk keyed by the mtime of every scanned directory plus the engine
177
+ changelist, with a 10-minute backstop (`MAX_AGE_MS`) because content-only `.uplugin` edits do
178
+ not move the parent directory's mtime.
179
+
180
+ ## Descriptor fidelity
181
+
182
+ Field names, host types, and loading phases are transcribed from the engine's serializers
183
+ (UE 5.8): `ProjectDescriptor.cpp:100-225`, `PluginDescriptor.cpp:263-409`,
184
+ `ModuleDescriptor.cpp`, `PluginReferenceDescriptor.cpp:130-180`, cross-checked against 897
185
+ `.uplugin` files in UE 5.8 `Engine/Plugins`. Consequences:
186
+
187
+ - `.uproject` / `.uplugin` are parsed as JSONC (comments and trailing commas are legal in the
188
+ engine's reader).
189
+ - `Modules` is an array of objects keyed by `Name`; `Type` is required
190
+ (`Found Module entry 'X' with a missing 'Type' field`), `LoadingPhase` defaults to `Default`.
191
+ - A plugin reference without `"Enabled"` is rejected by UE 5.8 — reported, not guessed.
192
+ - Unknown keys are reported as `… is not serialized by FPluginDescriptor`, and the known-key
193
+ set is wide enough (including legacy `Whitelist*`/`Blacklist*` and pre-5.x keys) that genuine
194
+ engine files produce no noise.
195
+ - `.uplugin` has no `Enabled` key; enablement is `EnabledByDefault` + the project reference.
196
+ - `IsBetaVersion` is metadata in UE 5.8 (no engine/UBT beta gate exists).
197
+
198
+ ## Known limits
199
+
200
+ - Registry access needs `pwsh` (PowerShell 7). Without it, registry sources are unavailable and
201
+ resolution relies on env, directory scans, and inline associations.
202
+ - `unreal_resolve_engine` reports what the machine can prove. It does not install, register, or
203
+ migrate engines, and never writes registry keys (the engine's `RegisterEngineInstallation`
204
+ does; this package deliberately does not).
205
+ - v1 is inspection only. `.ini` provenance is out of scope.
206
+ - The definition index is a scanner, not a parser: it finds declarations (`class`, `struct`,
207
+ `enum`, `union`, `UFUNCTION`/`UPROPERTY`/`UCLASS`-marked members, delegates, aliases) and
208
+ records `Owner:Member` qualified names. It does not resolve types, overloads, call sites, or
209
+ inheritance — that is what clangd is for when it is installed. Macro-heavy code can still be
210
+ misread: the scanner is regex-based, so a definition hidden inside an unusual macro spelling may
211
+ be missed, and `#if` branches are all recorded (a symbol declared in a disabled branch is a
212
+ candidate, not a guarantee).
213
+ - `moduleRefs` and `includePaths` come from UnrealBuildTool Query, which needs a buildable
214
+ target for the project; when Query fails the index still builds and says so.
215
+ - The engine index is never built implicitly: scans are opt-in through `unreal_index`. The header
216
+ index is never built implicitly either, for the same reason: a path answer must not cost a scan.
217
+ - `action: "compiledb"` needs UnrealBuildTool (so an engine installation) and, for the
218
+ `clangd --check` acceptance step, a clangd binary — `CLANGD_PATH`, the Visual Studio LLVM
219
+ toolset, the WinGet LLVM package, or `C:/Program Files/LLVM/bin`. Without clangd the database is
220
+ still produced and reported; only the check is skipped.
221
+ - On an installed engine the compile database covers the project's translation units (engine
222
+ modules are precompiled and contribute no compile actions). The engine-wide scope of
223
+ `project+engine` is the source-built case, and there it is a ~1 GB artifact taking minutes.
224
+ `project+engine` now includes `Engine/Plugins` sources as well as `Engine/Source` — that is where
225
+ plugin `.cpp` bodies live, and a database without them silently drops every plugin implementation
226
+ file from clangd.
227
+ - An empty clangd answer is **evidence, not a bug to route around**. On an installed engine the
228
+ database has no engine translation units, so "clangd found no references in
229
+ `MoverComponent.cpp`" describes the setup, not a defect pi-unreal fixes: it answers declaration,
230
+ module, include-path and dependency facts by a compiler-free route, and says what it scanned.
231
+ - Engine area selection is a cost dial, not a completeness claim. `DEFAULT_ENGINE_AREAS =
232
+ ["Runtime"]` covers the engine's **own** gameplay code (`AActor`, `UAnimInstance`,
233
+ `USceneComponent`, 18,566 files) — in an installed engine that is not every gameplay type, because
234
+ the ones shipped by plugins (`Mover`, `PoseSearch`, `PhysicsControl`, `BlendStack`, `SmartObjects`,
235
+ `StateTree`, `GameplayAbilities`) live under `Engine/Plugins/<Category>/<Plugin>/Source/<module>`.
236
+ Engine-scope indexes add the plugin roots the `.uproject` enables; the rest are opt-in via
237
+ `engineAreas: ["Plugins"]` (~2.8× the `Runtime` scan) or `["Plugins/Animation/PhysicsControl"]`
238
+ for one plugin. `Editor` and `Developer` are opt-in and roughly triple the scan.
239
+ - pi-unreal answers C++ **declaration, module, include-path and dependency** facts. It does not
240
+ answer asset questions: UENUM index order (Blueprint enumerators are stored in index order, so
241
+ `4 = OnGround` is an asset fact), UStruct field types, UCLASS/CDO instance values, component
242
+ lists, parent chains, Blueprint graphs. Those need the `unreal-engine` editor MCP with the editor
243
+ running; a C++ scan cannot see them and must not pretend to.
244
+ - Index reuse requires the *same root set*: an index built over two modules is never used to answer
245
+ a `Runtime`-wide query. Partial coverage reported as an answer is worse than a rebuild.