Files
vertical-partition-system-p…/Source/VerticalPartition/Private/VerticalPartitionModule.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

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);