Unrealzilla/Source/Unrealzilla/Private/BugSubmissionForm.cpp

480 lines
16 KiB
C++

// ©2022 Batty Bovine Productions, LLC. All Rights Reserved.
#include "BugSubmissionForm.h"
#include "BugMarkerActor.h"
#include "CommonButtonBase.h"
#include "CommonTextBlock.h"
#include "HttpModule.h"
#include "JsonObjectConverter.h"
#include "UnrealzillaGlobalSettings.h"
#include "Components/CircularThrobber.h"
#include "Components/Overlay.h"
#include "Components/VerticalBox.h"
#include "Kismet/GameplayStatics.h"
const FText FormatFloatToText(float Float, int32 IntegralDigits, int32 FractionalDigits)
{
const float Rounded = FMath::RoundToInt(Float);
if(FMath::Abs(Float - Rounded) < FMath::Pow(10.0f, -1 * FractionalDigits))
{
Float = Rounded;
}
FNumberFormattingOptions NumberFormat;
NumberFormat.MinimumIntegralDigits = IntegralDigits;
NumberFormat.MaximumIntegralDigits = INT32_MAX;
NumberFormat.MinimumFractionalDigits = FractionalDigits;
NumberFormat.MaximumFractionalDigits = FractionalDigits;
NumberFormat.UseGrouping = false;
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;
GConfig->GetString(TEXT("/Script/EngineSettings.GeneralProjectSettings"), TEXT("ProjectVersion"), GameVersion, GGameIni);
if (GameVersion.IsEmpty())
{
GameVersion = "1.0.0.0";
}
return GameVersion;
}
void UBugSubmissionForm::NativeOnInitialized()
{
this->ShowProcessingOverlayLoading();
this->ProductNameValue->SetText(FText::AsCultureInvariant(GetDefault<UUnrealzillaGlobalSettings>()->ProductName));
const FString FullURL = GetDefault<UUnrealzillaGlobalSettings>()->SubmissionServer + "/rest.cgi";
// Assemble query data into key:value pairs
TMap<FString, FString> QueryData;
QueryData.Add("api_key", GetDefault<UUnrealzillaGlobalSettings>()->APIKey);
const FString QueryString = FormatQueryString(QueryData);
// Query the server for information about the current product
FHttpModule &HttpModule = FHttpModule::Get();
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> ProductRequest = HttpModule.CreateRequest();
ProductRequest->SetVerb(TEXT("GET"));
ProductRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
ProductRequest->SetURL(FullURL + "/product/" + GetDefault<UUnrealzillaGlobalSettings>()->ProductName + "?" + QueryString);
ProductRequest->OnProcessRequestComplete().BindUObject(this, &UBugSubmissionForm::ServerProductInfoResponse);
ProductRequest->ProcessRequest();
// Send a second query to retrieve severity options
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> 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, &UBugSubmissionForm::ServerSeverityInfoResponse);
SeverityRequest->ProcessRequest();
// Send a third query to retrieve platform options
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> 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, &UBugSubmissionForm::ServerPlatformInfoResponse);
PlatformsRequest->ProcessRequest();
// Send a final query to retrieve OS options
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> 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, &UBugSubmissionForm::ServerOSInfoResponse);
OSRequest->ProcessRequest();
this->SubmitButton->OnClicked().AddUObject(this, &UBugSubmissionForm::SubmitForm);
this->CancelButton->OnClicked().AddUObject(this, &UBugSubmissionForm::CancelForm);
}
void UBugSubmissionForm::SetMarkerData(ABugMarkerActor *BugMarker)
{
this->BugMarkerActor = BugMarker;
this->MapName = FText::AsCultureInvariant(this->GetWorld()->GetMapName().RightChop(this->GetWorld()->StreamingLevelsPrefix.Len()));
const FVector MarkerLocation = 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));
const FText MarkerLocationText = FText::Format(NSLOCTEXT("BugSubmissionForm", "MarkerLocation", "{X},{Y},{Z}"), MarkerLocationArgs);
const FVector MarkerUpVector = this->BugMarkerActor->GetActorUpVector();
FFormatNamedArguments MarkerUpVectorArgs;
MarkerUpVectorArgs.Add("X", FormatFloatToText(MarkerUpVector.X, 1, 4));
MarkerUpVectorArgs.Add("Y", FormatFloatToText(MarkerUpVector.Y, 1, 4));
MarkerUpVectorArgs.Add("Z", FormatFloatToText(MarkerUpVector.Z, 1, 4));
const FText MarkerUpVectorText = FText::Format(NSLOCTEXT("BugSubmissionForm", "MarkerUpVector", "{X},{Y},{Z}"), MarkerUpVectorArgs);
FFormatNamedArguments MapDataArgs;
MapDataArgs.Add("MarkerLocation", MarkerLocationText);
MapDataArgs.Add("MarkerUpVector", MarkerUpVectorText);
const FText MapDataText = FText::Format(NSLOCTEXT("BugSubmissionForm", "MapDataFormat", "{MarkerLocation}:{MarkerUpVector}"), MapDataArgs);
this->MarkerLocation = MapDataText;
}
void UBugSubmissionForm::ShowProcessingOverlayLoading()
{
this->ProcessingRequestErrorText->SetText(FText::AsCultureInvariant(""));
this->ProcessingRequestOverlay->SetVisibility(ESlateVisibility::Visible);
this->ProcessingRequestThrobber->SetVisibility(ESlateVisibility::Visible);
this->ProcessingRequestErrorBox->SetVisibility(ESlateVisibility::Collapsed);
}
void UBugSubmissionForm::ShowProcessingOverlayMessage(const FString Message)
{
this->ProcessingRequestErrorText->SetText(FText::AsCultureInvariant(Message));
this->ProcessingRequestOverlay->SetVisibility(ESlateVisibility::Visible);
this->ProcessingRequestThrobber->SetVisibility(ESlateVisibility::Collapsed);
this->ProcessingRequestErrorBox->SetVisibility(ESlateVisibility::Visible);
}
void UBugSubmissionForm::HideProcessingOverlay()
{
this->ProcessingRequestOverlay->SetVisibility(ESlateVisibility::Collapsed);
}
void UBugSubmissionForm::SubmitForm()
{
this->ShowProcessingOverlayLoading();
const FString FullURL = GetDefault<UUnrealzillaGlobalSettings>()->SubmissionServer + "/rest.cgi";
const FString RequestURL = "/bug";
// Assemble query data into key:value pairs
TMap<FString, FString> QueryData;
QueryData.Add("api_key", GetDefault<UUnrealzillaGlobalSettings>()->APIKey);
const FString SummaryText = this->SummaryEntryBox->GetText().ToString();
const FString CommentText = this->CommentEntryBox->GetText().ToString();
const FString DefaultStatus = GetDefault<UUnrealzillaGlobalSettings>()->DefaultStatus;
FString PostJsonString;
FJSONPostBug PostData;
PostData.product = GetDefault<UUnrealzillaGlobalSettings>()->ProductName;
PostData.version = this->VersionButton->GetText().ToString();
PostData.platform = this->HardwareButton->GetText().ToString();
PostData.op_sys = this->OSButton->GetText().ToString();
PostData.component = this->ComponentButton->GetText().ToString();
PostData.severity = this->SeverityButton->GetText().ToString();
PostData.cf_mapname = this->MapName.ToString();
PostData.cf_location = this->MarkerLocation.ToString();
PostData.summary = SummaryText;
PostData.description = CommentText;
if (!DefaultStatus.IsEmpty())
{
PostData.status = DefaultStatus;
}
FJsonObjectConverter::UStructToJsonObjectString(PostData, PostJsonString);
if (PostData.version.IsEmpty())
{
this->ShowProcessingOverlayMessage("You must select a version number.");
return;
}
if (PostData.platform.IsEmpty() || PostData.op_sys.IsEmpty())
{
PostData.platform = "All";
PostData.op_sys = "All";
}
if (PostData.component.IsEmpty())
{
this->ShowProcessingOverlayMessage("You must select a component.");
return;
}
if (PostData.severity.IsEmpty())
{
this->ShowProcessingOverlayMessage("You must select a severity level.");
return;
}
if (SummaryText.IsEmpty())
{
this->ShowProcessingOverlayMessage("You must provide a summary.");
return;
}
FHttpModule &HttpModule = FHttpModule::Get();
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->SetContentAsString(PostJsonString);
Request->OnProcessRequestComplete().BindUObject(this, &UBugSubmissionForm::ServerPOSTResponse);
Request->ProcessRequest();
this->OnFormSubmit.ExecuteIfBound();
}
void UBugSubmissionForm::CancelForm()
{
this->OnFormCancelled.ExecuteIfBound();
this->CloseForm();
this->BugMarkerActor->Destroy();
}
void UBugSubmissionForm::CloseForm()
{
UGameplayStatics::GetPlayerController(this, 0)->SetInputMode(FInputModeGameOnly());
UGameplayStatics::GetPlayerController(this, 0)->bShowMouseCursor = false;
this->RemoveFromParent();
}
void UBugSubmissionForm::ServerPOSTResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONPostResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->ShowProcessingOverlayMessage(ResponseData.message);
}
else
{
this->CloseForm();
}
}
else
{
this->ServerConnectionError(Request->GetStatus());
}
}
void UBugSubmissionForm::ServerProductInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONProductResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->ShowProcessingOverlayMessage(ResponseData.message);
}
else
{
if (!ResponseData.products.IsEmpty())
{
const FJSONProductData &ProductData = ResponseData.products[0];
if (ProductData.name == GetDefault<UUnrealzillaGlobalSettings>()->ProductName)
{
for (const FJSONComponentData &ComponentData : ProductData.components)
{
this->ComponentList.Add(ComponentData.name);
}
for (const FJSONVersionData &VersionData : ProductData.versions)
{
this->VersionsList.Add(VersionData.name);
}
}
}
if (this->VersionsList.Contains(GetGameVersion()))
{
this->VersionButton->SetText(FText::AsCultureInvariant(GetGameVersion()));
}
else if (this->VersionsList.Contains("unspecified"))
{
this->VersionButton->SetText(FText::AsCultureInvariant("unspecified"));
}
else if (this->VersionsList.Contains("Latest"))
{
this->VersionButton->SetText(FText::AsCultureInvariant("Latest"));
}
else if (!this->VersionsList.IsEmpty())
{
this->VersionButton->SetText(FText::AsCultureInvariant(this->VersionsList[0]));
}
this->CheckIfAllInitialResponsesAreIn();
}
}
else
{
this->ServerConnectionError(Request->GetStatus());
}
}
void UBugSubmissionForm::ServerSeverityInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONFieldResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->ShowProcessingOverlayMessage(ResponseData.message);
}
else
{
if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "bug_severity")
{
for (const FJSONFieldValueData &FieldValue : ResponseData.fields[0].values)
{
this->SeverityList.Add(FieldValue.name);
}
}
this->CheckIfAllInitialResponsesAreIn();
}
}
else
{
this->ServerConnectionError(Request->GetStatus());
}
}
void UBugSubmissionForm::ServerPlatformInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONFieldResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->ShowProcessingOverlayMessage(ResponseData.message);
}
else
{
if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "rep_platform")
{
for (const FJSONFieldValueData &FieldValue : ResponseData.fields[0].values)
{
this->PlatformsList.Add(FieldValue.name);
}
}
this->CheckIfAllInitialResponsesAreIn();
}
}
else
{
this->ServerConnectionError(Request->GetStatus());
}
}
void UBugSubmissionForm::ServerOSInfoResponse(FHttpRequestPtr Request, FHttpResponsePtr Response, bool Success)
{
if (Success)
{
FJSONFieldResponse ResponseData;
FString JSONResponse = Response->GetContentAsString();
FJsonObjectConverter::JsonObjectStringToUStruct(JSONResponse, &ResponseData);
if (ResponseData.error)
{
this->ShowProcessingOverlayMessage(ResponseData.message);
}
else
{
if (!ResponseData.fields.IsEmpty() && ResponseData.fields[0].name == "op_sys")
{
for (const FJSONFieldValueData &FieldValue : ResponseData.fields[0].values)
{
this->OSList.Add(FieldValue.name);
}
}
this->CheckIfAllInitialResponsesAreIn();
}
}
else
{
this->ServerConnectionError(Request->GetStatus());
}
}
void UBugSubmissionForm::CheckIfAllInitialResponsesAreIn()
{
if (!this->ComponentList.IsEmpty() && !this->VersionsList.IsEmpty() && !this->SeverityList.IsEmpty() &&
!this->PlatformsList.IsEmpty() && !this->OSList.IsEmpty())
{
// Set these as defaults in case nothing below changes this setting
this->HardwareButton->SetText(FText::AsCultureInvariant("All"));
this->OSButton->SetText(FText::AsCultureInvariant("All"));
if (this->PlatformsList.Contains("PC")) // Try our best to auto-detect PC hardware
{
if (UGameplayStatics::GetPlatformName() == "Windows" && this->OSList.Contains("Windows"))
{
this->HardwareButton->SetText(FText::AsCultureInvariant("PC"));
this->OSButton->SetText(FText::AsCultureInvariant("Windows"));
}
else if (UGameplayStatics::GetPlatformName() == "Linux" && this->OSList.Contains("Linux"))
{
this->HardwareButton->SetText(FText::AsCultureInvariant("PC"));
this->OSButton->SetText(FText::AsCultureInvariant("Linux"));
}
else if (UGameplayStatics::GetPlatformName() == "Mac" && this->OSList.Contains("Mac OS"))
{
this->HardwareButton->SetText(FText::AsCultureInvariant("All"));
this->OSButton->SetText(FText::AsCultureInvariant("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"))
{
this->HardwareButton->SetText(FText::AsCultureInvariant("Macintosh"));
this->OSButton->SetText(FText::AsCultureInvariant("Mac OS"));
}
}
}
this->HideProcessingOverlay();
}
}
void UBugSubmissionForm::ServerConnectionError(const EHttpRequestStatus::Type Status)
{
switch (Status) {
case EHttpRequestStatus::Failed_ConnectionError:
this->ShowProcessingOverlayMessage("Unable to connect to the server");
break;
case EHttpRequestStatus::NotStarted:
this->ShowProcessingOverlayMessage("Connection could not start");
break;
case EHttpRequestStatus::Failed:
this->ShowProcessingOverlayMessage("Connection failed");
break;
default:
this->ShowProcessingOverlayMessage("Request failed for unknown reasons");
}
}