From da89e35eb255d07aa31ada9d5faabdd075868a23 Mon Sep 17 00:00:00 2001 From: Ange-Marie MAURIN Date: Wed, 16 Jul 2025 02:41:08 +0200 Subject: [PATCH] Added Module Remote To add HTTP basic RemoteControl --- DTFluxAPI.uplugin | 5 + .../Private/DTFluxCoreSubsystem.cpp | 28 +- .../Public/DTFluxCoreSubsystem.h | 5 +- Source/DTFluxRemote/DTFluxRemote.Build.cs | 29 ++ .../Private/DTFluxRemoteMessage.cpp | 6 + .../Private/DTFluxRemoteModule.cpp | 19 + .../Private/DTFluxRemoteSubsystem.cpp | 376 ++++++++++++++++++ .../DTFluxRemote/Public/DTFluxRemoteMessage.h | 75 ++++ .../DTFluxRemote/Public/DTFluxRemoteModule.h | 13 + .../Public/DTFluxRemoteSubsystem.h | 82 ++++ .../DTFluxUtilities/DTFluxUtilities.Build.cs | 1 + .../Private/RundownController.cpp | 114 ++++++ .../Public/RundownController.h | 41 ++ 13 files changed, 769 insertions(+), 25 deletions(-) create mode 100644 Source/DTFluxRemote/DTFluxRemote.Build.cs create mode 100644 Source/DTFluxRemote/Private/DTFluxRemoteMessage.cpp create mode 100644 Source/DTFluxRemote/Private/DTFluxRemoteModule.cpp create mode 100644 Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp create mode 100644 Source/DTFluxRemote/Public/DTFluxRemoteMessage.h create mode 100644 Source/DTFluxRemote/Public/DTFluxRemoteModule.h create mode 100644 Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h create mode 100644 Source/DTFluxUtilities/Private/RundownController.cpp create mode 100644 Source/DTFluxUtilities/Public/RundownController.h diff --git a/DTFluxAPI.uplugin b/DTFluxAPI.uplugin index f999a51..ffdb222 100644 --- a/DTFluxAPI.uplugin +++ b/DTFluxAPI.uplugin @@ -49,6 +49,11 @@ "Name": "DTFluxAPIStatus", "Type": "Editor", "LoadingPhase": "Default" + }, + { + "Name": "DTFluxRemote", + "Type": "Editor", + "LoadingPhase": "Default" } ], "Plugins": [ diff --git a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp index c787110..6a9627f 100644 --- a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp +++ b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp @@ -432,6 +432,7 @@ FGuid UDTFluxCoreSubsystem::InitContestRankingsDisplay(const int ContestId) // no need to request ContestRankings; if (IsContestRankingSealed(ContestId)) { + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("ContestRankings already Sealed for ContestId %i"), ContestId); const FGuid DisplayRequestId = FGuid::NewGuid(); OnContestRankingDisplayReady.Broadcast(DisplayRequestId, true); return DisplayRequestId; @@ -548,7 +549,7 @@ FGuid UDTFluxCoreSubsystem::InitSplitRankingsDisplay(const int ContestId, const EDTFluxApiDataType::SplitRanking, ContestId, StageId, SplitId, OnSuccess, OnError, true); return DisplayRequestId; } - UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("DTFluxDatastorage unavailable ...")); + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("DTFluxDataStorage unavailable ...")); OnSplitRankingDisplayReady.Broadcast(FGuid(), false); return FGuid(); } @@ -607,25 +608,7 @@ bool UDTFluxCoreSubsystem::GetSplitRankingForBib(const int ContestId, const int return false; } -bool UDTFluxCoreSubsystem::GetContestRanking(const int ContestId, FDTFluxContestRanking& OutContestRanking) -{ - if (DataStorage) - { - FDTFluxContest Contest; - if (GetContestForId(ContestId, Contest)) - { - for (auto& Ranking : DataStorage->ContestRankings[ContestId].Rankings) - { - OutContestRanking = Ranking; - return true; - } - } - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Unable to find ContestRanking for ContestId %i"), ContestId); - return false; - } - UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("DataStorage not available")); - return false; -} + bool UDTFluxCoreSubsystem::GetContestRankings(const int ContestId, FDTFluxContestRankings& OutContestRankings) @@ -638,8 +621,9 @@ bool UDTFluxCoreSubsystem::GetContestRankings(const int ContestId, if (NetworkSubsystem) { UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Requesting ContestRankings for ContestId %i"), ContestId); - TArray TackedContestIds = {ContestId}; - TrackedRequestContestRankings(TackedContestIds); + TArray TrackedContestIds; + TrackedContestIds.Add(ContestId); + TrackedRequestContestRankings(TrackedContestIds); return false; } UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("NetworkSubsystem unavailable")); diff --git a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h index 66f7d6b..3edca76 100644 --- a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h +++ b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h @@ -121,7 +121,6 @@ public: UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") FGuid InitSplitRankingsDisplay(const int ContestId, const int StageId, const int SplitId); - UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") bool GetStageRankingForBib(const int ContestId, const int StageId, const int Bib, @@ -129,8 +128,8 @@ public: UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") bool GetSplitRankingForBib(const int ContestId, const int StageId, const int SplitId, const int Bib, FDTFluxSplitRanking& OutSplitRankings); - UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - bool GetContestRanking(const int ContestId, FDTFluxContestRanking& OutContestRanking); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") bool GetContestRankings(const int ContestId, FDTFluxContestRankings& OutContestRankings); UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") diff --git a/Source/DTFluxRemote/DTFluxRemote.Build.cs b/Source/DTFluxRemote/DTFluxRemote.Build.cs new file mode 100644 index 0000000..d144e4c --- /dev/null +++ b/Source/DTFluxRemote/DTFluxRemote.Build.cs @@ -0,0 +1,29 @@ +using UnrealBuildTool; + +public class DTFluxRemote : ModuleRules +{ + public DTFluxRemote(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); + + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "HttpServer", + "JsonUtilities", + "Json", + } + ); + } +} \ No newline at end of file diff --git a/Source/DTFluxRemote/Private/DTFluxRemoteMessage.cpp b/Source/DTFluxRemote/Private/DTFluxRemoteMessage.cpp new file mode 100644 index 0000000..97cc83c --- /dev/null +++ b/Source/DTFluxRemote/Private/DTFluxRemoteMessage.cpp @@ -0,0 +1,6 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "DTFluxRemoteMessage.h" + + diff --git a/Source/DTFluxRemote/Private/DTFluxRemoteModule.cpp b/Source/DTFluxRemote/Private/DTFluxRemoteModule.cpp new file mode 100644 index 0000000..09107ed --- /dev/null +++ b/Source/DTFluxRemote/Private/DTFluxRemoteModule.cpp @@ -0,0 +1,19 @@ +#include "DTFluxRemoteModule.h" + +#define LOCTEXT_NAMESPACE "FDTFluxRemoteModule" + +DEFINE_LOG_CATEGORY(logDTFluxRemote); + +void FDTFluxRemoteModule::StartupModule() +{ + +} + +void FDTFluxRemoteModule::ShutdownModule() +{ + +} + +#undef LOCTEXT_NAMESPACE + +IMPLEMENT_MODULE(FDTFluxRemoteModule, DTFluxRemote) \ No newline at end of file diff --git a/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp b/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp new file mode 100644 index 0000000..fe48c70 --- /dev/null +++ b/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp @@ -0,0 +1,376 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "DTFluxRemoteSubsystem.h" +#include "DTFluxRemoteSubsystem.h" +#include "DTFluxRemoteModule.h" +#include "DTFluxRemoteModule.h" +#include "HttpServerModule.h" +#include "IHttpRouter.h" +#include "Json.h" +#include "Engine/Engine.h" +#include "Misc/DateTime.h" + + +void UDTFluxRemoteSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + + UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Initialized")); + + // Auto-start server (optionnel) + StartHTTPServer(63350); +} + +void UDTFluxRemoteSubsystem::Deinitialize() +{ + StopHTTPServer(); + + UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux API Subsystem Deinitialized")); + + Super::Deinitialize(); +} + +bool UDTFluxRemoteSubsystem::StartHTTPServer(int32 Port) +{ + if (bServerRunning) + { + UE_LOG(logDTFluxRemote, Warning, TEXT("HTTP Server already running on port %d"), ServerPort); + return true; + } + + ServerPort = Port; + + // Get HTTP Server Module + FHttpServerModule& HttpServerModule = FHttpServerModule::Get(); + + // Create router + HttpRouter = HttpServerModule.GetHttpRouter(ServerPort); + + if (!HttpRouter.IsValid()) + { + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to create HTTP router for port %d"), ServerPort); + return false; + } + + // Setup routes + SetupRoutes(); + + // Start listening + HttpServerModule.StartAllListeners(); + + bServerRunning = true; + + UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server started on port %d"), ServerPort); + UE_LOG(logDTFluxRemote, Log, TEXT("Base URL: http://localhost:%d/dtflux/api/v1"), ServerPort); + UE_LOG(logDTFluxRemote, Log, TEXT("Available routes:")); + UE_LOG(logDTFluxRemote, Log, TEXT(" POST /dtflux/api/v1/title")); + UE_LOG(logDTFluxRemote, Log, TEXT(" POST /dtflux/api/v1/title-bib")); + UE_LOG(logDTFluxRemote, Log, TEXT(" POST /dtflux/api/v1/commands")); + + return true; +} + +void UDTFluxRemoteSubsystem::StopHTTPServer() +{ + if (!bServerRunning) + { + return; + } + + // Remove route handlers + if (HttpRouter.IsValid()) + { + HttpRouter->UnbindRoute(TitleRouteHandle); + HttpRouter->UnbindRoute(TitleBibRouteHandle); + HttpRouter->UnbindRoute(CommandsRouteHandle); + } + + // Stop server + FHttpServerModule& HttpServerModule = FHttpServerModule::Get(); + HttpServerModule.StopAllListeners(); + + HttpRouter.Reset(); + bServerRunning = false; + + UE_LOG(logDTFluxRemote, Log, TEXT("DTFlux HTTP API Server stopped")); +} + +bool UDTFluxRemoteSubsystem::IsHTTPServerRunning() const +{ + return bServerRunning; +} + +void UDTFluxRemoteSubsystem::SetupRoutes() +{ + if (!HttpRouter.IsValid()) + { + return; + } + + // Route: POST /dtflux/api/v1/title + TitleRouteHandle = HttpRouter->BindRoute( + FHttpPath(TEXT("/dtflux/api/v1/title")), + EHttpServerRequestVerbs::VERB_GET, + FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleRequest) + ); + + // Route: POST /dtflux/api/v1/title-bib + TitleBibRouteHandle = HttpRouter->BindRoute( + FHttpPath(TEXT("/dtflux/api/v1/title-bib")), + EHttpServerRequestVerbs::VERB_GET, + FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleTitleBibRequest) + ); + + // Route: POST /dtflux/api/v1/commands + CommandsRouteHandle = HttpRouter->BindRoute( + FHttpPath(TEXT("/dtflux/api/v1/commands")), + EHttpServerRequestVerbs::VERB_GET, + FHttpRequestHandler::CreateUObject(this, &UDTFluxRemoteSubsystem::HandleCommandsRequest) + ); + + UE_LOG(logDTFluxRemote, Log, TEXT("HTTP routes configured successfully")); +} + +bool UDTFluxRemoteSubsystem::HandleTitleRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) +{ + UE_LOG(logDTFluxRemote, Log, TEXT("Received Title request")); + + // Parse JSON + TSharedPtr JsonObject = ParseJsonFromRequest(Request); + if (!JsonObject.IsValid()) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json"))); + return true; + } + + // Parse title data + FDTFluxRemoteTitleData TitleData; + if (!ParseTitleData(JsonObject, TitleData)) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title data format")), TEXT("application/json"))); + return true; + } + + // Broadcast event (execute on Game Thread) + AsyncTask(ENamedThreads::GameThread, [this, TitleData]() + { + OnTitleReceived.Broadcast(TitleData); + }); + + // Send success response + OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title data received")), TEXT("application/json"))); + return true; +} + +bool UDTFluxRemoteSubsystem::HandleTitleBibRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) +{ + UE_LOG(logDTFluxRemote, Log, TEXT("Received Title-Bib request")); + + TSharedPtr JsonObject = ParseJsonFromRequest(Request); + if (!JsonObject.IsValid()) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json"))); + return true; + } + + FDTFluxRemoteBibData BibData; + if (!ParseTitleBibData(JsonObject, BibData)) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid title-bib data format")), TEXT("application/json"))); + return true; + } + + AsyncTask(ENamedThreads::GameThread, [this, BibData]() + { + OnTitleBibReceived.Broadcast(BibData); + }); + + OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title-bib data received")), TEXT("application/json"))); + return true; +} + +bool UDTFluxRemoteSubsystem::HandleCommandsRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete) +{ + UE_LOG(logDTFluxRemote, Log, TEXT("Received Commands request")); + + TSharedPtr JsonObject = ParseJsonFromRequest(Request); + if (!JsonObject.IsValid()) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid JSON")), TEXT("application/json"))); + return true; + } + + FDTFluxRemoteCommandData CommandData; + if (!ParseCommandData(JsonObject, CommandData)) + { + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("Invalid command data format")), TEXT("application/json"))); + return true; + } + + AsyncTask(ENamedThreads::GameThread, [this, CommandData]() + { + OnCommandReceived.Broadcast(CommandData); + }); + + OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Command data received")), TEXT("application/json"))); + return true; +} + +TSharedPtr UDTFluxRemoteSubsystem::ParseJsonFromRequest(const FHttpServerRequest& Request) +{ + // Get request body + TArray Body = Request.Body; + FString JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast(Body.GetData()))); + + UE_LOG(logDTFluxRemote, Verbose, TEXT("Received JSON: %s"), *JsonString); + + // Parse JSON + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + + if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) + { + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to parse JSON: %s"), *JsonString); + return nullptr; + } + + return JsonObject; +} + +FString UDTFluxRemoteSubsystem::CreateSuccessResponse(const FString& Message) +{ + TSharedPtr ResponseObject = MakeShareable(new FJsonObject); + ResponseObject->SetBoolField(TEXT("success"), true); + ResponseObject->SetStringField(TEXT("message"), Message); + ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601()); + + FString OutputString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer); + + return OutputString; +} + +FString UDTFluxRemoteSubsystem::CreateErrorResponse(const FString& Error, int32 Code) +{ + TSharedPtr ResponseObject = MakeShareable(new FJsonObject); + ResponseObject->SetBoolField(TEXT("success"), false); + ResponseObject->SetStringField(TEXT("error"), Error); + ResponseObject->SetNumberField(TEXT("code"), Code); + ResponseObject->SetStringField(TEXT("timestamp"), FDateTime::Now().ToIso8601()); + + FString OutputString; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&OutputString); + FJsonSerializer::Serialize(ResponseObject.ToSharedRef(), Writer); + + return OutputString; +} + +bool UDTFluxRemoteSubsystem::ParseTitleData(const TSharedPtr& JsonObject, FDTFluxRemoteTitleData& OutData) +{ + if (!JsonObject.IsValid()) + { + return false; + } + + // Parse title fields + JsonObject->TryGetStringField(TEXT("LastName"), OutData.LastName); + JsonObject->TryGetStringField(TEXT("FirsName"), OutData.FirstName); + JsonObject->TryGetStringField(TEXT("Function1"), OutData.Function1); + JsonObject->TryGetStringField(TEXT("Function2"), OutData.Function2); + + UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Title Data - LastName: %s, FirstName: %s"), *OutData.LastName, *OutData.FirstName); + return true; +} + +bool UDTFluxRemoteSubsystem::ParseTitleBibData(const TSharedPtr& JsonObject, FDTFluxRemoteBibData& OutData) +{ + if (!JsonObject.IsValid()) + { + return false; + } + + JsonObject->TryGetNumberField(TEXT("Bib"), OutData.Bib); + + + + UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Title-Bib Data - Bib: %i"), OutData.Bib); + + return true; +} + +bool UDTFluxRemoteSubsystem::ParseCommandData(const TSharedPtr& JsonObject, FDTFluxRemoteCommandData& OutData) +{ + if (!JsonObject.IsValid()) + { + return false; + } + + JsonObject->TryGetNumberField(TEXT("type"), OutData.Type); + JsonObject->TryGetStringField(TEXT("Data"), OutData.Data); + + UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Command Data - Command Type: %i, Data: %s"), + OutData.Type, *OutData.Data); + + return true; +} + +// Manual processing functions for testing +bool UDTFluxRemoteSubsystem::ProcessTitleData(const FString& JsonString) +{ + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + + if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) + { + return false; + } + FDTFluxRemoteTitleData TitleData; + if (ParseTitleData(JsonObject, TitleData)) + { + OnTitleReceived.Broadcast(TitleData); + return true; + } + return false; +} + +bool UDTFluxRemoteSubsystem::ProcessTitleBibData(const FString& JsonString) +{ + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + + if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) + { + return false; + } + + FDTFluxRemoteBibData TitleBibData; + if (ParseTitleBibData(JsonObject, TitleBibData)) + { + OnTitleBibReceived.Broadcast(TitleBibData); + return true; + } + + return false; +} + +bool UDTFluxRemoteSubsystem::ProcessCommandData(const FString& JsonString) +{ + TSharedPtr JsonObject; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + + if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) + { + return false; + } + + FDTFluxRemoteCommandData CommandData; + if (ParseCommandData(JsonObject, CommandData)) + { + OnCommandReceived.Broadcast(CommandData); + return true; + } + + return false; +} \ No newline at end of file diff --git a/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h b/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h new file mode 100644 index 0000000..ef3cbe2 --- /dev/null +++ b/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h @@ -0,0 +1,75 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "DTFluxRemoteMessage.generated.h" + + +USTRUCT(BlueprintType) +struct FDTFluxRemoteBasicData +{ + GENERATED_BODY() +public: + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FDateTime UpdateAt = FDateTime::Now(); + FDTFluxRemoteBasicData() = default; + FDTFluxRemoteBasicData(const FDateTime& InUpdateAt): UpdateAt(InUpdateAt){}; +}; + + + +USTRUCT(BlueprintType) +struct FDTFluxRemoteTitleData : public FDTFluxRemoteBasicData +{ + GENERATED_BODY() +public: + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FString FirstName = ""; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FString LastName = ""; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FString Function1 = ""; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FString Function2 = ""; + + FDTFluxRemoteTitleData() = default; + FDTFluxRemoteTitleData(const FString InFirstName, const FString InLastName, const FString InFunction1, const FString InFunction2): + FirstName(InFirstName), LastName(InLastName), Function1(InFunction1), Function2(InFunction2){}; +}; + + +USTRUCT(BlueprintType) +struct FDTFluxRemoteBibData : public FDTFluxRemoteBasicData +{ + GENERATED_BODY() +public: + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + int Bib = -1; + + FDTFluxRemoteBibData() = default; + FDTFluxRemoteBibData(int InBib): Bib(InBib){}; +}; +USTRUCT(BlueprintType) +struct FDTFluxRemoteCommandData : public FDTFluxRemoteBasicData +{ + GENERATED_BODY() +public: + FDTFluxRemoteCommandData() = default; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + int Type = -1; + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + FString Data = ""; + + FDTFluxRemoteCommandData(int InType, FString InData): + Type(InType), Data(InData){}; +}; + diff --git a/Source/DTFluxRemote/Public/DTFluxRemoteModule.h b/Source/DTFluxRemote/Public/DTFluxRemoteModule.h new file mode 100644 index 0000000..deb638e --- /dev/null +++ b/Source/DTFluxRemote/Public/DTFluxRemoteModule.h @@ -0,0 +1,13 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Modules/ModuleManager.h" + +DTFLUXREMOTE_API DECLARE_LOG_CATEGORY_EXTERN(logDTFluxRemote, Log, All); + +class DTFLUXREMOTE_API FDTFluxRemoteModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; +}; diff --git a/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h b/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h new file mode 100644 index 0000000..6fcfbf8 --- /dev/null +++ b/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h @@ -0,0 +1,82 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/EngineSubsystem.h" +#include "DTFluxRemoteMessage.h" +#include "HttpRouteHandle.h" +#include "IHttpRouter.h" +#include "DTFluxRemoteSubsystem.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTitleReceived, const FDTFluxRemoteTitleData&, TitleData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTitleBibReceived, const FDTFluxRemoteBibData&, TitleBibData); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCommandReceived, const FDTFluxRemoteCommandData&, CommandData); + +/** + * + */ +UCLASS(BlueprintType, Category="DTFlux|Remote") +class DTFLUXREMOTE_API UDTFluxRemoteSubsystem : public UEngineSubsystem +{ + GENERATED_BODY() +public: + public: + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + UPROPERTY(BlueprintAssignable, Category = "DTFlux API") + FOnTitleReceived OnTitleReceived; + + UPROPERTY(BlueprintAssignable, Category = "DTFlux API") + FOnTitleBibReceived OnTitleBibReceived; + + UPROPERTY(BlueprintAssignable, Category = "DTFlux API") + FOnCommandReceived OnCommandReceived; + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + bool StartHTTPServer(int32 Port = 63350); + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + void StopHTTPServer(); + + UFUNCTION(BlueprintCallable, Category = "DTFlux API", BlueprintPure) + bool IsHTTPServerRunning() const; + + UFUNCTION(BlueprintCallable, Category = "DTFlux API", BlueprintPure) + int32 GetServerPort() const { return ServerPort; } + + // Manual data processing (for testing) + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + bool ProcessTitleData(const FString& JsonString); + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + bool ProcessTitleBibData(const FString& JsonString); + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + bool ProcessCommandData(const FString& JsonString); + +private: + void SetupRoutes(); + + bool HandleTitleRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete); + bool HandleTitleBibRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete); + bool HandleCommandsRequest(const FHttpServerRequest& Request, const FHttpResultCallback& OnComplete); + + TSharedPtr ParseJsonFromRequest(const FHttpServerRequest& Request); + FString CreateSuccessResponse(const FString& Message = TEXT("Success")); + FString CreateErrorResponse(const FString& Error, int32 Code = 400); + + bool ParseTitleData(const TSharedPtr& JsonObject, FDTFluxRemoteTitleData& OutData); + bool ParseTitleBibData(const TSharedPtr& JsonObject, FDTFluxRemoteBibData& OutData); + bool ParseCommandData(const TSharedPtr& JsonObject, FDTFluxRemoteCommandData& OutData); + +private: + TSharedPtr HttpRouter; + int32 ServerPort = 63350; + bool bServerRunning = false; + + FHttpRouteHandle TitleRouteHandle; + FHttpRouteHandle TitleBibRouteHandle; + FHttpRouteHandle CommandsRouteHandle; +}; diff --git a/Source/DTFluxUtilities/DTFluxUtilities.Build.cs b/Source/DTFluxUtilities/DTFluxUtilities.Build.cs index 90a32b7..ea005f8 100644 --- a/Source/DTFluxUtilities/DTFluxUtilities.Build.cs +++ b/Source/DTFluxUtilities/DTFluxUtilities.Build.cs @@ -22,6 +22,7 @@ public class DTFluxUtilities : ModuleRules "SlateCore", "DTFluxCore", "DTFluxCoreSubsystem", + "AvalancheMedia", } ); } diff --git a/Source/DTFluxUtilities/Private/RundownController.cpp b/Source/DTFluxUtilities/Private/RundownController.cpp new file mode 100644 index 0000000..b3fe4df --- /dev/null +++ b/Source/DTFluxUtilities/Private/RundownController.cpp @@ -0,0 +1,114 @@ +// =============================================== +// 3. SOURCE FILE (.CPP) - TOUS LES INCLUDES +// =============================================== + +// YourRundownController.cpp +#include "RundownController.h" + +#include "DTFluxUtilitiesModule.h" +#include "Rundown/AvaRundown.h" +#include "Rundown/AvaRundownPage.h" +#include "Engine/Engine.h" +#include "Engine/World.h" +#include "UObject/UObjectGlobals.h" +#include "UObject/ConstructorHelpers.h" +#include "TimerManager.h" +#include "Logging/LogMacros.h" +#include "Components/StaticMeshComponent.h" +#include "Materials/MaterialInterface.h" +#include "Blueprint/UserWidget.h" + +DEFINE_LOG_CATEGORY_STATIC(LogYourRundownController, Log, All); + +// =============================================== +// 4. IMPLÉMENTATION SIMPLE +// =============================================== + +ARundownController::ARundownController() +{ + PrimaryActorTick.bCanEverTick = false; +} + +bool ARundownController::LoadRundown(const TSoftObjectPtr RundownAsset) +{ + if (RundownAsset.IsNull()) + { + UE_LOG(logDTFluxUtilities, Error, TEXT("RundownAsset Null")); + return false; + } + + // Charger l'asset rundown + CurrentRundown = RundownAsset.LoadSynchronous(); + + if (!CurrentRundown) + { + UE_LOG(LogYourRundownController, Error, TEXT("Failed to load rundown: %s"), *RundownAsset.ToString()); + return false; + } + + // Initialiser le contexte de playback + CurrentRundown->InitializePlaybackContext(); + + UE_LOG(LogYourRundownController, Log, TEXT("Successfully loaded rundown: %s"), *RundownAsset.ToString()); + return true; +} + +bool ARundownController::PlayPage(int32 PageId) +{ + if (!CurrentRundown) + { + UE_LOG(LogYourRundownController, Error, TEXT("No rundown loaded. Call LoadRundown first.")); + return false; + } + + // Vérifier que la page existe + const FAvaRundownPage& Page = CurrentRundown->GetPage(PageId); + if (!Page.IsValidPage()) + { + UE_LOG(LogYourRundownController, Error, TEXT("Invalid page ID: %d"), PageId); + return false; + } + + // Jouer la page + bool bSuccess = CurrentRundown->PlayPage(PageId, EAvaRundownPagePlayType::PlayFromStart); + + if (bSuccess) + { + CurrentPageId = PageId; + UE_LOG(LogYourRundownController, Log, TEXT("Playing page %d: %s"), PageId, *Page.GetPageName()); + } + else + { + UE_LOG(LogYourRundownController, Warning, TEXT("Failed to play page %d"), PageId); + } + + return bSuccess; +} + +bool ARundownController::StopPage(int32 PageId) +{ + if (!CurrentRundown) + { + UE_LOG(LogYourRundownController, Error, TEXT("No rundown loaded")); + return false; + } + + bool bSuccess = CurrentRundown->StopPage(PageId, EAvaRundownPageStopOptions::Default, false); + + if (bSuccess) + { + UE_LOG(LogYourRundownController, Log, TEXT("Stopped page %d"), PageId); + } + + return bSuccess; +} + +FString ARundownController::ListePages() +{ + if (!CurrentRundown) + { + UE_LOG(logDTFluxUtilities, Error, TEXT("No rundown loaded")); + return FString(); + } + return FString(); +} diff --git a/Source/DTFluxUtilities/Public/RundownController.h b/Source/DTFluxUtilities/Public/RundownController.h new file mode 100644 index 0000000..41d1935 --- /dev/null +++ b/Source/DTFluxUtilities/Public/RundownController.h @@ -0,0 +1,41 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "Rundown/AvaRundown.h" +#include "UObject/SoftObjectPath.h" +#include "UObject/ObjectMacros.h" +#include "RundownController.generated.h" + + + + +UCLASS(BlueprintType, Blueprintable) +class DTFLUXUTILITIES_API ARundownController : public AActor +{ + GENERATED_BODY() + +public: + ARundownController(); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + bool LoadRundown(const TSoftObjectPtr RundownAsset); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + bool PlayPage(int32 PageId); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + bool StopPage(int32 PageId); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + FString ListePages(); + +protected: + UPROPERTY(BlueprintReadOnly, Category = "DTFlux") + TObjectPtr CurrentRundown; + + UPROPERTY(BlueprintReadWrite, Category = "DTFlux") + int32 CurrentPageId = 1; +}; \ No newline at end of file