[{"id":"A35","fact":"**Re-adding a streaming level that is ITSELF a World Partition map asserts the editor, from UE 5.5 onward.** `UEditorLevelUtils::SetStreamingClassForLevel` removes and re-adds the level, which re-enters `ULevel::OnLevelLoaded`; when that level has a UWorldPartition and the owning world is an editor world, OnLevelLoaded reaches `check(!WorldPartition->IsInitialized())` and would Initialize a second time. This kills the editor rather than failing the call, and it fires on a perfectly healthy level - unlike the three asserts already guarded on that path, which are all torn-down-world states.","source":"UE 5.3 / 5.4 / 5.5 / 5.6 / 5.7 / 5.8","engine":"","ue":[],"bl":[],"section":"MifBridge API contracts","value":"Engine source, read at each installed version: `check(!WorldPartition->IsInitialized())` is ABSENT from Level.cpp on 5.3 and 5.4 and PRESENT on 5.5, 5.6, 5.7 and 5.8 (5.8 at Level.cpp:3516, inside the `bIsMainWorldLevel || bInitializeForEditor` branch where bInitializeForEditor = !OwningWorld->IsGameWorld()). Observed live as an editor kill on UE 5.8 during the 2026-09-14 sweep - test_uncovered_reads7 took the editor down and the crash stack named MifBridgeStreaming.cpp:1320 through MifDeferToNextTick. Both accessors the guard needs, `ULevel::GetWorldPartition()` and `UWorldPartition::IsInitialized()`, are declared on all six engines, so the guard needs no version gate."},{"id":"M9eba72","fact":"Failure is the presence of an `error` key, never the absence of `ok` - a response can carry warnings, notes and partial results alongside ok:true, and several endpoints deliberately report a measured zero rather than failing. Correspondingly, never pipe a mutating call to /dev/null and never treat curl's exit code as the operation's result: curl exits 0 on any HTTP response, including a JSON body reporting failure.","source":"docs/16_DASHBOARD.md:234 ('Failure is the presence of `error`, never the absence of `ok`.') and the MifBridge MCP server's own instruction block; docs/02_GOTCHAS.md § Never silence a mutating call (:863-874)","engine":"","ue":[],"bl":[],"section":"MifBridge API contract","value":"`delete_level_actor requires confirm:true; {\"ok\":false,\"error\":\"delete_level_actor requires confirm=true\"} was silently discarded by a >/dev/null cleanup script, so three landscapes accumulated on top of each other and the z-fighting got blamed on the landscape`"},{"id":"M5b4e61","fact":"Endpoints fall in exactly three buckets: read-only (no transaction, so a pure read never pollutes the undo stack), self-managed (no outer transaction — mandatory for a full CompileBlueprint, a world swap, landscape Import(), whole-package asset ops and undo/redo, because a full compile inside a transaction means a later Ctrl-Z restores pointers to a freed class/CDO and crashes the editor), and transacted (one blanket FScopedTransaction).","source":"MifBridge Source/MifBridge/Private/MifBridgeCommon.cpp — IsReadOnlyEndpoint at :708, IsSelfManagedEndpoint at :845 (with the per-entry rationale in comments :851-880, incl. the engine's own ensure(!GIsTransacting) in UTransBuffer::BeginInternal), RunEndpoint's three-way test at :1569-1582. CORRECTION: the candidate's :","engine":"","ue":[],"bl":[],"section":"MifBridge transaction policy"},{"id":"Ma281ff","fact":"The Change Pivot Apply button writes WorldSettings.LevelInstancePivotOffset, which is then PRE-multiplied into the level transform - the mechanism behind editor-vs-game coordinate disagreement. (The engine half is verified; the measured MIF_BotanistHub value (-97927.5,-42609.0,2923.0) exists only as a memory record, not in the repo.)","source":"Engine/Source/Runtime/Engine/Private/LevelInstance/LevelInstanceEditorPivot.cpp:26-46, ILevelInstanceEditorPivotInterface::UpdateOffset (NOT FLevelInstanceEditorPivotHelper::SetPivot as previously cited); applied at LevelInstanceLevelStreaming.cpp:364 and LevelInstanceEditorInstanceActor.cpp:76","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE level instances","value":"`FVector NewPivotOffset = InitState.LevelOffset - LocalToPivot; then LevelTransform = FTransform(WorldSettings->LevelInstancePivotOffset) * LevelTransform`"},{"id":"Me93ca7","fact":"AInstancedFoliageActor::AddFoliageInfo is public, FOLIAGE_API and returns a live FFoliageInfo& but never creates Implementation, so the next AddInstancesImpl null-derefs on its first statement; the real API is AddFoliageType, which follows every branch with CreateImplementation. AddFoliageType also RETURNS a possibly-different UFoliageType* (it duplicates blueprint/foreign types into the IFA), and the returned pointer must be used for every subsequent AddInstance.","source":"docs/01_POSTMORTEMS.md PM-009 (:1245). Engine 5.3.2 source  Runtime/Foliage/Private/InstancedFoliage.cpp verified verbatim: :2294 is exactly `Implementation->PreAddInstances(InSettings, InNewInstances.Num());`, and AddFoliageInfo at :3013-3021 sets only IFA and FoliageTypeUpdateGuid before returning. Duplication via Duplica","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE foliage","value":"`crash: EXCEPTION_ACCESS_VIOLATION reading 0x0 in FFoliageInfo::AddInstancesImpl() at InstancedFoliage.cpp:2294 (Implementation->PreAddInstances(...))`"},{"id":"M288cf2","fact":"ALandscapeProxy::EditorApplySpline silently does nothing on an unknown PaintLayer, returns without deforming on a layers-enabled landscape with an unresolved edit-layer name, and has an unguarded GetLandscapeInfo()->LandscapeActor.Get() deref that faults when no ULandscapeInfo is registered.","source":"Engine/Source/Runtime/Landscape/Classes/LandscapeProxy.h:865 (the header states the no-op); Runtime/Landscape/Private/LandscapeBlueprintSupport.cpp:19 (function), :24 (unguarded GetLandscapeInfo() deref), :26-31 (edit-layer-name early return)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE landscape","value":"`@param PaintLayer - LayerInfo to paint, or none to skip painting. The landscape must be configured with the same layer info in one of its layers or this will do nothing!`"},{"id":"M76a965","fact":"ULandscapeHeightfieldCollisionComponent::GetHeight is not linkable from a plugin — the class is UCLASS(MinimalAPI) and the method carries no export macro; the exported equivalent is ALandscapeProxy::GetHeightAtLocation.","source":"Engine/Source/Runtime/Landscape/Classes/LandscapeHeightfieldCollisionComponent.h:39 (MinimalAPI) and :316 (unexported GetHeight); exported alternative Runtime/Landscape/Classes/LandscapeProxy.h:927 — all three exact","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE landscape","value":"`LANDSCAPE_API TOptional<float> GetHeightAtLocation(FVector Location, EHeightfieldSource HeightFieldSource = EHeightfieldSource::Complex) const;`"},{"id":"M21060b","fact":"Landscape height is uint16 with 32768 == the landscape actor's Z, and one step is DrawScale.Z / 128 (LANDSCAPE_ZSCALE) world units - at the default scale of 100 that is 0.78125 units and roughly +/-25,600 units of usable relief. WorldToHeight clamps, or a +/-40,000-unit request wraps the uint16 into spikes.","source":"MifBridgeLandscape.cpp anonymous namespace - kLandscapeZScale :62, WorldToHeight :99-102, HeightToWorld :104-106, all verified verbatim; header comment :17-19. docs/08_LANDSCAPE.md § Height encoding.","engine":"","ue":[],"bl":[],"section":"UE landscape","value":"`constexpr double kLandscapeZScale = 1.0 / 128.0; WorldToHeight: (uint16)FMath::Clamp(32768.0 + Steps, 0.0, 65535.0); HeightToWorld: ((double)Height - 32768.0) * HeightStepWorld(ZScale)`"},{"id":"M7defeb","fact":"quadsPerSection must be 7/15/31/63/127/255 - any other value builds fine and renders with cracks between sections because the LOD chain assumes power-of-two-minus-one; the handler rejects it up front rather than letting you discover it visually.","source":"MifBridgeLandscape.cpp create_landscape - default 63 at :370, Allowed list at :371, refusal message at :375 ('other values crack between sections'); docs/08_LANDSCAPE.md § Gotchas encoded in the handlers","engine":"","ue":[],"bl":[],"section":"UE landscape","value":"`static const TArray<int32> Allowed = { 7, 15, 31, 63, 127, 255 };`"},{"id":"M064365","fact":"CORRECTED TWICE - 2026-09-20 for the engine boundary, 2026-09-21 for the mechanism. Edit layers are a CHOICE on 5.3-5.6 and always on from 5.7, where `CanHaveLayersContent()` is `return true;` and `ToggleCanHaveLayersContent()` is empty (U169). This row used to say that with layers on, the direct FLandscapeEditDataInterface writes of sculpt/paint \"land in a layer that is never composited\". They did not: they landed in the MERGED heightmap and weightmaps, which the next composite regenerates from the layers, so they were thrown away (U184). create_landscape now leaves layers OFF by default on 5.3-5.6, sets bCanHaveLayersContent BEFORE Import when editLayers:true (toggling the still-empty landscape crashes 5.6.1), and sculpt/paint/import write through a layer (U185). On every engine it must call CreateLandscapeInfo + UpdateLayerInfoMap after Import.","source":"MifBridgeLandscape.cpp - create_landscape's editLayers block, ResolveEditLayer, CompositeAfterLayerWrite; docs/08_LANDSCAPE.md § Gotchas encoded in the handlers","engine":"","ue":[],"bl":[],"section":"UE landscape","value":"`Landscape->bCanHaveLayersContent = bWantEditLayers;` under `#if !MIF_ENGINE_AT_LEAST(5, 7)`"},{"id":"M2f5498","fact":"ULandscapeComponent::UpdateCollisionData dereferences editor-only heightmap Source data unguarded in four places, so it cannot run against cooked-only landscape content.","source":"Engine/Source/Runtime/Landscape/Private/LandscapeEdit.cpp - function at :1321, unguarded Source uses at :1327, :1329, :1332, :1336 (exactly the four lines cited)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE landscape","value":"`check(GetHeightmap()->Source.IsValid()); verify(...Source.GetMipData(...)) x3`"},{"id":"M1b20ad","fact":"A landscape whose material samples a runtime virtual texture needs BOTH the RVT in the landscape's RuntimeVirtualTextures array and an ARuntimeVirtualTextureVolume bounding the region, or the terrain renders black; the editor's 'Create Volumes' button is pure UI because ALandscapeProxy::bSetCreateRuntimeVirtualTextureVolumes is a Transient VisibleAnywhere placeholder nothing reads. Order matters - SetBoundsAlignActor before SetBounds, because SetBounds reads the align actor.","source":"MifBridgeLandscape.cpp bind_landscape_rvt :1793-1797 - the three calls verified verbatim and in that order; docs/08_LANDSCAPE.md § Runtime Virtual Textures, which names FLandscapeProxyUIDetails::CreateRuntimeVirtualTextureVolume as the engine path being mirrored","engine":"","ue":[],"bl":[],"section":"UE landscape RVT","value":"`NewVolume->VirtualTextureComponent->SetVirtualTexture(RVT); NewVolume->VirtualTextureComponent->SetBoundsAlignActor(Landscape); RuntimeVirtualTexture::SetBounds(NewVolume->VirtualTextureComponent);`"},{"id":"WP1","fact":"When a partitioned map initialises in the editor (not cooking), `UWorldPartition` loads the always-loaded actors, and loads spatially loaded ones by region ONLY as the user's last loaded regions - and only if it is the main partition, streaming is enabled, it is not a commandlet, not automation testing, and the per-user setting `EnableLoadingOfLastLoadedRegions` is on. The same function has ONE other loading route, `ForceLoadedActors`, and it is all-or-nothing: the adapter exists only when the WorldPartitionEditor module's `GetDisableLoadingInEditor()` is on (main partition only), and then EVERY descriptor's guid is added and loaded. With that setting off (the default path) the list is empty and the route does nothing","source":"`Runtime/Engine/Private/WorldPartition/WorldPartition.cpp`, `UWorldPartition::Initialize`: adapter created :588, every guid added :653-656, force-loaded :672-676, editor-and-not-cooking guard :701, always-loaded :704-708, the four conditions :712, the setting :715, `LoadLastLoadedRegions()` :718","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE World Partition — what the editor loads when a map opens","value":"`ForceLoadedActors = GetDisableLoadingInEditor() ? new FLoaderAdapterActorList : nullptr`; per descriptor `if (ForceLoadedActors) ForceLoadedActorGuids.Add(guid)`; `AlwaysLoadedActors->Load()`; `if (IsMainWorldPartition() && IsStreamingEnabled() && !IsRunningCommandlet() && !GIsAutomationTesting) if (GetEnableLoadingOfLastLoadedRegions()) LoadLastLoadedRegions()`"},{"id":"WP2","fact":"The \"last loaded regions\" saved for next time are only the bounding boxes of editor loader adapters that are loaded AND user-created (plus loaded, user-created location volumes, by name). A region loaded by code that does not mark its adapter user-created is not remembered, and a load with no bounding box is never saved as a region","source":"same file, `GetUserLoadedEditorRegions` :1857-1872 (the test :1865); `SavePerUserSettings` :1874 stores them :1880, location volumes :1882-1898","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE World Partition — what the editor loads when a map opens","value":"`if (Adapter->GetBoundingBox().IsSet() && Adapter->IsLoaded() && Adapter->GetUserCreated()) Result.Add(box)`"},{"id":"WP3","fact":"**With streaming off, every actor in a partitioned map loads when it opens, and none can be pinned.** Each descriptor is force-flagged non-spatially-loaded whenever streaming is off in the editor; a force-flagged descriptor (or one with invalid bounds) reports `GetIsSpatiallyLoaded() == false`; the always-loaded adapter spans the whole world box and takes exactly the non-spatially-loaded actors, and it is loaded on every editor open (WP1). Pinning refuses any actor that is not spatially loaded unless it is runtime-only (e.g. an HLOD). So on a streaming-off map \"every descriptor already loaded\" is structural - no placement of actors changes it","source":"`WorldPartition.cpp`: flag set at initialize :651, on actor add :1258, on toggle :1348; `IsStreamingEnabledInEditor` :1413-1416; `IsStreamingEnabled` :864-866; `FLoaderAdapterAlwaysLoadedActors` :253-262. `Runtime/Engine/Public/WorldPartition/WorldPartitionActorDesc.h` :106; `Runtime/Engine/Private/WorldPartition/WorldPartitionActorDescView.cpp` :62-64; `Runtime/Engine/Private/WorldPartition/LoaderAdapter/LoaderAdapterPinnedActors.cpp` :26-30 (descriptor) and :55-56 (actor)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE World Partition — what the editor loads when a map opens","value":"`bIsForcedNonSpatiallyLoaded = !IsStreamingEnabledInEditor()`; `IsStreamingEnabledInEditor() = override.IsSet() ? *override : (bEnableStreaming && SupportsStreaming())`; desc `GetIsSpatiallyLoaded() = (forced || !boundsValid) ? false : raw`; always-loaded adapter: box ±HALF_WORLD_MAX, `bIncludeSpatiallyLoadedActors=false`, `bIncludeNonSpatiallyLoadedActors=true`; pin: `if (!GetIsSpatiallyLoaded() && !GetActorIsRuntimeOnly()) return false`"},{"id":"WP4","fact":"Toggling streaming on a live world takes effect at once: every descriptor is unhashed, re-flagged and rehashed, and a NEW always-loaded adapter is built and loaded before the old one is unloaded - so turning streaming on unloads, there and then, every spatially loaded actor no loaded region covers","source":"`WorldPartition.cpp` `SetEnableStreaming` :1334-1341 calls `OnEnableStreamingChanged` :1343-1364 (re-flag :1345-1350, adapter swap :1352-1358)","engine":"5.3.2","ue":["5.3"],"bl":[],"section":"UE World Partition — what the editor loads when a map opens","value":"`OnEnableStreamingChanged`: per desc `Unhash; bIsForcedNonSpatiallyLoaded = !IsStreamingEnabledInEditor(); Hash`; `new FLoaderAdapterAlwaysLoadedActors ... ->Load(); Old->Unload()`"},{"id":"B105","fact":"**99% GPU utilisation at 34 C means thrashing, not computing.** `utilization.gpu` counts memory-controller activity, so a card streaming weights over PCIe reports the same 99% as one doing real math while staying near idle temperature. A card genuinely saturated with compute sits far hotter. Useful as a first-glance tell that a generation is swap-bound rather than slow.","source":"measured 2026-09-18 during the default-mode run: `nvidia-smi --query-gpu=memory.used,utilization.gpu,temperature.gpu` returned `15667 MiB, 99 %, 34` while the sampler was taking 658 s per step.","engine":"5.0.1","ue":[],"bl":[],"section":"UE materials, actor identity and PIE"},{"id":"U293","fact":"**A map's external actors are packages under `<mount>/__ExternalActors__/<map path>/`, each named from the MD5 of its lowercased actor path in base 36: two hash folders, then the rest - `ZZ/ZZ/...` in the Original scheme, `Z/ZZ/...` in the Reduced one.** So the owning map is the package path with its last three segments dropped, and a package whose map no longer exists is an orphan; `lint_blueprints`' external_actors_orphaned tests exactly that.","source":"5.3 `Level.cpp:3514`, 5.3 `Level.cpp:3474`, 5.3 `Level.cpp:3480`, 5.3 `Level.cpp:3500`, 5.4 `Level.cpp:3643`, 5.5 `Level.cpp:3759`, 5.6 `Level.cpp:4103`, 5.7 `Level.cpp:4130`, 5.8 `Level.cpp:4151`, 5.8 `Level.cpp:4069`, 5.8 `Level.cpp:4075`, 5.8 `Level.cpp:4099`","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE World Partition external actors - where a map's actor packages live"},{"id":"U434","fact":"**`FEditorFileUtils::SaveLevel` (SaveWorld) writes over whatever is at the target path without asking, and answers true.** Nothing loaded there: `DeleteExistingMapPackages` deletes every .uasset under `ULevel::GetExternalObjectsPaths(target)`, recursively, then the world is saved at the target - a copy (`ObjectTools::DuplicateSingleObject`) when the open world's own package is on disk, the package renamed in place when it is not. Measured on a 5.8.2 probe 2026-09-27 through save_level_as, before its guard: onto a saved World Partition level that was not loaded, all 4 of its external actor files were deleted (one path written again by the new level's own actor) and its .umap overwritten; onto a Material saved and not loaded, the folder then held only the .umap - the .uasset was gone. Something LOADED at the target: `Package->Rename(REN_Test)` fails and a dialog opens (\"A level with that name already exists\"), which under GIsRunningUnattendedScript is not shown, and SaveLevel returns false. The open world's own path is a plain save.","source":"5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:344`; 5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:729`; 5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:752`; 5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:797`; 5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:838`; 5.3.2 `Editor/UnrealEd/Private/FileHelpers.cpp:859`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:582`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:1023`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:1046`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:1091`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:1137`; 5.8 `Editor/UnrealEd/Private/FileHelpers.cpp:1158`","engine":"5.3.2-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE object allocation and new packages - a name already taken, and a name with `//`"},{"id":"U438","fact":"**Grouping an actor that is already in a group takes it out of that group first, and a group left with no member actor (and at most one subgroup) destroys itself.** AGroupActor::Add calls the old parent's Remove before adding; Remove ends in PostRemove, which on `GroupActors.Num() == 0 && SubGroups.Num() <= 1` takes the group out of its own parent group, the world's ActiveGroupActors and its layers, and calls EditorDestroyActor unless garbage collection is running. So one group_actors call can change - or destroy - a group it does not name, which is why the bulk preview refuses to group an actor that is already grouped.","source":"5.3 `Editor/UnrealEd/Private/GroupActor.cpp:740-767` (Add; the old parent's Remove at :751), :770-787 (Remove), :789 (PostRemove; the condition at :792, EditorDestroyActor at :826); 5.8 the same bodies at :765-792 (Remove at :776), :795-812, :814 (condition :817, EditorDestroyActor :851). Read 2026-09-27.","engine":"","ue":[],"bl":[],"section":"UE editor grouping, classic layers and foliage - what a plugin can call, and what a restore needs"},{"id":"U439","fact":"**GroupActors is gated by the grouping mode too, not only UngroupActors (U424), and the mode is on by default on 5.3 as on 5.8.** On 5.8 GroupActors' body runs only `if(IsGroupingActive() && CanGroupActors(ActorsToGroup))`, on 5.3 `if(IsGroupingActive())`; with the mode off it returns nullptr and says nothing.","source":"5.8 `Editor/UnrealEd/Private/ActorGroupingUtils.cpp:54` (GroupActors), :56 (the condition), :14 (`bool UActorGroupingUtils::bGroupingActive = true;`); 5.3 :51, :53, :12, and UngroupActors' guard at 5.3 :157-159. Read 2026-09-27.","engine":"","ue":[],"bl":[],"section":"UE editor grouping, classic layers and foliage - what a plugin can call, and what a restore needs"},{"id":"U457","fact":"**In a level without World Partition, `UEditorActorSubsystem::DestroyActor(s)` can move OTHER actors out of their World Outliner folders to the root: DestroyActors broadcasts FEditorDelegates::OnDeleteActorsBegin and OnDeleteActorsEnd, the Scene Outliner answers by deleting every folder selected in it, and deleting a folder in such a level re-paths its actors to the parent folder.** A folder made through FActorFolders is added to the Outliner selected, after the Outliner clears its selection, so the folder created last is the one at risk. Measured with A put in a new F1, B in a new F2 and C in the existing F1, then an unrelated X deleted: on 5.3.2 only B moved; on 5.4.4 A, B and C moved; 5.5.4 and 5.8.2 kept all three; a World Partition level keeps them on every engine measured. `delete_level_actor` and every other MifBridge destroy now put back what the destroy moved and say `foldersRestored`.","source":"5.3 `Editor/UnrealEd/Private/Subsystems/EditorActorSubsystem.cpp:551` (OnDeleteActorsBegin) and :589 (OnDeleteActorsEnd), both in DestroyActors; 5.3 `Editor/SceneOutliner/Private/ActorBrowsingMode.cpp:84-85` (both bound) and :2374-2382 (DeleteFoldersBegin and DeleteFoldersEnd called); 5.3 `Editor/SceneOutliner/Private/SSceneOutliner.cpp:1745-1749` (the selected folders cached) and :1751-1792 (each one deleted, its children re-parented); 5.3 `Editor/SceneOutliner/Private/ActorFolderTreeItem.cpp:297-299` (actors re-pathed only when the level does not use actor folders); 5.3 `Editor/SceneOutliner/Private/ActorHierarchy.cpp:789-798` (a created folder is added with Select and Rename) and 5.3 `Editor/SceneOutliner/Private/SSceneOutliner.cpp:997-1000` (Select clears the selection first). tools/test_delete_keeps_folders.py DK101/DK102: red at ffbb4bd5 on 5.3.2, green at 92bf8ed2 on 5.3.2, 5.4.4 and 5.8.2 (foldersRestored 1 and 2 on 5.3 and 5.4, absent on 5.8). Read and measured 2026-09-28, fix agent a9b1bd52; 5.5.4 by the release agent a2e6e71d","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE editor grouping, classic layers and foliage - what a plugin can call, and what a restore needs"},{"id":"U545","fact":"**A landscape height unit is scale.z / 128 cm: on WorldLandscape (scale.z 512, actor Z 0) one unit is 4 cm and stored 32870 reads as 408 cm.**","source":"measured 2026-10-13 on 5.8.2: sample_landscape heights 32870 / 32877 / 32879 -> z 408 / 436 / 444 cm (9 units = 36 cm); Art/Terrain/world_heightmap.json scale_cm [100, 100, 512]","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U546","fact":"**On a landscape with a Splines-type edit layer (ULandscapeEditLayerSplines), a newly laid landscape spline (a World Partition LandscapeSplineActor) is composited into the terrain at once: the ground under it is set to the spline's height, with no Apply step.**","source":"measured 2026-10-14 on 5.8.2 (World Partition landscape, 1 m quads, Z scale 512): add_landscape_spline with control points at z 900 cm; sample_landscape under the middle point went from 540 to 900 cm. The splines layer was made by hand in the Edit Layers panel; Python lists LandscapeEditLayerSplines but has no call to create one","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U547","fact":"**Destroying a LandscapeSplineActor from Python (EditorActorSubsystem.destroy_actor) does NOT recomposite the splines layer: the terrain keeps the deleted spline's shape until Landscape.force_layers_full_update() runs, after which it returns exactly to the layers below.**","source":"measured 2026-10-14 on 5.8.2: after destroy_actor, the sample under the deleted spline still read 900 cm; after force_layers_full_update it read 540 cm, the value before the spline","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U548","fact":"**Setting a LandscapeSplineControlPoint's `location` with set_editor_property (after modify()) changes the stored value but does not rebuild the segments or the terrain**: Python cannot reach the control point's update (the editor drag tool's path), and LandscapeSplinesComponent's ControlPoints/Segments are protected from Python. Code-side edits must recreate the spline, then call force_layers_full_update.","source":"measured 2026-10-14 on 5.8.2: location read back moved (local y -2000 to 4000), but samples at the old and new spots were unchanged (900 and 244 cm)","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U549","fact":"**ULandscapeSplineControlPoint::AutoCalcRotation(false) may face a control point either way along its spline; even with bAlwaysRotateForward=true the forcing applies only to points with exactly two connected segments, so end points can still face backwards. A point facing against its neighbours twists the spline's ribbon, and the twisted part writes nothing to the Splines edit layer (on the inside of curves the terrain is left unshaped). Set each point's Rotation from the path direction instead.**","source":"5.8: Runtime/Landscape/Private/LandscapeSplines.cpp:2094-2099 (\"always rotate forward only applies when there is exactly 2 segments connected\"); measured 2026-10-14 on 5.8.2: after AutoCalcRotation(false), 85 of 164 code-laid control points (list_landscape_splines, rotation.yaw vs path direction) were flipped 180 deg, often mixed within one spline. Terrain more than 10 cm above the target went from 16 runs on 14 splines to 1 run (a self-overlapping path) after Rotation was set from the path tangent","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U541","fact":"**Placing a PCG volume through its actor factory sets the new actor's scale to UPCGEngineSettings::VolumeScale, default (25, 25, 10)**, in UPCGVolumeFactory::PreSpawnActor, so a volume spawned that way is 25 x 25 x 10 the size asked for unless the scale is reset afterward.","source":"5.8: Plugins/PCG/Source/PCGEditor/Private/PCGVolumeFactory.cpp:43-52; 5.8: Plugins/PCG/Source/PCG/Public/PCGEngineSettings.h:20; measured 2026-10-13 (add_volume graph, tools/test_mat_pcg_land_fx_13.py, pushed 44b011de)","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U560","fact":"**A LandscapeTexturePatch on a procedural patch edit layer (LandscapePatchEditLayer) in ALPHA_BLEND mode overwrites the composite of the edit layers BELOW it wherever its alpha is non-zero, so a sculpt written into a lower layer can come out lower (or higher) than written near roads or other patch-baked areas, while that patch layer's own stored heights read back as 32768 (no delta) everywhere. A persistent sculpt layer placed ABOVE the patch layer is added after the blend; writing the difference (wanted - composite) there gives the wanted surface exactly.**","source":"measured 2026-10-16 on 5.8.3 on a World Partition landscape (1 m quads, Z scale 512, layers Layer / Water / Roads / Splines / a patch layer of 50 202 m texture patches, blend ALPHA_BLEND, priority 1075): vertices written +36 cm on the base layer composited up to 36 cm lower beside a patch-baked road while every other layer exported 32768 there; after a new persistent layer was added on top (add_landscape_edit_layer) and the difference imported into it, all 322 written vertices of the exported composite matched the targets and no other vertex changed","engine":"5.8 (measured on 5.8.3 only; patch blend source not read)","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U564","fact":"**`EditorActorSubsystem.get_all_level_actors()` does not return the actors inside loaded Level Instances (only the LevelInstance actors themselves); `GameplayStatics.get_all_actors_of_class(editor_world, cls)` does return them. A script that collects actors to ignore, edit or audit must use the latter when level instances are in play.**","source":"measured 2026-10-16 on 5.8.3: BP_Door actors gathered with get_all_level_actors -> 977, with GameplayStatics.get_all_actors_of_class(world, BP_Door class) -> 1060 (the 83 extra are the doors inside the level's Level Instances; their labels, e.g. Door_ShopStreet2, did not appear in the first list)","engine":"5.8 (measured on 5.8.3 only)","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U566","fact":"**Persistent landscape sculpt edit layers combine ADDITIVELY, with raw 32768 as \"no change\", and this holds for a persistent layer stacked ABOVE a procedural `LandscapePatchEditLayer`.** A persistent layer added on top with `add_landscape_edit_layer` exported as raw 32768 at every one of 346,801 region samples. Importing `32768 - d` into it (d up to 7 raw steps, 12,491 samples non-zero) moved the composite surface by exactly `-d` at all 346,801 samples (exported composite before and after, compared sample by sample: 346,801 exact, 12,491 changed, 0 mismatches). The patch layer below kept its own contribution. Corollary, measured on the same landscape: lowering the BOTTOM persistent layer under a patch layer's footprint is only partly visible, because the patch blends over it. Of 9,954 such vertices, 2,271 followed fully, 5,562 partly and 2,121 not at all. So a cut the patch must not swallow goes in a persistent layer above it.","source":"measured 2026-10-16 on a live UE 5.8.3 editor with `export_landscape_heightmap` (edit_layer and composite) and `import_landscape_heightmap {edit_layer}`, comparing the raw .r16 exports with numpy","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U596","fact":"**Landscape heightmap units: one uint16 step is `scale.z / 128` cm, with 32768 at the actor's Z. At Z scale 512 a step is 4 cm and the range is -1310.72 m .. +1310.68 m around the actor.**","source":"`export_landscape_heightmap` reply on UE 5.8.3, 2026-10-17: `cmPerFileUnit` 4, `worldZAtZero` -131072, `worldZAtMax` 131068 for a landscape at Z scale 512; engine's LANDSCAPE_ZSCALE = 1/128.","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U597","fact":"**A landscape with 127 quads per section and 2 sections per component has 254 quads per component, so N components give N x 254 + 1 vertices per side (3 -> 763, 2 -> 509, 8 -> 2033, 15 -> 3811); neighbouring landscapes at the same scale share an edge when one starts where the other's last vertex is.**","source":"`create_landscape` replies on UE 5.8.3, 2026-10-17: vertsX/Y 763 x 3811, 509 x 3811, 2033 x 509, 2033 x 763; imports of heightmaps cut from one global grid verified sample by sample.","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U621","fact":"**A Level Instance's edit pivot is a transient actor (label Pivot) spawned in the instance's loaded level; set_actor_transform answers \"actor not found\" for it. Moved by K2_SetActorLocation and applied by any property write on it, it writes the edited level's LevelInstancePivotOffset (-200 for a 200 cm move); after commit the LevelInstance actor stays where it was and its actors move by the offset in the world** (RS1603 on 5.3.2, 5.5.4, 5.7.4 and 5.8.3, 2026-10-18).","source":"5.3 `LevelInstanceEditorPivot.cpp:59`; 5.8 `LevelInstanceEditorPivot.cpp:59`","engine":"5.3-5.8","ue":["5.3","5.4","5.5","5.6","5.7","5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"},{"id":"U622","fact":"**On a hash-set World Partition map a new RuntimePartitions[i].HLODSetups element gets its PartitionLayer only in the owner's PostEditChangeChainProperty, which finds the element by GetArrayIndex; notified without array indices the layer stays null, and the setup's Name handler then dereferences it and the editor crashes.** Measured twice on 5.8.3 through edit_container add then set_property Name (2026-10-18); with edit_container sending the chain and the indices the setup, its layers, a RuntimePartitionLHGrid layer and DefaultHLODLayer hold through a save and reopen (RS1606 on 5.5.4, 5.7.4, 5.8.3).","source":"5.8 `WorldPartitionRuntimeHashSet.cpp:1042`","engine":"5.8","ue":["5.8"],"bl":[],"section":"UE generic routes settled live - create_asset, set_property, edit_container, call_function, evaluate_sequence (UE 5.8.2 scratch editor, 2026-10-04)"}]