Several attempts were made to fix a PIE-specific RecastNavMesh registration failure that occurs when the editor's "Play" button calls OpenLevel(BaseLevel) from inside a PIE session — the level's RecastNavMesh ends up pending-kill and UE silently substitutes an empty AbstractNavData fallback, breaking AI pathfinding. Workarounds tried (and reverted): cascading re-registrations through RegisterNavData / OnInitializeActors / AddNavigationSystemToWorld / Build / SpawnMissingNavigationData; intermediate transition map; duplicated BaseLevel sublevel suffixed _default. None worked — the cause is internal to PIE's OpenLevel handling. Standalone Game and packaged builds initialize the nav system correctly, so production is unaffected. Document the workflow so future contributors don't waste time chasing this PIE-only bug. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
137 lines
8.1 KiB
Markdown
137 lines
8.1 KiB
Markdown
# PS_ProserveEditor - Project Guidelines
|
||
|
||
## Project Overview
|
||
Unreal Engine 5.5 project with runtime editor plugin (PS_Editor). The plugin enables runtime scene editing in packaged builds: object placement, property editing, scenario definition, splines, lighting, and JSON scene save/load.
|
||
|
||
## Architecture
|
||
- **Plugin**: PS_Editor (runtime plugin, must work in packaged builds)
|
||
- **UI**: UMG driven by C++ (Slate)
|
||
- **Gizmos**: Custom actor with BasicShapes + unlit material (InteractiveToolsFramework is editor-only)
|
||
- **Serialization**: JSON via Json/JsonUtilities modules (FJsonObjectConverter)
|
||
- **Input**: Enhanced Input system (AZERTY: arrows + A/Q movement, E/R/T modes)
|
||
|
||
## Editor vs. Gameplay execution boundary — CRITICAL
|
||
|
||
The plugin runs in two distinct modes and it's essential never to leak editor-only code
|
||
into the gameplay runtime loaded via `UPS_Editor_SceneLoader::LoadScene` (standalone).
|
||
|
||
### Editor runtime mode
|
||
Activated when the `APS_Editor_PlayerController` + `APS_Editor_HUD` + `UPS_Editor_MainWidget*`
|
||
stack is created (typical during authoring — the user is in the runtime editor).
|
||
- `UPS_Editor_SceneLoader::bIsEditorModeActive == true`
|
||
- The HUD widget drives property editing, catalogue spawn, save/load, simulate, timeline
|
||
- Interface methods called **only** by the widget (never in gameplay):
|
||
- `GetCustomOptionsForProperty` / `GetCustomOptionsWithValuesForProperty` / `GetCurrentDisplayForProperty`
|
||
- `OnEditorModeChanged`, `OnEditorTick`
|
||
- `OnEditorNeedsReinit`, `OnEditorNeedsRespawn`
|
||
|
||
### Gameplay mode
|
||
The project (PROSERVE) loads a scenario via `UPS_Editor_SceneLoader::LoadScene` from a GameMode
|
||
BeginPlay, without ever instantiating the editor PlayerController / HUD / widget.
|
||
- `UPS_Editor_SceneLoader::bIsEditorModeActive == false`
|
||
- Actors spawn, their properties are applied from JSON, `OnPropertiesLoaded` fires once
|
||
- The timeline subsystem auto-plays if the scenario has tracks, driven **server-side only**
|
||
|
||
### Interface methods that fire in BOTH modes
|
||
- `OnPropertiesLoaded` — after every scene load, editor or gameplay (applied once post-spawn)
|
||
- `OnEditorPropertyChanged` — fires both in editor (on UI edit via `ApplyPropertyFromUI`) AND
|
||
in gameplay (on server when the timeline applies a new keyframe value via
|
||
`TimelineSubsystem::ApplyTrackAtTime`). Despite the name, the "Editor" refers to the
|
||
PS_Editor plugin — the event is the canonical hook for "a property changed at runtime".
|
||
In gameplay only the **server** invokes it; client-side reaction must go through
|
||
`ReplicatedUsing=OnRep_X` RepNotify on the replicated property.
|
||
|
||
### Rule when adding new features
|
||
- Anything tied to property panel UI (new interface method, new widget logic, dropdown
|
||
helpers, etc.) lives in the widget only — no extra work to scope it out of gameplay
|
||
- Anything added to the `SceneLoader::LoadScene` path runs in gameplay and must handle
|
||
`NM_Client`, replication, and server-authority correctly (the timeline subsystem is the
|
||
reference pattern)
|
||
- Expensive BP implementations of editor-UI interface methods (DataTable lookups, class
|
||
resolution, etc.) can stay expensive — they never run outside the authoring session
|
||
|
||
### How to guard code that could run in either mode
|
||
```cpp
|
||
if (UPS_Editor_SceneLoader::IsInEditorMode())
|
||
{
|
||
// editor-only behaviour
|
||
}
|
||
```
|
||
|
||
### Known PIE-only AI quirk — use Standalone for AI testing
|
||
|
||
When the editor's "Play" button calls `UGameplayStatics::OpenLevel(BaseLevel)` from a PIE
|
||
session, the BaseLevel's `RecastNavMesh` actor ends up pending-kill at registration time
|
||
(UE logs `RegistrationFailed_DataPendingKill` ×2 and silently spawns an empty
|
||
`AbstractNavData-Default` as fallback). All `FindPathAsync` / `ProjectPointToNavigation`
|
||
queries return empty after that — pathfinding looks broken with no error.
|
||
|
||
This is **PIE-specific**:
|
||
- Standalone Game (Window → Standalone) — works correctly
|
||
- Packaged builds — work correctly
|
||
- PIE direct on BaseLevel (no editor map / OpenLevel involved) — works correctly
|
||
- PIE on editor map → click Play (OpenLevel transition inside PIE) — broken
|
||
|
||
Multiple workaround attempts have all failed (cascading re-registrations through
|
||
`RegisterNavData` / `OnInitializeActors` / `AddNavigationSystemToWorld` / `Build` /
|
||
`SpawnMissingNavigationData`; transition map; duplicated sublevel suffixed `_default`).
|
||
The cause is internal to PIE's OpenLevel handling and cannot be fixed from the plugin
|
||
side without engine modifications.
|
||
|
||
**Workflow:** for AI iteration, run **Standalone Game** instead of PIE. It exercises the
|
||
exact same code path as packaged builds.
|
||
or gate on `bIsCurrentlyLoading` to skip BP init during JSON restore.
|
||
|
||
## UI Modes — IMPORTANT for widget changes
|
||
|
||
The runtime editor ships with **multiple widget implementations** that must all stay in sync:
|
||
- `PS_Editor_MainWidget.cpp/.h` — modern UMG-based widget (primary target long-term)
|
||
- `PS_Editor_MainWidget_Legacy.cpp/.h` — Slate-only legacy widget (**currently active** as of April 2026)
|
||
- `PS_Editor_MainWidget_UMG.cpp/.h` — UMG variant
|
||
|
||
The user can switch between them via a `UIMode` setting. At any given time only one is running, but the project keeps all three compiled.
|
||
|
||
**Rule:** when changing any behavior inside a widget (new interface calls, new property row logic, new UI controls, bug fixes in RebuildDynamicProperties / ApplyPropertyFromUI / RefreshDynamicPropertyValues, etc.), **apply the change to ALL widget implementations at the same time**. Otherwise the user's current UIMode gets the fix and the others don't — the "it's like you didn't change anything" bug.
|
||
|
||
Symptom that the wrong widget was edited: logs you added don't appear in the Output Log despite the build being clean and the DLL being correctly loaded. Always grep for the function name across all `PS_Editor_MainWidget*.cpp` files before assuming a single edit is sufficient.
|
||
|
||
## Naming Conventions
|
||
- All plugin files and classes use the `PS_Editor_` prefix (e.g., `PS_Editor_GameMode`, `PS_Editor_Pawn`)
|
||
- Code and comments in English
|
||
- Standard UE5 prefixes: `A` for Actors, `U` for UObjects, `F` for structs, `E` for enums, `I` for interfaces
|
||
|
||
## Code Quality
|
||
- Prefer correct, complete implementations over minimal ones.
|
||
- Use appropriate data structures and algorithms — don't brute-force what has a known better solution.
|
||
- When fixing a bug, fix the root cause, not the symptom.
|
||
- If something requires error handling or validation to work reliably, include it without asking.
|
||
|
||
## Workflow — CRITICAL
|
||
- **Do not make behavior / logic changes without first explaining the plan and waiting for the
|
||
user's go-ahead.** Explain what the change will be, why it fixes the issue, and what side
|
||
effects it could have. Only after the user approves (or asks to proceed), apply the edit.
|
||
- Diagnostic-only additions (temporary `UE_LOG`, read-only inspections) are fine without
|
||
approval because they can't regress the project.
|
||
- Refactors, algorithm changes, comportement changes, new defaults, removed features — all
|
||
require approval first, even when they seem obvious.
|
||
- When revealing new data (e.g. logs) suggests a fix, STATE the proposed fix in plain words
|
||
and wait. Don't pre-emptively write + apply the code change in the same turn.
|
||
|
||
## Build
|
||
- Engine: Unreal Engine 5.5
|
||
- Plugin location: `Unreal/Plugins/PS_Editor/`
|
||
- Game module: `PS_ProserveEditor` (in `Unreal/Source/PS_ProserveEditor/`)
|
||
- CLI build command (Editor, Development):
|
||
```
|
||
"<UE_INSTALL>/Engine/Build/BatchFiles/Build.bat" PS_ProserveEditorEditor Win64 Development -Project="<REPO>/Unreal/PS_ProserveEditor.uproject" -WaitMutex -FromMsBuild
|
||
```
|
||
UE install path can be found via registry: `HKLM\SOFTWARE\EpicGames\Unreal Engine\5.5` > `InstalledDirectory`
|
||
|
||
## Project Memory
|
||
Shared project memory is stored in `.claude/memory/` (versioned in the repository). When starting a new session on any machine, read these files to restore context:
|
||
|
||
- `MEMORY.md` — Index of all memory files with one-line summaries
|
||
- `project_ps_editor.md` — Project scope, technical decisions, current phase
|
||
- `feedback_naming.md` — Naming conventions and user preferences
|
||
- `reference_build.md` — Full build command and UE install path details
|