@aeonsparx/pi-unreal 1.0.0 → 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 +89 -0
- package/README.md +85 -88
- package/docs/internals.md +66 -5
- package/extensions/unreal/index.ts +18 -4
- package/extensions/unreal/lib/pi-tools.ts +70 -20
- package/extensions/unreal/lib/store.ts +633 -43
- package/extensions/unreal/lib/tools.ts +491 -31
- package/extensions/unreal/lib/ubt.ts +7 -0
- package/package.json +2 -1
- package/prompts/unreal-context.md +10 -0
- package/skills/ue-engine-resolution/SKILL.md +38 -5
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/README.md
CHANGED
|
@@ -10,6 +10,32 @@ the owning module. No compiler, no Visual Studio, no Rider, no clangd required.
|
|
|
10
10
|
It is **read-only**: it never writes to your project, never touches the Windows registry, never
|
|
11
11
|
launches the editor, and blocks three kinds of edit that damage Unreal work.
|
|
12
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
|
+
|
|
13
39
|
## What you get in a session
|
|
14
40
|
|
|
15
41
|
**Context is detected for you.** When the session starts in a directory that contains a
|
|
@@ -29,8 +55,8 @@ project?", "Where is `FLpMovementDirectionThresholds` defined?", "What do I need
|
|
|
29
55
|
| `unreal_plugin_inspect` | One `.uplugin` as the engine parses it: modules, loading phases, version fields, plugin dependencies |
|
|
30
56
|
| `unreal_symbol` | "Where is this class / struct / function / `UPROPERTY` / delegate declared?" One line per hit: kind, file:line, module |
|
|
31
57
|
| `unreal_symbol_list` | "What does this project define?" Names by prefix — `ULp`, `ANiMy`, `UAnim` |
|
|
32
|
-
| `unreal_context` | "What does this file/module include, define, and depend on?" Include paths, dependency lists, the real `WITH_*` defines |
|
|
33
|
-
| `unreal_index` | Builds and reports the source index, and the clangd compile database on request |
|
|
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 |
|
|
34
60
|
|
|
35
61
|
**Slash commands you can type yourself:**
|
|
36
62
|
|
|
@@ -53,11 +79,12 @@ modules: Lp (Runtime), LpEditor (Editor)
|
|
|
53
79
|
plugins declared: 25
|
|
54
80
|
plugin index: 897 .uplugin (cache hit)
|
|
55
81
|
plugin references:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
SomeLocalPlugin: resolved ← Project
|
|
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)
|
|
59
85
|
RetiredPlugin: missing
|
|
60
86
|
totals: 22 resolved, 3 missing of 25 references
|
|
87
|
+
engine plugins available with NO .uproject reference (EnabledByDefault true): GameplayCameras, IKRig
|
|
61
88
|
```
|
|
62
89
|
|
|
63
90
|
Example — `unreal_symbol` for an engine type (engine index already built):
|
|
@@ -70,6 +97,36 @@ unreal_symbol 'AActor' scope=engine: 1 definition(s)
|
|
|
70
97
|
That is a definition line, not a grep dump: grepping a hot engine class name returns ~176 files of
|
|
71
98
|
noise, which is exactly what burns a session's context.
|
|
72
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
|
+
|
|
73
130
|
## Guardrails (automatic, nothing to enable)
|
|
74
131
|
|
|
75
132
|
| Situation | What happens |
|
|
@@ -94,8 +151,18 @@ Because of that model:
|
|
|
94
151
|
- Symbol lookups never stall. If a question needs engine sources and no engine index exists, the
|
|
95
152
|
agent reports the cost and how to pay it, and asks you. Say **yes** once per machine —
|
|
96
153
|
`/unreal-cache stats` shows what you have. Engine scans default to `engineAreas: ["Runtime"]`,
|
|
97
|
-
which is
|
|
98
|
-
`Editor` and `Developer` are opt-in and roughly triple the scan.
|
|
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.
|
|
99
166
|
- Ask for `unreal_index` with `action: "build"`, `scope: "engine"` when you want engine-wide
|
|
100
167
|
navigation, and `action: "compiledb"` when you want clangd's references/call hierarchy.
|
|
101
168
|
- After a branch switch, a rebase, adding a plugin, or an engine version change, say "rebuild the
|
|
@@ -104,17 +171,6 @@ Because of that model:
|
|
|
104
171
|
inside your project tree or your engine installation. The compile database encodes this
|
|
105
172
|
machine's absolute paths, so it is machine-local by design.
|
|
106
173
|
|
|
107
|
-
## Requirements
|
|
108
|
-
|
|
109
|
-
- **Windows**, with **PowerShell 7** (`pwsh`) for registry-based engine discovery. Without it the
|
|
110
|
-
extension still works: resolution falls back to `UNREAL_ENGINE` / `UE_ROOT` /
|
|
111
|
-
`PI_UNREAL_ENGINE_ROOT`, the Launcher's install list, and a scan of
|
|
112
|
-
`C:/Program Files/Epic Games`.
|
|
113
|
-
- An Unreal Engine installation the project can resolve to (Launcher install or source build).
|
|
114
|
-
Descriptor field handling is transcribed from UE 5.8.
|
|
115
|
-
- Nothing else. No compiler, no IDE language service. clangd is optional enrichment for
|
|
116
|
-
`compiledb` and the `clangd --check` acceptance step.
|
|
117
|
-
|
|
118
174
|
## Troubleshooting
|
|
119
175
|
|
|
120
176
|
| Report | What to do |
|
|
@@ -122,82 +178,23 @@ Because of that model:
|
|
|
122
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"`). |
|
|
123
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. |
|
|
124
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). |
|
|
125
|
-
| `unreal_symbol` finds nothing in scope `engine` |
|
|
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. |
|
|
126
183
|
| Results look stale after a rebase or engine update | Re-run with `refresh: true`, or `/unreal-cache clear`. |
|
|
127
184
|
| `no plugin named "X" in the index` | That reference is genuinely not on disk. `/unreal` shows which roots were scanned. |
|
|
128
185
|
|
|
129
|
-
|
|
186
|
+
### What it deliberately does not do
|
|
130
187
|
|
|
131
188
|
- It does not install, register, migrate, or "fix" an engine, and never writes registry keys.
|
|
132
189
|
- It does not build, cook, package, launch the editor, or edit `.ini` files.
|
|
133
190
|
- It does not hand-edit assets, and blocks attempts to.
|
|
134
191
|
- Symbol lookup reports **declarations**. Type resolution, overloads, call sites, inheritance and
|
|
135
|
-
diagnostics are clangd's job when clangd is installed.
|
|
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.
|
|
136
200
|
- `.ini` provenance, live-coding state, and asset metadata are out of scope for v1.
|
|
137
|
-
|
|
138
|
-
## Install
|
|
139
|
-
|
|
140
|
-
Published as **`@aeonsparx/pi-unreal`** on npmjs.
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
pi install npm:@aeonsparx/pi-unreal@1.0.0 # pinned version, reproducible
|
|
144
|
-
pi install git:github.com/Aeonsparx-Interactive/pi-unreal@v1.0 # follows patch releases on the v1.0 line
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
The `npm:` route installs from the registry and pins an exact version. The `git:` route clones the
|
|
148
|
-
repository and checks out the ref: a line alias (`@v1.0`) is a moving tag that patch releases
|
|
149
|
-
force-move, so `pi` picks up patches on update; pin `@v1.0.2` for a fixed snapshot. Loading straight
|
|
150
|
-
from a clone still works and needs no install: `pi -e ./extensions/unreal/index.ts`.
|
|
151
|
-
|
|
152
|
-
## Releasing (maintainers)
|
|
153
|
-
|
|
154
|
-
| `package.json` bump | GitHub Release | npmjs |
|
|
155
|
-
| --- | --- | --- |
|
|
156
|
-
| `major` / `minor` | **new** release `v<major>.<minor>` | staged `<major>.<minor>.<patch>` |
|
|
157
|
-
| `patch` | **updates** the existing `v<major>.<minor>` release | staged `<major>.<minor>.<patch>` |
|
|
158
|
-
|
|
159
|
-
Merge a PR that bumps `version` into `main`. `.github/workflows/release.yml` runs `bun test` on
|
|
160
|
-
Linux and Windows, pushes the immutable tag `v<major>.<minor>.<patch>`, moves the `v<major>.<minor>`
|
|
161
|
-
alias, creates or updates that release, then stages the tarball with `npm stage publish`. **Nothing
|
|
162
|
-
is public until a human approves it** (2FA, from any machine):
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
npm stage list @aeonsparx/pi-unreal # stage ids (no 2FA)
|
|
166
|
-
npm stage view <stage-id> # metadata
|
|
167
|
-
npm stage download <stage-id> # inspect the exact tarball that will publish
|
|
168
|
-
npm stage approve <stage-id> # 2FA - this is the publish act
|
|
169
|
-
npm stage reject <stage-id> # 2FA - frees the staged version number
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
The same queue is on npmjs.com (Staged Packages tab). `npm stage` requires npm >= 11.15.0
|
|
173
|
-
(`npm install -g npm@12`) and Node >= 22.14.0. `NPM_TOKEN` (repository secret) is a Granular Access
|
|
174
|
-
Token with Read+Write and **no** 2FA bypass: CI can stage, only you can publish. Staged versions
|
|
175
|
-
occupy their version number, so a rejected stage burns `x.y.z` until `npm stage reject` frees it.
|
|
176
|
-
Provenance is deliberately off in v1 (`--provenance` needs a Provenance-scope token, and the
|
|
177
|
-
attestation path through stage-then-approve is unverified).
|
|
178
|
-
|
|
179
|
-
## Developers
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
bun test # 66 tests: resolver, descriptors, plugins, cache, gate, scanner, index, compile-db pipeline, fixture hygiene
|
|
183
|
-
bun run typecheck # tsc --noEmit
|
|
184
|
-
bun test/live-check.ts "C:/path/to/Project.uproject" # live machine smoke
|
|
185
|
-
bun test/keys.census.mjs "C:/Program Files/Epic Games/UE_5.8/Engine/Plugins"
|
|
186
|
-
bun run tmp/smoke.mjs project "C:/path/to/Project" # symbol index against a real project
|
|
187
|
-
bun run tmp/smoke.mjs query "C:/path/to/Project" # UnrealBuildTool Query round trip
|
|
188
|
-
bun run tmp/smoke.mjs engine "C:/path/to/Project" # Engine/Source index (minutes, opt-in)
|
|
189
|
-
bun run tmp/smoke.mjs compiledb "C:/path/to/Project" # clangd database + `clangd --check` acceptance
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
From a clone, load the extension into a session without installing it:
|
|
193
|
-
|
|
194
|
-
```bash
|
|
195
|
-
pi -e ./extensions/unreal/index.ts
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
Layout: `extensions/unreal/index.ts` (tools, commands, context card, convention gate),
|
|
199
|
-
`extensions/unreal/lib/` (resolver, descriptors, plugin index, symbol index, UBT Query, compile
|
|
200
|
-
database), `prompts/unreal-context.md`, `skills/ue-engine-resolution/`, `test/`.
|
|
201
|
-
|
|
202
|
-
Design notes and measured internals — engine resolution algorithm, descriptor fidelity, plugin
|
|
203
|
-
discovery, the index and compile-database pipeline — are in [docs/internals.md](docs/internals.md).
|
package/docs/internals.md
CHANGED
|
@@ -17,19 +17,34 @@ compiler-free sources:
|
|
|
17
17
|
through RiderLink, and both derive their context from the same UBT rules this layer reads.
|
|
18
18
|
2. **A regex definition scanner** over the project, its enabled plugins, and the engine areas its
|
|
19
19
|
modules actually depend on — `class`/`struct`/`enum`/`union`, `UFUNCTION`, `UPROPERTY`,
|
|
20
|
-
delegates, aliases, with `Owner:Member` qualified names and forward declarations marked.
|
|
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).
|
|
21
25
|
|
|
22
26
|
Artifacts live in `$PI_UNREAL_CACHE_DIR/indexes/<key>/` (`meta.json`, `defs.tsv`, `files.tsv`,
|
|
23
27
|
plus `includes.tsv` in `full` mode): plain TSV — greppable, inspectable, shared across sessions
|
|
24
28
|
and projects — and never inside the project tree or the engine installation.
|
|
25
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
|
+
|
|
26
38
|
Measured on this box (UE 5.8.3 installed engine; Lp project with 25 enabled plugins):
|
|
27
39
|
|
|
28
40
|
| Scope | Files | Definitions | Cold build |
|
|
29
41
|
| --- | --- | --- | --- |
|
|
30
42
|
| Project + plugins | 2,405 | 38,901 | 6.7 s |
|
|
31
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) |
|
|
32
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 |
|
|
33
48
|
| Cached lookup | — | — | ~0.3 s |
|
|
34
49
|
|
|
35
50
|
Cost discipline is the design. A lookup builds the project index (seconds) and uses an engine
|
|
@@ -43,8 +58,36 @@ Forward declarations rank last, so `unreal_symbol AActor` returns
|
|
|
43
58
|
the `class AActor;` line in `PhysicsCollision.h`.
|
|
44
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
|
|
45
60
|
dependencies (`.uproject` modules → recursive `PublicDependencyModuleNames` → plugin-supplied
|
|
61
|
+
modules → recursive `PublicDependencyModuleNames` → plugin-supplied
|
|
46
62
|
modules → `Engine/Source/<area>/*`), so the index contains what this project can compile against,
|
|
47
|
-
not the whole engine.
|
|
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.
|
|
48
91
|
|
|
49
92
|
One engine fact worth knowing: UBT Query reports `"defines": []` for every module. The effective
|
|
50
93
|
preprocessor set lives in a forced include —
|
|
@@ -169,7 +212,8 @@ Field names, host types, and loading phases are transcribed from the engine's se
|
|
|
169
212
|
candidate, not a guarantee).
|
|
170
213
|
- `moduleRefs` and `includePaths` come from UnrealBuildTool Query, which needs a buildable
|
|
171
214
|
target for the project; when Query fails the index still builds and says so.
|
|
172
|
-
- The engine index is never built implicitly: scans are opt-in through `unreal_index`.
|
|
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.
|
|
173
217
|
- `action: "compiledb"` needs UnrealBuildTool (so an engine installation) and, for the
|
|
174
218
|
`clangd --check` acceptance step, a clangd binary — `CLANGD_PATH`, the Visual Studio LLVM
|
|
175
219
|
toolset, the WinGet LLVM package, or `C:/Program Files/LLVM/bin`. Without clangd the database is
|
|
@@ -177,8 +221,25 @@ Field names, host types, and loading phases are transcribed from the engine's se
|
|
|
177
221
|
- On an installed engine the compile database covers the project's translation units (engine
|
|
178
222
|
modules are precompiled and contribute no compile actions). The engine-wide scope of
|
|
179
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.
|
|
180
231
|
- Engine area selection is a cost dial, not a completeness claim. `DEFAULT_ENGINE_AREAS =
|
|
181
|
-
["Runtime"]` covers
|
|
182
|
-
18,566 files
|
|
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.
|
|
183
244
|
- Index reuse requires the *same root set*: an index built over two modules is never used to answer
|
|
184
245
|
a `Runtime`-wide query. Partial coverage reported as an answer is worse than a rebuild.
|
|
@@ -131,12 +131,20 @@ function engineLines(resolution: EngineResolution): string[] {
|
|
|
131
131
|
return lines;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
/**
|
|
135
|
+
* One line per `.uproject` reference, plus the modules that reference buys: the status alone never
|
|
136
|
+
* said which C++ a plugin contributes, so 'resolved' answered nothing about `UCharacterMoverComponent`.
|
|
137
|
+
* `index` adds the plugins the project does NOT reference: availability and reference status are
|
|
138
|
+
* different facts, and conflating them is what made 'no Mover reference' read as 'no Mover'.
|
|
139
|
+
*/
|
|
140
|
+
function pluginLines(results: PluginReferenceResult[], limit: number, index?: PluginIndex): string[] {
|
|
135
141
|
const lines: string[] = [];
|
|
136
142
|
const shown = results.slice(0, limit);
|
|
137
143
|
for (const result of shown) {
|
|
138
144
|
const status = result.plugin ? `${result.status} ← ${result.plugin.source}` : result.status;
|
|
139
|
-
|
|
145
|
+
const modules = result.plugin?.modules ?? [];
|
|
146
|
+
const moduleText = modules.length ? ` · modules ${modules.map((module) => `${module.moduleName}(${module.type ?? "Runtime"})`).join(", ")}` : "";
|
|
147
|
+
lines.push(` ${result.name}: ${status}${moduleText}`);
|
|
140
148
|
for (const note of result.notes) lines.push(` · ${note}`);
|
|
141
149
|
}
|
|
142
150
|
if (results.length > limit) lines.push(` … ${results.length - limit} more references (raise pluginLimit)`);
|
|
@@ -144,6 +152,12 @@ function pluginLines(results: PluginReferenceResult[], limit: number): string[]
|
|
|
144
152
|
for (const result of results) counts.set(result.status, (counts.get(result.status) ?? 0) + 1);
|
|
145
153
|
const summary = [...counts.entries()].map(([status, count]) => `${count} ${status}`).join(", ");
|
|
146
154
|
lines.push(` totals: ${summary} of ${results.length} references`);
|
|
155
|
+
if (index) {
|
|
156
|
+
const referenced = new Set(results.map((result) => result.name.toLowerCase()));
|
|
157
|
+
const free = index.entries.filter((entry) => entry.source === "Engine" && entry.enabledByDefault && !referenced.has(entry.name.toLowerCase()));
|
|
158
|
+
const names = [...new Set(free.map((entry) => entry.name))].sort((a, b) => a.localeCompare(b));
|
|
159
|
+
lines.push(` engine plugins available with NO .uproject reference (EnabledByDefault true): ${names.length ? names.join(", ") : "none"}`);
|
|
160
|
+
}
|
|
147
161
|
return lines;
|
|
148
162
|
}
|
|
149
163
|
|
|
@@ -381,7 +395,7 @@ const projectInspectTool = defineTool({
|
|
|
381
395
|
pluginResults.push(...resolveProjectPlugins(descriptor, built.index, resolution.version));
|
|
382
396
|
lines.push(`plugin index: ${built.index.scannedFiles} .uplugin over [${built.index.scannedDirs.join(" | ")}] (cache ${built.cache})`);
|
|
383
397
|
lines.push("plugin references:");
|
|
384
|
-
lines.push(...pluginLines(pluginResults, params.pluginLimit ?? 40));
|
|
398
|
+
lines.push(...pluginLines(pluginResults, params.pluginLimit ?? 40, pluginIndex));
|
|
385
399
|
} else {
|
|
386
400
|
lines.push(`plugin references: ${descriptor.plugins.length} declared, not resolved against disk`);
|
|
387
401
|
}
|
|
@@ -514,7 +528,7 @@ async function reportProject(args: string, ctx: ExtensionCommandContext): Promis
|
|
|
514
528
|
if (resolution.status !== "unresolved") {
|
|
515
529
|
const built = await getIndexFor(resolution.engineRoot, projectDirOf(descriptor), descriptor, resolution.version);
|
|
516
530
|
lines.push(`plugin index: ${built.index.scannedFiles} .uplugin (cache ${built.cache})`);
|
|
517
|
-
lines.push(...pluginLines(resolveProjectPlugins(descriptor, built.index, resolution.version), 25));
|
|
531
|
+
lines.push(...pluginLines(resolveProjectPlugins(descriptor, built.index, resolution.version), 25, built.index));
|
|
518
532
|
}
|
|
519
533
|
}
|
|
520
534
|
ctx.ui.notify(truncateLines(lines.join("\n"), 40).text, "info");
|