Files
vertical-partition-system-p…/Source/VerticalPartitionEditor/Private/VerticalPartitionEditorModule.cpp
Bonchellon 04ad6d3f25 Initial commit: Vertical Partition Streaming plugin (UE 5.7)
Bounded vertical route partition / streaming for handcrafted spiral climb maps
(Only Up / Only Climb). Not World Partition, not Level Instances as core arch.

- Z-cell segmentation + optional XY subcells, editor builder + commandlet
- NonDestructive visibility backend + streaming-level backend
- Offline HLOD proxies: MergedMesh / SimplifiedProxy (ProxyLOD)
- Universal Level Instance + Packed Level Actor + HISM support (flatten+merge)
- Vertical-aware runtime streamer (full/preload/HLOD/unload, hysteresis,
  velocity prediction, fall-risk), WP-style debug viz + vp.* console
- Safe-by-default: only static decor streams; sky/lights/gameplay stay persistent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:42:41 +03:00

265 lines
8.9 KiB
C++

// Copyright IHY. Vertical Partition Streaming plugin (editor module).
//
// Installs the runtime->editor bridge so the volume's CallInEditor buttons and the
// vp.* build console commands reach UVerticalPartitionBuilder. Also implements the
// editor-only chunk preview.
#include "Modules/ModuleManager.h"
#include "VerticalPartitionVolume.h"
#include "VerticalPartitionBuilder.h"
#include "VerticalPartitionDescriptor.h"
#include "VerticalPartitionStatics.h"
#include "VerticalPartitionLog.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Components/StaticMeshComponent.h"
#include "DrawDebugHelpers.h"
#include "EngineUtils.h"
#include "Misc/PackageName.h"
#include "UObject/Package.h"
static const FName VPPreviewTag(TEXT("VPHLODPreview"));
#if WITH_EDITOR
#include "Editor.h"
#endif
namespace
{
void LogReport(const FVerticalPartitionBuildReport& R, const TCHAR* Title)
{
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] %s: scanned=%d inside=%d excluded=%d cells=%d empty=%d warn=%d err=%d largest=%s(%d)"),
Title, R.TotalActorsScanned, R.ActorsInsideVolume, R.ActorsExcluded, R.GeneratedCells, R.EmptyCells,
R.WarningCount, R.ErrorCount, *R.LargestCell.ToString(), R.LargestCellActorCount);
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Cyan,
FString::Printf(TEXT("VP %s: %d cells, %d warnings, %d errors"), Title, R.GeneratedCells, R.WarningCount, R.ErrorCount));
}
for (const FVerticalPartitionWarning& W : R.Warnings)
{
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] WARN [%s] %s"), *UVerticalPartitionStatics::WarningTypeToString(W.Type), *W.Message);
}
for (const FVerticalPartitionError& E : R.Errors)
{
UE_LOG(LogVerticalPartition, Error, TEXT("[VP] ERR [%s] %s"), *UVerticalPartitionStatics::WarningTypeToString(E.Type), *E.Message);
}
}
// Persistent coloured cell boxes. GREEN = cell has a baked HLOD proxy, ORANGE = cell
// has streamed actors but no proxy (it will just hide when far), GREY = empty Z slice.
void PreviewChunks(AVerticalPartitionVolume* Volume)
{
#if WITH_EDITOR
if (!Volume || !GEditor) { return; }
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World) { return; }
const FVerticalPartitionSettings& S = Volume->Settings;
const FBox Bounds = Volume->GetVolumeBounds();
UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous();
FlushPersistentDebugLines(World);
DrawDebugBox(World, Bounds.GetCenter(), Bounds.GetExtent(), FColor::Cyan, true, -1.f, 0, 30.f);
const int32 NumZ = UVerticalPartitionStatics::GetNumZCells(Bounds, S);
int32 CellsWithHLOD = 0;
for (int32 Z = 0; Z < NumZ; ++Z)
{
const FVerticalCellId Id(0, 0, Z);
const FBox CB = UVerticalPartitionStatics::GetCellBounds(Id, Bounds, S);
int32 Actors = 0;
bool bHasCell = false;
bool bHasHLOD = false;
if (Desc)
{
for (const FVerticalCellDescriptor& CD : Desc->Cells)
{
if (CD.CellId.Z == Z)
{
bHasCell = true;
Actors += CD.ActorCount;
if (CD.HLODProxyAsset.IsValid()) { bHasHLOD = true; }
}
}
}
if (bHasHLOD) { ++CellsWithHLOD; }
const FColor Color = !bHasCell ? FColor(90, 90, 90)
: (bHasHLOD ? FColor(40, 200, 80) : FColor(235, 140, 30));
DrawDebugBox(World, CB.GetCenter(), CB.GetExtent(), Color, true, -1.f, 0, 12.f);
const FString Label = bHasCell
? FString::Printf(TEXT("Z%d %s actors:%d"), Z, bHasHLOD ? TEXT("[HLOD]") : TEXT("[no-HLOD]"), Actors)
: FString::Printf(TEXT("Z%d (empty/persistent)"), Z);
DrawDebugString(World, CB.GetCenter(), Label, nullptr, Color, -1.f, true);
}
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Preview: %d Z cells (%s). GREEN=HLOD proxy, ORANGE=streamed/no-proxy, GREY=empty. %d cells have a proxy. Use 'Clear Preview' to remove."),
NumZ, Desc ? TEXT("from descriptor") : TEXT("no descriptor yet — Build first for HLOD colours"), CellsWithHLOD);
#endif
}
int32 ClearPreviewActors(UWorld* World)
{
int32 Removed = 0;
#if WITH_EDITOR
if (!World) { return 0; }
for (TActorIterator<AStaticMeshActor> It(World); It; ++It)
{
if (It->ActorHasTag(VPPreviewTag))
{
It->Destroy();
++Removed;
}
}
#endif
return Removed;
}
void ClearPreview(AVerticalPartitionVolume* Volume)
{
#if WITH_EDITOR
if (!GEditor) { return; }
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World) { return; }
FlushPersistentDebugLines(World);
const int32 Removed = ClearPreviewActors(World);
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Preview cleared (%d HLOD preview actors removed)."), Removed);
#endif
}
// Spawn the baked proxy meshes as temporary editor actors so the HLOD geometry is
// directly visible in the viewport. They sit exactly over the source (pivot-at-zero),
// so hide the real decor to compare. Tagged + transient; removed by Clear Preview.
void SpawnHLODPreview(AVerticalPartitionVolume* Volume)
{
#if WITH_EDITOR
if (!Volume || !GEditor) { return; }
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World) { return; }
UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous();
if (!Desc)
{
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] SpawnHLODPreview: no descriptor — run Build first."));
return;
}
ClearPreviewActors(World);
int32 Spawned = 0;
for (const FVerticalCellDescriptor& CD : Desc->Cells)
{
if (!CD.HLODProxyAsset.IsValid()) { continue; }
UStaticMesh* Mesh = Cast<UStaticMesh>(CD.HLODProxyAsset.TryLoad());
if (!Mesh) { continue; }
FActorSpawnParameters Params;
Params.ObjectFlags |= RF_Transient;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AStaticMeshActor* Actor = World->SpawnActor<AStaticMeshActor>(FVector::ZeroVector, FRotator::ZeroRotator, Params);
if (!Actor) { continue; }
Actor->Tags.Add(VPPreviewTag);
Actor->SetActorLabel(FString::Printf(TEXT("VP_HLODPreview_Z%d"), CD.CellId.Z));
if (UStaticMeshComponent* Comp = Actor->GetStaticMeshComponent())
{
Comp->SetMobility(EComponentMobility::Movable);
Comp->SetStaticMesh(Mesh);
Comp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
++Spawned;
}
UE_LOG(LogVerticalPartition, Log, TEXT("[VP] Spawned %d HLOD proxy preview actors (tag '%s'). Hide your static decor to compare; 'Clear Preview' removes them."),
Spawned, *VPPreviewTag.ToString());
if (GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 8.f, FColor::Green, FString::Printf(TEXT("VP: %d HLOD proxy preview actors spawned"), Spawned));
}
#endif
}
void HandleAction(AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action)
{
if (!Volume) { return; }
FVerticalPartitionBuildReport Report;
switch (Action)
{
case EVerticalPartitionAction::Analyze:
Report = UVerticalPartitionBuilder::AnalyzeCurrentLevel(Volume);
LogReport(Report, TEXT("Analyze"));
break;
case EVerticalPartitionAction::Build:
UVerticalPartitionBuilder::Build(Volume, Report);
LogReport(Report, TEXT("Build"));
break;
case EVerticalPartitionAction::RebuildChangedCells:
UVerticalPartitionBuilder::RebuildChangedCells(Volume, Report);
LogReport(Report, TEXT("Rebuild"));
break;
case EVerticalPartitionAction::RebuildHLOD:
UVerticalPartitionBuilder::RebuildHLOD(Volume, Report);
LogReport(Report, TEXT("RebuildHLOD"));
break;
case EVerticalPartitionAction::Validate:
Report = UVerticalPartitionBuilder::ValidateCells(Volume);
LogReport(Report, TEXT("Validate"));
break;
case EVerticalPartitionAction::PreviewChunks:
PreviewChunks(Volume);
break;
case EVerticalPartitionAction::SpawnHLODPreview:
SpawnHLODPreview(Volume);
break;
case EVerticalPartitionAction::ClearPreview:
ClearPreview(Volume);
break;
case EVerticalPartitionAction::ClearGenerated:
UVerticalPartitionBuilder::ClearGenerated(Volume);
break;
case EVerticalPartitionAction::Commit:
UVerticalPartitionBuilder::CommitConversion(Volume, Report);
LogReport(Report, TEXT("Commit"));
break;
case EVerticalPartitionAction::RestoreBackup:
UVerticalPartitionBuilder::RestoreFromBackup(Volume->GetWorld());
break;
case EVerticalPartitionAction::DumpReport:
if (UVerticalPartitionDescriptor* Desc = Volume->Descriptor.LoadSynchronous())
{
const FString Map = Volume->GetWorld() ? FPackageName::GetShortName(Volume->GetWorld()->GetOutermost()->GetName()) : TEXT("Map");
UVerticalPartitionBuilder::DumpReport(Desc->LastReport, Map);
}
break;
}
}
}
class FVerticalPartitionEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override
{
#if WITH_EDITOR
FVerticalPartitionEditorBridge::Handler = [](AVerticalPartitionVolume* Volume, EVerticalPartitionAction Action)
{
HandleAction(Volume, Action);
};
#endif
}
virtual void ShutdownModule() override
{
#if WITH_EDITOR
FVerticalPartitionEditorBridge::Handler = nullptr;
#endif
}
};
IMPLEMENT_MODULE(FVerticalPartitionEditorModule, VerticalPartitionEditor);