Bug markers can now be shown and hidden in the current level via static functions.

This commit is contained in:
Jamie Greunbaum 2023-03-25 23:47:12 -04:00
parent 5d7435e67f
commit 89c759894c
9 changed files with 394 additions and 74 deletions

Binary file not shown.

View File

@ -0,0 +1,164 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#include "BugMarkerLoader.h"
#include "BugMarkerActor.h"
#include "HttpModule.h"
#include "JsonObjectConverter.h"
#include "UnrealzillaGlobalSettings.h"
#include "Kismet/GameplayStatics.h"
void ABugMarkerLoader::BeginPlay()
{
Super::BeginPlay();
const FString FullURL = GetDefault<UUnrealzillaGlobalSettings>()->SubmissionServer + "/rest.cgi";
TArray<FString> StatusQueries;
if (GetDefault<UUnrealzillaGlobalSettings>()->bShowUnresolvedBugs)
{
for (const FString Unresolved : GetDefault<UUnrealzillaGlobalSettings>()->UnresolvedStatuses)
{
StatusQueries.Add("status=" + Unresolved);
}
}
if (GetDefault<UUnrealzillaGlobalSettings>()->bShowInProgressBugs)
{
for (const FString InProgress : GetDefault<UUnrealzillaGlobalSettings>()->InProgressStatuses)
{
StatusQueries.Add("status=" + InProgress);
}
}
if (GetDefault<UUnrealzillaGlobalSettings>()->bShowResolvedBugs)
{
for (const FString Resolved : GetDefault<UUnrealzillaGlobalSettings>()->ResolvedStatuses)
{
StatusQueries.Add("status=" + Resolved);
}
}
StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len()));
StatusQueries.Add("api_key=" + GetDefault<UUnrealzillaGlobalSettings>()->APIKey);
const FString QueryString = FString::Join(StatusQueries, TEXT("&"));
FHttpModule &HttpModule = FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> SeverityRequest = HttpModule.CreateRequest();
SeverityRequest->SetVerb(TEXT("GET"));
SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString);
SeverityRequest->OnProcessRequestComplete().BindUObject(this, &ABugMarkerLoader::ServerBugResponse);
SeverityRequest->ProcessRequest();
}
void ABugMarkerLoader::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
this->GetWorldTimerManager().ClearTimer(this->NewBatchTimerHandle);
this->GetWorldTimerManager().ClearTimer(this->UnloadAllTimerHandle);
Super::EndPlay(EndPlayReason);
}
void ABugMarkerLoader::ServerBugResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONBugResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->Destroy();
return;
}
else
{
this->BugBatch = ResponseData.bugs;
this->GetWorldTimerManager().SetTimer(this->NewBatchTimerHandle, this, &ABugMarkerLoader::LoadNewBatch, 1.0f/*this->GetWorld()->GetDeltaSeconds()*/);
}
}
else
{
this->Destroy();
return;
}
}
void ABugMarkerLoader::LoadNewBatch()
{
while (!this->BugBatch.IsEmpty())
{
const FJSONBugData BugData = this->BugBatch[0];
FString LocationString, UpVectorString;
BugData.cf_location.Split(":", &LocationString, &UpVectorString);
FString LocationX, LocationY, LocationZ;
LocationString.Split(",", &LocationX, &LocationY);
LocationY.Split(",", &LocationY, &LocationZ);
FString UpVectorX, UpVectorY, UpVectorZ;
UpVectorString.Split(",", &UpVectorX, &UpVectorY);
UpVectorY.Split(",", &UpVectorY, &UpVectorZ);
const FVector Location(FCString::Atof(*LocationX),FCString::Atof(*LocationY),FCString::Atof(*LocationZ));
const FVector UpVector(FCString::Atof(*UpVectorX),FCString::Atof(*UpVectorY),FCString::Atof(*UpVectorZ));
TSubclassOf<ABugMarkerActor> Class = StaticLoadClass(ABugMarkerActor::StaticClass(), this, BUG_MARKER_ACTOR_BP);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
const FTransform Transform = FTransform(FRotationMatrix::MakeFromZ(UpVector).Rotator(), Location, FVector::OneVector);
ABugMarkerActor *Marker = this->GetWorld()->SpawnActorDeferred<ABugMarkerActor>(Class, Transform, this);
Marker->SetBugID(BugData.id);
Marker->SetBugSummary(BugData.summary);
if (GetDefault<UUnrealzillaGlobalSettings>()->UnresolvedStatuses.Contains(BugData.status))
{
Marker->SetBugStatus(EBugStatus::Unresolved);
}
else if (GetDefault<UUnrealzillaGlobalSettings>()->InProgressStatuses.Contains(BugData.status))
{
Marker->SetBugStatus(EBugStatus::InProgress);
}
else if (GetDefault<UUnrealzillaGlobalSettings>()->ResolvedStatuses.Contains(BugData.status))
{
Marker->SetBugStatus(EBugStatus::Resolved);
}
Marker->FinishSpawning(Transform);
this->Markers.Add(Marker);
this->BugBatch.RemoveAt(0);
if (!(this->BugBatch.Num() % GetDefault<UUnrealzillaGlobalSettings>()->LoadMarkersBatch))
{
break;
}
}
if (!this->BugBatch.IsEmpty())
{
this->GetWorldTimerManager().SetTimer(this->NewBatchTimerHandle, this, &ABugMarkerLoader::LoadNewBatch, this->GetWorld()->GetDeltaSeconds());
}
}
void ABugMarkerLoader::UnloadAll()
{
this->GetWorldTimerManager().ClearTimer(this->NewBatchTimerHandle);
while (!this->Markers.IsEmpty())
{
AActor *Marker = this->Markers[0];
this->Markers.RemoveAt(0);
Marker->Destroy();
if (!(this->Markers.Num() % GetDefault<UUnrealzillaGlobalSettings>()->UnloadMarkersBatch))
{
break;
}
}
if (!this->Markers.IsEmpty())
{
this->GetWorldTimerManager().SetTimer(this->UnloadAllTimerHandle, this, &ABugMarkerLoader::UnloadAll, this->GetWorld()->GetDeltaSeconds());
}
else
{
this->Destroy();
}
}

View File

@ -3,11 +3,13 @@
#include "BugPlacerPawn.h"
#include "BugMarkerActor.h"
#include "BugMarkerLoader.h"
#include "ExitingBugPlacementScreen.h"
#include "UnrealzillaGlobalSettings.h"
#include "Components/MaterialBillboardComponent.h"
#include "Components/SphereComponent.h"
#include "EngineUtils.h"
#include "GameFramework/Character.h"
#include "GameFramework/FloatingPawnMovement.h"
#include "HAL/IConsoleManager.h"
@ -118,8 +120,9 @@ void ABugPlacerPawn::PlaceBugMarker()
#if WITH_EDITOR
SpawnParams.bHideFromSceneOutliner = true;
#endif
ABugMarkerActor *Marker = this->GetWorld()->SpawnActor<ABugMarkerActor>(this->BugMarkerBlueprintClass.Get(), this->PlacementMarkerRoot->GetComponentTransform(), SpawnParams);
TSubclassOf<ABugMarkerActor> Class = StaticLoadClass(ABugMarkerActor::StaticClass(), nullptr, BUG_MARKER_ACTOR_BP);
ABugMarkerActor *Marker = this->GetWorld()->SpawnActor<ABugMarkerActor>(Class.Get(), this->PlacementMarkerRoot->GetComponentTransform(), SpawnParams);
Marker->LoadBugSubmissionForm();
}
}
@ -168,10 +171,42 @@ void ABugPlacerPawn::ExitCanceled()
}
void ABugPlacerPawn::SpawnBugPlacerPawn(const UObject *WorldContextObject, TSubclassOf<ABugPlacerPawn> Class)
void ABugPlacerPawn::SpawnBugPlacerPawn(const UObject *WorldContextObject)
{
TSubclassOf<ABugPlacerPawn> Class = StaticLoadClass(ABugPlacerPawn::StaticClass(), nullptr, BUG_PLACER_PAWN_BP);
const FTransform &Transform = UGameplayStatics::GetPlayerCameraManager(WorldContextObject, 0)->GetActorTransform();
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
WorldContextObject->GetWorld()->SpawnActor<ABugPlacerPawn>(Class.Get(), Transform, SpawnParams);
WorldContextObject->GetWorld()->SpawnActor<ABugPlacerPawn>(Class, Transform, SpawnParams);
}
void ABugPlacerPawn::ShowBugMarkersInLevel(const UObject *WorldContextObject)
{
for (TActorIterator<AActor> Iterator(WorldContextObject->GetWorld(), ABugMarkerLoader::StaticClass()); Iterator; ++Iterator)
{
return;
}
WorldContextObject->GetWorld()->SpawnActor<ABugMarkerLoader>(ABugMarkerLoader::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator);
}
void ABugPlacerPawn::HideBugMarkersInLevel(const UObject *WorldContextObject)
{
for (TActorIterator<AActor> Iterator(WorldContextObject->GetWorld(), ABugMarkerLoader::StaticClass()); Iterator; ++Iterator)
{
ABugMarkerLoader *BugMarkerLoader = Cast<ABugMarkerLoader>(*Iterator);
BugMarkerLoader->UnloadAll();
}
}
const FString ABugPlacerPawn::FormatQueryString(const TMap<FString, FString> &QueryData)
{
TArray<FString> AssembledKeyValuePairs;
TArray<FString> QueryKeys;
QueryData.GenerateKeyArray(QueryKeys);
for (const FString &QueryKey : QueryKeys)
{
AssembledKeyValuePairs.Add(QueryKey + "=" + QueryData[QueryKey]);
}
return FString::Join(AssembledKeyValuePairs, TEXT("&"));
}

View File

@ -3,6 +3,7 @@
#include "BugSubmissionForm.h"
#include "BugMarkerActor.h"
#include "BugPlacerPawn.h"
#include "CommonButtonBase.h"
#include "CommonTextBlock.h"
#include "HttpModule.h"
@ -31,19 +32,6 @@ const FText FormatFloatToText(float Float, int32 IntegralDigits, int32 Fractiona
return FText::AsNumber(Float, &NumberFormat);
}
const FString FormatQueryString(const TMap<FString, FString> &QueryData)
{
TArray<FString> AssembledKeyValuePairs;
TArray<FString> QueryKeys;
QueryData.GenerateKeyArray(QueryKeys);
for (const FString &QueryKey : QueryKeys)
{
AssembledKeyValuePairs.Add(QueryKey + "=" + QueryData[QueryKey]);
}
return FString::Join(AssembledKeyValuePairs, TEXT("&"));
}
const FString GetGameVersion()
{
FString GameVersion;
@ -67,7 +55,7 @@ void UBugSubmissionForm::NativeOnInitialized()
// Assemble query data into key:value pairs
TMap<FString, FString> QueryData;
QueryData.Add("api_key", GetDefault<UUnrealzillaGlobalSettings>()->APIKey);
const FString QueryString = FormatQueryString(QueryData);
const FString QueryString = ABugPlacerPawn::FormatQueryString(QueryData);
// Query the server for information about the current product
FHttpModule &HttpModule = FHttpModule::Get();
@ -113,11 +101,11 @@ void UBugSubmissionForm::SetMarkerData(ABugMarkerActor *BugMarker)
this->MapName = FText::AsCultureInvariant(this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len()));
const FVector MarkerLocation = this->BugMarkerActor->GetActorLocation();
const FVector MarkerLocationVector = this->BugMarkerActor->GetActorLocation();
FFormatNamedArguments MarkerLocationArgs;
MarkerLocationArgs.Add("X", FormatFloatToText(MarkerLocation.X, 1, 4));
MarkerLocationArgs.Add("Y", FormatFloatToText(MarkerLocation.Y, 1, 4));
MarkerLocationArgs.Add("Z", FormatFloatToText(MarkerLocation.Z, 1, 4));
MarkerLocationArgs.Add("X", FormatFloatToText(MarkerLocationVector.X, 1, 4));
MarkerLocationArgs.Add("Y", FormatFloatToText(MarkerLocationVector.Y, 1, 4));
MarkerLocationArgs.Add("Z", FormatFloatToText(MarkerLocationVector.Z, 1, 4));
const FText MarkerLocationText = FText::Format(NSLOCTEXT("BugSubmissionForm", "MarkerLocation", "{X},{Y},{Z}"), MarkerLocationArgs);
const FVector MarkerUpVector = this->BugMarkerActor->GetActorUpVector();
@ -224,7 +212,7 @@ void UBugSubmissionForm::SubmitForm()
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = HttpModule.CreateRequest();
Request->SetVerb(TEXT("POST"));
Request->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
Request->SetURL(FullURL + RequestURL + "?" + FormatQueryString(QueryData));
Request->SetURL(FullURL + RequestURL + "?" + ABugPlacerPawn::FormatQueryString(QueryData));
Request->SetContentAsString(PostJsonString);
Request->OnProcessRequestComplete().BindUObject(this, &UBugSubmissionForm::ServerPOSTResponse);
Request->ProcessRequest();

View File

@ -6,6 +6,17 @@
#include "BugMarkerActor.generated.h"
#define BUG_MARKER_ACTOR_BP TEXT("Blueprint'/Unrealzilla/BP_BugMarkerActor.BP_BugMarkerActor_C'")
UENUM()
enum class EBugStatus : uint8
{
Unresolved UMETA(DisplayName="Unresolved"),
InProgress UMETA(DisplayName="In Progress"),
Resolved UMETA(DisplayName="Resolved"),
MAX UMETA(Hidden)
};
UCLASS()
class UNREALZILLA_API ABugMarkerActor : public AActor
@ -15,4 +26,13 @@ class UNREALZILLA_API ABugMarkerActor : public AActor
public:
UFUNCTION(BlueprintImplementableEvent)
void LoadBugSubmissionForm();
void SetBugID(const uint32 &ID) { this->BugID = ID; }
void SetBugSummary(const FString &Summary) { this->BugSummary = Summary; }
void SetBugStatus(const EBugStatus &Status) { this->BugStatus = Status; }
private:
uint32 BugID = 0;
FString BugSummary;
EBugStatus BugStatus;
};

View File

@ -0,0 +1,88 @@
// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
#include "BugMarkerLoader.generated.h"
/**
* JSON structs for bug lists
*/
USTRUCT(Blueprintable)
struct FJSONBugData
{
GENERATED_BODY()
public:
UPROPERTY()
int32 id = -1;
UPROPERTY()
FString summary;
UPROPERTY()
FString component;
UPROPERTY()
FString cf_mapname;
UPROPERTY()
FString cf_location;
UPROPERTY()
FString platform;
UPROPERTY()
FString op_sys;
UPROPERTY()
bool is_open = true;
UPROPERTY()
FString severity;
UPROPERTY()
FString status;
UPROPERTY()
FString resolution;
UPROPERTY()
int32 dupe_of = -1;
};
USTRUCT(Blueprintable)
struct FJSONBugResponse
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly)
TArray<FJSONBugData> bugs;
UPROPERTY(BlueprintReadOnly)
bool error = false;
UPROPERTY(BlueprintReadOnly)
int32 code = -1;
UPROPERTY(BlueprintReadOnly)
FString message;
UPROPERTY(BlueprintReadOnly)
FString documentation;
};
/**
* END JSON structs for bug lists
*/
UCLASS()
class UNREALZILLA_API ABugMarkerLoader : public AActor
{
GENERATED_BODY()
public:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
void UnloadAll();
private:
void ServerBugResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success);
void LoadNewBatch();
TArray<AActor*> Markers;
TArray<FJSONBugData> BugBatch;
FTimerHandle NewBatchTimerHandle;
FTimerHandle UnloadAllTimerHandle;
};

View File

@ -7,6 +7,8 @@
#include "BugPlacerPawn.generated.h"
#define BUG_PLACER_PAWN_BP TEXT("Blueprint'/Unrealzilla/BP_BugPlacerPawn.BP_BugPlacerPawn_C'")
UCLASS()
class UNREALZILLA_API ABugPlacerPawn : public APawn
@ -25,7 +27,16 @@ public:
void Deactivate();
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject"))
static void SpawnBugPlacerPawn(const UObject *WorldContextObject, TSubclassOf<class ABugPlacerPawn> Class);
static void SpawnBugPlacerPawn(const UObject *WorldContextObject);
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject"))
static void ShowBugMarkersInLevel(const UObject *WorldContextObject);
UFUNCTION(BlueprintCallable, meta=(WorldContext="WorldContextObject"))
static void HideBugMarkersInLevel(const UObject *WorldContextObject);
UFUNCTION(BlueprintPure)
static const FString FormatQueryString(const TMap<FString, FString> &QueryData);
protected:
UFUNCTION(BlueprintCallable)
@ -60,8 +71,6 @@ protected:
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TObjectPtr<class UFloatingPawnMovement> PawnMovement;
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)
TSubclassOf<class ABugMarkerActor> BugMarkerBlueprintClass;
UPROPERTY(BlueprintReadOnly, EditDefaultsOnly)
TSubclassOf<class UExitingBugPlacementScreen> ExitingBugPlacementScreenClass;
@ -75,11 +84,10 @@ private:
uint8 bCurrentTraceHit : 1;
TObjectPtr<class ACharacter> OriginalPlayer;
TObjectPtr<class ABugMarkerActor> BugMarker;
TObjectPtr<class UExitingBugPlacementScreen> ExitingBugPlacementScreen;
//UPROPERTY(EditDefaultsOnly, meta=(UIMin="0.0", UIMax="0.25"))
// float TraceTimerDelay = 0.05f;
FTimerHandle TraceTimerHandle;
};

View File

@ -65,41 +65,41 @@ public:
*/
/**
* JSON structs for bug lists
*/
USTRUCT(Blueprintable)
struct FJSONBugData
{
GENERATED_BODY()
public:
UPROPERTY()
FString component;
UPROPERTY()
FString cf_mapname;
UPROPERTY()
FString cf_location;
};
USTRUCT(Blueprintable)
struct FJSONBugResponse
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadOnly)
TArray<FJSONBugData> bugs;
UPROPERTY(BlueprintReadOnly)
bool error = false;
UPROPERTY(BlueprintReadOnly)
int32 code = -1;
UPROPERTY(BlueprintReadOnly)
FString message;
UPROPERTY(BlueprintReadOnly)
FString documentation;
};
/**
* END JSON structs for bug lists
*/
///**
// * JSON structs for bug lists
// */
//USTRUCT(Blueprintable)
//struct FJSONBugData
//{
// GENERATED_BODY()
//public:
// UPROPERTY()
// FString component;
// UPROPERTY()
// FString cf_mapname;
// UPROPERTY()
// FString cf_location;
//};
//
//USTRUCT(Blueprintable)
//struct FJSONBugResponse
//{
// GENERATED_BODY()
//public:
// UPROPERTY(BlueprintReadOnly)
// TArray<FJSONBugData> bugs;
// UPROPERTY(BlueprintReadOnly)
// bool error = false;
// UPROPERTY(BlueprintReadOnly)
// int32 code = -1;
// UPROPERTY(BlueprintReadOnly)
// FString message;
// UPROPERTY(BlueprintReadOnly)
// FString documentation;
//};
///**
// * END JSON structs for bug lists
// */
/**

View File

@ -23,6 +23,10 @@ public:
UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly, Category="Bug Placement", meta=(DisplayName="Arbitrary Placement Distance"))
float ArbitraryBugPlacementDistance = 250.0f;
// The status to use when filing a new bug. A status such as "UNCONFIRMED" is suggested.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting")
FString DefaultStatus;
// The Bugzilla server where bugs will be posted.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting")
FString SubmissionServer;
@ -36,32 +40,45 @@ public:
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting")
int32 BugReportWidgetDepth = 0;
// The status to use when filing a new bug. A status such as "UNCONFIRMED" is suggested.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
FString DefaultStatus;
// Whether to show unresolved bugs when displaying bug report markers.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
bool bShowUnresolvedBugs = false;
// Whether to show in-progress bugs when displaying bug report markers.
bool bShowUnresolvedBugs = true;
// Colour tint to use for unresolved bugs.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
bool bShowInProgressBugs = false;
// Whether to show resolved bugs when displaying bug report markers.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
bool bShowResolvedBugs = false;
FLinearColor UnresolvedTint;
// A list of bug statuses that represent unresolved bugs in your Bugzilla install.
// Generally this would be something like "UNCONFIRMED" and "CONFIRMED".
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
TArray<FString> UnresolvedStatuses;
// Whether to show in-progress bugs when displaying bug report markers.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
bool bShowInProgressBugs = true;
// Colour tint to use for in-progress bugs.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
FLinearColor InProgressTint;
// A list of bug statuses that represent in progress bugs in your Bugzilla install.
// Generally this would include "IN_PROGRESS".
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
TArray<FString> InProgressStatuses;
// Whether to show resolved bugs when displaying bug report markers.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
bool bShowResolvedBugs = false;
// Colour tint to use for resolved bugs.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
FLinearColor ResolvedTint;
// A list of bug statuses that represent resolved bugs in your Bugzilla install.
// Generally this would include "RESOLVED" and "VERIFIED".
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
TArray<FString> ResolvedStatuses;
// How many bug markers to show in one batch. Each batch loads on a new frame.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
int32 LoadMarkersBatch = 10;
// How many bug markers to hide in one batch. Each batch loads on a new frame.
UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking")
int32 UnloadMarkersBatch = 25;
public:
virtual void PostInitProperties() override;