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>
180 lines
8.4 KiB
C++
180 lines
8.4 KiB
C++
// Copyright IHY. Vertical Partition Streaming plugin.
|
|
//
|
|
// Runtime module impl + the vp.* console command surface. Build/editor commands
|
|
// route through FVerticalPartitionEditorBridge (editor builds only); runtime
|
|
// commands talk to the world's UVerticalPartitionRuntimeSubsystem / manager.
|
|
#include "Modules/ModuleManager.h"
|
|
#include "HAL/IConsoleManager.h"
|
|
#include "EngineUtils.h"
|
|
#include "Engine/World.h"
|
|
#include "VerticalPartitionLog.h"
|
|
#include "VerticalPartitionTypes.h"
|
|
#include "VerticalPartitionVolume.h"
|
|
#include "VerticalPartitionManager.h"
|
|
#include "VerticalPartitionRuntimeSubsystem.h"
|
|
|
|
#if WITH_EDITOR
|
|
#include "Editor.h"
|
|
#include "Selection.h"
|
|
#endif
|
|
|
|
DEFINE_LOG_CATEGORY(LogVerticalPartition);
|
|
|
|
namespace VPConsole
|
|
{
|
|
static AVerticalPartitionVolume* FindVolume(UWorld* World)
|
|
{
|
|
if (!World) { return nullptr; }
|
|
#if WITH_EDITOR
|
|
// Prefer an editor-selected volume when available.
|
|
if (GEditor)
|
|
{
|
|
if (USelection* Sel = GEditor->GetSelectedActors())
|
|
{
|
|
for (FSelectionIterator It(*Sel); It; ++It)
|
|
{
|
|
if (AVerticalPartitionVolume* V = Cast<AVerticalPartitionVolume>(*It))
|
|
{
|
|
return V;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
for (TActorIterator<AVerticalPartitionVolume> It(World); It; ++It)
|
|
{
|
|
return *It;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static UVerticalPartitionRuntimeSubsystem* GetSub(UWorld* World)
|
|
{
|
|
return World ? World->GetSubsystem<UVerticalPartitionRuntimeSubsystem>() : nullptr;
|
|
}
|
|
|
|
static AVerticalPartitionManager* FindManager(UWorld* World)
|
|
{
|
|
if (!World) { return nullptr; }
|
|
for (TActorIterator<AVerticalPartitionManager> It(World); It; ++It)
|
|
{
|
|
return *It;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
static void RouteEditor(UWorld* World, EVerticalPartitionAction Action, const TCHAR* Name)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (AVerticalPartitionVolume* V = FindVolume(World))
|
|
{
|
|
FVerticalPartitionEditorBridge::Execute(V, Action);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s: no AVerticalPartitionVolume in the level."), Name);
|
|
}
|
|
#else
|
|
UE_LOG(LogVerticalPartition, Warning, TEXT("[VP] %s is an editor-only command."), Name);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
class FVerticalPartitionModule : public IModuleInterface
|
|
{
|
|
public:
|
|
virtual void StartupModule() override
|
|
{
|
|
using namespace VPConsole;
|
|
IConsoleManager& CM = IConsoleManager::Get();
|
|
auto Add = [this, &CM](const TCHAR* Name, const TCHAR* Help, FConsoleCommandWithWorldAndArgsDelegate Del)
|
|
{
|
|
Commands.Add(CM.RegisterConsoleCommand(Name, Help, Del, ECVF_Default));
|
|
};
|
|
|
|
// ---- runtime: reporting ----
|
|
Add(TEXT("vp.DumpState"), TEXT("Dump runtime streaming stats."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); } }));
|
|
Add(TEXT("vp.DumpCells"), TEXT("Dump every cell's state."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { S->DumpCells(); } }));
|
|
Add(TEXT("vp.DumpWarnings"), TEXT("Dump build warnings/errors from the descriptor."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { S->DumpWarnings(); } }));
|
|
Add(TEXT("vp.ProfileStreaming"), TEXT("Log a one-shot streaming profile snapshot."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { S->DumpRuntimeStats(); S->DumpCells(); } }));
|
|
|
|
// ---- runtime: forcing / overrides ----
|
|
Add(TEXT("vp.ForceCellFull"), TEXT("vp.ForceCellFull <Z> - pin a cell to Full."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellFull(FCString::Atoi(*A[0])); } } }));
|
|
Add(TEXT("vp.ForceCellHLOD"), TEXT("vp.ForceCellHLOD <Z> - pin a cell to HLOD."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ForceCellHLOD(FCString::Atoi(*A[0])); } } }));
|
|
Add(TEXT("vp.UnforceCell"), TEXT("vp.UnforceCell <Z> - release a forced cell."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->UnforceCell(FCString::Atoi(*A[0])); } } }));
|
|
Add(TEXT("vp.UnforceAll"), TEXT("Release all forced cells and ShowOnly mode."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { S->UnforceAll(); } }));
|
|
Add(TEXT("vp.ShowOnlyCell"), TEXT("vp.ShowOnlyCell <Z> - Full only this cell, unload the rest."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->ShowOnlyCell(FCString::Atoi(*A[0])); } } }));
|
|
Add(TEXT("vp.SetFullRange"), TEXT("vp.SetFullRange <Below> <Above>."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetFullRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
|
Add(TEXT("vp.SetHLODRange"), TEXT("vp.SetHLODRange <Below> <Above>."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 1) { S->SetHLODRange(FCString::Atoi(*A[0]), FCString::Atoi(*A[1])); } } }));
|
|
Add(TEXT("vp.SetCellHeight"), TEXT("vp.SetCellHeight <cm> (affects prediction only at runtime)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* S = GetSub(W)) { if (A.Num() > 0) { S->SetCellHeight(FCString::Atof(*A[0])); } } }));
|
|
Add(TEXT("vp.TeleportToCell"), TEXT("vp.TeleportToCell <Z> - move the tracked pawn to a cell."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& A, UWorld* W)
|
|
{ if (auto* M = FindManager(W)) { if (A.Num() > 0) { M->TeleportToCell(FCString::Atoi(*A[0])); } } }));
|
|
|
|
// ---- editor/build (routed via the bridge) ----
|
|
Add(TEXT("vp.Analyze"), TEXT("Analyze the current level (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::Analyze, TEXT("vp.Analyze")); }));
|
|
Add(TEXT("vp.BuildFromSelectedVolume"), TEXT("Build using the selected/first volume (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::Build, TEXT("vp.BuildFromSelectedVolume")); }));
|
|
Add(TEXT("vp.Validate"), TEXT("Validate cells against the level (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::Validate, TEXT("vp.Validate")); }));
|
|
Add(TEXT("vp.Rebuild"), TEXT("Rebuild changed cells (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::RebuildChangedCells, TEXT("vp.Rebuild")); }));
|
|
Add(TEXT("vp.RebuildHLOD"), TEXT("Rebuild HLOD proxies (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::RebuildHLOD, TEXT("vp.RebuildHLOD")); }));
|
|
Add(TEXT("vp.PreviewChunks"), TEXT("Draw coloured cell boxes in the editor viewport (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::PreviewChunks, TEXT("vp.PreviewChunks")); }));
|
|
Add(TEXT("vp.PreviewHLOD"), TEXT("Spawn baked HLOD proxy meshes in the editor viewport (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::SpawnHLODPreview, TEXT("vp.PreviewHLOD")); }));
|
|
Add(TEXT("vp.PreviewClear"), TEXT("Clear preview boxes + spawned HLOD preview actors (editor)."),
|
|
FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>&, UWorld* W)
|
|
{ RouteEditor(W, EVerticalPartitionAction::ClearPreview, TEXT("vp.PreviewClear")); }));
|
|
}
|
|
|
|
virtual void ShutdownModule() override
|
|
{
|
|
IConsoleManager& CM = IConsoleManager::Get();
|
|
for (IConsoleCommand* Cmd : Commands)
|
|
{
|
|
if (Cmd) { CM.UnregisterConsoleObject(Cmd); }
|
|
}
|
|
Commands.Reset();
|
|
}
|
|
|
|
private:
|
|
TArray<IConsoleCommand*> Commands;
|
|
};
|
|
|
|
IMPLEMENT_MODULE(FVerticalPartitionModule, VerticalPartition);
|