// Copyright 2020 Phyronnaz #include "VoxelWorld.h" #include "VoxelGenerators/VoxelGenerator.h" #include "VoxelGenerators/VoxelGeneratorCache.h" #include "IVoxelPool.h" #include "VoxelSettings.h" #include "VoxelDefaultPool.h" #include "VoxelWorldRootComponent.h" #include "VoxelRender/IVoxelRenderer.h" #include "VoxelRender/IVoxelLODManager.h" #include "VoxelRender/VoxelToolRendering.h" #include "VoxelRender/LODManager/VoxelDefaultLODManager.h" #include "VoxelRender/VoxelProceduralMeshComponent.h" #include "VoxelRender/MaterialCollections/VoxelMaterialCollectionBase.h" #include "VoxelRender/MaterialCollections/VoxelInstancedMaterialCollection.h" #include "VoxelRender/Renderers/VoxelDefaultRenderer.h" #include "VoxelData/VoxelData.h" #include "VoxelData/VoxelSaveUtilities.h" #include "VoxelMultiplayer/VoxelMultiplayerTcp.h" #include "VoxelTools/VoxelBlueprintLibrary.h" #include "VoxelTools/VoxelDataTools.h" #include "VoxelTools/VoxelToolHelpers.h" #include "VoxelPlaceableItems/VoxelPlaceableItemManager.h" #include "VoxelPlaceableItems/Actors/VoxelPlaceableItemActorHelper.h" #include "VoxelPlaceableItems/Actors/VoxelAssetActor.h" #include "VoxelPlaceableItems/Actors/VoxelDisableEditsBox.h" #include "VoxelComponents/VoxelInvokerComponent.h" #include "VoxelDebug/VoxelDebugManager.h" #include "VoxelDebug/VoxelLineBatchComponent.h" #include "VoxelEvents/VoxelEventManager.h" #include "VoxelMessages.h" #include "VoxelFeedbackContext.h" #include "VoxelUtilities/VoxelThreadingUtilities.h" #include "EngineUtils.h" #include "Engine/World.h" #include "Engine/Engine.h" #include "Engine/NetDriver.h" #include "Engine/NetConnection.h" #include "Misc/MessageDialog.h" #include "Misc/FileHelper.h" #include "Misc/FeedbackContext.h" #include "Components/BillboardComponent.h" #include "Components/HierarchicalInstancedStaticMeshComponent.h" #include "UObject/ConstructorHelpers.h" #include "HAL/PlatformFilemanager.h" #include "TimerManager.h" #include "Materials/Material.h" #include "Framework/Notifications/NotificationManager.h" #include "Framework/Application/SlateApplication.h" #include "Widgets/Notifications/SNotificationList.h" #include "Serialization/BufferArchive.h" #include "Serialization/MemoryReader.h" void AVoxelWorld::FGameThreadTasks::Flush() { VOXEL_FUNCTION_COUNTER(); check(IsInGameThread()); TFunction Task; while (Tasks.Dequeue(Task)) { Task(); } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// AVoxelWorld::AVoxelWorld() : LODDynamicSettings(MakeVoxelShared()) , RendererDynamicSettings(MakeVoxelShared()) { MultiplayerInterface = UVoxelMultiplayerTcpInterface::StaticClass(); PrimaryActorTick.bCanEverTick = true; PrimaryActorTick.bHighPriority = true; WorldRoot = CreateDefaultSubobject("Root"); LineBatchComponent = CreateDefaultSubobject("LineBatchComponent"); RootComponent = WorldRoot; #if WITH_EDITOR auto* SpriteComponent = CreateEditorOnlyDefaultSubobject(TEXT("Sprite")); if (SpriteComponent) { static ConstructorHelpers::FObjectFinder SpriteFinder(TEXT("/Engine/EditorResources/S_Terrain")); SpriteComponent->Sprite = SpriteFinder.Object; SpriteComponent->SetRelativeScale3D(FVector(0.5f)); SpriteComponent->bHiddenInGame = true; SpriteComponent->bIsScreenSizeScaled = true; SpriteComponent->SpriteInfo.Category = TEXT("Voxel World"); SpriteComponent->SpriteInfo.DisplayName = FText::FromString("Voxel World"); SpriteComponent->SetupAttachment(RootComponent); SpriteComponent->bReceivesDecals = false; } // Automatically refresh material on property change/material recompile const auto RefreshMaterial = [=](UMaterialInterface* Material) { if (!Material || !bAutomaticallyRefreshMaterials || !IsCreated()) { return; } bool bUsed = false; if (MaterialConfig == EVoxelMaterialConfig::RGB) { if (VoxelMaterial == Material) { bUsed = true; } else { for (auto& LODMaterial : LODMaterials) { if (LODMaterial.Material == Material) { bUsed = true; break; } } } } else { if (MaterialCollection && MaterialCollection->GetMaterialIndex(Material->GetFName()) != -1) { bUsed = true; } else { for (auto& LODMaterialCollection : LODMaterialCollections) { if (LODMaterialCollection.MaterialCollection && LODMaterialCollection.MaterialCollection->GetMaterialIndex(Material->GetFName()) != -1) { bUsed = true; break; } } } } if (bUsed) { UVoxelBlueprintLibrary::ApplyNewMaterials(this); } }; UMaterial::OnMaterialCompilationFinished().AddWeakLambda(this, RefreshMaterial); const auto TryRefreshMaterials = [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) { if (!bAutomaticallyRefreshMaterials) { return; } RefreshMaterial(Cast(Object)); if (MaterialConfig != EVoxelMaterialConfig::RGB && Object->IsA()) { bool bUsed = false; if (MaterialCollection == Object) { bUsed = true; } else { for (auto& It : LODMaterialCollections) { if (It.MaterialCollection == Object) { bUsed = true; break; } } } if (auto* MaterialCollectionInstance = Cast(MaterialCollection)) { if (MaterialCollectionInstance->LayersSource == Object) { bUsed = true; } } if (bUsed) { UVoxelBlueprintLibrary::ApplyNewMaterials(this); if (PropertyChangedEvent.MemberProperty && PropertyChangedEvent.MemberProperty->GetFName() == GET_MEMBER_NAME_STATIC(UVoxelInstancedMaterialCollection, MaxMaterialsToBlendAtOnce)) { // Need to recompute the chunks if MaxMaterialsToBlendAtOnce changed, as they are built with the wrong number of indices UVoxelBlueprintLibrary::UpdateAll(this); } } } }; const auto TryRefreshFoliage = [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) { }; FCoreUObjectDelegates::OnObjectPropertyChanged.AddWeakLambda(this, [=](UObject* Object, FPropertyChangedEvent& PropertyChangedEvent) { if (PropertyChangedEvent.ChangeType == EPropertyChangeType::Interactive) { return; } if (!Object || !IsCreated()) { return; } TryRefreshMaterials(Object, PropertyChangedEvent); TryRefreshFoliage(Object, PropertyChangedEvent); }); #endif } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void AVoxelWorld::CreateWorld(FVoxelWorldCreateInfo Info) { VOXEL_FUNCTION_COUNTER(); if (IsCreated()) { FVoxelMessages::Error("Can't create world: already created"); return; } #if WITH_EDITOR if (GetWorld()->WorldType == EWorldType::Editor || GetWorld()->WorldType == EWorldType::EditorPreview) { // Called in editor - probably by a BP construction script // Forward to CreateInEditor CreateInEditor(Info); return; } #endif PlayType = EVoxelPlayType::Game; CreateWorldInternal(Info); if (bUseCameraIfNoInvokersFound && UVoxelInvokerComponentBase::GetInvokers(GetWorld()).Num() == 0) { const auto NetMode = GetWorld()->GetNetMode(); if (NetMode != ENetMode::NM_Standalone) { if (NetMode != ENetMode::NM_DedicatedServer) // Not spawned yet { FVoxelMessages::Warning("Voxel World: Can't use camera as invoker in multiplayer! You need to add a VoxelInvokerComponent to your character"); } } else { auto* CameraInvoker = NewObject(this, NAME_None, RF_Transient); CameraInvoker->SetupAttachment(GetRootComponent(), NAME_None); CameraInvoker->RegisterComponent(); LOG_VOXEL(Log, TEXT("No Voxel Invoker found, using camera as invoker")); } } } void AVoxelWorld::DestroyWorld() { VOXEL_FUNCTION_COUNTER(); if (!IsCreated()) { FVoxelMessages::Error("Can't destroy world: not created"); return; } OnWorldDestroyed.Broadcast(); DestroyWorldInternal(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FVoxelIntBox AVoxelWorld::GetWorldBounds() const { if (bUseCustomWorldBounds) { return FVoxelUtilities::GetCustomBoundsForDepth( FVoxelIntBox::SafeConstruct(CustomWorldBounds.Min, CustomWorldBounds.Max), RenderOctreeDepth); } else { return FVoxelUtilities::GetBoundsFromDepth(RenderOctreeDepth); } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void AVoxelWorld::SetGeneratorObject(UVoxelGenerator* NewGenerator) { Generator = NewGenerator; } void AVoxelWorld::SetGeneratorClass(TSubclassOf NewGeneratorClass) { Generator = NewGeneratorClass.Get(); } void AVoxelWorld::SetRenderOctreeDepth(int32 NewDepth) { RenderOctreeDepth = FMath::Max(1, FVoxelUtilities::ClampDepth(NewDepth)); WorldSizeInVoxel = FVoxelUtilities::GetSizeFromDepth(RenderOctreeDepth); } void AVoxelWorld::SetWorldSize(int32 NewWorldSizeInVoxels) { SetWorldSize(uint32(FMath::Max(0, NewWorldSizeInVoxels))); } void AVoxelWorld::SetWorldSize(uint32 NewWorldSizeInVoxels) { SetRenderOctreeDepth(FVoxelUtilities::GetDepthFromSize(NewWorldSizeInVoxels)); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FIntVector AVoxelWorld::GlobalToLocal(const FVector& Position, EVoxelWorldCoordinatesRounding Rounding) const { const FVector LocalPosition = GetTransform().InverseTransformPosition(Position) / VoxelSize; FIntVector VoxelPosition; switch (Rounding) { case EVoxelWorldCoordinatesRounding::RoundToNearest: VoxelPosition = FVoxelUtilities::RoundToInt(LocalPosition); break; case EVoxelWorldCoordinatesRounding::RoundUp: VoxelPosition = FVoxelUtilities::CeilToInt(LocalPosition); break; case EVoxelWorldCoordinatesRounding::RoundDown: VoxelPosition = FVoxelUtilities::FloorToInt(LocalPosition); break; default: ensure(false); } return VoxelPosition - *WorldOffset; } FVector AVoxelWorld::GlobalToLocalFloatBP(const FVector& Position) const { return GlobalToLocalFloat(Position).ToFloat(); } FVoxelVector AVoxelWorld::GlobalToLocalFloat(const FVector& Position) const { return GetTransform().InverseTransformPosition(Position) / VoxelSize - FVoxelVector(*WorldOffset); } FVector AVoxelWorld::LocalToGlobal(const FIntVector& Position) const { return GetTransform().TransformPosition(VoxelSize * FVector(Position + *WorldOffset)); } FVector AVoxelWorld::LocalToGlobalFloatBP(const FVector& Position) const { return LocalToGlobalFloat(Position); } FVector AVoxelWorld::LocalToGlobalFloat(const FVoxelVector& Position) const { return GetTransform().TransformPosition((VoxelSize * (Position + *WorldOffset)).ToFloat()); } TArray AVoxelWorld::GetNeighboringPositions(const FVector& GlobalPosition) const { return TArray(FVoxelUtilities::GetNeighbors(GlobalToLocalFloat(GlobalPosition))); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void AVoxelWorld::SetOffset(const FIntVector& OffsetInVoxels) { VOXEL_FUNCTION_COUNTER(); CHECK_VOXELWORLD_IS_CREATED_IMPL(this,); *WorldOffset = OffsetInVoxels; Renderer->RecomputeMeshPositions(); } void AVoxelWorld::AddOffset(const FIntVector& OffsetInVoxels, bool bMoveActor) { VOXEL_FUNCTION_COUNTER(); CHECK_VOXELWORLD_IS_CREATED_IMPL(this,); if (bMoveActor) { SetActorLocation(GetTransform().TransformPosition(VoxelSize * FVector(OffsetInVoxels))); } SetOffset(*WorldOffset - OffsetInVoxels); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// FVoxelGeneratorInit AVoxelWorld::GetGeneratorInit() const { return FVoxelGeneratorInit( VoxelSize, FVoxelUtilities::GetSizeFromDepth(RenderOctreeDepth), RenderType, MaterialConfig, MaterialCollection, this); } UVoxelMultiplayerInterface* AVoxelWorld::CreateMultiplayerInterfaceInstance() { FVoxelMessages::Info(FUNCTION_ERROR("Multiplayer with TCP require Voxel Plugin Pro")); return nullptr; } UVoxelMultiplayerInterface* AVoxelWorld::GetMultiplayerInterfaceInstance() const { FVoxelMessages::Info(FUNCTION_ERROR("Multiplayer with TCP require Voxel Plugin Pro")); return nullptr; } void AVoxelWorld::SetCollisionResponseToChannel(ECollisionChannel Channel, ECollisionResponse NewResponse) { VOXEL_FUNCTION_COUNTER(); CollisionPresets.SetResponseToChannel(Channel, NewResponse); if (IsCreated()) { GetWorldRoot().SetCollisionResponseToChannel(Channel, NewResponse); Renderer->ApplyToAllMeshes([&](UVoxelProceduralMeshComponent& MeshComponent) { MeshComponent.SetCollisionResponseToChannel(Channel, NewResponse); }); } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void AVoxelWorld::BeginPlay() { VOXEL_FUNCTION_COUNTER(); Super::BeginPlay(); UpdateCollisionProfile(); if (!IsCreated() && bCreateWorldAutomatically) { // Allow other actors begin play to run before creating the world, if they need to create the global pool auto& TimerManager = GetWorld()->GetTimerManager(); TimerManager.SetTimerForNextTick(this, &AVoxelWorld::CreateWorld); } } void AVoxelWorld::EndPlay(const EEndPlayReason::Type EndPlayReason) { VOXEL_FUNCTION_COUNTER(); Super::EndPlay(EndPlayReason); if (IsCreated()) { DestroyWorld(); } } void AVoxelWorld::Tick(float DeltaTime) { VOXEL_FUNCTION_COUNTER(); if (GetWorld()->WorldType != EWorldType::Editor && GetWorld()->WorldType != EWorldType::EditorPreview) // We don't want to tick the BP in preview { Super::Tick(DeltaTime); } if (IsCreated()) { WorldRoot->TickWorldRoot(); GameThreadTasks->Flush(); #if WITH_EDITOR if (PlayType == EVoxelPlayType::Preview && Data->IsDirty()) { MarkPackageDirty(); } #endif } } void AVoxelWorld::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) { VOXEL_FUNCTION_COUNTER(); if (!IsCreated() || !bEnableCustomWorldRebasing) { Super::ApplyWorldOffset(InOffset, bWorldShift); } else { const FVector RelativeOffset = InOffset / VoxelSize; const FIntVector IntegerOffset = FVoxelUtilities::RoundToInt(RelativeOffset); const FVector GlobalIntegerOffset = FVector(IntegerOffset) * VoxelSize; const FVector Diff = InOffset - GlobalIntegerOffset; Super::ApplyWorldOffset(Diff, bWorldShift); *WorldOffset += IntegerOffset; Renderer->RecomputeMeshPositions(); } } void AVoxelWorld::OnConstruction(const FTransform& Transform) { Super::OnConstruction(Transform); #if WITH_EDITOR if (bIsToggled && !IsCreated() && !GetDefault()->bDisableAutoPreview && (GetWorld()->WorldType == EWorldType::EditorPreview || GetWorld()->WorldType == EWorldType::Editor || GetWorld()->WorldType == EWorldType::GamePreview)) { CreateInEditor(); } #endif } void AVoxelWorld::UnregisterAllComponents(bool bForReregister) { #if WITH_EDITOR if (bDisableComponentUnregister) { Super::UnregisterAllComponents(true); // Do as if it was a reregister so that proc mesh are ignored } else #endif { Super::UnregisterAllComponents(bForReregister); } } #if WITH_EDITOR bool AVoxelWorld::ShouldTickIfViewportsOnly() const { return true; } #endif void AVoxelWorld::BeginDestroy() { if (IsCreated()) { DestroyWorld(); } // Forward to BeginDestroy AFTER destroying the world, else the components are invalid // Note: when exiting the components are still invalid. This doesn't seem to be really useful, but might as well do it here still Super::BeginDestroy(); } void AVoxelWorld::Serialize(FArchive& Ar) { Super::Serialize(Ar); // Temp fix if (!IsTemplate()) { CollisionPresets.FixupData(this); } } void AVoxelWorld::PostLoad() { Super::PostLoad(); if (!ProcMeshClass) { ProcMeshClass = UVoxelProceduralMeshComponent::StaticClass(); } FVoxelDefaultPool::FixPriorityCategories(PriorityCategories); FVoxelDefaultPool::FixPriorityOffsets(PriorityOffsets); SetRenderOctreeDepth(RenderOctreeDepth); if (int32(UVConfig) >= int32(EVoxelUVConfig::Max)) { UVConfig = EVoxelUVConfig::GlobalUVs; } } #if WITH_EDITOR void AVoxelWorld::PreEditChange(FProperty* PropertyThatWillChange) { bDisableComponentUnregister = true; Super::PreEditChange(PropertyThatWillChange); bDisableComponentUnregister = false; } void AVoxelWorld::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) { if (!ProcMeshClass) { ProcMeshClass = UVoxelProceduralMeshComponent::StaticClass(); } if (auto* Property = PropertyChangedEvent.Property) { const FName Name = Property->GetFName(); if (Name == GET_MEMBER_NAME_STATIC(FVoxelLODMaterialsBase, StartLOD)) { for (auto& It : LODMaterials) { It.EndLOD = FMath::Max(It.StartLOD, It.EndLOD); } for (auto& It : LODMaterialCollections) { It.EndLOD = FMath::Max(It.StartLOD, It.EndLOD); } } else if (Name == GET_MEMBER_NAME_STATIC(FVoxelLODMaterialsBase, EndLOD)) { for (auto& It : LODMaterials) { It.StartLOD = FMath::Min(It.StartLOD, It.EndLOD); } for (auto& It : LODMaterialCollections) { It.StartLOD = FMath::Min(It.StartLOD, It.EndLOD); } } } if (auto* Property = PropertyChangedEvent.MemberProperty) { const FName Name = Property->GetFName(); if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, RenderOctreeDepth)) { SetRenderOctreeDepth(RenderOctreeDepth); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, WorldSizeInVoxel)) { SetWorldSize(WorldSizeInVoxel); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, MaterialsHardness)) { for (auto& It : MaterialsHardness) { if (!It.Key.IsEmpty()) { It.Key = FString::FromInt(FMath::Clamp(TCString::Atoi(*It.Key), 0, 255)); } else if (It.Value == 0) { It.Value = 1; } It.Value = FMath::Max(It.Value, 0.f); } MaterialsHardness.KeySort([](const FString& A, const FString& B) { return TCString::Atoi(*A) < TCString::Atoi(*B); }); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, SpawnersCollisionDistanceInVoxel)) { SpawnersCollisionDistanceInVoxel = FMath::CeilToInt(SpawnersCollisionDistanceInVoxel / 32.f) * 32; } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, ChunksClustersSize)) { ChunksClustersSize = FMath::Max(ChunksClustersSize, RENDER_CHUNK_SIZE); const int32 PowerOf2 = FMath::RoundToInt(FMath::Log2(float(ChunksClustersSize))); ChunksClustersSize = 1 << PowerOf2; ChunksClustersSize = FMath::Max(ChunksClustersSize, RENDER_CHUNK_SIZE); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, PriorityCategories)) { FVoxelDefaultPool::FixPriorityCategories(PriorityCategories); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, PriorityOffsets)) { FVoxelDefaultPool::FixPriorityOffsets(PriorityOffsets); } else if (Name == GET_MEMBER_NAME_STATIC(AVoxelWorld, MaterialConfig)) { if (IsCreated() && UVoxelBlueprintLibrary::HasMaterialData(this)) { const auto Result = FMessageDialog::Open( EAppMsgType::YesNo, VOXEL_LOCTEXT("Changing the material config with existing material data! Do you want to clear that data (recommended)?")); if (Result == EAppReturnType::Yes) { UVoxelBlueprintLibrary::ClearMaterialData(this); GetData().MarkAsDirty(); } } } OnPropertyChanged_Interactive.Broadcast(); if (IsCreated() && PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive) { OnPropertyChanged.Broadcast(); if (Property->HasMetaData("Recreate")) { UVoxelBlueprintLibrary::Recreate(this, true); } else if (Property->HasMetaData("RecreateRender")) { RecreateRender(); } else if (Property->HasMetaData("RecreateSpawners")) { RecreateSpawners(); } else if (Property->HasMetaData("UpdateAll")) { UpdateDynamicLODSettings(); GetLODManager().UpdateBounds(FVoxelIntBox::Infinite); } else if (Property->HasMetaData("UpdateLODs")) { UpdateDynamicLODSettings(); GetLODManager().ForceLODsUpdate(); } else if (Property->HasMetaData("UpdateRenderer")) { UpdateDynamicRendererSettings(); GetRenderer().ApplyNewMaterials(); } } } if (bUseCustomWorldBounds) { CustomWorldBounds = GetWorldBounds(); } bDisableComponentUnregister = true; Super::PostEditChangeProperty(PropertyChangedEvent); bDisableComponentUnregister = false; } #endif void AVoxelWorld::UpdateCollisionProfile() { #if WITH_EDITOR // This is needed to restore transient collision profile data. CollisionPresets.LoadProfileData(false); #endif } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void AVoxelWorld::OnWorldLoadedCallback() { LOG_VOXEL(Log, TEXT("%s took %fs to generate"), *GetName(), FPlatformTime::Seconds() - TimeOfCreation); bIsLoaded = true; OnWorldLoaded.Broadcast(); } TVoxelSharedRef AVoxelWorld::CreateDebugManager() const { VOXEL_FUNCTION_COUNTER(); return FVoxelDebugManager::Create(FVoxelDebugManagerSettings(this, PlayType, Pool.ToSharedRef(), Data.ToSharedRef())); } TVoxelSharedRef AVoxelWorld::CreateData() const { VOXEL_FUNCTION_COUNTER(); return FVoxelData::Create(FVoxelDataSettings(this, PlayType), DataOctreeInitialSubdivisionDepth); } TVoxelSharedRef AVoxelWorld::CreatePool() const { VOXEL_FUNCTION_COUNTER(); const auto CreateOwnPool = [&](int32 InNumberOfThreads, bool bInConstantPriorities) { return FVoxelDefaultPool::Create( FMath::Max(1, InNumberOfThreads), bInConstantPriorities, PriorityCategories, PriorityOffsets); }; if (PlayType == EVoxelPlayType::Preview) { return CreateOwnPool(NumberOfThreadsForPreview, true); } else { if (bCreateGlobalPool) { const auto ExistingPool = IVoxelPool::GetPoolForWorld(GetWorld()); if (!ExistingPool.IsValid()) { const auto NewPool = CreateOwnPool(NumberOfThreads, bConstantPriorities); IVoxelPool::SetWorldPool(GetWorld(), NewPool, GetName()); return NewPool; } else { FVoxelMessages::Warning( "CreateGlobalPool = true but global or world pool is already created! Using existing one, NumberOfThreads will be ignored.\n" "Consider setting CreateGlobalPool to false and calling CreateWorldVoxelThreadPool at BeginPlay (for instance in your level blueprint).", this); return ExistingPool.ToSharedRef(); } } else { const auto ExistingPool = IVoxelPool::GetPoolForWorld(GetWorld()); if (ExistingPool.IsValid()) { return ExistingPool.ToSharedRef(); } else { FVoxelMessages::Warning( "CreateGlobalPool = false but global pool isn't created! Creating it with default setting NumberOfThreads = 2. " "You need to call CreateWorldVoxelThreadPool at BeginPlay (for instance in your level blueprint).", this); const auto NewPool = CreateOwnPool(NumberOfThreads, bConstantPriorities); IVoxelPool::SetWorldPool(GetWorld(), NewPool, GetName()); return NewPool; } } } } TVoxelSharedRef AVoxelWorld::CreateRenderer() const { VOXEL_FUNCTION_COUNTER(); return FVoxelDefaultRenderer::Create(FVoxelRendererSettings( this, PlayType, WorldRoot, Data.ToSharedRef(), Pool.ToSharedRef(), ToolRenderingManager.ToSharedRef(), DebugManager.ToSharedRef(), false)); } TVoxelSharedRef AVoxelWorld::CreateLODManager() const { VOXEL_FUNCTION_COUNTER(); return FVoxelDefaultLODManager::Create( FVoxelLODSettings(this, PlayType, Renderer.ToSharedRef(), Pool.ToSharedRef()), this, LODDynamicSettings); } TVoxelSharedPtr AVoxelWorld::CreateEventManager() const { VOXEL_FUNCTION_COUNTER(); return FVoxelEventManager::Create(FVoxelEventManagerSettings(this, PlayType)); } TVoxelSharedPtr AVoxelWorld::CreateToolRenderingManager() const { VOXEL_FUNCTION_COUNTER(); return MakeVoxelShared(); } void AVoxelWorld::CreateWorldInternal(const FVoxelWorldCreateInfo& Info) { VOXEL_FUNCTION_COUNTER(); check(!IsCreated()); LOG_VOXEL(Log, TEXT("Loading world")); bIsCreated = true; bIsLoaded = false; TimeOfCreation = FPlatformTime::Seconds(); if (!Generator.IsValid()) { FVoxelMessages::Error("Invalid generator!", this); } // Setup root ApplyCollisionSettingsToRoot(); if (PlayType == EVoxelPlayType::Game && WorldRoot->BodyInstance.bSimulatePhysics && CollisionTraceFlag == ECollisionTraceFlag::CTF_UseComplexAsSimple) { FVoxelMessages::Error("Simulate physics requires using Simple collisions (either 'Simple And Complex' or 'Use Simple Collision As Complex')", this); } GetLineBatchComponent().Flush(); *WorldOffset = FIntVector::ZeroValue; UpdateDynamicLODSettings(); UpdateDynamicRendererSettings(); ensure(!GeneratorCache); GeneratorCache = NewObject(this); GeneratorCache->SetGeneratorInit(GetGeneratorInit()); GameThreadTasks = MakeVoxelShared(); if (Info.bOverrideData) { ensure(!(Info.DataOverride && Info.DataOverride_Raw)); if (Info.DataOverride) { if (Info.DataOverride->IsCreated()) { Data = Info.DataOverride->GetDataSharedPtr(); } else { FVoxelMessages::Warning(FUNCTION_ERROR("Info.DataOverride is not created! Can't copy data from it."), this); } } else if (Info.DataOverride_Raw) { Data = Info.DataOverride_Raw; } else { FVoxelMessages::Warning(FUNCTION_ERROR("Info.bOverrideData is true, but DataOverride is null!"), this); } } if (!Data) { Data = CreateData(); } Pool = CreatePool(); DebugManager = CreateDebugManager(); EventManager = CreateEventManager(); ToolRenderingManager = CreateToolRenderingManager(); Renderer = CreateRenderer(); Renderer->OnWorldLoaded.AddUObject(this, &AVoxelWorld::OnWorldLoadedCallback); LODManager = CreateLODManager(); if (SpawnerConfig) { FVoxelMessages::Info("Spawners are only available in Voxel Plugin Pro", this); } if (bEnableMultiplayer) { FVoxelMessages::Info("TCP Multiplayer is only available in Voxel Plugin Pro", this); } if (Info.bOverrideData && Info.bOverrideSave) { FVoxelMessages::Warning(FUNCTION_ERROR("Cannot use Info.bOverrideSave if Info.bOverrideData is true!"), this); } if (!Info.bOverrideData) { // Load if possible if (Info.bOverrideSave) { UVoxelDataTools::LoadFromSave(this, Info.SaveOverride); } else if (SaveObject) { LoadFromSaveObject(); if (!IsCreated()) // if LoadFromSaveObject destroyed the world { return; } } // Add placeable items AFTER loading if (!PlaceableItemManager) { // This lets objects adds new items without having to specify a custom class PlaceableItemManager = NewObject(); } PlaceableItemManager->SetExternalGeneratorCache(GeneratorCache); PlaceableItemManager->Clear(); PlaceableItemManager->Generate(); PlaceableItemManager->ApplyToData(GetData()); PlaceableItemManager->DrawDebug(*this, GetLineBatchComponent()); // Do that after Clear/Generate ApplyPlaceableItems(); // Let events add other items OnGenerateWorld.Broadcast(); ensure(!PlaceableItemActorHelper); PlaceableItemActorHelper = NewObject(this); PlaceableItemActorHelper->Initialize(); } if (PlayType == EVoxelPlayType::Preview) { UVoxelProceduralMeshComponent::SetVoxelCollisionsFrozen(false); } } void AVoxelWorld::DestroyWorldInternal() { VOXEL_FUNCTION_COUNTER(); check(IsCreated()); #if WITH_EDITOR if (PlayType == EVoxelPlayType::Preview) { SaveData(); } #endif LOG_VOXEL(Log, TEXT("Unloading world")); bIsCreated = false; bIsLoaded = false; Data.Reset(); Pool.Reset(); DebugManager->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), DebugManager); EventManager->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), EventManager); Renderer->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), Renderer); LODManager->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), LODManager); WorldOffset = MakeVoxelShared(FIntVector::ZeroValue); GameThreadTasks->Flush(); GameThreadTasks.Reset(); // Clear generator cache to avoid keeping instances alive if (ensure(GeneratorCache)) { GeneratorCache->ClearCache(); GeneratorCache->MarkAsGarbage(); GeneratorCache = nullptr; } if (PlaceableItemActorHelper) { PlaceableItemActorHelper->MarkAsGarbage(); PlaceableItemActorHelper = nullptr; } DestroyVoxelComponents(); if (LineBatchComponent) { // Note: components might be destroyed if called in BeginDestroy (eg if actor deleted in the editor) LineBatchComponent->Flush(); } } void AVoxelWorld::DestroyVoxelComponents() { VOXEL_FUNCTION_COUNTER(); auto Components = GetComponents(); // need a copy as we are modifying it when destroying comps for (auto& Component : Components) { if (IsValid(Component) && Component->HasAnyFlags(RF_Transient) && (Component->IsA() || Component->IsA())) { Component->DestroyComponent(); } } } void AVoxelWorld::LoadFromSaveObject() { VOXEL_FUNCTION_COUNTER(); check(SaveObject); if (SaveObject->Depth == -1) { // Not saved yet return; } FVoxelUncompressedWorldSave Save; UVoxelSaveUtilities::DecompressVoxelSave(SaveObject->Save, Save); if (Save.Const().GetDepth() == -1) { FVoxelMessages::Error("Invalid Save Object!", this); return; } if (Save.Const().GetDepth() > Data->Depth) { LOG_VOXEL(Warning, TEXT("Save Object depth is bigger than world depth, the save data outside world bounds will be ignored")); } if (!UVoxelDataTools::LoadFromSave(this, Save)) { const auto Result = FMessageDialog::Open( EAppMsgType::YesNoCancel, VOXEL_LOCTEXT("Some errors occured when loading from the save object. Do you want to continue? This might corrupt the save object\n" "Note: always keep your voxel world toggled when renaming assets referenced by it, else the references won't be updated")); if (Result != EAppReturnType::Yes) { Data->ClearData(); PlayType = EVoxelPlayType::Game; // Hack DestroyWorldInternal(); } } } void AVoxelWorld::ApplyPlaceableItems() { VOXEL_FUNCTION_COUNTER(); TArray PlaceableItemActors; for (TActorIterator ActorItr(GetWorld()); ActorItr; ++ActorItr) { auto* Actor = *ActorItr; if (Actor->IsA() && !bMergeAssetActors) { continue; } if (Actor->IsA() && !bMergeDisableEditsBoxes) { continue; } if (Actor->bOnlyImportIntoPreviewWorld && Actor->PreviewWorld != this) { continue; } PlaceableItemActors.Add(Actor); } TGuardValue AllowScriptsInEditor(GAllowActorScriptExecutionInEditor, true); TMap Priorities; for (auto* It : PlaceableItemActors) { Priorities.Add(It, It->K2_GetPriority()); } PlaceableItemActors.Sort([&](auto& ActorA, auto& ActorB) { return Priorities[&ActorA] < Priorities[&ActorB]; }); for (auto* PlaceableItemActor : PlaceableItemActors) { PlaceableItemActor->K2_AddItemToWorld(this); } } void AVoxelWorld::UpdateDynamicLODSettings() const { LODDynamicSettings->MinLOD = FVoxelUtilities::ClampMesherDepth(MinLOD); LODDynamicSettings->MaxLOD = FVoxelUtilities::ClampMesherDepth(MaxLOD); LODDynamicSettings->InvokerDistanceThreshold = InvokerDistanceThreshold; LODDynamicSettings->ChunksCullingLOD = FVoxelUtilities::ClampDepth(ChunksCullingLOD); LODDynamicSettings->bEnableRender = bRenderWorld; LODDynamicSettings->bEnableCollisions = PlayType == EVoxelPlayType::Game ? bEnableCollisions : true; LODDynamicSettings->bComputeVisibleChunksCollisions = PlayType == EVoxelPlayType::Game ? bComputeVisibleChunksCollisions : true; LODDynamicSettings->VisibleChunksCollisionsMaxLOD = FVoxelUtilities::ClampDepth(PlayType == EVoxelPlayType::Game ? VisibleChunksCollisionsMaxLOD : 32); LODDynamicSettings->bEnableNavmesh = bEnableNavmesh; // bEnableNavmesh is needed for path previews in editor LODDynamicSettings->bComputeVisibleChunksNavmesh = bComputeVisibleChunksNavmesh; LODDynamicSettings->VisibleChunksNavmeshMaxLOD = FVoxelUtilities::ClampDepth(VisibleChunksNavmeshMaxLOD); } void AVoxelWorld::UpdateDynamicRendererSettings() const { VOXEL_FUNCTION_COUNTER(); TArray MaterialCollectionsToInitialize; for (int32 LOD = 0; LOD < 32; LOD++) { auto& LODData = RendererDynamicSettings->LODData[LOD]; // Copy materials LODData.Material = nullptr; for (auto& It : LODMaterials) { if (It.StartLOD <= LOD && LOD <= It.EndLOD) { if (LODData.Material.IsValid()) { FVoxelMessages::Warning(FString::Printf(TEXT("Multiple materials are assigned to LOD %d!"), LOD), this); } LODData.Material = It.Material; } } if (!LODData.Material.IsValid()) { LODData.Material = VoxelMaterial; } // Copy material collection LODData.MaterialCollection = nullptr; for (auto& It : LODMaterialCollections) { if (It.StartLOD <= LOD && LOD <= It.EndLOD) { if (LODData.MaterialCollection.IsValid()) { FVoxelMessages::Warning(FString::Printf(TEXT("Multiple material collections are assigned to LOD %d!"), LOD), this); } LODData.MaterialCollection = It.MaterialCollection; } } if (!LODData.MaterialCollection.IsValid()) { LODData.MaterialCollection = MaterialCollection; } // Set MaxMaterialIndices if (auto* Collection = LODData.MaterialCollection.Get()) { LODData.MaxMaterialIndices.Set(FMath::Max(Collection->GetMaxMaterialIndices(), 1)); MaterialCollectionsToInitialize.AddUnique(Collection); } else { LODData.MaxMaterialIndices.Set(1); } } // Initialize all used collections for (auto* Collection : MaterialCollectionsToInitialize) { Collection->InitializeCollection(); } } void AVoxelWorld::ApplyCollisionSettingsToRoot() const { VOXEL_FUNCTION_COUNTER(); WorldRoot->CollisionTraceFlag = CollisionTraceFlag; WorldRoot->CanCharacterStepUpOn = CanCharacterStepUpOn; WorldRoot->SetGenerateOverlapEvents(bGenerateOverlapEvents); // Only copy collision data - we don't want to override the physics settings WorldRoot->BodyInstance.CopyRuntimeBodyInstancePropertiesFrom(&CollisionPresets); WorldRoot->BodyInstance.SetObjectType(CollisionPresets.GetObjectType()); WorldRoot->BodyInstance.SetPhysMaterialOverride(PhysMaterialOverride); WorldRoot->BodyInstance.bNotifyRigidBodyCollision = bNotifyRigidBodyCollision; WorldRoot->BodyInstance.bUseCCD = bUseCCD; WorldRoot->RecreatePhysicsState(); } void AVoxelWorld::RecreateRender() { VOXEL_FUNCTION_COUNTER(); check(IsCreated()); UpdateDynamicLODSettings(); UpdateDynamicRendererSettings(); LODManager->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), LODManager); Renderer->Destroy(); FVoxelUtilities::DeleteTickable(GetWorld(), Renderer); DestroyVoxelComponents(); Renderer = CreateRenderer(); LODManager = CreateLODManager(); } void AVoxelWorld::RecreateSpawners() { if (SpawnerConfig) { FVoxelMessages::Info("Spawners are only available in Voxel Plugin Pro", this); } } void AVoxelWorld::RecreateAll(const FVoxelWorldCreateInfo& Info) { check(IsCreated()); DestroyWorldInternal(); CreateWorldInternal(Info); } #if WITH_EDITOR void AVoxelWorld::Toggle() { MarkPackageDirty(); if (IsCreated()) { // ensure(bIsToggled); bIsToggled = false; DestroyWorldInternal(); } else { // ensure(!bIsToggled); bIsToggled = true; CreateInEditor(); if (!IsCreated()) // Load failed { bIsToggled = false; } } } void AVoxelWorld::CreateInEditor(const FVoxelWorldCreateInfo& Info) { if (!IVoxelWorldEditor::GetVoxelWorldEditor()) { return; } if (!VoxelWorldEditor) { UClass* VoxelWorldEditorClass = IVoxelWorldEditor::GetVoxelWorldEditor()->GetVoxelWorldEditorClass(); if (VoxelWorldEditorClass) { // Create/Find VoxelWorldEditor for (TActorIterator It(GetWorld(), VoxelWorldEditorClass); It; ++It) { VoxelWorldEditor = *It; break; } if (!VoxelWorldEditor) { FActorSpawnParameters Transient; Transient.ObjectFlags = RF_Transient; VoxelWorldEditor = GetWorld()->SpawnActor(VoxelWorldEditorClass, Transient); } } } BindEditorDelegates(this); if (IsCreated()) { DestroyWorldInternal(); } PlayType = EVoxelPlayType::Preview; CreateWorldInternal(Info); } void AVoxelWorld::SaveData() { if (IsGarbageCollecting()) { return; } check(IsCreated()); if (Data->IsDirty() && GetTransientPackage() != nullptr) { if (!SaveObject && ensure(IVoxelWorldEditor::GetVoxelWorldEditor())) { const EAppReturnType::Type Result = FMessageDialog::Open( EAppMsgType::YesNo, VOXEL_LOCTEXT("The voxel world Save Object is null. You need to create one now if you don't want to lose your changes.\n\nSave changes?")); if (Result == EAppReturnType::No) { // Clear dirty flag so we don't get more annoying popups Data->ClearDirtyFlag(); return; } Modify(); SaveObject = IVoxelWorldEditor::GetVoxelWorldEditor()->CreateSaveObject(); } if (SaveObject) { { FVoxelScopedSlowTask Progress(3); Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Rounding voxels")); if (GetDefault()->bRoundBeforeSaving) { UVoxelDataTools::RoundVoxels(this, FVoxelIntBox::Infinite); } FVoxelUncompressedWorldSave Save; Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Creating save")); UVoxelDataTools::GetSave(this, Save); Progress.EnterProgressFrame(1.f, VOXEL_LOCTEXT("Compressing save")); UVoxelSaveUtilities::CompressVoxelSave(Save, SaveObject->Save); } SaveObject->CopyDepthFromSave(); SaveObject->MarkPackageDirty(); FNotificationInfo Info = FNotificationInfo(VOXEL_LOCTEXT("Voxel world saved!")); Info.CheckBoxState = ECheckBoxState::Checked; FSlateNotificationManager::Get().AddNotification(Info); Data->ClearDirtyFlag(); } else { FNotificationInfo Info = FNotificationInfo(VOXEL_LOCTEXT("Voxel world not saved: no Save Object!")); Info.CheckBoxState = ECheckBoxState::Unchecked; FSlateNotificationManager::Get().AddNotification(Info); } if (bAutomaticallySaveToFile) { FText Error; if (!SaveToFile(GetDefaultFilePath(), Error)) { FMessageDialog::Open(EAppMsgType::Ok, Error); } } } } inline bool CanLoad(const TVoxelSharedPtr& Data) { if (Data->IsDirty()) { const auto Result = FMessageDialog::Open(EAppMsgType::YesNoCancel, VOXEL_LOCTEXT("There are unsaved changes. Loading will overwrite them. Confirm?")); if (Result != EAppReturnType::Yes) { return false; } } return true; } void AVoxelWorld::LoadFromSaveObjectEditor() { check(SaveObject); if (!CanLoad(Data)) { return; } LoadFromSaveObject(); if (IsCreated()) { const FNotificationInfo Info(VOXEL_LOCTEXT("Loaded!")); FSlateNotificationManager::Get().AddNotification(Info); } } bool AVoxelWorld::SaveToFile(const FString& Path, FText& Error) { check(IsCreated()); if (Path.IsEmpty()) { Error = VOXEL_LOCTEXT("Empty Save File Path"); return false; } else if (!FPaths::ValidatePath(Path, &Error)) { return false; } IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile(); PlatformFile.CreateDirectoryTree(*FPaths::GetPath(Path)); FBufferArchive Archive(true); FVoxelCompressedWorldSave CompressedSave; UVoxelDataTools::GetCompressedSave(this, CompressedSave); CompressedSave.Serialize(Archive); if (FFileHelper::SaveArrayToFile(Archive, *Path)) { const FNotificationInfo Info(FText::Format(VOXEL_LOCTEXT("{0} was successfully created"), FText::FromString(Path))); FSlateNotificationManager::Get().AddNotification(Info); if (CompressedSave.Objects.Num() > 0) { const FNotificationInfo OtherInfo(FText::Format(VOXEL_LOCTEXT("The voxel data has {0} placeable items (eg, data assets)! These will not be saved"), CompressedSave.Objects.Num())); FSlateNotificationManager::Get().AddNotification(OtherInfo); } return true; } else { Error = FText::Format(VOXEL_LOCTEXT("Error when creating {0}"), FText::FromString(Path)); return false; } } bool AVoxelWorld::LoadFromFile(const FString& Path, FText& Error) { if (!CanLoad(Data)) { Error = VOXEL_LOCTEXT("Canceled"); return false; } TArray Array; if (!FFileHelper::LoadFileToArray(Array, *Path)) { Error = FText::Format(VOXEL_LOCTEXT("Error when reading {0}"), FText::FromString(Path)); return false; } FMemoryReader Reader(Array); FVoxelCompressedWorldSave CompressedSave; CompressedSave.Serialize(Reader); UVoxelDataTools::LoadFromCompressedSave(this, CompressedSave); const FNotificationInfo Info(FText::Format(VOXEL_LOCTEXT("{0} was successfully loaded"), FText::FromString(Path))); FSlateNotificationManager::Get().AddNotification(Info); return true; } FString AVoxelWorld::GetDefaultFilePath() const { FString Path = SaveFilePath; if (Path.IsEmpty()) { return Path; } Path.RemoveFromEnd(".voxelsave"); int32 Year = 0, Month = 0, DayOfWeek = 0, Day = 0, Hour = 0, Min = 0, Sec = 0, MSec = 0; FPlatformTime::SystemTime(Year, Month, DayOfWeek, Day, Hour, Min, Sec, MSec); const FString Sep = TEXT("_"); const FString Date = FString::FromInt(Year) + Sep + FString::FromInt(Month) + Sep + FString::FromInt(Day) + Sep + FString::FromInt(Hour) + Sep + FString::FromInt(Min) + Sep + FString::FromInt(Sec) + Sep + FString::FromInt(MSec); Path += Date; Path += ".voxelsave"; return Path; } void AVoxelWorld::OnPreSaveWorld(uint32 SaveFlags, UWorld* World) { if (IsCreated() && PlayType == EVoxelPlayType::Preview) { SaveData(); } } void AVoxelWorld::OnPreBeginPIE(bool bIsSimulating) { if (PlayType == EVoxelPlayType::Preview && IsCreated()) { DestroyWorldInternal(); } } void AVoxelWorld::OnEndPIE(bool bIsSimulating) { if (PlayType == EVoxelPlayType::Preview && ensure(!IsCreated()) && bIsToggled) { CreateInEditor(); } } void AVoxelWorld::OnPrepareToCleanseEditorObject(UObject* Object) { if (PlayType == EVoxelPlayType::Preview && IsCreated()) { DestroyWorldInternal(); } } void AVoxelWorld::OnPreExit() { bIsToggled = false; // Disable saving data } void AVoxelWorld::OnApplyObjectToActor(UObject* Object, AActor* Actor) { if (Actor != this) { return; } if (auto* CastedObject = Cast(Object)) { MarkPackageDirty(); VoxelMaterial = CastedObject; MaterialConfig = EVoxelMaterialConfig::RGB; RecreateRender(); } } void IVoxelWorldEditor::SetVoxelWorldEditor(TSharedPtr InVoxelWorldEditor) { check(!VoxelWorldEditor.IsValid()); VoxelWorldEditor = InVoxelWorldEditor; } TSharedPtr IVoxelWorldEditor::VoxelWorldEditor; IVoxelEditorDelegatesInterface::FBindEditorDelegates IVoxelEditorDelegatesInterface::BindEditorDelegatesDelegate; #endif