diff --git a/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp b/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp index a5d7a87..7fd05d4 100644 --- a/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp +++ b/Source/DTFluxRemote/Private/DTFluxRemoteSubsystem.cpp @@ -11,6 +11,7 @@ #include "IHttpRouter.h" #include "Rundown/AvaRundown.h" #include "Json.h" +#include "JsonObjectConverter.h" #include "Engine/Engine.h" #include "Misc/DateTime.h" @@ -129,6 +130,16 @@ bool UDTFluxRemoteSubsystem::IsHTTPServerRunning() const return bServerRunning; } +void UDTFluxRemoteSubsystem::ResetPendingTitleData() +{ + bHasPendingTitleRequest = false; +} + +void UDTFluxRemoteSubsystem::ResetPendingBibData() +{ + bHasPendingTitleBibRequest = false; +} + void UDTFluxRemoteSubsystem::SetupRoutes() { if (!HttpRouter.IsValid()) @@ -184,9 +195,19 @@ bool UDTFluxRemoteSubsystem::HandleTitleRequest(const FHttpServerRequest& Reques AsyncTask(ENamedThreads::GameThread, [this, TitleData]() { OnTitleReceived.Broadcast(TitleData); + if (RemotedRundown && RemotedRundown->IsValidLowLevel()) + { + PendingTitleData = TitleData; + bHasPendingTitleRequest = true; + UE_LOG(logDTFluxRemote, Log, TEXT("Playing page %i"), TitleData.RundownPageId); + RemotedRundown->PlayPage(TitleData.RundownPageId, EAvaRundownPagePlayType::PlayFromStart); + } + else + { + UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded")); + } }); - // Send success response OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Title data received")), TEXT("application/json"))); return true; } @@ -239,9 +260,18 @@ bool UDTFluxRemoteSubsystem::HandleCommandsRequest(const FHttpServerRequest& Req AsyncTask(ENamedThreads::GameThread, [this, CommandData]() { OnCommandReceived.Broadcast(CommandData); + if (RemotedRundown && RemotedRundown->IsValidLowLevel()) + { + RemotedRundown->StopPage(CommandData.RundownPageId, EAvaRundownPageStopOptions::None, false); + UE_LOG(logDTFluxRemote, Log, TEXT("Stoping page %i"), CommandData.RundownPageId); + } + else + { + UE_LOG(logDTFluxRemote, Warning, TEXT("No rundown loaded")); + } }); - OnComplete(FHttpServerResponse::Create(CreateSuccessResponse(TEXT("Command data received")), TEXT("application/json"))); + OnComplete(FHttpServerResponse::Create(CreateErrorResponse(TEXT("OK")), TEXT("application/json"))); return true; } @@ -249,7 +279,17 @@ TSharedPtr UDTFluxRemoteSubsystem::ParseJsonFromRequest(const FHttp { // Get request body TArray Body = Request.Body; - FString JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast(Body.GetData()))); + FString JsonString; + if (Body.Num() > 0) + { + // Ajouter un null terminator si nécessaire + if (Body.Last() != 0) + { + Body.Add(0); + } + + JsonString = FString(UTF8_TO_TCHAR(reinterpret_cast(Body.GetData()))); + } UE_LOG(logDTFluxRemote, Verbose, TEXT("Received JSON: %s"), *JsonString); @@ -299,53 +339,65 @@ bool UDTFluxRemoteSubsystem::ParseTitleData(const TSharedPtr& JsonO { if (!JsonObject.IsValid()) { + UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteTitleData")); 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; + if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) + { + UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteTitleData")); + return true; + } + + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteTitleData")); + return false; } bool UDTFluxRemoteSubsystem::ParseTitleBibData(const TSharedPtr& JsonObject, FDTFluxRemoteBibData& OutData) { if (!JsonObject.IsValid()) { + UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteBibData")); return false; } - JsonObject->TryGetNumberField(TEXT("Bib"), OutData.Bib); - - - - UE_LOG(logDTFluxRemote, Log, TEXT("Parsed Title-Bib Data - Bib: %i"), OutData.Bib); + if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) + { + UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteBibData")); + return true; + } - return true; + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteBibData")); + return false; } bool UDTFluxRemoteSubsystem::ParseCommandData(const TSharedPtr& JsonObject, FDTFluxRemoteCommandData& OutData) { if (!JsonObject.IsValid()) { + UE_LOG(logDTFluxRemote, Error, TEXT("Invalid JSON object for %s"), TEXT("FDTFluxRemoteCommandData")); 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); + if (FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutData)) + { + UE_LOG(logDTFluxRemote, Log, TEXT("Successfully parsed %s"), TEXT("FDTFluxRemoteCommandData")); + return true; + } - return true; + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to convert JSON to %s struct"),TEXT("FDTFluxRemoteCommandData")); + return false; } void UDTFluxRemoteSubsystem::UnloadCurrentRundown() { + if (RemotedRundown) + { + UE_LOG(logDTFluxRemote, Log, TEXT("Unloading current rundown")); + // Ici vous pouvez ajouter une logique de nettoyage si nécessaire + // Par exemple : RemotedRundown->StopAllPages(); + RemotedRundown = nullptr; + } } void UDTFluxRemoteSubsystem::LoadRundownFromSettings() @@ -366,7 +418,6 @@ void UDTFluxRemoteSubsystem::LoadRundownFromSettings() return; } - // Si c'est le même rundown, pas besoin de recharger if (RemotedRundown && RemotedRundown == RundownAsset.LoadSynchronous()) { UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString()); @@ -385,6 +436,47 @@ void UDTFluxRemoteSubsystem::LoadRundownFromSettings() { UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown from settings: %s"), *RundownAsset.ToString()); } + LoadRundown(RundownAsset); +} + +bool UDTFluxRemoteSubsystem::LoadRundown(const TSoftObjectPtr& RundownAsset) +{ + if (RundownAsset.IsNull()) + { + UE_LOG(logDTFluxRemote, Warning, TEXT("Cannot load rundown: asset is null")); + UnloadCurrentRundown(); + return false; + } + + // Charger le rundown de manière synchrone + UAvaRundown* LoadedRundown = RundownAsset.LoadSynchronous(); + + // Vérifier si le rundown est déjà chargé + if (RemotedRundown && RemotedRundown == LoadedRundown) + { + UE_LOG(logDTFluxRemote, Log, TEXT("Rundown already loaded: %s"), *RundownAsset.ToString()); + return true; + } + + // Décharger l'ancien rundown d'abord + UnloadCurrentRundown(); + + // Assigner le nouveau rundown + RemotedRundown = LoadedRundown; + + // Vérifier que le chargement a réussi + if (RemotedRundown && RemotedRundown->IsValidLowLevel()) + { + RemotedRundown->InitializePlaybackContext(); + UE_LOG(logDTFluxRemote, Log, TEXT("Successfully loaded rundown: %s"), *RundownAsset.ToString()); + return true; + } + else + { + UE_LOG(logDTFluxRemote, Error, TEXT("Failed to load rundown: %s"), *RundownAsset.ToString()); + RemotedRundown = nullptr; + return false; + } } #if WITH_EDITOR diff --git a/Source/DTFluxRemote/Private/DTFluxRemotedLevelController.cpp b/Source/DTFluxRemote/Private/DTFluxRemotedLevelController.cpp new file mode 100644 index 0000000..dc42a0c --- /dev/null +++ b/Source/DTFluxRemote/Private/DTFluxRemotedLevelController.cpp @@ -0,0 +1,220 @@ +// DTFluxRemoteActor.cpp +#include "DTFluxRemotedLevelController.h" +#include "DTFluxRemoteSubsystem.h" +#include "Engine/Engine.h" +#include "Engine/World.h" + +ADTFluxRemotedLevelController::ADTFluxRemotedLevelController() +{ + PrimaryActorTick.bCanEverTick = false; + RemoteSubsystem = nullptr; + bEventsBound = false; + + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Constructor called")); +} + +void ADTFluxRemotedLevelController::PostInitializeComponents() +{ + Super::PostInitializeComponents(); + + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: PostInitializeComponents called")); + + // Essayer de bind dès que possible + InitializeSubsystemBinding(); +} + +void ADTFluxRemotedLevelController::BeginPlay() +{ + Super::BeginPlay(); + + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: BeginPlay called")); + + // S'assurer que le binding est fait (au cas où PostInitializeComponents aurait échoué) + if (!bEventsBound) + { + InitializeSubsystemBinding(); + } +} + +void ADTFluxRemotedLevelController::InitializeSubsystemBinding() +{ + // Éviter le double binding + if (bEventsBound) + { + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Events already bound, skipping")); + return; + } + + // Récupérer le subsystem + if (UWorld* World = GetWorld()) + { + RemoteSubsystem = GEngine->GetEngineSubsystem(); + + if (RemoteSubsystem) + { + // Bind les events du subsystem + RemoteSubsystem->OnTitleReceived.AddDynamic( + this, &ADTFluxRemotedLevelController::OnTitleDataReceived + ); + RemoteSubsystem->OnTitleBibReceived.AddDynamic( + this, &ADTFluxRemotedLevelController::OnTitleBibDataReceived + ); + + RemoteSubsystem->OnCommandReceived.AddDynamic( + this, &ADTFluxRemotedLevelController::OnCommandDataReceived + ); + + bEventsBound = true; + + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Successfully bound to subsystem events")); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("DTFluxRemoteActor: DTFluxRemoteSubsystem not available yet")); + } + } + else + { + UE_LOG(LogTemp, Warning, TEXT("DTFluxRemoteActor: World not available yet")); + } +} + +void ADTFluxRemotedLevelController::EnsureSubsystemBinding() +{ + if (!bEventsBound) + { + InitializeSubsystemBinding(); + } +} + +void ADTFluxRemotedLevelController::EndPlay(const EEndPlayReason::Type EndPlayReason) +{ + // Unbind les events pour éviter les fuites mémoire + if (RemoteSubsystem && bEventsBound) + { + if (TitleReceivedHandle.IsValid()) + { + RemoteSubsystem->OnTitleReceived.RemoveDynamic(this, &ADTFluxRemotedLevelController::OnTitleDataReceived); + TitleReceivedHandle.Reset(); + } + + if (TitleBibReceivedHandle.IsValid()) + { + RemoteSubsystem->OnTitleBibReceived.RemoveDynamic(this, &ADTFluxRemotedLevelController::OnTitleBibDataReceived); + TitleBibReceivedHandle.Reset(); + } + + if (CommandReceivedHandle.IsValid()) + { + RemoteSubsystem->OnCommandReceived.RemoveDynamic(this, &ADTFluxRemotedLevelController::OnCommandDataReceived); + CommandReceivedHandle.Reset(); + } + + bEventsBound = false; + + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Unbound from subsystem events")); + } + + Super::EndPlay(EndPlayReason); +} + +void ADTFluxRemotedLevelController::OnTitleDataReceived(const FDTFluxRemoteTitleData& TitleData) +{ + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Received Title Data - %s %s (RundownPageId: %d)"), + *TitleData.FirstName, *TitleData.LastName, TitleData.RundownPageId); + + // Broadcast l'event Blueprint + OnTitleReceived.Broadcast(TitleData); + + // Appeler l'event Blueprint implémentable + BP_OnTitleDataReceived(TitleData); + + // Appeler la fonction virtuelle C++ (peut être overridée dans les classes dérivées) + HandleTitleData(TitleData); +} + +void ADTFluxRemotedLevelController::OnTitleBibDataReceived(const FDTFluxRemoteBibData& BibData) +{ + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Received Title Bib Data - Bib: %d"), BibData.Bib); + + // Broadcast l'event Blueprint + OnTitleBibReceived.Broadcast(BibData); + + // Appeler l'event Blueprint implémentable + BP_OnTitleBibDataReceived(BibData); + + // Appeler la fonction virtuelle C++ (peut être overridée dans les classes dérivées) + HandleTitleBibData(BibData); +} + +void ADTFluxRemotedLevelController::OnCommandDataReceived(const FDTFluxRemoteCommandData& CommandData) +{ + UE_LOG(LogTemp, Log, TEXT("DTFluxRemoteActor: Received Command Data - Type: %s, : RundownPageId %i"), + *CommandData.Type, CommandData.RundownPageId); + + // Broadcast l'event Blueprint + OnCommandReceived.Broadcast(CommandData); + + // Appeler l'event Blueprint implémentable + BP_OnCommandDataReceived(CommandData); + + // Appeler la fonction virtuelle C++ (peut être overridée dans les classes dérivées) + HandleCommandData(CommandData); +} + +bool ADTFluxRemotedLevelController::IsSubsystemAvailable() const +{ + return RemoteSubsystem && RemoteSubsystem->IsValidLowLevel(); +} + +bool ADTFluxRemotedLevelController::IsHTTPServerRunning() const +{ + if (RemoteSubsystem) + { + return RemoteSubsystem->IsHTTPServerRunning(); + } + return false; +} + +void ADTFluxRemotedLevelController::StartHTTPServer(int32 Port) +{ + if (RemoteSubsystem) + { + RemoteSubsystem->StartHTTPServer(Port); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("DTFluxRemoteActor: Cannot start HTTP server - subsystem not available")); + } +} + +void ADTFluxRemotedLevelController::StopHTTPServer() +{ + if (RemoteSubsystem) + { + RemoteSubsystem->StopHTTPServer(); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("DTFluxRemoteActor: Cannot stop HTTP server - subsystem not available")); + } +} + +// Implémentations par défaut des fonctions virtuelles C++ +void ADTFluxRemotedLevelController::HandleTitleData_Implementation(const FDTFluxRemoteTitleData& TitleData) +{ + // Implémentation par défaut - peut être overridée dans les classes dérivées + UE_LOG(LogTemp, Verbose, TEXT("DTFluxRemoteActor: Handling Title Data (default implementation)")); +} + +void ADTFluxRemotedLevelController::HandleTitleBibData_Implementation(const FDTFluxRemoteBibData& BibData) +{ + // Implémentation par défaut - peut être overridée dans les classes dérivées + UE_LOG(LogTemp, Verbose, TEXT("DTFluxRemoteActor: Handling Title Bib Data (default implementation)")); +} + +void ADTFluxRemotedLevelController::HandleCommandData_Implementation(const FDTFluxRemoteCommandData& CommandData) +{ + // Implémentation par défaut - peut être overridée dans les classes dérivées + UE_LOG(LogTemp, Verbose, TEXT("DTFluxRemoteActor: Handling Command Data (default implementation)")); +} \ No newline at end of file diff --git a/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h b/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h index ef3cbe2..c29002f 100644 --- a/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h +++ b/Source/DTFluxRemote/Public/DTFluxRemoteMessage.h @@ -14,6 +14,8 @@ public: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") FDateTime UpdateAt = FDateTime::Now(); + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") + int RundownPageId = -1; FDTFluxRemoteBasicData() = default; FDTFluxRemoteBasicData(const FDateTime& InUpdateAt): UpdateAt(InUpdateAt){}; }; @@ -38,6 +40,7 @@ public: 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){}; @@ -64,12 +67,10 @@ public: FDTFluxRemoteCommandData() = default; UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") - int Type = -1; + FString Type = "Stop"; - UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category="DTFlux|Remote") - FString Data = ""; - FDTFluxRemoteCommandData(int InType, FString InData): - Type(InType), Data(InData){}; + FDTFluxRemoteCommandData(FString InType): + Type(InType){}; }; diff --git a/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h b/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h index decefb2..86d5359 100644 --- a/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h +++ b/Source/DTFluxRemote/Public/DTFluxRemoteSubsystem.h @@ -22,7 +22,6 @@ class DTFLUXREMOTE_API UDTFluxRemoteSubsystem : public UEngineSubsystem { GENERATED_BODY() public: - public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; @@ -57,6 +56,25 @@ public: UFUNCTION(BlueprintCallable, Category = "DTFlux API") bool ProcessCommandData(const FString& JsonString); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DTFlux API") + bool bHasPendingTitleRequest = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DTFlux API") + bool bHasPendingTitleBibRequest = false; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DTFlux API") + FDTFluxRemoteTitleData PendingTitleData; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "DTFlux API") + FDTFluxRemoteBibData PendingTitleBibData; + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + void ResetPendingTitleData(); + + UFUNCTION(BlueprintCallable, Category = "DTFlux API") + void ResetPendingBibData(); + private: void SetupRoutes(); @@ -84,6 +102,7 @@ private: void UnloadCurrentRundown(); void LoadRundownFromSettings(); + bool LoadRundown(const TSoftObjectPtr& RundownAsset); #if WITH_EDITOR FDelegateHandle SettingsRundownChangedHandle; diff --git a/Source/DTFluxRemote/Public/DTFluxRemotedLevelController.h b/Source/DTFluxRemote/Public/DTFluxRemotedLevelController.h new file mode 100644 index 0000000..49deb95 --- /dev/null +++ b/Source/DTFluxRemote/Public/DTFluxRemotedLevelController.h @@ -0,0 +1,94 @@ +// DTFluxRemotedLevelController.h +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "DTFluxRemoteSubsystem.h" +#include "DTFluxRemotedLevelController.generated.h" + +UCLASS(BlueprintType, Blueprintable) +class DTFLUXREMOTE_API ADTFluxRemotedLevelController : public AActor +{ + GENERATED_BODY() + +public: + ADTFluxRemotedLevelController(); + +protected: + virtual void PostInitializeComponents() override; + virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; + + // Subsystem et binding + UPROPERTY(BlueprintReadOnly, Category = "DTFlux") + UDTFluxRemoteSubsystem* RemoteSubsystem; + + FDelegateHandle TitleReceivedHandle; + FDelegateHandle TitleBibReceivedHandle; + FDelegateHandle CommandReceivedHandle; + bool bEventsBound; + + // Fonctions de binding + void InitializeSubsystemBinding(); + + // ✅ CORRECTION : Callbacks avec UFUNCTION() + UFUNCTION() + void OnTitleDataReceived(const FDTFluxRemoteTitleData& TitleData); + + UFUNCTION() + void OnTitleBibDataReceived(const FDTFluxRemoteBibData& BibData); + + UFUNCTION() + void OnCommandDataReceived(const FDTFluxRemoteCommandData& CommandData); + +public: + // Events Blueprint-friendly + UPROPERTY(BlueprintAssignable, Category = "DTFlux Events") + FOnTitleReceived OnTitleReceived; + + UPROPERTY(BlueprintAssignable, Category = "DTFlux Events") + FOnTitleBibReceived OnTitleBibReceived; + + UPROPERTY(BlueprintAssignable, Category = "DTFlux Events") + FOnCommandReceived OnCommandReceived; + + // Fonctions utilitaires + UFUNCTION(BlueprintCallable, Category = "DTFlux") + bool IsSubsystemAvailable() const; + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + bool IsHTTPServerRunning() const; + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + void StartHTTPServer(int32 Port = 63350); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + void StopHTTPServer(); + + UFUNCTION(BlueprintCallable, Category = "DTFlux") + void EnsureSubsystemBinding(); + +protected: + // Events Blueprint implémentables + UFUNCTION(BlueprintImplementableEvent, Category = "DTFlux Events") + void BP_OnTitleDataReceived(const FDTFluxRemoteTitleData& TitleData); + + UFUNCTION(BlueprintImplementableEvent, Category = "DTFlux Events") + void BP_OnTitleBibDataReceived(const FDTFluxRemoteBibData& BibData); + + UFUNCTION(BlueprintImplementableEvent, Category = "DTFlux Events") + void BP_OnCommandDataReceived(const FDTFluxRemoteCommandData& CommandData); + + // Fonctions virtuelles C++ + UFUNCTION(BlueprintNativeEvent, Category = "DTFlux Events") + void HandleTitleData(const FDTFluxRemoteTitleData& TitleData); + virtual void HandleTitleData_Implementation(const FDTFluxRemoteTitleData& TitleData); + + UFUNCTION(BlueprintNativeEvent, Category = "DTFlux Events") + void HandleTitleBibData(const FDTFluxRemoteBibData& BibData); + virtual void HandleTitleBibData_Implementation(const FDTFluxRemoteBibData& BibData); + + UFUNCTION(BlueprintNativeEvent, Category = "DTFlux Events") + void HandleCommandData(const FDTFluxRemoteCommandData& CommandData); + virtual void HandleCommandData_Implementation(const FDTFluxRemoteCommandData& CommandData); +}; \ No newline at end of file