diff --git a/Source/Unrealzilla/Private/BugMarkerSubsystem.cpp b/Source/Unrealzilla/Private/BugMarkerSubsystem.cpp index ab816be..22ef8da 100644 --- a/Source/Unrealzilla/Private/BugMarkerSubsystem.cpp +++ b/Source/Unrealzilla/Private/BugMarkerSubsystem.cpp @@ -6,7 +6,8 @@ #include "BugMarkerActor.h" #include "HttpModule.h" #include "JsonObjectConverter.h" -#include "ServerAPI.h" +#include "ServerBugzillaAPI.h" +#include "ServerJiraAPI.h" #include "UnrealzillaGlobalSettings.h" DEFINE_LOG_CATEGORY(BugMarkerSubsystemLog); @@ -14,9 +15,35 @@ DEFINE_LOG_CATEGORY(BugMarkerSubsystemLog); void UBugMarkerSubsystem::Initialize(FSubsystemCollectionBase &Collection) { - if (!(this->ServerAPI = NewObject(this))) + switch (GetDefault()->BugReportPlatform) { - UE_LOG(BugMarkerSubsystemLog, Error, TEXT("Could not create Server API class.")); + case EBugReportPlatform::Bugzilla: + this->ServerAPI = NewObject(this); + break; + case EBugReportPlatform::Jira: + // this->ServerAPI = NewObject(this); + // break; + default: + this->ServerAPI = NewObject(this); + } + + if (!this->ServerAPI) + { + UE_LOG(BugMarkerSubsystemLog, Error, TEXT("Could not create Server API class; this will certainly crash eventually")); + } +} + + +FString UBugMarkerSubsystem::GetProjectName() +{ + switch (GetDefault()->BugReportPlatform) + { + case EBugReportPlatform::Bugzilla: + return GetDefault()->BugzillaProductName; + case EBugReportPlatform::Jira: + //return GetDefault()->JiraProjectName; + default: + return "No project"; } } diff --git a/Source/Unrealzilla/Private/BugSubmissionForm.cpp b/Source/Unrealzilla/Private/BugSubmissionForm.cpp index bcd71b6..347dd63 100644 --- a/Source/Unrealzilla/Private/BugSubmissionForm.cpp +++ b/Source/Unrealzilla/Private/BugSubmissionForm.cpp @@ -37,8 +37,6 @@ void UBugSubmissionForm::NativeOnInitialized() { this->ShowProcessingOverlayLoading(); - this->ProductNameValue->SetText(FText::AsCultureInvariant(GetDefault()->ProductName)); - UBugMarkerSubsystem *BugMarkerSubsystem = UGameplayStatics::GetPlayerController(this, 0)->GetLocalPlayer()->GetSubsystem(); BugMarkerSubsystem->FormPrepResponse.BindUObject(this, &UBugSubmissionForm::PrepareFormData); BugMarkerSubsystem->PrepareSubmissionFormData(); diff --git a/Source/Unrealzilla/Private/ServerAPI.cpp b/Source/Unrealzilla/Private/ServerAPI.cpp index 585326e..6c14cbf 100644 --- a/Source/Unrealzilla/Private/ServerAPI.cpp +++ b/Source/Unrealzilla/Private/ServerAPI.cpp @@ -24,511 +24,20 @@ const FString GetGameVersion() void UServerAPI::ReturnListOfBugs() { - const FString FullURL = GetDefault()->SubmissionServer + "/rest.cgi"; - - TArray StatusQueries; - if (GetDefault()->bShowUnresolvedBugs) - { - for (const FString Unresolved : GetDefault()->UnresolvedStatuses) - { - StatusQueries.Add("status=" + Unresolved); - } - } - if (GetDefault()->bShowInProgressBugs) - { - for (const FString InProgress : GetDefault()->InProgressStatuses) - { - StatusQueries.Add("status=" + InProgress); - } - } - if (GetDefault()->bShowResolvedBugs) - { - for (const FString Resolved : GetDefault()->ResolvedStatuses) - { - StatusQueries.Add("status=" + Resolved); - } - } - StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); - StatusQueries.Add("api_key=" + GetDefault()->APIKey); - const FString QueryString = FString::Join(StatusQueries, TEXT("&")); - - FHttpModule &HttpModule = FHttpModule::Get(); - TSharedRef SeverityRequest = HttpModule.CreateRequest(); - SeverityRequest->SetVerb(TEXT("GET")); - SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); - SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ListOfBugsResponse); - SeverityRequest->ProcessRequest(); -} - -void UServerAPI::ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONBugResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (!ResponseData.error) - { - TArray BugData; - for (const FBugzillaJSONBugData &BugzillaData : ResponseData.bugs) - { - FUnrealzillaBugData Bug; - Bug.ID = BugzillaData.id; - Bug.Summary = BugzillaData.summary; - Bug.Component = BugzillaData.component; - Bug.MapName = BugzillaData.cf_mapname; - Bug.MapLocation = BugzillaData.cf_location; - Bug.Platform = BugzillaData.platform; - Bug.OperatingSystem = BugzillaData.op_sys; - Bug.Severity = BugzillaData.severity; - Bug.Status = BugzillaData.status; - Bug.Resolution = BugzillaData.resolution; - Bug.DuplicateOf = BugzillaData.dupe_of; - Bug.bIsOpen = BugzillaData.is_open; - BugData.Add(Bug); - } - this->BugDataResponse.Execute(BugData); - } - else - { - this->CreateError(EErrorVerb::GET, ResponseData); - } - } + this->BugDataResponse.Execute(TArray()); } void UServerAPI::PrepareForm() { - const FString FullURL = GetDefault()->SubmissionServer + "/rest.cgi"; - - // Assemble query data into key:value pairs - TMap QueryData; - QueryData.Add("api_key", GetDefault()->APIKey); - const FString QueryString = UServerAPI::FormatQueryString(QueryData); - - // Query the server for information about the current product - FHttpModule &HttpModule = FHttpModule::Get(); - TSharedRef ProductRequest = HttpModule.CreateRequest(); - ProductRequest->SetVerb(TEXT("GET")); - ProductRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - ProductRequest->SetURL(FullURL + "/product/" + GetDefault()->ProductName + "?" + QueryString); - ProductRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerProductInfoResponse); - ProductRequest->ProcessRequest(); - - // Send a second query to retrieve severity options - TSharedRef SeverityRequest = HttpModule.CreateRequest(); - SeverityRequest->SetVerb(TEXT("GET")); - SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - SeverityRequest->SetURL(FullURL + "/field/bug/bug_severity" + "?" + QueryString); - SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerSeverityInfoResponse); - SeverityRequest->ProcessRequest(); - - // Send a third query to retrieve platform options - TSharedRef PlatformsRequest = HttpModule.CreateRequest(); - PlatformsRequest->SetVerb(TEXT("GET")); - PlatformsRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - PlatformsRequest->SetURL(FullURL + "/field/bug/rep_platform" + "?" + QueryString); - PlatformsRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerPlatformInfoResponse); - PlatformsRequest->ProcessRequest(); - - // Send a final query to retrieve OS options - TSharedRef OSRequest = HttpModule.CreateRequest(); - OSRequest->SetVerb(TEXT("GET")); - OSRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - OSRequest->SetURL(FullURL + "/field/bug/op_sys" + "?" + QueryString); - OSRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerOSInfoResponse); - OSRequest->ProcessRequest(); + this->FormDataResponse.Execute(FUnrealzillaFormPrepData()); } void UServerAPI::SendFormData(const FUnrealzillaPostData &PostData) { - const FString FullURL = GetDefault()->SubmissionServer + "/rest.cgi"; - const FString RequestURL = "/bug"; - - // Assemble query data into key:value pairs - TMap QueryData; - QueryData.Add("api_key", GetDefault()->APIKey); - - const FString DefaultStatus = GetDefault()->DefaultStatus; - - FBugzillaJSONPostBug PostDataJSON; - PostDataJSON.product = GetDefault()->ProductName; - PostDataJSON.version = PostData.Version; - PostDataJSON.platform = PostData.Platform; - PostDataJSON.op_sys = PostData.OS; - PostDataJSON.component = PostData.Component; - PostDataJSON.severity = PostData.Severity; - PostDataJSON.cf_mapname = PostData.MapName; - PostDataJSON.cf_location = PostData.MapLocation; - PostDataJSON.summary = PostData.Summary; - PostDataJSON.description = PostData.Comment; - if (!DefaultStatus.IsEmpty()) - { - PostDataJSON.status = DefaultStatus; - } - - if (PostDataJSON.version.IsEmpty()) - { - this->CreateError("You must select a version number."); - return; - } - if (PostDataJSON.platform.IsEmpty() || PostDataJSON.op_sys.IsEmpty()) - { - PostDataJSON.platform = "All"; - PostDataJSON.op_sys = "All"; - } - if (PostDataJSON.component.IsEmpty()) - { - this->CreateError("You must select a component."); - return; - } - if (PostDataJSON.severity.IsEmpty()) - { - this->CreateError("You must select a severity level."); - return; - } - if (PostDataJSON.summary.IsEmpty()) - { - this->CreateError("You must provide a summary."); - return; - } - - FString PostJsonString; - FJsonObjectConverter::UStructToJsonObjectString(PostDataJSON, PostJsonString); - - FHttpModule &HttpModule = FHttpModule::Get(); - TSharedRef Request = HttpModule.CreateRequest(); - Request->SetVerb(TEXT("POST")); - Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - Request->SetURL(FullURL + RequestURL + "?" + UServerAPI::FormatQueryString(QueryData)); - Request->SetContentAsString(PostJsonString); - Request->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerPOSTResponse); - Request->ProcessRequest(); + this->BugDataResponse.Execute(TArray()); } -void UServerAPI::ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONPostResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - this->CreateError(EErrorVerb::POST, ResponseData); - } - else - { - // Use the response's bug ID to get the info from the newly filed bug report and update its marker - - const FString FullURL = GetDefault()->SubmissionServer + "/rest.cgi"; - - TArray StatusQueries; - StatusQueries.Add("id=" + FString::FromInt(ResponseData.id)); - if (GetDefault()->bShowUnresolvedBugs) - { - for (const FString Unresolved : GetDefault()->UnresolvedStatuses) - { - StatusQueries.Add("status=" + Unresolved); - } - } - if (GetDefault()->bShowInProgressBugs) - { - for (const FString InProgress : GetDefault()->InProgressStatuses) - { - StatusQueries.Add("status=" + InProgress); - } - } - if (GetDefault()->bShowResolvedBugs) - { - for (const FString Resolved : GetDefault()->ResolvedStatuses) - { - StatusQueries.Add("status=" + Resolved); - } - } - StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); - StatusQueries.Add("api_key=" + GetDefault()->APIKey); - const FString QueryString = FString::Join(StatusQueries, TEXT("&")); - - FHttpModule &HttpModule = FHttpModule::Get(); - TSharedRef SeverityRequest = HttpModule.CreateRequest(); - SeverityRequest->SetVerb(TEXT("GET")); - SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); - SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); - SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerAPI::ServerPOSTUpdateMarkerResponse); - SeverityRequest->ProcessRequest(); - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONBugResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - this->CreateError(EErrorVerb::GET, ResponseData); - } - else - { - if (!ResponseData.bugs.IsEmpty()) - { - TArray BugData; - FUnrealzillaBugData Bug; - Bug.ID = ResponseData.bugs[0].id; - Bug.Summary = ResponseData.bugs[0].summary; - Bug.Component = ResponseData.bugs[0].component; - Bug.MapName = ResponseData.bugs[0].cf_mapname; - Bug.MapLocation = ResponseData.bugs[0].cf_location; - Bug.Platform = ResponseData.bugs[0].platform; - Bug.OperatingSystem = ResponseData.bugs[0].op_sys; - Bug.Severity = ResponseData.bugs[0].severity; - Bug.Status = ResponseData.bugs[0].status; - Bug.Resolution = ResponseData.bugs[0].resolution; - Bug.DuplicateOf = ResponseData.bugs[0].dupe_of; - Bug.bIsOpen = ResponseData.bugs[0].is_open; - BugData.Add(Bug); - this->BugDataResponse.Execute(BugData); - } - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONProductResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - //this->ShowProcessingOverlayMessage(ResponseData.message); - this->CreateError(EErrorVerb::GET, ResponseData); - } - else - { - if (!ResponseData.products.IsEmpty()) - { - const FBugzillaJSONProductData &ProductData = ResponseData.products[0]; - if (ProductData.name == GetDefault()->ProductName) - { - for (const FBugzillaJSONComponentData &ComponentData : ProductData.components) - { - this->ComponentsList.Add(ComponentData.name); - } - for (const FBugzillaJSONVersionData &VersionData : ProductData.versions) - { - this->VersionsList.Add(VersionData.name); - } - } - - this->CheckIfAllFormResponsesAreIn(); - } - else - { - FStringFormatOrderedArguments Args; - Args.Add(FStringFormatArg(GetDefault()->ProductName)); - //this->ShowProcessingOverlayMessage(FString::Format(TEXT("Could not find data for a product called {0}"), Args), true); - this->CreateError(FString::Format(TEXT("Could not find data for a product called {0}"), Args)); - } - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONFieldResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - //this->ShowProcessingOverlayMessage(ResponseData.message); - this->CreateError(EErrorVerb::GET, ResponseData); - } - else - { - if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "bug_severity") - { - for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) - { - this->SeverityList.Add(FieldValue.name); - } - } - - this->CheckIfAllFormResponsesAreIn(); - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONFieldResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - //this->ShowProcessingOverlayMessage(ResponseData.message); - this->CreateError(EErrorVerb::GET, ResponseData); - } - else - { - if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "rep_platform") - { - for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) - { - this->PlatformsList.Add(FieldValue.name); - } - } - - this->CheckIfAllFormResponsesAreIn(); - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) -{ - if (Success) - { - FBugzillaJSONFieldResponse ResponseData; - FString JSONResponse = Response->GetContentAsString(); - FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); - - if (ResponseData.error) - { - //this->ShowProcessingOverlayMessage(ResponseData.message); - this->CreateError(EErrorVerb::GET, ResponseData); - } - else - { - if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "op_sys") - { - for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) - { - this->OSList.Add(FieldValue.name); - } - } - - this->CheckIfAllFormResponsesAreIn(); - } - } - else - { - this->ServerConnectionError(Request->GetStatus()); - } -} - -void UServerAPI::CheckIfAllFormResponsesAreIn() -{ - if (!this->ComponentsList.IsEmpty() && !this->VersionsList.IsEmpty() && !this->SeverityList.IsEmpty() && - !this->PlatformsList.IsEmpty() && !this->OSList.IsEmpty()) - { - FUnrealzillaFormPrepData Data; - - Data.ComponentsList = this->ComponentsList; - Data.SeverityList = this->SeverityList; - Data.VersionsList = this->VersionsList; - Data.PlatformsList = this->PlatformsList; - Data.OSList = this->OSList; - - // Find a default version number to use if possible - if (this->VersionsList.Contains(GetGameVersion())) - { - Data.DetectedVersion = GetGameVersion(); - } - else if (this->VersionsList.Contains("unspecified")) - { - Data.DetectedVersion = "unspecified"; - } - else if (this->VersionsList.Contains("Latest")) - { - Data.DetectedVersion = "Latest"; - } - else if (!this->VersionsList.IsEmpty()) - { - Data.DetectedVersion = this->VersionsList[0]; - } - - // Set these as defaults in case nothing below changes this setting - Data.DetectedHardware = "All"; - Data.DetectedOS = "All"; - - if (this->PlatformsList.Contains("PC")) - { - // Try our best to auto-detect PC hardware - if (UGameplayStatics::GetPlatformName() == "Windows" && this->OSList.Contains("Windows")) - { - Data.DetectedHardware = "PC"; - Data.DetectedOS = "Windows"; - } - else if (UGameplayStatics::GetPlatformName() == "Linux" && this->OSList.Contains("Linux")) - { - Data.DetectedHardware = "PC"; - Data.DetectedOS = "Linux"; - } - else if (UGameplayStatics::GetPlatformName() == "Mac" && this->OSList.Contains("Mac OS")) - { - Data.DetectedHardware = "All"; - Data.DetectedOS = "Mac OS"; - } - } - if (UGameplayStatics::GetPlatformName() == "Mac") - { - // Try our best to auto-detect Macintosh hardware - if (this->PlatformsList.Contains("Macintosh")) - { - if (this->OSList.Contains("Mac OS")) - { - Data.DetectedHardware = "Macintosh"; - Data.DetectedOS = "Mac OS"; - } - } - } - - this->FormDataResponse.Execute(Data); - - this->ComponentsList.Empty(); - this->VersionsList.Empty(); - this->SeverityList.Empty(); - this->PlatformsList.Empty(); - this->OSList.Empty(); - } -} - void UServerAPI::ServerConnectionError(const EHttpRequestStatus::Type Status) { switch (Status) { diff --git a/Source/Unrealzilla/Private/ServerBugzillaAPI.cpp b/Source/Unrealzilla/Private/ServerBugzillaAPI.cpp new file mode 100644 index 0000000..287056e --- /dev/null +++ b/Source/Unrealzilla/Private/ServerBugzillaAPI.cpp @@ -0,0 +1,518 @@ +// ©2022 Batty Bovine Productions, LLC. All Rights Reserved. + + +#include "ServerBugzillaAPI.h" + +#include "HttpModule.h" +#include "JsonObjectConverter.h" +#include "UnrealzillaGlobalSettings.h" + +#include "Kismet/GameplayStatics.h" + + +void UServerBugzillaAPI::ReturnListOfBugs() +{ + const FString FullURL = GetDefault()->BugzillaSubmissionServer + "rest.cgi"; + + TArray StatusQueries; + if (GetDefault()->bShowUnresolvedBugs) + { + for (const FString Unresolved : GetDefault()->UnresolvedStatuses) + { + StatusQueries.Add("status=" + Unresolved); + } + } + if (GetDefault()->bShowInProgressBugs) + { + for (const FString InProgress : GetDefault()->InProgressStatuses) + { + StatusQueries.Add("status=" + InProgress); + } + } + if (GetDefault()->bShowResolvedBugs) + { + for (const FString Resolved : GetDefault()->ResolvedStatuses) + { + StatusQueries.Add("status=" + Resolved); + } + } + StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); + StatusQueries.Add("api_key=" + GetDefault()->BugzillaAPIKey); + const FString QueryString = FString::Join(StatusQueries, TEXT("&")); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ListOfBugsResponse); + SeverityRequest->ProcessRequest(); +} + +void UServerBugzillaAPI::ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONBugResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (!ResponseData.error) + { + TArray BugData; + for (const FBugzillaJSONBugData &BugzillaData : ResponseData.bugs) + { + FUnrealzillaBugData Bug; + Bug.ID = BugzillaData.id; + Bug.Summary = BugzillaData.summary; + Bug.Component = BugzillaData.component; + Bug.MapName = BugzillaData.cf_mapname; + Bug.MapLocation = BugzillaData.cf_location; + Bug.Platform = BugzillaData.platform; + Bug.OperatingSystem = BugzillaData.op_sys; + Bug.Severity = BugzillaData.severity; + Bug.Status = BugzillaData.status; + Bug.Resolution = BugzillaData.resolution; + Bug.DuplicateOf = BugzillaData.dupe_of; + Bug.bIsOpen = BugzillaData.is_open; + BugData.Add(Bug); + } + this->BugDataResponse.Execute(BugData); + } + else + { + this->CreateError(EErrorVerb::GET, ResponseData); + } + } +} + +void UServerBugzillaAPI::PrepareForm() +{ + const FString FullURL = GetDefault()->BugzillaSubmissionServer + "/rest.cgi"; + + // Assemble query data into key:value pairs + TMap QueryData; + QueryData.Add("api_key", GetDefault()->BugzillaAPIKey); + const FString QueryString = UServerAPI::FormatQueryString(QueryData); + + // Query the server for information about the current product + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef ProductRequest = HttpModule.CreateRequest(); + ProductRequest->SetVerb(TEXT("GET")); + ProductRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + ProductRequest->SetURL(FullURL + "/product/" + GetDefault()->BugzillaProductName + "?" + QueryString); + ProductRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerProductInfoResponse); + ProductRequest->ProcessRequest(); + + // Send a second query to retrieve severity options + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/field/bug/bug_severity" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerSeverityInfoResponse); + SeverityRequest->ProcessRequest(); + + // Send a third query to retrieve platform options + TSharedRef PlatformsRequest = HttpModule.CreateRequest(); + PlatformsRequest->SetVerb(TEXT("GET")); + PlatformsRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + PlatformsRequest->SetURL(FullURL + "/field/bug/rep_platform" + "?" + QueryString); + PlatformsRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerPlatformInfoResponse); + PlatformsRequest->ProcessRequest(); + + // Send a final query to retrieve OS options + TSharedRef OSRequest = HttpModule.CreateRequest(); + OSRequest->SetVerb(TEXT("GET")); + OSRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + OSRequest->SetURL(FullURL + "/field/bug/op_sys" + "?" + QueryString); + OSRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerOSInfoResponse); + OSRequest->ProcessRequest(); +} + +void UServerBugzillaAPI::SendFormData(const FUnrealzillaPostData &PostData) +{ + const FString FullURL = GetDefault()->BugzillaSubmissionServer + "/rest.cgi"; + const FString RequestURL = "/bug"; + + // Assemble query data into key:value pairs + TMap QueryData; + QueryData.Add("api_key", GetDefault()->BugzillaAPIKey); + + const FString DefaultStatus = GetDefault()->DefaultStatus; + + FBugzillaJSONPostBug PostDataJSON; + PostDataJSON.product = GetDefault()->BugzillaProductName; + PostDataJSON.version = PostData.Version; + PostDataJSON.platform = PostData.Platform; + PostDataJSON.op_sys = PostData.OS; + PostDataJSON.component = PostData.Component; + PostDataJSON.severity = PostData.Severity; + PostDataJSON.cf_mapname = PostData.MapName; + PostDataJSON.cf_location = PostData.MapLocation; + PostDataJSON.summary = PostData.Summary; + PostDataJSON.description = PostData.Comment; + if (!DefaultStatus.IsEmpty()) + { + PostDataJSON.status = DefaultStatus; + } + + if (PostDataJSON.version.IsEmpty()) + { + this->CreateError("You must select a version number."); + return; + } + if (PostDataJSON.platform.IsEmpty() || PostDataJSON.op_sys.IsEmpty()) + { + PostDataJSON.platform = "All"; + PostDataJSON.op_sys = "All"; + } + if (PostDataJSON.component.IsEmpty()) + { + this->CreateError("You must select a component."); + return; + } + if (PostDataJSON.severity.IsEmpty()) + { + this->CreateError("You must select a severity level."); + return; + } + if (PostDataJSON.summary.IsEmpty()) + { + this->CreateError("You must provide a summary."); + return; + } + + FString PostJsonString; + FJsonObjectConverter::UStructToJsonObjectString(PostDataJSON, PostJsonString); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef Request = HttpModule.CreateRequest(); + Request->SetVerb(TEXT("POST")); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetURL(FullURL + RequestURL + "?" + UServerAPI::FormatQueryString(QueryData)); + Request->SetContentAsString(PostJsonString); + Request->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerPOSTResponse); + Request->ProcessRequest(); +} + + +void UServerBugzillaAPI::ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONPostResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + this->CreateError(EErrorVerb::POST, ResponseData); + } + else + { + // Use the response's bug ID to get the info from the newly filed bug report and update its marker + + const FString FullURL = GetDefault()->BugzillaSubmissionServer + "/rest.cgi"; + + TArray StatusQueries; + StatusQueries.Add("id=" + FString::FromInt(ResponseData.id)); + if (GetDefault()->bShowUnresolvedBugs) + { + for (const FString Unresolved : GetDefault()->UnresolvedStatuses) + { + StatusQueries.Add("status=" + Unresolved); + } + } + if (GetDefault()->bShowInProgressBugs) + { + for (const FString InProgress : GetDefault()->InProgressStatuses) + { + StatusQueries.Add("status=" + InProgress); + } + } + if (GetDefault()->bShowResolvedBugs) + { + for (const FString Resolved : GetDefault()->ResolvedStatuses) + { + StatusQueries.Add("status=" + Resolved); + } + } + StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); + StatusQueries.Add("api_key=" + GetDefault()->BugzillaAPIKey); + const FString QueryString = FString::Join(StatusQueries, TEXT("&")); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerBugzillaAPI::ServerPOSTUpdateMarkerResponse); + SeverityRequest->ProcessRequest(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONBugResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.bugs.IsEmpty()) + { + TArray BugData; + FUnrealzillaBugData Bug; + Bug.ID = ResponseData.bugs[0].id; + Bug.Summary = ResponseData.bugs[0].summary; + Bug.Component = ResponseData.bugs[0].component; + Bug.MapName = ResponseData.bugs[0].cf_mapname; + Bug.MapLocation = ResponseData.bugs[0].cf_location; + Bug.Platform = ResponseData.bugs[0].platform; + Bug.OperatingSystem = ResponseData.bugs[0].op_sys; + Bug.Severity = ResponseData.bugs[0].severity; + Bug.Status = ResponseData.bugs[0].status; + Bug.Resolution = ResponseData.bugs[0].resolution; + Bug.DuplicateOf = ResponseData.bugs[0].dupe_of; + Bug.bIsOpen = ResponseData.bugs[0].is_open; + BugData.Add(Bug); + this->BugDataResponse.Execute(BugData); + } + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONProductResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.products.IsEmpty()) + { + const FBugzillaJSONProductData &ProductData = ResponseData.products[0]; + if (ProductData.name == GetDefault()->BugzillaProductName) + { + for (const FBugzillaJSONComponentData &ComponentData : ProductData.components) + { + this->ComponentsList.Add(ComponentData.name); + } + for (const FBugzillaJSONVersionData &VersionData : ProductData.versions) + { + this->VersionsList.Add(VersionData.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + else + { + FStringFormatOrderedArguments Args; + Args.Add(FStringFormatArg(GetDefault()->BugzillaProductName)); + //this->ShowProcessingOverlayMessage(FString::Format(TEXT("Could not find data for a product called {0}"), Args), true); + this->CreateError(FString::Format(TEXT("Could not find data for a product called {0}"), Args)); + } + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "bug_severity") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->SeverityList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "rep_platform") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->PlatformsList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "op_sys") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->OSList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerBugzillaAPI::CheckIfAllFormResponsesAreIn() +{ + if (!this->ComponentsList.IsEmpty() && !this->VersionsList.IsEmpty() && !this->SeverityList.IsEmpty() && + !this->PlatformsList.IsEmpty() && !this->OSList.IsEmpty()) + { + FUnrealzillaFormPrepData Data; + + Data.ComponentsList = this->ComponentsList; + Data.SeverityList = this->SeverityList; + Data.VersionsList = this->VersionsList; + Data.PlatformsList = this->PlatformsList; + Data.OSList = this->OSList; + + // Find a default version number to use if possible + if (this->VersionsList.Contains(GetGameVersion())) + { + Data.DetectedVersion = GetGameVersion(); + } + else if (this->VersionsList.Contains("unspecified")) + { + Data.DetectedVersion = "unspecified"; + } + else if (this->VersionsList.Contains("Latest")) + { + Data.DetectedVersion = "Latest"; + } + else if (!this->VersionsList.IsEmpty()) + { + Data.DetectedVersion = this->VersionsList[0]; + } + + // Set these as defaults in case nothing below changes this setting + Data.DetectedHardware = "All"; + Data.DetectedOS = "All"; + + if (this->PlatformsList.Contains("PC")) + { + // Try our best to auto-detect PC hardware + if (UGameplayStatics::GetPlatformName() == "Windows" && this->OSList.Contains("Windows")) + { + Data.DetectedHardware = "PC"; + Data.DetectedOS = "Windows"; + } + else if (UGameplayStatics::GetPlatformName() == "Linux" && this->OSList.Contains("Linux")) + { + Data.DetectedHardware = "PC"; + Data.DetectedOS = "Linux"; + } + else if (UGameplayStatics::GetPlatformName() == "Mac" && this->OSList.Contains("Mac OS")) + { + Data.DetectedHardware = "All"; + Data.DetectedOS = "Mac OS"; + } + } + if (UGameplayStatics::GetPlatformName() == "Mac") + { + // Try our best to auto-detect Macintosh hardware + if (this->PlatformsList.Contains("Macintosh")) + { + if (this->OSList.Contains("Mac OS")) + { + Data.DetectedHardware = "Macintosh"; + Data.DetectedOS = "Mac OS"; + } + } + } + + this->FormDataResponse.Execute(Data); + + this->ComponentsList.Empty(); + this->VersionsList.Empty(); + this->SeverityList.Empty(); + this->PlatformsList.Empty(); + this->OSList.Empty(); + } +} diff --git a/Source/Unrealzilla/Private/ServerJiraAPI.cpp b/Source/Unrealzilla/Private/ServerJiraAPI.cpp new file mode 100644 index 0000000..9cd7b4c --- /dev/null +++ b/Source/Unrealzilla/Private/ServerJiraAPI.cpp @@ -0,0 +1,518 @@ +// ©2022 Batty Bovine Productions, LLC. All Rights Reserved. + + +#include "ServerJiraAPI.h" + +#include "HttpModule.h" +#include "JsonObjectConverter.h" +#include "UnrealzillaGlobalSettings.h" + +#include "Kismet/GameplayStatics.h" + + +void UServerJiraAPI::ReturnListOfBugs() +{ + const FString FullURL = GetDefault()->JiraSubmissionServer + "/rest"; + + TArray StatusQueries; + if (GetDefault()->bShowUnresolvedBugs) + { + for (const FString Unresolved : GetDefault()->UnresolvedStatuses) + { + StatusQueries.Add("status=" + Unresolved); + } + } + if (GetDefault()->bShowInProgressBugs) + { + for (const FString InProgress : GetDefault()->InProgressStatuses) + { + StatusQueries.Add("status=" + InProgress); + } + } + if (GetDefault()->bShowResolvedBugs) + { + for (const FString Resolved : GetDefault()->ResolvedStatuses) + { + StatusQueries.Add("status=" + Resolved); + } + } + StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); + StatusQueries.Add("api_key=" + GetDefault()->JiraAPIKey); + const FString QueryString = FString::Join(StatusQueries, TEXT("&")); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ListOfBugsResponse); + SeverityRequest->ProcessRequest(); +} + +void UServerJiraAPI::ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONBugResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (!ResponseData.error) + { + TArray BugData; + for (const FBugzillaJSONBugData &BugzillaData : ResponseData.bugs) + { + FUnrealzillaBugData Bug; + Bug.ID = BugzillaData.id; + Bug.Summary = BugzillaData.summary; + Bug.Component = BugzillaData.component; + Bug.MapName = BugzillaData.cf_mapname; + Bug.MapLocation = BugzillaData.cf_location; + Bug.Platform = BugzillaData.platform; + Bug.OperatingSystem = BugzillaData.op_sys; + Bug.Severity = BugzillaData.severity; + Bug.Status = BugzillaData.status; + Bug.Resolution = BugzillaData.resolution; + Bug.DuplicateOf = BugzillaData.dupe_of; + Bug.bIsOpen = BugzillaData.is_open; + BugData.Add(Bug); + } + this->BugDataResponse.Execute(BugData); + } + else + { + this->CreateError(EErrorVerb::GET, ResponseData); + } + } +} + +void UServerJiraAPI::PrepareForm() +{ + const FString FullURL = GetDefault()->JiraSubmissionServer + "/rest"; + + // Assemble query data into key:value pairs + TMap QueryData; + QueryData.Add("api_key", GetDefault()->JiraAPIKey); + const FString QueryString = UServerAPI::FormatQueryString(QueryData); + + // Query the server for information about the current product + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef ProductRequest = HttpModule.CreateRequest(); + ProductRequest->SetVerb(TEXT("GET")); + ProductRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + ProductRequest->SetURL(FullURL + "/product/" + GetDefault()->JiraProjectName + "?" + QueryString); + ProductRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerProductInfoResponse); + ProductRequest->ProcessRequest(); + + // Send a second query to retrieve severity options + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/field/bug/bug_severity" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerSeverityInfoResponse); + SeverityRequest->ProcessRequest(); + + // Send a third query to retrieve platform options + TSharedRef PlatformsRequest = HttpModule.CreateRequest(); + PlatformsRequest->SetVerb(TEXT("GET")); + PlatformsRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + PlatformsRequest->SetURL(FullURL + "/field/bug/rep_platform" + "?" + QueryString); + PlatformsRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerPlatformInfoResponse); + PlatformsRequest->ProcessRequest(); + + // Send a final query to retrieve OS options + TSharedRef OSRequest = HttpModule.CreateRequest(); + OSRequest->SetVerb(TEXT("GET")); + OSRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + OSRequest->SetURL(FullURL + "/field/bug/op_sys" + "?" + QueryString); + OSRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerOSInfoResponse); + OSRequest->ProcessRequest(); +} + +void UServerJiraAPI::SendFormData(const FUnrealzillaPostData &PostData) +{ + const FString FullURL = GetDefault()->JiraSubmissionServer + "/rest"; + const FString RequestURL = "/bug"; + + // Assemble query data into key:value pairs + TMap QueryData; + QueryData.Add("api_key", GetDefault()->JiraAPIKey); + + const FString DefaultStatus = GetDefault()->DefaultStatus; + + FBugzillaJSONPostBug PostDataJSON; + PostDataJSON.product = GetDefault()->JiraProjectName; + PostDataJSON.version = PostData.Version; + PostDataJSON.platform = PostData.Platform; + PostDataJSON.op_sys = PostData.OS; + PostDataJSON.component = PostData.Component; + PostDataJSON.severity = PostData.Severity; + PostDataJSON.cf_mapname = PostData.MapName; + PostDataJSON.cf_location = PostData.MapLocation; + PostDataJSON.summary = PostData.Summary; + PostDataJSON.description = PostData.Comment; + if (!DefaultStatus.IsEmpty()) + { + PostDataJSON.status = DefaultStatus; + } + + if (PostDataJSON.version.IsEmpty()) + { + this->CreateError("You must select a version number."); + return; + } + if (PostDataJSON.platform.IsEmpty() || PostDataJSON.op_sys.IsEmpty()) + { + PostDataJSON.platform = "All"; + PostDataJSON.op_sys = "All"; + } + if (PostDataJSON.component.IsEmpty()) + { + this->CreateError("You must select a component."); + return; + } + if (PostDataJSON.severity.IsEmpty()) + { + this->CreateError("You must select a severity level."); + return; + } + if (PostDataJSON.summary.IsEmpty()) + { + this->CreateError("You must provide a summary."); + return; + } + + FString PostJsonString; + FJsonObjectConverter::UStructToJsonObjectString(PostDataJSON, PostJsonString); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef Request = HttpModule.CreateRequest(); + Request->SetVerb(TEXT("POST")); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetURL(FullURL + RequestURL + "?" + UServerAPI::FormatQueryString(QueryData)); + Request->SetContentAsString(PostJsonString); + Request->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerPOSTResponse); + Request->ProcessRequest(); +} + + +void UServerJiraAPI::ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONPostResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + this->CreateError(EErrorVerb::POST, ResponseData); + } + else + { + // Use the response's bug ID to get the info from the newly filed bug report and update its marker + + const FString FullURL = GetDefault()->JiraSubmissionServer + "/rest"; + + TArray StatusQueries; + StatusQueries.Add("id=" + FString::FromInt(ResponseData.id)); + if (GetDefault()->bShowUnresolvedBugs) + { + for (const FString Unresolved : GetDefault()->UnresolvedStatuses) + { + StatusQueries.Add("status=" + Unresolved); + } + } + if (GetDefault()->bShowInProgressBugs) + { + for (const FString InProgress : GetDefault()->InProgressStatuses) + { + StatusQueries.Add("status=" + InProgress); + } + } + if (GetDefault()->bShowResolvedBugs) + { + for (const FString Resolved : GetDefault()->ResolvedStatuses) + { + StatusQueries.Add("status=" + Resolved); + } + } + StatusQueries.Add("cf_mapname=" + this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len())); + StatusQueries.Add("api_key=" + GetDefault()->JiraAPIKey); + const FString QueryString = FString::Join(StatusQueries, TEXT("&")); + + FHttpModule &HttpModule = FHttpModule::Get(); + TSharedRef SeverityRequest = HttpModule.CreateRequest(); + SeverityRequest->SetVerb(TEXT("GET")); + SeverityRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + SeverityRequest->SetURL(FullURL + "/bug" + "?" + QueryString); + SeverityRequest->OnProcessRequestComplete().BindUObject(this, &UServerJiraAPI::ServerPOSTUpdateMarkerResponse); + SeverityRequest->ProcessRequest(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONBugResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.bugs.IsEmpty()) + { + TArray BugData; + FUnrealzillaBugData Bug; + Bug.ID = ResponseData.bugs[0].id; + Bug.Summary = ResponseData.bugs[0].summary; + Bug.Component = ResponseData.bugs[0].component; + Bug.MapName = ResponseData.bugs[0].cf_mapname; + Bug.MapLocation = ResponseData.bugs[0].cf_location; + Bug.Platform = ResponseData.bugs[0].platform; + Bug.OperatingSystem = ResponseData.bugs[0].op_sys; + Bug.Severity = ResponseData.bugs[0].severity; + Bug.Status = ResponseData.bugs[0].status; + Bug.Resolution = ResponseData.bugs[0].resolution; + Bug.DuplicateOf = ResponseData.bugs[0].dupe_of; + Bug.bIsOpen = ResponseData.bugs[0].is_open; + BugData.Add(Bug); + this->BugDataResponse.Execute(BugData); + } + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONProductResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.products.IsEmpty()) + { + const FBugzillaJSONProductData &ProductData = ResponseData.products[0]; + if (ProductData.name == GetDefault()->JiraProjectName) + { + for (const FBugzillaJSONComponentData &ComponentData : ProductData.components) + { + this->ComponentsList.Add(ComponentData.name); + } + for (const FBugzillaJSONVersionData &VersionData : ProductData.versions) + { + this->VersionsList.Add(VersionData.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + else + { + FStringFormatOrderedArguments Args; + Args.Add(FStringFormatArg(GetDefault()->JiraProjectName)); + //this->ShowProcessingOverlayMessage(FString::Format(TEXT("Could not find data for a product called {0}"), Args), true); + this->CreateError(FString::Format(TEXT("Could not find data for a product called {0}"), Args)); + } + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "bug_severity") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->SeverityList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "rep_platform") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->PlatformsList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success) +{ + if (Success) + { + FBugzillaJSONFieldResponse ResponseData; + FString JSONResponse = Response->GetContentAsString(); + FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData); + + if (ResponseData.error) + { + //this->ShowProcessingOverlayMessage(ResponseData.message); + this->CreateError(EErrorVerb::GET, ResponseData); + } + else + { + if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "op_sys") + { + for (const FBugzillaJSONFieldValueData &FieldValue : ResponseData.fields[0].values) + { + this->OSList.Add(FieldValue.name); + } + } + + this->CheckIfAllFormResponsesAreIn(); + } + } + else + { + this->ServerConnectionError(Request->GetStatus()); + } +} + +void UServerJiraAPI::CheckIfAllFormResponsesAreIn() +{ + if (!this->ComponentsList.IsEmpty() && !this->VersionsList.IsEmpty() && !this->SeverityList.IsEmpty() && + !this->PlatformsList.IsEmpty() && !this->OSList.IsEmpty()) + { + FUnrealzillaFormPrepData Data; + + Data.ComponentsList = this->ComponentsList; + Data.SeverityList = this->SeverityList; + Data.VersionsList = this->VersionsList; + Data.PlatformsList = this->PlatformsList; + Data.OSList = this->OSList; + + // Find a default version number to use if possible + if (this->VersionsList.Contains(GetGameVersion())) + { + Data.DetectedVersion = GetGameVersion(); + } + else if (this->VersionsList.Contains("unspecified")) + { + Data.DetectedVersion = "unspecified"; + } + else if (this->VersionsList.Contains("Latest")) + { + Data.DetectedVersion = "Latest"; + } + else if (!this->VersionsList.IsEmpty()) + { + Data.DetectedVersion = this->VersionsList[0]; + } + + // Set these as defaults in case nothing below changes this setting + Data.DetectedHardware = "All"; + Data.DetectedOS = "All"; + + if (this->PlatformsList.Contains("PC")) + { + // Try our best to auto-detect PC hardware + if (UGameplayStatics::GetPlatformName() == "Windows" && this->OSList.Contains("Windows")) + { + Data.DetectedHardware = "PC"; + Data.DetectedOS = "Windows"; + } + else if (UGameplayStatics::GetPlatformName() == "Linux" && this->OSList.Contains("Linux")) + { + Data.DetectedHardware = "PC"; + Data.DetectedOS = "Linux"; + } + else if (UGameplayStatics::GetPlatformName() == "Mac" && this->OSList.Contains("Mac OS")) + { + Data.DetectedHardware = "All"; + Data.DetectedOS = "Mac OS"; + } + } + if (UGameplayStatics::GetPlatformName() == "Mac") + { + // Try our best to auto-detect Macintosh hardware + if (this->PlatformsList.Contains("Macintosh")) + { + if (this->OSList.Contains("Mac OS")) + { + Data.DetectedHardware = "Macintosh"; + Data.DetectedOS = "Mac OS"; + } + } + } + + this->FormDataResponse.Execute(Data); + + this->ComponentsList.Empty(); + this->VersionsList.Empty(); + this->SeverityList.Empty(); + this->PlatformsList.Empty(); + this->OSList.Empty(); + } +} diff --git a/Source/Unrealzilla/Public/BugMarkerSubsystem.h b/Source/Unrealzilla/Public/BugMarkerSubsystem.h index c728b09..1eaeac6 100644 --- a/Source/Unrealzilla/Public/BugMarkerSubsystem.h +++ b/Source/Unrealzilla/Public/BugMarkerSubsystem.h @@ -8,6 +8,7 @@ #include "API/BugzillaJSONStructs.h" #include "Interfaces/IHttpRequest.h" #include "Interfaces/IHttpResponse.h" +#include "Kismet/GameplayStatics.h" #include "BugMarkerSubsystem.generated.h" @@ -32,6 +33,11 @@ public: void SubmitForm(const struct FUnrealzillaPostData &PostData); + UFUNCTION(BlueprintPure, meta=(WorldContext="WorldContextObject")) + static UBugMarkerSubsystem *GetBugMarkerSubsystem(const UObject *WorldContextObject) { return UGameplayStatics::GetPlayerController(WorldContextObject, 0)->GetLocalPlayer()->GetSubsystem(); } + + static FString GetProjectName(); + TObjectPtr &GetServerAPI() { return this->ServerAPI; } DECLARE_DELEGATE_OneParam(FBugMarkerSubsystemFormPrep, const struct FUnrealzillaFormPrepData &); diff --git a/Source/Unrealzilla/Public/ServerAPI.h b/Source/Unrealzilla/Public/ServerAPI.h index 982e865..9a17719 100644 --- a/Source/Unrealzilla/Public/ServerAPI.h +++ b/Source/Unrealzilla/Public/ServerAPI.h @@ -21,9 +21,9 @@ class UNREALZILLA_API UServerAPI : public UObject GENERATED_BODY() public: - void ReturnListOfBugs(); - void PrepareForm(); - void SendFormData(const FUnrealzillaPostData &PostData); + virtual void ReturnListOfBugs(); + virtual void PrepareForm(); + virtual void SendFormData(const FUnrealzillaPostData &PostData); DECLARE_DELEGATE_OneParam(FBugListResponseDelegate, const TArray&); FBugListResponseDelegate BugDataResponse; @@ -40,16 +40,7 @@ public: TArray GetPlatformsList() const { return this->PlatformsList; } TArray GetOSList() const { return this->OSList; } -private: - void ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - - void ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); - void CheckIfAllFormResponsesAreIn(); +protected: void ServerConnectionError(const EHttpRequestStatus::Type Status); void CreateError(const EErrorVerb &Verb, const FBugzillaJSONPostResponse &Data); diff --git a/Source/Unrealzilla/Public/ServerBugzillaAPI.h b/Source/Unrealzilla/Public/ServerBugzillaAPI.h new file mode 100644 index 0000000..0a353c6 --- /dev/null +++ b/Source/Unrealzilla/Public/ServerBugzillaAPI.h @@ -0,0 +1,33 @@ +// ©2022 Batty Bovine Productions, LLC. All Rights Reserved. + +#pragma once + +#include "ServerAPI.h" + +#include "ServerBugzillaAPI.generated.h" + + +/** + * Class for communicating with REST API on Bugzilla + */ +UCLASS() +class UNREALZILLA_API UServerBugzillaAPI : public UServerAPI +{ + GENERATED_BODY() + +public: + virtual void ReturnListOfBugs() override; + virtual void PrepareForm() override; + virtual void SendFormData(const FUnrealzillaPostData &PostData) override; + +private: + void ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + + void ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void CheckIfAllFormResponsesAreIn(); +}; diff --git a/Source/Unrealzilla/Public/ServerJiraAPI.h b/Source/Unrealzilla/Public/ServerJiraAPI.h new file mode 100644 index 0000000..a782016 --- /dev/null +++ b/Source/Unrealzilla/Public/ServerJiraAPI.h @@ -0,0 +1,33 @@ +// ©2022 Batty Bovine Productions, LLC. All Rights Reserved. + +#pragma once + +#include "ServerAPI.h" + +#include "ServerJiraAPI.generated.h" + + +/** + * Class for communicating with REST API on Jira + */ +UCLASS() +class UNREALZILLA_API UServerJiraAPI : public UServerAPI +{ + GENERATED_BODY() + +public: + virtual void ReturnListOfBugs() override; + virtual void PrepareForm() override; + virtual void SendFormData(const FUnrealzillaPostData &PostData) override; + +private: + void ListOfBugsResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + + void ServerPOSTUpdateMarkerResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success); + void CheckIfAllFormResponsesAreIn(); +}; diff --git a/Source/Unrealzilla/Public/UnrealzillaGlobalSettings.h b/Source/Unrealzilla/Public/UnrealzillaGlobalSettings.h index 96edce1..6988059 100644 --- a/Source/Unrealzilla/Public/UnrealzillaGlobalSettings.h +++ b/Source/Unrealzilla/Public/UnrealzillaGlobalSettings.h @@ -8,6 +8,14 @@ #include "UnrealzillaGlobalSettings.generated.h" +UENUM() +enum class EBugReportPlatform : uint8 +{ + Bugzilla UMETA(DisplayName="Bugzilla"), + Jira UMETA(DisplayName="Jira") +}; + + /** * Global settings for Unrealzilla classes */ @@ -17,6 +25,10 @@ class UNREALZILLA_API UUnrealzillaGlobalSettings : public UDeveloperSettingsBack GENERATED_BODY() public: + // Platform to use for reporting bugs + UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly) + EBugReportPlatform BugReportPlatform = EBugReportPlatform::Bugzilla; + // The distance to send the trace out when placing bug markers in the world. UPROPERTY(Config, BlueprintReadOnly, EditDefaultsOnly, Category="Bug Placement", meta=(DisplayName="Precise Placement Distance")) float BugPlacementTraceDistance = 1500.0f; @@ -24,60 +36,80 @@ 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 viewport depth of the bug report interface widget. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting") + int32 BugReportWidgetDepth = 0; // The Bugzilla server where bugs will be posted. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla") - FString SubmissionServer; + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla", meta=(DisplayName="Server")) + FString BugzillaSubmissionServer; + // URI path to your Jira instance's REST API. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla", meta=(DisplayName="Bugzilla REST URI")) + FString BugzillaRESTURI = "rest/"; // The name of the product for which bugs will be posted. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla") - FString ProductName; + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla", meta=(DisplayName="Product Name")) + FString BugzillaProductName; // The API key to use when posting bugs. All bugs will be posted under the account of the owner of this API key. UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla", meta=(DisplayName="API Key")) - FString APIKey; - // The viewport depth of the bug report interface widget. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla") - int32 BugReportWidgetDepth = 0; - // The status to use when filing a new bug. A status such as "UNCONFIRMED" is suggested. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Bugzilla") - FString DefaultStatus; + FString BugzillaAPIKey; + + // The Jira server where bugs will be posted. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Jira", meta=(DisplayName="Server")) + FString JiraSubmissionServer; + // URI path to your Jira instance's REST API. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Jira", meta=(DisplayName="Jira REST URI")) + FString JiraRESTURI = "rest/"; + // The name of the project for which bugs will be posted. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Jira", meta=(DisplayName="Project Name")) + FString JiraProjectName; + // The username to use when posting bugs. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Jira", meta=(DisplayName="Username")) + FString JiraUsername; + // The API key to use when posting bugs. Using the account password should work here, but it is + // highly recommended that you generate a proper API key in the Atlassian account of the above user. + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Reporting|Jira", meta=(DisplayName="API Key")) + FString JiraAPIKey; // Whether to show unresolved bugs when displaying bug report markers. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Unresolved") bool bShowUnresolvedBugs = true; // Colour tint to use for unresolved bugs. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Unresolved") FLinearColor UnresolvedTint; - // A list of bug statuses that represent unresolved bugs in your Bugzilla install. + // A list of bug statuses that represent unresolved bugs on your server. // Generally this would be something like "UNCONFIRMED" and "CONFIRMED". - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Unresolved") TArray UnresolvedStatuses; // Whether to show in-progress bugs when displaying bug report markers. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|In Progress") bool bShowInProgressBugs = true; // Colour tint to use for in-progress bugs. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|In Progress") FLinearColor InProgressTint; - // A list of bug statuses that represent in progress bugs in your Bugzilla install. + // A list of bug statuses that represent in progress bugs on your server. // Generally this would include "IN_PROGRESS". - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|In Progress") TArray InProgressStatuses; // Whether to show resolved bugs when displaying bug report markers. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Resolved") bool bShowResolvedBugs = false; // Colour tint to use for resolved bugs. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Resolved") FLinearColor ResolvedTint; - // A list of bug statuses that represent resolved bugs in your Bugzilla install. + // A list of bug statuses that represent resolved bugs on your server. // Generally this would include "RESOLVED" and "VERIFIED". - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Resolved") TArray ResolvedStatuses; // How many bug markers to show in one batch. Each batch loads on a new frame. - UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Marking") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Display") 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") + UPROPERTY(Config, EditDefaultsOnly, BlueprintReadOnly, Category="Markers|Display") int32 UnloadMarkersBatch = 25; public: