From 25a27c171cc26cdb2b6e587ceb96045658521736 Mon Sep 17 00:00:00 2001 From: Zach Graziano Date: Fri, 8 Sep 2023 13:34:30 -0400 Subject: [PATCH] Revert "Hathora SDK Improvements (#3)" (#5) This reverts commit 9a1506988f3be157fe9c72f5b2cf1f20f7cb6c1a. --- SDKDemo/Plugins/HathoraSDK/HathoraSDK.uplugin | 3 +- .../Source/HathoraSDK/HathoraSDK.Build.cs | 28 +- .../Source/HathoraSDK/Private/HathoraPing.cpp | 109 +++++ .../Source/HathoraSDK/Private/HathoraSDK.cpp | 386 +----------------- .../HathoraSDK/Private/HathoraSdkConfig.cpp | 8 + .../Source/HathoraSDK/Public/HathoraPing.h | 30 ++ .../Source/HathoraSDK/Public/HathoraSDK.h | 59 +-- .../HathoraSDK/Public/HathoraSdkConfig.h | 20 + SDKDemo/Source/SDKDemo/SDemoMenuWidget.cpp | 24 +- 9 files changed, 203 insertions(+), 464 deletions(-) create mode 100644 SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraPing.cpp create mode 100644 SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSdkConfig.cpp create mode 100644 SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraPing.h create mode 100644 SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSdkConfig.h diff --git a/SDKDemo/Plugins/HathoraSDK/HathoraSDK.uplugin b/SDKDemo/Plugins/HathoraSDK/HathoraSDK.uplugin index a24cf0c..e9214f3 100644 --- a/SDKDemo/Plugins/HathoraSDK/HathoraSDK.uplugin +++ b/SDKDemo/Plugins/HathoraSDK/HathoraSDK.uplugin @@ -18,8 +18,7 @@ { "Name": "HathoraSDK", "Type": "Runtime", - "LoadingPhase": "PostConfigInit", - "AdditionalDependencies": "HTTP" + "LoadingPhase": "Default" } ] } \ No newline at end of file diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/HathoraSDK.Build.cs b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/HathoraSDK.Build.cs index 1de0e6b..3f95960 100644 --- a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/HathoraSDK.Build.cs +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/HathoraSDK.Build.cs @@ -7,18 +7,22 @@ public class HathoraSDK : ModuleRules public HathoraSDK(ReadOnlyTargetRules Target) : base(Target) { PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - + PublicIncludePaths.AddRange( new string[] { + // ... add public include paths required here ... } ); - - + + PrivateIncludePaths.AddRange( new string[] { + "HathoraSDK/Private" + // ... add other private include paths required here ... } ); - + + PublicDependencyModuleNames.AddRange( new string[] { @@ -32,15 +36,21 @@ public HathoraSDK(ReadOnlyTargetRules Target) : base(Target) { "CoreUObject", "Engine", - "HTTP", + "Slate", + "SlateCore", "Json", "JsonUtilities", - "libWebSockets", - "SSL", + "HTTP", "WebSockets", } ); - - AddEngineThirdPartyPrivateStaticDependencies(Target, "OpenSSL", "libWebSockets"); + + + DynamicallyLoadedModuleNames.AddRange( + new string[] + { + // ... add any modules that your module loads dynamically here ... + } + ); } } diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraPing.cpp b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraPing.cpp new file mode 100644 index 0000000..468b9d6 --- /dev/null +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraPing.cpp @@ -0,0 +1,109 @@ +// Copyright 2023 Hathora, Inc. +#include "HathoraPing.h" +#include "DiscoveredPingEndpoint.h" +#include "HttpModule.h" +#include "JsonObjectConverter.h" +#include "WebSocketsModule.h" +#include "IWebSocket.h" +#include "HathoraSDK.h" +#include "Interfaces/IHttpRequest.h" +#include "Interfaces/IHttpResponse.h" + +UHathoraPing::UHathoraPing(const FObjectInitializer& ObjectInitializer) : UObject(ObjectInitializer) +{ + this->HathoraSdkConfig = NewObject(); +} + +void UHathoraPing::GetRegionalPings(const FOnGetRegionalPingsDelegate& OnComplete) +{ + + FHttpRequestRef Request = FHttpModule::Get().CreateRequest(); + Request->OnProcessRequestComplete().BindLambda([&, OnComplete](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess) mutable { + TArray PingEndpoints; + if (bSuccess && Response.IsValid() && FJsonObjectConverter::JsonArrayStringToUStruct(Response->GetContentAsString(), &PingEndpoints)) + { + PingUrlsAndAggregateTimes(PingEndpoints, OnComplete); + } + else + { + UE_LOG(LogHathoraSDK, Warning, TEXT("Could not retrieve ping endpoints")); + TMap Pings; + if (!OnComplete.ExecuteIfBound(Pings)) + { + UE_LOG(LogHathoraSDK, Warning, TEXT("[GetRegionalPings] function pointer was not valid, so OnComplete will not be called")); + } + } + }); + + Request->SetURL(FString::Printf(TEXT("%s/discovery/v1/ping"), *this->HathoraSdkConfig->GetBaseUrl())); + Request->ProcessRequest(); +} + + +void UHathoraPing::PingUrlsAndAggregateTimes( + const TArray& PingEndpoints, const FOnGetRegionalPingsDelegate& OnComplete) +{ + TSharedPtr> Pings = MakeShared>(); + TSharedPtr CompletedPings = MakeShared(0); + + const int32 PingsToComplete = PingEndpoints.Num(); + + // aggregate the results of N asynchronous operations into a single TMap. + for (const FDiscoveredPingEndpoint& PingEndpoint : PingEndpoints) + { + + GetPingTime(PingEndpoint, FOnGetPingDelegate::CreateLambda([ PingEndpoint, CompletedPings, Pings, PingsToComplete, OnComplete](int32 PingTime, bool bWasSuccesful) { + if (bWasSuccesful) + { + UE_LOG(LogHathoraSDK, Log, TEXT("Ping to %s (%s:%d) took: %d ms"), *PingEndpoint.Region, *PingEndpoint.Host, PingEndpoint.Port, PingTime); + Pings->Add(PingEndpoint.Region, PingTime); + } + // Regardless of whether the ping was successful, we will mark it complete. + if (++(*CompletedPings) == PingsToComplete) + { + UE_LOG(LogHathoraSDK, Log, TEXT("Pings to all Hathora regions complete.")); + (void)OnComplete.ExecuteIfBound(*Pings); + } + })); + } +} + + +DECLARE_DELEGATE_TwoParams(FOnGetPingDelegate, int32 /* Ping */, bool /* bWasSuccessful */); + +void UHathoraPing::GetPingTime(const FDiscoveredPingEndpoint& PingEndpoint, const FOnGetPingDelegate& OnComplete) +{ + const FString& MessageText = TEXT("PING"); + const FString& Url = FString::Printf(TEXT("wss://%s:%d/ws"), *PingEndpoint.Host, PingEndpoint.Port); + + // Unfortunately, we can't nest the OnMessage handler inside OnConnected, + TSharedPtr StartTime = MakeShared(0.0); + TSharedPtr WebSocket = FWebSocketsModule::Get().CreateWebSocket(Url); + + WebSocket->OnConnectionError().AddLambda([OnComplete](const FString& Reason) { + UE_LOG(LogHathoraSDK, Warning, TEXT("failed to connect to ping server due to %s"), *Reason); + (void)OnComplete.ExecuteIfBound(0, false); + }); + + WebSocket->OnMessage().AddLambda([MessageText, WebSocket, StartTime, OnComplete](const FString& Message) { + if (StartTime.IsValid() && *StartTime != 0.0 && Message == MessageText) + { + const int32 PingTimeMs = static_cast((FPlatformTime::Seconds() - *StartTime) * 1000); + (void)OnComplete.ExecuteIfBound(PingTimeMs, true); + WebSocket->Close(); + } + }); + + WebSocket->OnClosed().AddLambda([](int32 StatusCode, const FString& Reason, bool bWasClean) { + UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("websocket closed %s with status code %d because %s"), + bWasClean ? TEXT("cleanly") : TEXT("uncleanly"), StatusCode, *Reason); + }); + + WebSocket->OnConnected().AddLambda([StartTime, WebSocket, MessageText, Url]() { + UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("websocket connection to %s established"), *Url); + *StartTime = FPlatformTime::Seconds(); + WebSocket->Send(MessageText); + }); + + WebSocket->Connect(); +} diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSDK.cpp b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSDK.cpp index ee45021..ef73371 100644 --- a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSDK.cpp +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSDK.cpp @@ -2,398 +2,18 @@ #include "HathoraSDK.h" -#include "DiscoveredPingEndpoint.h" - -#include "Async/Async.h" -#include "HAL/RunnableThread.h" -#include "HttpModule.h" -#include "Interfaces/IHttpRequest.h" -#include "Interfaces/IHttpResponse.h" -#include "JsonObjectConverter.h" -#include "Ssl.h" -#include "TimerManager.h" - -#if PLATFORM_WINDOWS -#include "Windows/WindowsHWrapper.h" -#include "Windows/AllowWindowsPlatformTypes.h" -#endif - -// Work around a conflict between a UI namespace defined by engine code and a typedef in OpenSSL -#define UI UI_ST -// Work around assertion macros in ue4 -#undef verify -THIRD_PARTY_INCLUDES_START -#include "libwebsockets.h" -THIRD_PARTY_INCLUDES_END -#undef UI - -#if PLATFORM_WINDOWS -#include "Windows/HideWindowsPlatformTypes.h" -#endif - -#include - #define LOCTEXT_NAMESPACE "FHathoraSDKModule" DEFINE_LOG_CATEGORY(LogHathoraSDK) void FHathoraSDKModule::StartupModule() { - // Load Configuration - BaseUrl = TEXT("https://api.hathora.dev"); - GConfig->GetString(TEXT("HathoraSDK"), TEXT("BaseUrl"), BaseUrl, GGameIni); - - GetConnectionInfoTimeoutSeconds = 90.0f; - GConfig->GetFloat(TEXT("HathoraSDK"), TEXT("GetConnectionInfoTimeoutSeconds"), GetConnectionInfoTimeoutSeconds, GGameIni); - - GetConnectionInfoDelaysSeconds = { "0.5", "1.0", "2.0", "3.0" }; - GConfig->GetArray(TEXT("HathoraSDK"), TEXT("GetConnectionInfoDelaysSeconds"), GetConnectionInfoDelaysSeconds, GGameIni); + // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module } void FHathoraSDKModule::ShutdownModule() { -} - -void FHathoraSDKModule::SetRetryManager(TSharedPtr& httpRetryManager) -{ - HttpRetryManager = httpRetryManager; -} - -TSharedRef FHathoraSDKModule::NewRequest() -{ - static const TSet retryCodes(TArray({ 400, 500, 501, 502, 503, 504 })); - static const TSet retryVerbs(TArray({ FName(TEXT("GET")), FName(TEXT("HEAD")), FName(TEXT("POST")) })); - - if (!HttpRetryManager.IsValid()) { - // Fallback to the Http Manager without retries - return FHttpModule::Get().CreateRequest(); - } - - TSharedRef result = HttpRetryManager->CreateRequest( - 2, - FHttpRetrySystem::FRetryTimeoutRelativeSecondsSetting(), - retryCodes, - retryVerbs); - result->SetTimeout(10.0f); - - return result; -} - -// -// GetRegionalPings -// - -struct CollectPingState { - bool PingSent; - bool Disconnected; - double StartTime; - int32 PingTimeMs; -}; - -lws* CollectRegionPingConnect(lws_context* context, const std::string& host, int port, CollectPingState* state) -{ - struct lws_client_connect_info i; - - memset(&i, 0, sizeof(i)); - - i.context = context; - i.port = port; - i.address = host.c_str(); - i.path = "/ws"; - i.host = i.address; - i.origin = i.address; - i.ssl_connection = LCCSCF_USE_SSL; - i.protocol = ""; - i.local_protocol_name = ""; - i.userdata = state; - - return lws_client_connect_via_info(&i); -} - -int CollectRegionPingsCallback(lws* Connection, lws_callback_reasons Reason, void* UserData, void* Data, size_t Length) -{ - auto state = reinterpret_cast(UserData); - - switch (Reason) { - - case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: - { - FUTF8ToTCHAR Convert((const ANSICHAR*)Data, Length); - FString CloseReasonString(Convert.Length(), Convert.Get()); - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("LWS_CALLBACK_CLIENT_CONNECTION_ERROR, Reason=%s"), *CloseReasonString); - state->Disconnected = true; - return -1; - } - - case LWS_CALLBACK_CLIENT_ESTABLISHED: - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("LWS_CALLBACK_CLIENT_ESTABLISHED")); - lws_callback_on_writable(Connection); - break; - - case LWS_CALLBACK_CLIENT_WRITEABLE: - if (!state->PingSent) - { - uint8_t ping[LWS_PRE + 125]; - - state->PingSent = true; - state->StartTime = FPlatformTime::Seconds(); - - int toSend = lws_snprintf(reinterpret_cast(ping) + LWS_PRE, 125, "PING"); - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("Sending PING")); - - int sent = lws_write(Connection, ping + LWS_PRE, toSend, LWS_WRITE_PING); - if (sent < toSend) - { - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("Send PING failed %d"), sent); - return -1; - } - - lws_callback_on_writable(Connection); - } - break; - - case LWS_CALLBACK_CLIENT_CLOSED: - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("LWS_CALLBACK_CLIENT_CLOSED")); - state->Disconnected = true; - return -1; - - case LWS_CALLBACK_CLIENT_RECEIVE_PONG: - UE_LOG(LogHathoraSDK, VeryVerbose, TEXT("LWS_CALLBACK_CLIENT_RECEIVE_PONG")); - state->PingTimeMs = static_cast((FPlatformTime::Seconds() - state->StartTime) * 1000); - state->Disconnected = true; - return -1; - - case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS: - case LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS: - { - FSslModule::Get().GetCertificateManager().AddCertificatesToSslContext(static_cast(UserData)); - return 0; - } - case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION: - { - Data = UserData; - break; - } - - default: - break; - } - - return lws_callback_http_dummy(Connection, Reason, UserData, Data, Length); -} - -int CollectRegionPing(const FDiscoveredPingEndpoint& Endpoint) -{ - const lws_protocols protocols[] = - { - { - "", - CollectRegionPingsCallback, - 0, - 0, - }, - { NULL, NULL, 0, 0 } - }; - - struct lws_context_creation_info info; - memset(&info, 0, sizeof(info)); - info.options = LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT; - info.port = CONTEXT_PORT_NO_LISTEN; - info.protocols = protocols; - - lws_context* context = lws_create_context(&info); - if (!context) - { - UE_LOG(LogHathoraSDK, Error, TEXT("lws init failed")); - return -1; - } - - CollectPingState state; - memset(&state, 0, sizeof(state)); - - std::string host(TCHAR_TO_UTF8(*Endpoint.Host)); - lws* connection = CollectRegionPingConnect(context, host, Endpoint.Port, &state); - - while (!state.Disconnected) - { - lws_service(context, 1000); - } - lws_context_destroy(context); - - UE_LOG(LogHathoraSDK, Log, TEXT("Ping to %s (%s:%d) took: %d ms"), *Endpoint.Region, *Endpoint.Host, Endpoint.Port, state.PingTimeMs); - - return state.PingTimeMs; -} - -void FHathoraSDKModule::GetRegionalPings(const FOnGetRegionalPingsDelegate& OnComplete) -{ - TSharedRef< IHttpRequest, ESPMode::ThreadSafe > request = NewRequest(); - request->SetURL(FString::Format(TEXT("{0}/discovery/v1/ping"), { BaseUrl })); - request->OnProcessRequestComplete().BindLambda([OnComplete](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess) { - TArray endpoints; - if (bSuccess && Response.IsValid() && FJsonObjectConverter::JsonArrayStringToUStruct(Response->GetContentAsString(), &endpoints)) - { - TSharedPtr> pings = MakeShared>(); - TSharedPtr pendingPings = MakeShared(endpoints.Num()); - - if (endpoints.Num() > 0) - { - for (const FDiscoveredPingEndpoint& endpoint : endpoints) - { - // Collect the ping on a worker thread - AsyncTask(ENamedThreads::AnyBackgroundThreadNormalTask, [endpoint, pings, pendingPings, OnComplete]() - { - int32 ping = CollectRegionPing(endpoint); - - // Post the results back to the main thread - AsyncTask(ENamedThreads::GameThread, [ping, endpoint, pings, pendingPings, OnComplete]() - { - if (ping > 0) - { - pings->Add(endpoint.Region, ping); - } - - *pendingPings -= 1; - if (*pendingPings <= 0) - { - OnComplete.ExecuteIfBound(*pings); - } - }); - }); - } - } - else - { - UE_LOG(LogHathoraSDK, Warning, TEXT("/discovery/v1/ping returned an empty list of regions")); - TMap emptyResult; - OnComplete.ExecuteIfBound(emptyResult); - } - } - else - { - UE_LOG(LogHathoraSDK, Warning, TEXT("Could not retrieve ping endpoints")); - TMap emptyResult; - OnComplete.ExecuteIfBound(emptyResult); - } - }); - request->ProcessRequest(); -} - -// -// GetConnectionInfo -// - -void FHathoraSDKModule::GetConnectionInfo(const FString& AppId, const FString& RoomId, const FConnectionInfoDelegate& OnComplete) -{ - GetConnectionInfo_Internal(AppId, RoomId, FPlatformTime::Seconds(), 0, OnComplete); -} - -void FHathoraSDKModule::GetConnectionInfo_Internal(const FString& AppId, const FString& RoomId, double StartTime, int32 RetryCount, const FConnectionInfoDelegate& OnComplete) -{ - TSharedRef< IHttpRequest, ESPMode::ThreadSafe > request = NewRequest(); - request->SetURL(FString::Format(TEXT("{0}/rooms/v2/{1}/connectioninfo/{2}"), { BaseUrl, AppId, RoomId })); - - request->OnProcessRequestComplete().BindLambda([this, AppId, RoomId, StartTime, RetryCount, OnComplete](FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess) { - if (!bSuccess || !Response.IsValid()) - { - UE_LOG(LogHathoraSDK, Warning, TEXT("rooms/v2/connectioninfo failed to connect")); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - if (Response->GetResponseCode() != 200) - { - UE_LOG(LogHathoraSDK, Warning, - TEXT("rooms/v2/connectioninfo failed AppId=\"%s\" RoomId=\"%s\" ResponseCode=%d Response=\"%s\""), - *AppId, *RoomId, Response->GetResponseCode(), *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - TSharedPtr jsonResponseBody; - TSharedRef> JsonReader = TJsonReaderFactory::Create(Response->GetContentAsString()); - if (!FJsonSerializer::Deserialize(JsonReader, jsonResponseBody)) - { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo failed to deserialize json AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - FString status; - if (!jsonResponseBody->TryGetStringField("status", status)) - { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo response missing status AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - if (status == TEXT("starting")) { - if ((FPlatformTime::Seconds() - StartTime) <= GetConnectionInfoTimeoutSeconds) - { - // Retry - double delay = 1.0f; - if (GetConnectionInfoDelaysSeconds.Num() > 0) - { - delay = FCString::Atof(*GetConnectionInfoDelaysSeconds[FGenericPlatformMath::Min(RetryCount, GetConnectionInfoDelaysSeconds.Num() - 1)]); - } - - auto retry = [this, AppId, RoomId, StartTime, RetryCount, OnComplete]() { - GetConnectionInfo_Internal(AppId, RoomId, StartTime, RetryCount + 1, OnComplete); - }; - - FTimerHandle unusedHandle; - GWorld->GetTimerManager().SetTimer(unusedHandle, retry, delay, false); - return; - } - else - { - // Timeout - UE_LOG(LogHathoraSDK, Warning, TEXT("rooms/v2/connectioninfo timed out waiting for room to be active AppId=\"%s\" RoomId=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - } - else if (status != TEXT("active")) { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo response has invalid status AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - const TSharedPtr* jsonExposedPort; - if (!jsonResponseBody->TryGetObjectField("exposedPort", jsonExposedPort)) - { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo response missing exposedPort AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - FString host; - if (!jsonExposedPort->Get()->TryGetStringField("host", host)) - { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo response missing host AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - int32 port = 0; - if (!jsonExposedPort->Get()->TryGetNumberField("port", port)) - { - UE_LOG(LogHathoraSDK, Error, TEXT("rooms/v2/connectioninfo response missing port AppId=\"%s\" RoomId=\"%s\" Response=\"%s\""), - *AppId, *RoomId, *Response->GetContentAsString()); - OnComplete.ExecuteIfBound(false, FString(), 0); - return; - } - - // Success - OnComplete.ExecuteIfBound(true, host, port); - }); - - request->ProcessRequest(); + // 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 diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSdkConfig.cpp b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSdkConfig.cpp new file mode 100644 index 0000000..440a423 --- /dev/null +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Private/HathoraSdkConfig.cpp @@ -0,0 +1,8 @@ +// Copyright 2023 Hathora, Inc. + + +#include "HathoraSdkConfig.h" + +UHathoraSdkConfig::UHathoraSdkConfig() { + BaseUrl = "https://api.hathora.dev"; +} \ No newline at end of file diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraPing.h b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraPing.h new file mode 100644 index 0000000..c1472e0 --- /dev/null +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraPing.h @@ -0,0 +1,30 @@ +// Copyright 2023 Hathora, Inc. +#pragma once + +#include "CoreMinimal.h" +#include "HathoraSdkConfig.h" +#include "Delegates/Delegate.h" +#include "HathoraSDK/Private/DiscoveredPingEndpoint.h" +#include "HathoraPing.generated.h" + +UCLASS() +class HATHORASDK_API UHathoraPing : public UObject +{ + GENERATED_BODY() +public: + UHathoraPing(const FObjectInitializer& ObjectInitializer); + typedef TDelegate&)> FOnGetRegionalPingsDelegate; + + void SetConfig(UHathoraSdkConfig* NewConfig) { HathoraSdkConfig = NewConfig; } + + // Get ping times to all available Hathora Cloud regions + // pings are returned in milliseconds + void GetRegionalPings(const FOnGetRegionalPingsDelegate& OnComplete); + +private: + UPROPERTY() + UHathoraSdkConfig* HathoraSdkConfig; + DECLARE_DELEGATE_TwoParams(FOnGetPingDelegate, int32 /* Ping */, bool /* bWasSuccessful */); + static void PingUrlsAndAggregateTimes(const TArray& PingEndpoints, const FOnGetRegionalPingsDelegate& OnComplete); + static void GetPingTime(const FDiscoveredPingEndpoint& PingEndpoint, const FOnGetPingDelegate& OnComplete); +}; diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSDK.h b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSDK.h index 50d78b1..8b9816f 100644 --- a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSDK.h +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSDK.h @@ -4,67 +4,12 @@ #include "CoreMinimal.h" #include "Modules/ModuleManager.h" -#include "HttpRetrySystem.h" DECLARE_LOG_CATEGORY_EXTERN(LogHathoraSDK, Log, All) - class FHathoraSDKModule : public IModuleInterface { -public: +public: + /** IModuleInterface implementation */ virtual void StartupModule() override; virtual void ShutdownModule() override; - - static FHathoraSDKModule* Get() - { - static FName hathoraSdk(TEXT("HathoraSDK")); - return &FModuleManager::LoadModuleChecked< FHathoraSDKModule >(hathoraSdk); - } - - /// - /// Provide an implementation of the Http Retry System Manager so underlying rest calls can be retried - /// on errors that are possibly temporary - /// - HATHORASDK_API void SetRetryManager(TSharedPtr& httpRetryManager); - - /// - /// Asyncronous call to get a map of current Hathora regions to client pings - /// - typedef TDelegate& /* PingMap */)> FOnGetRegionalPingsDelegate; - HATHORASDK_API void GetRegionalPings(const FOnGetRegionalPingsDelegate& OnComplete); - - /// - /// Get the hostname + port given an AppId and RoomId - /// This allows matchmaking services to return a result while the room is starting and not yet active - /// - DECLARE_DELEGATE_ThreeParams(FConnectionInfoDelegate, bool /* Success */, FString /* Host */, int32 /* Port */); - HATHORASDK_API void GetConnectionInfo(const FString& AppId, const FString& RoomId, const FConnectionInfoDelegate& OnComplete); - -protected: - - // - // Configuration stored in Game.ini under [HathoraSDK] - // - - /// - /// Base URL to use for Hathora API calls (https://api.hathora.dev) - /// - FString BaseUrl; - - /// - /// Total timeout to get the connection info for a room (that may have been starting) - /// - float GetConnectionInfoTimeoutSeconds; - - /// - /// Escalating sequence of retry delays for a room that's still starting - /// The last value is re-used for a long startup - /// - TArray GetConnectionInfoDelaysSeconds; - -protected: - - TSharedRef NewRequest(); - void GetConnectionInfo_Internal(const FString& AppId, const FString& RoomId, double StartTime, int32 RetryCount, const FConnectionInfoDelegate& OnComplete); - - TSharedPtr HttpRetryManager; }; diff --git a/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSdkConfig.h b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSdkConfig.h new file mode 100644 index 0000000..bf57f89 --- /dev/null +++ b/SDKDemo/Plugins/HathoraSDK/Source/HathoraSDK/Public/HathoraSdkConfig.h @@ -0,0 +1,20 @@ +// Copyright 2023 Hathora, Inc. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "HathoraSdkConfig.generated.h" + +UCLASS(Config = Game) +class HATHORASDK_API UHathoraSdkConfig : public UObject +{ + GENERATED_BODY() + +public: + UHathoraSdkConfig(); + const FString& GetBaseUrl() const { return BaseUrl; }; + +private: + FString BaseUrl; +}; diff --git a/SDKDemo/Source/SDKDemo/SDemoMenuWidget.cpp b/SDKDemo/Source/SDKDemo/SDemoMenuWidget.cpp index 47ea296..5d09cde 100644 --- a/SDKDemo/Source/SDKDemo/SDemoMenuWidget.cpp +++ b/SDKDemo/Source/SDKDemo/SDemoMenuWidget.cpp @@ -3,7 +3,7 @@ #include "SDemoMenuWidget.h" -#include "HathoraSDK.h" +#include "HathoraPing.h" #define LOCTEXT_NAMESPACE "Menu" void SDemoMenuWidget::Construct(const FArguments& InArgs) @@ -39,18 +39,16 @@ void SDemoMenuWidget::Construct(const FArguments& InArgs) FReply SDemoMenuWidget::OnClicked() const { - if (auto hathoraSdk = FHathoraSDKModule::Get()) - { - const FHathoraSDKModule::FOnGetRegionalPingsDelegate OnComplete = FHathoraSDKModule::FOnGetRegionalPingsDelegate::CreateWeakLambda(this, [this](TMap Results) { - for (const auto& Pair : Results) - { - const FString& Region = Pair.Key; - UE_LOG(LogTemp, Display, TEXT("Ping in %s is %d ms"), *Region, Pair.Value); - } - }); - hathoraSdk->GetRegionalPings(OnComplete); - } - + const UHathoraPing::FOnGetRegionalPingsDelegate OnComplete = UHathoraPing::FOnGetRegionalPingsDelegate::CreateLambda([]( + TMap Results) { + for (const auto& Pair : Results) + { + const FString& Region = Pair.Key; + UE_LOG(LogTemp, Display, TEXT("Ping in %s is %d ms"), *Region, Pair.Value); + } + }); + UHathoraPing* HathoraPing = NewObject(); + HathoraPing->GetRegionalPings(OnComplete); return FReply::Handled(); } #undef LOCTEXT_NAMESPACE