diff --git a/DockerFile.server b/DockerFile.server index d4f5da38..706ad4b9 100644 --- a/DockerFile.server +++ b/DockerFile.server @@ -1,15 +1,20 @@ FROM ubuntu USER root + RUN export DEBIAN_FRONTEND=noninteractive RUN apt-get update -RUN apt-get -yq install libglib2.0-dev libsecret-1-dev +RUN apt-get -yq install libglib2.0-dev libsecret-1-dev libcurl4 + RUN adduser ue --disabled-password -USER ue + +USER ue:ue COPY --chown=ue:ue /Releases/Server/. /home/ue/ +# RUN chmod a+x /home/ue/LinuxServer/FinalCypherServer.sh /home/ue/LinuxServer/FinalCypher/Binaries/Linux/FinalCypherServer-Linux-Shipping EXPOSE 7777/udp EXPOSE 7777/tcp -ENTRYPOINT ["/home/ue/LinuxServer/FinalCypherServer.sh", "-log"] \ No newline at end of file +ENTRYPOINT ["/home/ue/LinuxServer/FinalCypherServer.sh", "-log"] +# run [docker run -p 7777:7777/udp -t fc-server] for binding ports \ No newline at end of file diff --git a/Plugins/Agones/.gitignore b/Plugins/Agones/.gitignore new file mode 100644 index 00000000..bf510943 --- /dev/null +++ b/Plugins/Agones/.gitignore @@ -0,0 +1,2 @@ +Binaries/ +Intermediate/ \ No newline at end of file diff --git a/Plugins/Agones/Agones.uplugin b/Plugins/Agones/Agones.uplugin new file mode 100644 index 00000000..7952ded7 --- /dev/null +++ b/Plugins/Agones/Agones.uplugin @@ -0,0 +1,24 @@ +{ + "CanContainContent": false, + "Category": "Agones", + "CreatedBy": "", + "CreatedByURL": "", + "Description": "Agones SDK for Unreal, wrapping the Agones REST API.", + "DocsURL": "https://agones.dev/site/docs/guides/client-sdks/unreal/", + "FileVersion": 3, + "FriendlyName": "Agones", + "Installed": false, + "IsBetaVersion": false, + "IsExperimentalVersion": false, + "MarketplaceURL": "", + "Modules": [ + { + "LoadingPhase": "PreLoadingScreen", + "Name": "Agones", + "Type": "Runtime" + } + ], + "SupportURL": "https://github.com/googleforgames/agones", + "Version": 2, + "VersionName": "2.0.0" +} diff --git a/Plugins/Agones/Resources/Icon128.png b/Plugins/Agones/Resources/Icon128.png new file mode 100644 index 00000000..8e8bdc7e Binary files /dev/null and b/Plugins/Agones/Resources/Icon128.png differ diff --git a/Plugins/Agones/Source/Agones/Agones.Build.cs b/Plugins/Agones/Source/Agones/Agones.Build.cs new file mode 100644 index 00000000..05fd8dd6 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Agones.Build.cs @@ -0,0 +1,42 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using UnrealBuildTool; + +public class Agones : ModuleRules +{ + public Agones(ReadOnlyTargetRules target) : base(target) + { + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicIncludePaths.AddRange(new string[] {}); + PrivateIncludePaths.AddRange(new string[] {}); + PublicDependencyModuleNames.AddRange(new[] + { + "Core", + "Http", + "Json", + "JsonUtilities", + "WebSockets" + }); + PrivateDependencyModuleNames.AddRange( + new[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore" + }); + DynamicallyLoadedModuleNames.AddRange(new string[]{ }); + } +} diff --git a/Plugins/Agones/Source/Agones/Classes/Classes.h b/Plugins/Agones/Source/Agones/Classes/Classes.h new file mode 100644 index 00000000..49d97dd3 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Classes/Classes.h @@ -0,0 +1,375 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "CoreMinimal.h" +#include "Dom/JsonObject.h" + +#include "Classes.generated.h" + +USTRUCT(BlueprintType) +struct FObjectMeta +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Name; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Namespace; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Uid; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString ResourceVersion; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 Generation = 0; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 CreationTimestamp = 0; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 DeletionTimestamp = 0; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + TMap Annotations; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + TMap Labels; + + FObjectMeta() + { + } + + explicit FObjectMeta(TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("name"), Name); + JsonObject->TryGetStringField(TEXT("namespace"), Namespace); + JsonObject->TryGetStringField(TEXT("uid"), Uid); + JsonObject->TryGetStringField(TEXT("resource_version"), ResourceVersion); + JsonObject->TryGetNumberField(TEXT("generation"), Generation); + JsonObject->TryGetNumberField(TEXT("creation_timestamp"), CreationTimestamp); + JsonObject->TryGetNumberField(TEXT("deletion_timestamp"), DeletionTimestamp); + const TSharedPtr* AnnotationsJsonObject; + if (JsonObject->TryGetObjectField(TEXT("annotations"), AnnotationsJsonObject)) + { + for (const auto& Entry : (*AnnotationsJsonObject)->Values) + { + if (Entry.Value.IsValid() && !Entry.Value->IsNull()) + { + FJsonValueString Key = Entry.Key; + TSharedPtr Value = Entry.Value; + FString AnnotationKey = Key.AsString(); + FString AnnotationValue = Value->AsString(); + Annotations.Add(AnnotationKey, AnnotationValue); + } + } + } + const TSharedPtr* LabelsObject; + if (JsonObject->TryGetObjectField(TEXT("labels"), LabelsObject)) + { + for (const auto& Entry : (*LabelsObject)->Values) + { + if (Entry.Value.IsValid() && !Entry.Value->IsNull()) + { + FJsonValueString Key = Entry.Key; + TSharedPtr Value = Entry.Value; + FString LabelKey = Key.AsString(); + FString LabelValue = Value->AsString(); + Labels.Add(LabelKey, LabelValue); + } + } + } + } +}; + +USTRUCT(BlueprintType) +struct FHealth +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + bool bDisabled = false; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int32 PeriodSeconds = 0; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int32 FailureThreshold = 0; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int32 InitialDelaySeconds = 0; + + FHealth() + { + } + + explicit FHealth(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("disabled"), bDisabled); + JsonObject->TryGetNumberField(TEXT("period_seconds"), PeriodSeconds); + JsonObject->TryGetNumberField(TEXT("failure_threshold"), FailureThreshold); + JsonObject->TryGetNumberField(TEXT("initial_delay_seconds"), InitialDelaySeconds); + } +}; + +USTRUCT(BlueprintType) +struct FSpec +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FHealth Health; + + FSpec() + { + } + + explicit FSpec(const TSharedPtr JsonObject) + { + const TSharedPtr* HealthJsonObject; + if (JsonObject->TryGetObjectField(TEXT("health"), HealthJsonObject)) + { + Health = FHealth(*HealthJsonObject); + } + } +}; + +USTRUCT(BlueprintType) +struct FPort +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Name; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int32 Port = 0; + + FPort() + { + } + + explicit FPort(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("name"), Name); + JsonObject->TryGetNumberField(TEXT("port"), Port); + } +}; + +USTRUCT(BlueprintType) +struct FStatus +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString State; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Address; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + TArray Ports; + + FStatus() + { + } + + explicit FStatus(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringField(TEXT("state"), State); + JsonObject->TryGetStringField(TEXT("address"), Address); + const TArray>* PortsArray; + if (JsonObject->TryGetArrayField(TEXT("ports"), PortsArray)) + { + const int32 ArrLen = PortsArray->Num(); + for (int32 i = 0; i < ArrLen; ++i) + { + const TSharedPtr& PortItem = (*PortsArray)[i]; + if (PortItem.IsValid() && !PortItem->IsNull()) + { + FPort Port = FPort(PortItem->AsObject()); + Ports.Add(Port); + } + } + } + } +}; + +USTRUCT(BlueprintType) +struct FGameServerResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FStatus Status; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FObjectMeta ObjectMeta; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FSpec Spec; + + FGameServerResponse() + { + } + + explicit FGameServerResponse(const TSharedPtr JsonObject) + { + const TSharedPtr* ObjectMetaJsonObject; + if (JsonObject->TryGetObjectField(TEXT("object_meta"), ObjectMetaJsonObject)) + { + ObjectMeta = FObjectMeta(*ObjectMetaJsonObject); + } + const TSharedPtr* SpecJsonObject; + if (JsonObject->TryGetObjectField(TEXT("spec"), SpecJsonObject)) + { + Spec = FSpec(*SpecJsonObject); + } + const TSharedPtr* StatusJsonObject; + if (JsonObject->TryGetObjectField(TEXT("status"), StatusJsonObject)) + { + Status = FStatus(*StatusJsonObject); + } + } +}; + +USTRUCT(BlueprintType) +struct FKeyValuePair +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Key; + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString Value; +}; + +USTRUCT(BlueprintType) +struct FDuration +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 Seconds; +}; + +USTRUCT(BlueprintType) +struct FAgonesPlayer +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString PlayerID; +}; + +USTRUCT(BlueprintType) +struct FPlayerCapacity +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 Count; +}; + +USTRUCT(BlueprintType) +struct FEmptyResponse +{ + GENERATED_BODY() +}; + +USTRUCT(BlueprintType) +struct FAgonesError +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + FString ErrorMessage; +}; + +USTRUCT(BlueprintType) +struct FConnectedResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + bool bConnected = false; + + FConnectedResponse() + { + } + + explicit FConnectedResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("bool"), bConnected); + } +}; + +USTRUCT(BlueprintType) +struct FDisconnectResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + bool bDisconnected = false; + + FDisconnectResponse() + { + } + + explicit FDisconnectResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetBoolField(TEXT("bool"), bDisconnected); + } +}; + +USTRUCT(BlueprintType) +struct FCountResponse +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category="Agones") + int64 Count = 0; + + FCountResponse() + { + } + + explicit FCountResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetNumberField(TEXT("count"), Count); + } +}; + +USTRUCT(BlueprintType) +struct FConnectedPlayersResponse +{ + GENERATED_BODY() + + FConnectedPlayersResponse() + { + } + + UPROPERTY(BlueprintReadOnly, Category="Agones") + TArray ConnectedPlayers; + + explicit FConnectedPlayersResponse(const TSharedPtr JsonObject) + { + JsonObject->TryGetStringArrayField(TEXT("list"), ConnectedPlayers); + } +}; diff --git a/Plugins/Agones/Source/Agones/Private/Agones.cpp b/Plugins/Agones/Source/Agones/Private/Agones.cpp new file mode 100644 index 00000000..823d5733 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Private/Agones.cpp @@ -0,0 +1,32 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Agones.h" + +#define LOCTEXT_NAMESPACE "FAgonesModule" + +void FAgonesModule::StartupModule() +{ + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module +} + +void FAgonesModule::ShutdownModule() +{ + // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, + // we call this function before unloading the module. +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FAgonesModule, Agones) diff --git a/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp b/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp new file mode 100644 index 00000000..21fb1df4 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Private/AgonesComponent.cpp @@ -0,0 +1,584 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "AgonesComponent.h" + +#include "Engine/World.h" +#include "HttpModule.h" +#include "Interfaces/IHttpResponse.h" +#include "JsonUtilities/Public/JsonObjectConverter.h" +#include "TimerManager.h" +#include "WebSockets/Public/IWebSocket.h" +#include "WebSockets/Public/WebSocketsModule.h" + +UAgonesComponent::UAgonesComponent() +{ + PrimaryComponentTick.bCanEverTick = false; +} + +void UAgonesComponent::BeginPlay() +{ + Super::BeginPlay(); + HealthPing(HealthRateSeconds); + + if (bDisableAutoConnect) + { + return; + } + Connect(); +} + +void UAgonesComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + Super::EndPlay(EndPlayReason); + + const UWorld* World = GetWorld(); + if (World != nullptr) + { + World->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + World->GetTimerManager().ClearTimer(HealthTimerHandler); + World->GetTimerManager().ClearTimer(EnsureWebSocketTimerHandler); + } + + if (WatchWebSocket != nullptr && WatchWebSocket->IsConnected()) + { + WatchWebSocket->Close(); + } +} + +FHttpRequestRef UAgonesComponent::BuildAgonesRequest(const FString Path, const FHttpVerb Verb, const FString Content) +{ + FHttpModule* Http = &FHttpModule::Get(); + FHttpRequestRef Request = Http->CreateRequest(); + + Request->SetURL(FString::Format( + TEXT("http://localhost:{0}/{1}"), +{FStringFormatArg(HttpPort), FStringFormatArg(Path)} + )); + Request->SetVerb(Verb.ToString()); + Request->SetHeader(TEXT("Content-Type"), TEXT("application/json")); + Request->SetHeader(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); + Request->SetHeader(TEXT("Accepts"), TEXT("application/json")); + Request->SetContentAsString(Content); + return Request; +} + +void UAgonesComponent::HealthPing(const float RateSeconds) +{ + if (RateSeconds <= 0.0f) + { + return; + } + + FTimerDelegate TimerDel; + TimerDel.BindUObject(this, &UAgonesComponent::Health, FHealthDelegate(), FAgonesErrorDelegate()); + GetWorld()->GetTimerManager().ClearTimer(HealthTimerHandler); + GetWorld()->GetTimerManager().SetTimer(HealthTimerHandler, TimerDel, RateSeconds, true); +} + +void UAgonesComponent::Connect() +{ + FGameServerDelegate SuccessDel; + SuccessDel.BindUFunction(this, FName("ConnectSuccess")); + FTimerDelegate ConnectDel; + ConnectDel.BindUObject(this, &UAgonesComponent::GameServer, SuccessDel, FAgonesErrorDelegate()); + GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + GetWorld()->GetTimerManager().SetTimer(ConnectDelTimerHandle, ConnectDel, 5.f, true); +} + +void UAgonesComponent::ConnectSuccess(const FGameServerResponse GameServerResponse) +{ + GetWorld()->GetTimerManager().ClearTimer(ConnectDelTimerHandle); + Ready({}, {}); + ConnectedDelegate.Broadcast(GameServerResponse); +} + +void UAgonesComponent::Ready(const FReadyDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("ready"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GameServer(const FGameServerDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("gameserver", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FGameServerResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::EnsureWebSocketConnection() +{ + if (WatchWebSocket == nullptr) + { + if (!FModuleManager::LoadModulePtr(TEXT("WebSockets"))) + { + return; + } + + TMap Headers; + + // Make up a WebSocket-Key value. It can be anything! + Headers.Add(TEXT("Sec-WebSocket-Key"), FGuid::NewGuid().ToString(EGuidFormats::Short)); + Headers.Add(TEXT("Sec-WebSocket-Version"), TEXT("13")); + Headers.Add(TEXT("User-Agent"), TEXT("X-UnrealEngine-Agent")); + + // Unreal WebSockets are not able to do DNS resolution for localhost for some reason + // so this is using the IPv4 Loopback Address instead. + WatchWebSocket = FWebSocketsModule::Get().CreateWebSocket( + FString::Format(TEXT("ws://127.0.0.1:{0}/watch/gameserver"), + static_cast( + TArray>{ + FStringFormatArg(HttpPort) + } + ) + ), + TEXT("") + ); + + WatchWebSocket->OnRawMessage().AddUObject(this, &UAgonesComponent::HandleWatchMessage); + } + + if (WatchWebSocket != nullptr) + { + if (!WatchWebSocket->IsConnected()) + { + WatchWebSocket->Connect(); + } + + // Only start the timer if there is a websocket to check. + // This timer has nothing to do with health and only matters if the agent is somehow + // restarted, which would be a failure condition in normal operation. + if (!EnsureWebSocketTimerHandler.IsValid()) + { + FTimerDelegate TimerDel; + TimerDel.BindUObject(this, &UAgonesComponent::EnsureWebSocketConnection); + GetWorld()->GetTimerManager().SetTimer( + EnsureWebSocketTimerHandler, TimerDel, 15.0f, true); + } + } +} + +void UAgonesComponent::WatchGameServer(const FGameServerDelegate WatchDelegate) +{ + WatchGameServerCallbacks.Add(WatchDelegate); + EnsureWebSocketConnection(); +} + + void UAgonesComponent::DeserializeAndBroadcastWatch(FString const& JsonString) +{ + TSharedRef> const JsonReader = TJsonReaderFactory::Create(JsonString); + + TSharedPtr JsonObject; + const TSharedPtr* ResultObject = nullptr; + + if (!FJsonSerializer::Deserialize(JsonReader, JsonObject) || + !JsonObject.IsValid() || + !JsonObject->TryGetObjectField(TEXT("result"), ResultObject) || + !ResultObject->IsValid()) + { + return; + } + + FGameServerResponse const Result = FGameServerResponse(*ResultObject); + for (FGameServerDelegate const& Callback : WatchGameServerCallbacks) + { + if (Callback.IsBound()) + { + Callback.Execute(Result); + } + } +} + +void UAgonesComponent::HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining) +{ + if (BytesRemaining > 0) + { + WatchMessageBuffer.Append(UTF8_TO_TCHAR(static_cast(Data)), Size); + return; + } + + FString const Message = FString(Size, UTF8_TO_TCHAR(static_cast(Data))); + + // If the LHS of FString + is empty, it just uses the RHS directly so there's no copy here with an empty buffer. + DeserializeAndBroadcastWatch(WatchMessageBuffer + Message); + + // Faster to check and then empty vs blindly emptying - normal case is that the buffer is already empty + if (!WatchMessageBuffer.IsEmpty()) + { + WatchMessageBuffer.Empty(); + } +} + +void UAgonesComponent::SetLabel( + const FString& Key, const FString& Value, const FSetLabelDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FKeyValuePair Label = {Key, Value}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Label, Json)) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("error serializing key-value pair ({0}: {1}})"), + static_cast( + TArray>{ + FStringFormatArg(Key), + FStringFormatArg(Value) + }) + )}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("metadata/label", FHttpVerb::Put, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Health(const FHealthDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("health"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Shutdown(const FShutdownDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("shutdown"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::SetAnnotation( + const FString& Key, const FString& Value, const FSetAnnotationDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FKeyValuePair Label = {Key, Value}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Label, Json)) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("error serializing key-value pair ({0}: {1}})"), + static_cast( + TArray>{ + FStringFormatArg(Key), + FStringFormatArg(Value) + } + ) + )}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("metadata/annotation", FHttpVerb::Put, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Allocate(const FAllocateDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("allocate"); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::Reserve( + const int64 Seconds, const FReserveDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FDuration Duration = {Seconds}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Duration, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("reserve", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::PlayerConnect( + const FString PlayerId, const FPlayerConnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FAgonesPlayer Player = {PlayerId}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Player, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + // TODO(dom) - look at JSON encoding in UE4. + Json = Json.Replace(TEXT("playerId"), TEXT("playerID")); + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/connect", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::PlayerDisconnect( + const FString PlayerId, const FPlayerDisconnectDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FAgonesPlayer Player = {PlayerId}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(Player, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + // TODO(dom) - look at JSON encoding in UE4. + Json = Json.Replace(TEXT("playerId"), TEXT("playerID")); + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/disconnect", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FDisconnectResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::SetPlayerCapacity( + const int64 Count, const FSetPlayerCapacityDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + const FPlayerCapacity PlayerCapacity = {Count}; + FString Json; + if (!FJsonObjectConverter::UStructToJsonObjectString(PlayerCapacity, Json)) + { + ErrorDelegate.ExecuteIfBound({TEXT("Failed to serializing request")}); + return; + } + + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/capacity", FHttpVerb::Post, Json); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, FHttpResponsePtr HttpResponse, const bool bSucceeded) { + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound({}); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/capacity", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FCountResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/count", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FCountResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::IsPlayerConnected( + const FString PlayerId, const FIsPlayerConnectedDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest( + FString::Format(TEXT("alpha/player/connected/{0}"), + static_cast( + TArray>{ + FStringFormatArg(PlayerId) + } + ) + ), + FHttpVerb::Get, + "" + ); + + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +void UAgonesComponent::GetConnectedPlayers( + const FGetConnectedPlayersDelegate SuccessDelegate, const FAgonesErrorDelegate ErrorDelegate) +{ + FHttpRequestRef Request = BuildAgonesRequest("alpha/player/connected/{0}", FHttpVerb::Get, ""); + Request->OnProcessRequestComplete().BindWeakLambda(this, + [SuccessDelegate, ErrorDelegate](FHttpRequestPtr HttpRequest, const FHttpResponsePtr HttpResponse, const bool bSucceeded) { + TSharedPtr JsonObject; + + if (!IsValidJsonResponse(JsonObject, bSucceeded, HttpResponse, ErrorDelegate)) + { + return; + } + + SuccessDelegate.ExecuteIfBound(FConnectedPlayersResponse(JsonObject)); + }); + Request->ProcessRequest(); +} + +bool UAgonesComponent::IsValidResponse(const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) +{ + if (!bSucceeded) + { + ErrorDelegate.ExecuteIfBound({"Unsuccessful Call"}); + return false; + } + + if (!EHttpResponseCodes::IsOk(HttpResponse->GetResponseCode())) + { + ErrorDelegate.ExecuteIfBound( + {FString::Format(TEXT("Error Code - {0}"), + static_cast( + TArray>{ + FStringFormatArg(FString::FromInt(HttpResponse->GetResponseCode())) + }) + ) + } + ); + return false; + } + + return true; +} + +bool UAgonesComponent::IsValidJsonResponse(TSharedPtr& JsonObject, const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate) +{ + if (!IsValidResponse(bSucceeded, HttpResponse, ErrorDelegate)) + { + return false; + } + + TSharedPtr OutObject; + const FString Json = HttpResponse->GetContentAsString(); + const TSharedRef> JsonReader = TJsonReaderFactory<>::Create(Json); + if (!FJsonSerializer::Deserialize(JsonReader, OutObject) || !OutObject.IsValid()) + { + ErrorDelegate.ExecuteIfBound({FString::Format(TEXT("Failed to parse response - {0}"), + static_cast( + TArray>{ + FStringFormatArg(Json) + }) + ) + }); + return false; + } + + JsonObject = OutObject.ToSharedRef(); + return true; +} diff --git a/Plugins/Agones/Source/Agones/Public/Agones.h b/Plugins/Agones/Source/Agones/Public/Agones.h new file mode 100644 index 00000000..85d436a9 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Public/Agones.h @@ -0,0 +1,25 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "Modules/ModuleManager.h" + +class FAgonesModule : public IModuleInterface +{ +public: + /** IModuleInterface implementation */ + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Plugins/Agones/Source/Agones/Public/AgonesComponent.h b/Plugins/Agones/Source/Agones/Public/AgonesComponent.h new file mode 100644 index 00000000..29722aa6 --- /dev/null +++ b/Plugins/Agones/Source/Agones/Public/AgonesComponent.h @@ -0,0 +1,321 @@ +// Copyright 2020 Google LLC All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "Classes.h" +#include "Components/ActorComponent.h" +#include "CoreMinimal.h" +#include "Interfaces/IHttpRequest.h" +#include "WebSockets/Public/IWebSocket.h" + +#include "AgonesComponent.generated.h" + +DECLARE_DYNAMIC_DELEGATE_OneParam(FAgonesErrorDelegate, const FAgonesError&, Error); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FAllocateDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGameServerDelegate, const FGameServerResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetConnectedPlayersDelegate, const FConnectedPlayersResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetPlayerCapacityDelegate, const FCountResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FGetPlayerCountDelegate, const FCountResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FHealthDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FIsPlayerConnectedDelegate, const FConnectedResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FPlayerConnectDelegate, const FConnectedResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FPlayerDisconnectDelegate, const FDisconnectResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FReadyDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FReserveDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetAnnotationDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetLabelDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FSetPlayerCapacityDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_DELEGATE_OneParam(FShutdownDelegate, const FEmptyResponse&, Response); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FConnectedDelegate, const FGameServerResponse&, Response); + +class FHttpVerb +{ +public: + enum EVerb + { + Get, + Post, + Put + }; + + // ReSharper disable once CppNonExplicitConvertingConstructor + FHttpVerb(const EVerb Verb) : Verb(Verb) + { + } + + FString ToString() const + { + switch (Verb) + { + case Post: + return TEXT("POST"); + case Put: + return TEXT("PUT"); + case Get: + default: + return TEXT("GET"); + } + } + +private: + const EVerb Verb; +}; + +/** + * \brief UAgonesComponent is the Unreal Component to call to the Agones SDK. + * See - https://agones.dev/ for more information. + */ +UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent), Config = Game, defaultconfig) +class AGONES_API UAgonesComponent final : public UActorComponent +{ + GENERATED_BODY() + +public: + UAgonesComponent(); + + /** + * \brief HttpPort is the default Agones HTTP port to use. + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + FString HttpPort = "9358"; + + /** + * \brief HealthRateSeconds is the frequency to send Health calls. Value of 0 will disable auto health calls. + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + float HealthRateSeconds = 10.f; + + /** + * \brief bDisableAutoConnect will stop the component auto connecting (calling GamesServer and Ready). + */ + UPROPERTY(EditAnywhere, Category = Agones, Config) + bool bDisableAutoConnect; + + /** + * \brief ConnectedDelegate will be called once the Connect func gets a successful response from GameServer. + */ + UPROPERTY(BlueprintAssignable, Category = Agones) + FConnectedDelegate ConnectedDelegate; + + /** + * \brief BeginPlay is a built in UE4 function that is called as the component is created. + */ + virtual void BeginPlay() override; + + /** + * \brief EndPlay is a built in UE4 function that is called as the component is destroyed. + * \param EndPlayReason reason for Ending Play. + */ + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + /** + * \brief HealthPing loops calling the Health endpoint. + * \param RateSeconds rate at which the Health endpoint should be called. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Utility") + void HealthPing(float RateSeconds); + + /** + * \brief Connect will call /gameserver till a successful response then call /ready + * a delegate is called with the gameserver response after /ready call is made. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Utility") + void Connect(); + + /** + * \brief Allocate self marks this gameserver as Allocated. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Allocate(FAllocateDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief GameServer retrieve the GameServer details. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Configuration") + void GameServer(FGameServerDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief WatchGameServer subscribes a delegate to be called whenever game server details change. + * \param WatchDelegate - Called every time the game server data changes. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Configuration") + void WatchGameServer(FGameServerDelegate WatchDelegate); + + /** + * \brief Health sends a ping to the health check to indicate that this server is healthy. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Health(FHealthDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Ready marks the Game Server as ready to receive connections. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Ready(FReadyDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Reserve marks the Game Server as Reserved for a given duration. + * \param Seconds - Seconds that the Game Server will be reserved. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Reserve(int64 Seconds, FReserveDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief SetAnnotation sets a metadata annotation on the `GameServer` with the prefix 'agones.dev/sdk-' + * calling SetAnnotation("foo", "bar", {}, {}) will result in the annotation "agones.dev/sdk-foo: bar". + * \param Key + * \param Value + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Metadata") + void SetAnnotation(const FString& Key, const FString& Value, FSetAnnotationDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief SetLabel sets a metadata label on the `GameServer` with the prefix 'agones.dev/sdk-' + * calling SetLabel("foo", "bar", {}, {}) will result in the label "agones.dev/sdk-foo: bar". + * \param Key + * \param Value + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Metadata") + void SetLabel(const FString& Key, const FString& Value, FSetLabelDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief Shutdown marks the Game Server as ready to shutdown + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Lifecycle") + void Shutdown(FShutdownDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetConnectedPlayers returns the list of the currently connected player ids. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetConnectedPlayers(FGetConnectedPlayersDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetPlayerCapacity gets the last player capacity that was set through the SDK. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetPlayerCapacity(FGetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] GetPlayerCount returns the current player count + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void GetPlayerCount(FGetPlayerCountDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] IsPlayerConnected returns if the playerID is currently connected to the GameServer. + * \param PlayerId - PlayerID of player to check. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void IsPlayerConnected(FString PlayerId, FIsPlayerConnectedDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] PlayerConnect increases the SDK’s stored player count by one, and appends this playerID to status.players.id. + * \param PlayerId - PlayerID of connecting player. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void PlayerConnect(FString PlayerId, FPlayerConnectDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] PlayerDisconnect Decreases the SDK’s stored player count by one, and removes the playerID from + * status.players.id. + * + * \param PlayerId - PlayerID of disconnecting player. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void PlayerDisconnect(FString PlayerId, FPlayerDisconnectDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + + /** + * \brief [Alpha] SetPlayerCapacity changes the player capacity to a new value. + * \param Count - Capacity of game server. + * \param SuccessDelegate - Called on Successful call. + * \param ErrorDelegate - Called on Unsuccessful call. + */ + UFUNCTION(BlueprintCallable, Category = "Agones | Alpha | Player Tracking") + void SetPlayerCapacity(int64 Count, FSetPlayerCapacityDelegate SuccessDelegate, FAgonesErrorDelegate ErrorDelegate); + +private: + FHttpRequestRef BuildAgonesRequest( + FString Path = "", const FHttpVerb Verb = FHttpVerb::Post, const FString Content = "{}"); + + void HandleWatchMessage(const void* Data, SIZE_T Size, SIZE_T BytesRemaining); + + void DeserializeAndBroadcastWatch(FString const& JsonString); + + void EnsureWebSocketConnection(); + + FTimerHandle ConnectDelTimerHandle; + + FTimerHandle HealthTimerHandler; + + FTimerHandle EnsureWebSocketTimerHandler; + + TSharedPtr WatchWebSocket; + + FString WatchMessageBuffer; + + TArray WatchGameServerCallbacks; + + static bool IsValidResponse(const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate); + + static bool IsValidJsonResponse(TSharedPtr& JsonObject, const bool bSucceeded, const FHttpResponsePtr HttpResponse, FAgonesErrorDelegate ErrorDelegate); + + UFUNCTION(BlueprintInternalUseOnly) + void ConnectSuccess(FGameServerResponse GameServerResponse); +}; diff --git a/Source/FinalCypherServer.Target.cs b/Source/FinalCypherServer.Target.cs index fa05a501..911700ef 100644 --- a/Source/FinalCypherServer.Target.cs +++ b/Source/FinalCypherServer.Target.cs @@ -9,6 +9,7 @@ public FinalCypherServerTarget(TargetInfo Target) : base(Target) { Type = TargetType.Server; DefaultBuildSettings = BuildSettingsVersion.V2; + bUseLoggingInShipping = true; ExtraModuleNames.Add("FinalCypher"); } } \ No newline at end of file