diff --git a/Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp b/Source/DTFluxAPIStatus/Private/widgets/DTFluxStatusWidget.cpp similarity index 100% rename from Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp rename to Source/DTFluxAPIStatus/Private/widgets/DTFluxStatusWidget.cpp diff --git a/Source/DTFluxAPIStatus/Private/styles/DTFluxStatusStyle.cpp b/Source/DTFluxAPIStatus/Private/widgets/styles/DTFluxStatusStyle.cpp similarity index 87% rename from Source/DTFluxAPIStatus/Private/styles/DTFluxStatusStyle.cpp rename to Source/DTFluxAPIStatus/Private/widgets/styles/DTFluxStatusStyle.cpp index 7865908..96b351f 100644 --- a/Source/DTFluxAPIStatus/Private/styles/DTFluxStatusStyle.cpp +++ b/Source/DTFluxAPIStatus/Private/widgets/styles/DTFluxStatusStyle.cpp @@ -13,7 +13,7 @@ TSharedPtr FDTFluxStatusStyle::StyleSet = nullptr; void FDTFluxStatusStyle::RegisterStyle() { - if(StyleSet.IsValid()) return; + if (StyleSet.IsValid()) return; StyleSet = Create(); FSlateStyleRegistry::RegisterSlateStyle(*StyleSet); @@ -21,14 +21,12 @@ void FDTFluxStatusStyle::RegisterStyle() void FDTFluxStatusStyle::UnregisterStyle() { - if(StyleSet.IsValid()) + if (StyleSet.IsValid()) { FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet); ensure(StyleSet.IsUnique()); StyleSet.Reset(); } - - } void FDTFluxStatusStyle::ReloadTextures() @@ -38,9 +36,8 @@ void FDTFluxStatusStyle::ReloadTextures() TSharedPtr FDTFluxStatusStyle::Create() { TSharedPtr Style = MakeShareable(new FSlateStyleSet("DTFluxAPIStatusStyle")); - Style->SetContentRoot(IPluginManager::Get().FindPlugin("DTFluxAPI")->GetBaseDir()/TEXT("Resources")); + Style->SetContentRoot(IPluginManager::Get().FindPlugin("DTFluxAPI")->GetBaseDir() / TEXT("Resources")); - Style->Set("LevelEditor.Tab.Icon", new IMAGE_BRUSH_SVG("DTFluxServerStatusWhite", FVector2d(16)) ); + Style->Set("LevelEditor.Tab.Icon", new IMAGE_BRUSH_SVG("DTFluxServerStatusWhite", FVector2d(16))); return Style; } - diff --git a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp index 28b101c..44e5942 100644 --- a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp +++ b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp @@ -84,9 +84,6 @@ void UDTFluxCoreSubsystem::RegisterDelegates() &UDTFluxCoreSubsystem::ProcessSplitRanking ); - // ⚠️ ATTENTION : Vous avez un doublon ici ! - // NetworkSubsystem->OnReceivedTeamUpdate().BindUFunction(this, "ProcessTeamList"); - NetworkSubsystem->OnReceivedTeamStatusUpdate().BindUObject( this, &UDTFluxCoreSubsystem::ProcessTeamStatusUpdate @@ -205,6 +202,173 @@ void UDTFluxCoreSubsystem::SendRequest(const FString& Message) } } +bool UDTFluxCoreSubsystem::GetContestRankings(const int ContestId, + FDTFluxContestRankings& OutContestRankings) +{ + if (DataStorage->ContestRankings.Contains(ContestId)) + { + OutContestRankings = DataStorage->ContestRankings[ContestId]; + return true; + } + if (NetworkSubsystem) + { + TArray TackedContestIds = {ContestId}; + TrackedRequestContestRankings(TackedContestIds); + return false; + } + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("NetworkSubsystem unavailable")); + return false; +} + +bool UDTFluxCoreSubsystem::GetStageRankings(const int ContestId, const int StageId, + FDTFluxStageRankings& OutStageRankings) +{ + return GetStageRankingsWithKey(FDTFluxStageKey(ContestId, StageId), OutStageRankings); +} + +bool UDTFluxCoreSubsystem::GetSplitRankings(const int ContestId, const int StageId, const int SplitId, + FDTFluxSplitRankings& OutSplitRankings) +{ + return GetSplitRankingsWithKey(FDTFluxSplitKey(ContestId, StageId, SplitId), OutSplitRankings); +} + +bool UDTFluxCoreSubsystem::GetStageRankingsWithKey(const FDTFluxStageKey StageKey, + FDTFluxStageRankings& OutStageRankings, const bool bShouldUseCached) +{ + //We Have the data + if (DataStorage->StageRankings.Contains(StageKey) && bShouldUseCached) + { + OutStageRankings = DataStorage->StageRankings[StageKey]; + return true; + } + else + { + if (NetworkSubsystem) + { + TArray TackedStageKeys = {StageKey}; + TrackedRequestStageRankings(TackedStageKeys); + OutStageRankings = FDTFluxStageRankings(); + return false; + } + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("NetworkSubsystem unavailable")) + } + return false; +} + +bool UDTFluxCoreSubsystem::GetSplitRankingsWithKey(const FDTFluxSplitKey SplitKey, + FDTFluxSplitRankings& OutSplitRankings, const bool bShouldUseCached) +{ + //We Have the data + if (DataStorage->SplitRankings.Contains(SplitKey) && bShouldUseCached) + { + OutSplitRankings = DataStorage->SplitRankings[SplitKey]; + return true; + } + else + { + if (NetworkSubsystem) + { + TArray TackedSplitKey = {SplitKey}; + TrackedRequestSplitRankings(TackedSplitKey); + OutSplitRankings = FDTFluxSplitRankings(); + return false; + } + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("NetworkSubsystem unavailable")) + return false; + } +} + +TArray UDTFluxCoreSubsystem::TrackedRequestContestRankings(const TArray ForContests) +{ + if (NetworkSubsystem) + { + TArray RequestIds; + FOnDTFluxRequestSuccess OnSuccess = FOnDTFluxRequestSuccess::CreateLambda( + [this](const FDTFluxTrackedRequest& Request) + { + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("ContestRanking Request %s %s Success"), + *Request.RequestId.ToString(), *UEnum::GetValueAsString(Request.RequestType)); + }); + + FOnDTFluxRequestError OnError = FOnDTFluxRequestError::CreateLambda( + [this](const FDTFluxTrackedRequest& InReq, const FString& InError) + { + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("ContestRanking Request [%s] Error %s"), + *InReq.RequestId.ToString(), *InError); + }); + // if Contest is not ended + for (auto ContestId : ForContests) + { + FGuid ContestRequest = NetworkSubsystem->SendTrackedRequestWithCallbacks(EDTFluxApiDataType::ContestRanking, + ContestId, -1, -1, OnSuccess, OnError); + RequestIds.Add(ContestRequest); + } + return RequestIds; + } + return TArray(); +} + +TArray UDTFluxCoreSubsystem::TrackedRequestStageRankings(const TArray ForStages) +{ + if (NetworkSubsystem) + { + TArray RequestIds; + FOnDTFluxRequestSuccess OnSuccess = FOnDTFluxRequestSuccess::CreateLambda( + [this](const FDTFluxTrackedRequest& Request) + { + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Stage Request %s %s Success"), + *Request.RequestId.ToString(), *UEnum::GetValueAsString(Request.RequestType)); + }); + + FOnDTFluxRequestError OnError = FOnDTFluxRequestError::CreateLambda( + [this](const FDTFluxTrackedRequest& InReq, const FString& InError) + { + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("StageRanking Request [%s] Error %s"), + *InReq.RequestId.ToString(), *InError); + }); + // if Contest is not ended + for (auto StageKey : ForStages) + { + FGuid ContestRequest = NetworkSubsystem->SendTrackedRequestWithCallbacks(EDTFluxApiDataType::StageRanking, + StageKey.ContestId, StageKey.StageId, -1, OnSuccess, OnError); + RequestIds.Add(ContestRequest); + } + return RequestIds; + } + return TArray(); +} + +TArray UDTFluxCoreSubsystem::TrackedRequestSplitRankings(const TArray ForSplits) +{ + if (NetworkSubsystem) + { + TArray RequestIds; + FOnDTFluxRequestSuccess OnSuccess = FOnDTFluxRequestSuccess::CreateLambda( + [this](const FDTFluxTrackedRequest& Request) + { + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Stage Request %s %s Success"), + *Request.RequestId.ToString(), *UEnum::GetValueAsString(Request.RequestType)); + }); + + FOnDTFluxRequestError OnError = FOnDTFluxRequestError::CreateLambda( + [this](const FDTFluxTrackedRequest& InReq, const FString& InError) + { + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("StageRanking Request [%s] Error %s"), + *InReq.RequestId.ToString(), *InError); + }); + // if Contest is not ended + for (auto SplitKey : ForSplits) + { + FGuid ContestRequest = NetworkSubsystem->SendTrackedRequestWithCallbacks(EDTFluxApiDataType::SplitRanking, + SplitKey.ContestId, SplitKey.StageId, SplitKey.SplitId, OnSuccess, OnError); + RequestIds.Add(ContestRequest); + } + return RequestIds; + } + return TArray(); +} + + void UDTFluxCoreSubsystem::SendTeamListRequest() { if (NetworkSubsystem) @@ -250,15 +414,16 @@ void UDTFluxCoreSubsystem::RequestAllSplitRankingOfContest(int InContestId, int // TODO Implement this } -FDTFluxStageRankings UDTFluxCoreSubsystem::GetStageRankings(FDTFluxStageKey StageKey) -{ - if (DataStorage->StageRankings.Contains(StageKey)) - { - return DataStorage->StageRankings[StageKey]; - } - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Cannot find StageRankings for key [%s]"), *StageKey.GetDisplayName()); - return FDTFluxStageRankings(); -} +// +// FDTFluxStageRankings UDTFluxCoreSubsystem::GetStageRankings(FDTFluxStageKey StageKey) +// { +// if (DataStorage->StageRankings.Contains(StageKey)) +// { +// return DataStorage->StageRankings[StageKey]; +// } +// UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Cannot find StageRankings for key [%s]"), *StageKey.GetDisplayName()); +// return FDTFluxStageRankings(); +// } void UDTFluxCoreSubsystem::RequestAllSplitRankingOfStage(int InContestId, int InStageId, int InSplitId) { diff --git a/Source/DTFluxCoreSubsystem/Private/DTFluxPursuitManager.cpp b/Source/DTFluxCoreSubsystem/Private/DTFluxPursuitManager.cpp index 00fdd0a..1f2fb0d 100644 --- a/Source/DTFluxCoreSubsystem/Private/DTFluxPursuitManager.cpp +++ b/Source/DTFluxCoreSubsystem/Private/DTFluxPursuitManager.cpp @@ -13,35 +13,6 @@ UDTFluxPursuitManager::UDTFluxPursuitManager(const FObjectInitializer& ObjectIni { } -// TODO : Add way to pass MaxSimultaneousPursuit and MassStartDelay -// For now it's done in UPROPERTIES -void UDTFluxPursuitManager::InitPursuitForContests(const TArray InContests) -{ - if (InitSubSystems()) - { - for (const auto Contest : InContests) - { - FRequestData RequestData; - RequestData.ContestId = Contest.ContestId; - uint8 StageId = Contest.Stages.Last().StageId; - FGuid Guid = NetworkSubsystem->SendTrackedRequestWithCallback(EDTFluxApiDataType::StageRanking, - Contest.ContestId, StageId, -1, - FOnDTFluxTrackedRequestResponse::CreateUObject( - this, - &UDTFluxPursuitManager::OnRequestResponse), - FOnDTFluxTrackedRequestTimeout::CreateUObject( - this, - &UDTFluxPursuitManager::OnRequestTimeoutResponse), - FOnDTFluxRequestResponseError::CreateUObject( - this, - &UDTFluxPursuitManager::OnRequestError)); - - RequestData.RequestIds.Add(Guid); - PendingRequestData.Add(RequestData); - } - } -} - void UDTFluxPursuitManager::InitPursuit(const TArray InContestIds, const int MaxSimultaneousPursuit) { CoreSubsystem = Cast(GetOuter()); @@ -50,14 +21,19 @@ void UDTFluxPursuitManager::InitPursuit(const TArray InContestIds, const in UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("CoreSubsystem is not Available !!!")); return; } - TArray Contests = TArray(); + AllRankings.Reset(); for (const auto& ContestId : InContestIds) { FDTFluxContest Contest; - CoreSubsystem->GetContestForId(ContestId, Contest); - Contests.Add(Contest); - - InitPursuitForContests(Contests); + if (CoreSubsystem->GetContestForId(ContestId, Contest)) + { + BindRankings(); + FDTFluxStageKey StageKey = FDTFluxStageKey(ContestId, Contest.GetLastStageId()); + FDTFluxStageRankings TempStageRankings; + //Obtenir les ranking Frais. + CoreSubsystem->GetStageRankingsWithKey(StageKey, TempStageRankings, false); + PendingStageRanking.Add(StageKey, false); + } } } @@ -127,49 +103,6 @@ void UDTFluxPursuitManager::GetPursuit(TArray& OutPursuitFoc } } -void UDTFluxPursuitManager::OnRequestResponse(const FGuid& RequestId, FDTFluxServerResponse& Response) -{ - UE_LOG(logDTFluxCoreSubsystem, Log, - TEXT("UDTFluxPursuitManager::OnRequestResponse() Received Ranking For Stage %i"), Response.StageID) - UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("Response is %s"), *UEnum::GetValueAsString(Response.GetResponseType())) - //check if request - if (Response.GetResponseType() == EDTFluxApiDataType::StageRanking) - { - FDTFluxStageRankings Rankings; - if (Response.ParseStageRankingResponse(Rankings)) - { - FRequestData FoundData = FRequestData(); - for (auto& PendingReq : PendingRequestData) - { - // Check for a matching PendingReq - if (PendingReq.IsWaitingFor(RequestId, Rankings)) - { - FoundData = PendingReq; - // A request Is Terminated - UE_LOG(logDTFluxCoreSubsystem, Log, - TEXT("UDTFluxPursuitManager::OnRequestResponse() Ranking for Stage %i is complete"), - Response.StageID) - break; - } - } - if (InitPursuitForRequest(FoundData)) - { - OnPursuitSequenceReady.Broadcast(MassStartTime, NextFocusPursuits, NextFocusPursuits, bFocusIsTruncate); - } - } - } -} - -void UDTFluxPursuitManager::OnRequestTimeoutResponse(const FGuid& RequestId, const FString& TimeoutMessage) -{ - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Request Timeout [%s]"), *TimeoutMessage); -} - -void UDTFluxPursuitManager::OnRequestError(const FGuid& RequestId, const FString& ErrorMessage) -{ - UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("Request Error [%s]"), *ErrorMessage); -} - bool UDTFluxPursuitManager::InitSubSystems() { if (NetworkSubsystem) @@ -180,23 +113,64 @@ bool UDTFluxPursuitManager::InitSubSystems() return NetworkSubsystem != nullptr; } -bool UDTFluxPursuitManager::InitPursuitForRequest(FRequestData Data) +bool UDTFluxPursuitManager::BindRankings() +{ + if (CoreSubsystem) + { + if (!bIsRankingBounded) + { + CoreSubsystem->OnRequestedStageRankings.AddDynamic(this, &UDTFluxPursuitManager::OnRankingsReceived); + bIsRankingBounded = true; + } + return bIsRankingBounded; + } + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("CoreSubsystem is not Available !!!")); + return bIsRankingBounded = false; +} + +void UDTFluxPursuitManager::UnbindRankings() +{ + if (CoreSubsystem) + { + if (bIsRankingBounded) + { + CoreSubsystem->OnRequestedStageRankings.RemoveDynamic(this, &UDTFluxPursuitManager::OnRankingsReceived); + bIsRankingBounded = false; + return; + } + } + bIsRankingBounded = false; + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("CoreSubsystem is not Available !!!")); +} + +void UDTFluxPursuitManager::OnRankingsReceived(const FDTFluxStageKey NewStageKey, + const FDTFluxStageRankings NewStageRankings) +{ + if (PendingStageRanking.Contains(NewStageKey)) + { + PendingStageRanking.Remove(NewStageKey); + AllRankings.Add(NewStageRankings); + if (PendingStageRanking.IsEmpty()) + { + //everything is ready to go compute and start + UnbindRankings(); + LaunchPursuitSequence(); + } + } +} + +bool UDTFluxPursuitManager::LaunchPursuitSequence() { - //Clean Data - NextFocusPursuits.Empty(); - NextPursuits.Empty(); GroupedPursuit.Empty(); - TArray AllRankings; TArray AllPursuits; TMap TempGroups; - - + bIsSequenceDone = false; // Full the Array Of Rankings - for (auto& KeyPair : Data.StageRankings) + for (auto& Ranking : AllRankings) { - for (auto StageRanking : KeyPair.Value.Rankings) + for (auto StageRanking : Ranking.Rankings) { - int ContestId = KeyPair.Value.ContestId; + int ContestId = Ranking.ContestId; FDTFluxPursuitInfo PursuitInfo; PursuitInfo.StartTime = StageRanking.StartTime; PursuitInfo.Bib = StageRanking.Bib; @@ -204,11 +178,6 @@ bool UDTFluxPursuitManager::InitPursuitForRequest(FRequestData Data) AllPursuits.Add(PursuitInfo); } } - // Sort Rankings - // AllPursuits.Sort([](const FDTFluxPursuitInfo& A, const FDTFluxPursuitInfo& B) { - // return A.StartTime < B.StartTime; - // }); - for (auto& Pursuit : AllPursuits) { if (TempGroups.Contains(Pursuit.StartTime)) @@ -251,5 +220,13 @@ bool UDTFluxPursuitManager::InitPursuitForRequest(FRequestData Data) { return A.StartTimeGlobal < B.StartTimeGlobal; }); + + TArray FocusPursuits; + TArray NextPursuits; + bool bIsFocusTruncate = false; + + GetPursuit(FocusPursuits, NextPursuits, bIsFocusTruncate); + FPursuitStaterData PursuitData = FPursuitStaterData(FocusPursuits, NextPursuits, MassStartTime, bIsFocusTruncate); + CoreSubsystem->OnPursuitSequenceReady.Broadcast(PursuitData); return true; } diff --git a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h index 24179db..5542b00 100644 --- a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h +++ b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h @@ -2,6 +2,7 @@ #include "CoreMinimal.h" #include "Containers/Deque.h" +#include "Types/Struct/FDTFluxPursuitInfo.h" #include "Subsystems/EngineSubsystem.h" #include "Types/Struct/DTFluxRaceDataStructs.h" #include "Types/Struct/DTFluxTeamListStruct.h" @@ -15,6 +16,35 @@ class UDTFluxNetworkSubsystem; class UDTFluxModelAsset; class UDTFluxPursuitManager; +USTRUCT(BlueprintType) +struct FPursuitStaterData +{ + GENERATED_BODY() + +public: + FPursuitStaterData() = default; + + FPursuitStaterData(const TArray& InPursuitFocusNext, + const TArray& InPursuitNext, const FDateTime& InMassStartTime, + const bool InIsFocusTruncate) + : PursuitFocusNext(InPursuitFocusNext), PursuitNext(InPursuitNext), MassStartTime(InMassStartTime), + bIsFocusTruncate(InIsFocusTruncate) + { + }; + + UPROPERTY(BlueprintReadOnly, Category="DTFlux|Pursuit") + TArray PursuitFocusNext = TArray(); + UPROPERTY(BlueprintReadOnly, Category="DTFlux|Pursuit") + TArray PursuitNext = TArray(); + UPROPERTY(BlueprintReadOnly, Category="DTFlux|Pursuit") + FDateTime MassStartTime = FDateTime::MinValue(); + UPROPERTY(BlueprintReadOnly, Category="DTFlux|Pursuit") + bool bIsFocusTruncate = false; +}; + + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnPursuitSequenceReady, const FPursuitStaterData, PursuitInfoSequenceItem); + /** * */ @@ -24,15 +54,15 @@ class DTFLUXCORESUBSYSTEM_API UDTFluxCoreSubsystem : public UEngineSubsystem GENERATED_BODY() public: - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSplitRankings, FDTFluxSplitRankings&, SplitRankings); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSplitRankings, FDTFluxSplitRankings, SplitRankings); UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnSplitRankings OnSplitRankings; - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStageRankings, FDTFluxStageRankings&, StageRankings); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStageRankings, FDTFluxStageRankings, StageRankings); UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnStageRankings OnStageRankings; - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContestRankings, FDTFluxContestRankings&, ContestRankings); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContestRankings, FDTFluxContestRankings, ContestRankings); UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnContestRankings OnContestRankings; @@ -44,9 +74,46 @@ public: UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnTeamStatusUpdate OnTeamStatusUpdate; - DECLARE_DELEGATE_TwoParams(FOnRequestedStageRankings, const FDTFluxStageKey&, const FDTFluxContestRankings&); + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnRequestedStageRankings, const FDTFluxStageKey, StageKey, + const FDTFluxStageRankings, StageRankings); + + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnRequestedStageRankings OnRequestedStageRankings; + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") + FOnPursuitSequenceReady OnPursuitSequenceReady; + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + bool GetContestRankings(const int ContestId, FDTFluxContestRankings& OutContestRankings); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + bool GetStageRankings(const int ContestId, const int StageId, FDTFluxStageRankings& OutStageRankings); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + bool GetSplitRankings(const int ContestId, const int StageId, const int SplitId, + FDTFluxSplitRankings& OutSplitRankings); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + bool GetStageRankingsWithKey(const FDTFluxStageKey StageKey, FDTFluxStageRankings& OutStageRankings, + const bool bShouldUseCached = true); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + bool GetSplitRankingsWithKey(const FDTFluxSplitKey SplitKey, FDTFluxSplitRankings& OutSplitRankings, + const bool bShouldUseCached = true); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + TArray TrackedRequestContestRankings(const TArray ForContests); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + TArray TrackedRequestStageRankings(const TArray ForStages); + + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + TArray TrackedRequestSplitRankings(const TArray ForSplits); + + UPROPERTY(BlueprintReadOnly, Category="DTFlux|Core Subsystem") + UDTFluxPursuitManager* PursuitManager = nullptr; + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void SendTeamListRequest(); @@ -68,9 +135,6 @@ public: UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void RequestAllSplitRankingOfContest(int InContestId, int InStageId); - UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - FDTFluxStageRankings GetStageRankings(FDTFluxStageKey StageKey); - UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void RequestAllSplitRankingOfStage(int InContestId, int InStageId, int InSplitId); @@ -96,9 +160,6 @@ public: UFUNCTION() TArray GetContests(); - UPROPERTY(BlueprintReadOnly, Category="DTFlux|Core Subsystem") - UDTFluxPursuitManager* PursuitManager = nullptr; - protected: // ~Subsystem Interface virtual void Initialize(FSubsystemCollectionBase& Collection) override; diff --git a/Source/DTFluxCoreSubsystem/Public/DTFluxPursuitManager.h b/Source/DTFluxCoreSubsystem/Public/DTFluxPursuitManager.h index 85adf71..09acb27 100644 --- a/Source/DTFluxCoreSubsystem/Public/DTFluxPursuitManager.h +++ b/Source/DTFluxCoreSubsystem/Public/DTFluxPursuitManager.h @@ -65,10 +65,6 @@ struct FDTFluxPursuitGroup bool bIsFocus = false; }; -DECLARE_DYNAMIC_MULTICAST_DELEGATE_FourParams(FOnPursuitSequenceReady, const FDateTime, MassStartTime, - const TArray, - NextFocusPursuits, - const TArray, NextPursuit, bool, bIsTrtuncate); /** * @@ -81,11 +77,6 @@ class DTFLUXCORESUBSYSTEM_API UDTFluxPursuitManager : public UObject public: UDTFluxPursuitManager(const FObjectInitializer& ObjectInitializer); - UPROPERTY(BlueprintReadOnly, VisibleAnywhere) - TArray NextFocusPursuits; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere) - TArray NextPursuits; UPROPERTY(BlueprintReadOnly, VisibleAnywhere) bool bFocusIsTruncate = false; @@ -106,8 +97,6 @@ public: UPROPERTY() int CurrentIndex = -1; - UPROPERTY(BlueprintAssignable, Category="DTFlux|Pursuit") - FOnPursuitSequenceReady OnPursuitSequenceReady; UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="pursuit, launch, poursuite")) void InitPursuit(const TArray InContestIds, const int MaxSimultaneousPursuit = 7); @@ -116,29 +105,30 @@ public: void GetPursuit(TArray& OutPursuitFocusNext, TArray& OutPursuitNext, bool& BIsFocusTruncate, const int MaxSimultaneousPursuit = 7); - UFUNCTION() - void OnRequestResponse(const FGuid& RequestId, FDTFluxServerResponse& Response); - - UFUNCTION() - void OnRequestTimeoutResponse(const FGuid& RequestId, const FString& TimeoutMessage); - - UFUNCTION() - void OnRequestError(const FGuid& RequestId, const FString& ErrorMessage); - UFUNCTION() bool InitSubSystems(); + UFUNCTION() + bool BindRankings(); + + UFUNCTION() + void UnbindRankings(); + + UFUNCTION() + void OnRankingsReceived(const FDTFluxStageKey NewStageKey, const FDTFluxStageRankings NewStageRankings); + private: - TArray PendingRequestData; + TMap PendingStageRanking; + TArray AllRankings; UDTFluxCoreSubsystem* CoreSubsystem = nullptr; UDTFluxNetworkSubsystem* NetworkSubsystem = nullptr; UPROPERTY() bool bIsSequenceDone = true; + UPROPERTY() + bool bIsRankingBounded = false; UFUNCTION() void SetPursuitInfoIsMassStart(FDTFluxPursuitGroup NextFocusGroup); UFUNCTION() - void InitPursuitForContests(const TArray InContests); - UFUNCTION() - bool InitPursuitForRequest(FRequestData Data); + bool LaunchPursuitSequence(); }; diff --git a/Source/DTFluxNetwork/Private/DTFluxAsyncParser.cpp b/Source/DTFluxNetwork/Private/DTFluxAsyncParser.cpp new file mode 100644 index 0000000..bd0163a --- /dev/null +++ b/Source/DTFluxNetwork/Private/DTFluxAsyncParser.cpp @@ -0,0 +1,317 @@ +#include "DTFluxAsyncParser.h" + +#include "DTFluxNetworkModule.h" +#include "Struct/DTFluxServerResponseStruct.h" +#include "Async/AsyncWork.h" + +// ================================================================================================ +// IMPLÉMENTATION DE LA TÂCHE DE PARSING +// ================================================================================================ + +DECLARE_STATS_GROUP(TEXT("DTFlux"), STATGROUP_DTFlux, STATCAT_Advanced); + +DECLARE_CYCLE_STAT(TEXT("DTFlux Parsing Task"), STAT_FDTFluxParsingTask, STATGROUP_DTFlux); +DECLARE_CYCLE_STAT(TEXT("DTFlux Parsing Task DoWork"), STAT_FDTFluxParsingTask_DoWork, STATGROUP_DTFlux); + +FDTFluxParsingTask::FDTFluxParsingTask( + const FGuid& InRequestId, + const FString& InRawJsonData, + FOnParsingCompleted InOnCompleted, + FOnParsingFailed InOnFailed +) + : RequestId(InRequestId) + , RawJsonData(InRawJsonData) + , OnCompleted(InOnCompleted) + , OnFailed(InOnFailed) + , StartTime(FPlatformTime::Seconds()) +{ +} + +void FDTFluxParsingTask::DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) +{ + SCOPE_CYCLE_COUNTER(STAT_FDTFluxParsingTask_DoWork); + + UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("Starting async parsing for request %s"), *RequestId.ToString()); + + TSharedPtr ParsedResponse; + bool bParsingSuccess = false; + FString ErrorMessage; + + try + { + // === PARSING SUR LE THREAD WORKER === + + EDTFluxResponseStatus Status; + ParsedResponse = MakeShared(RawJsonData, Status, false); // Pas de logs sur worker thread + + if (Status == EDTFluxResponseStatus::Success) + { + bParsingSuccess = true; + UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("Async parsing successful for request %s"), + *RequestId.ToString()); + } + else + { + ErrorMessage = FString::Printf(TEXT("Parsing failed with status: %s"), + *UEnum::GetValueAsString(Status)); + UE_LOG(logDTFluxNetwork, Warning, TEXT("Async parsing failed for request %s: %s"), + *RequestId.ToString(), *ErrorMessage); + } + } + catch (const std::exception& e) + { + ErrorMessage = FString::Printf(TEXT("Exception during parsing: %s"), ANSI_TO_TCHAR(e.what())); + UE_LOG(logDTFluxNetwork, Error, TEXT("Exception during async parsing for request %s: %s"), + *RequestId.ToString(), *ErrorMessage); + } + catch (...) + { + ErrorMessage = TEXT("Unknown exception during parsing"); + UE_LOG(logDTFluxNetwork, Error, TEXT("Unknown exception during async parsing for request %s"), + *RequestId.ToString()); + } + + const float ParsingTime = (FPlatformTime::Seconds() - StartTime) * 1000.0f; // En millisecondes + + // === PROGRAMMER LA CALLBACK SUR LE MAIN THREAD === + + FFunctionGraphTask::CreateAndDispatchWhenReady( + [this, ParsedResponse, bParsingSuccess, ErrorMessage, ParsingTime]() + { + // Cette lambda s'exécute sur le main thread + if (bParsingSuccess && ParsedResponse.IsValid()) + { + OnCompleted.ExecuteIfBound(RequestId, ParsedResponse, true); + } + else + { + OnFailed.ExecuteIfBound(RequestId, ErrorMessage); + } + }, + TStatId(), + nullptr, + ENamedThreads::GameThread // Forcer l'exécution sur le main thread + ); +} + +// ================================================================================================ +// IMPLÉMENTATION DU PARSER ASYNCHRONE +// ================================================================================================ + +FDTFluxAsyncParser::FDTFluxAsyncParser() +{ + UE_LOG(logDTFluxNetwork, Log, TEXT("AsyncParser initialized")); +} + +FDTFluxAsyncParser::~FDTFluxAsyncParser() +{ + CancelAllParsing(); + UE_LOG(logDTFluxNetwork, Log, TEXT("AsyncParser destroyed")); +} + +void FDTFluxAsyncParser::ParseResponseAsync( + const FGuid& RequestId, + const FString& RawJsonData, + FOnParsingCompleted OnCompleted, + FOnParsingFailed OnFailed) +{ + if (RawJsonData.IsEmpty()) + { + OnFailed.ExecuteIfBound(RequestId, TEXT("Empty JSON data")); + return; + } + + // Créer la tâche de parsing + FGraphEventRef Task = FFunctionGraphTask::CreateAndDispatchWhenReady( + [RequestId, RawJsonData, OnCompleted, OnFailed]() + { + // Ce code s'exécute sur le worker thread + const double StartTime = FPlatformTime::Seconds(); + + TSharedPtr ParsedResponse; + bool bParsingSuccess = false; + FString ErrorMessage; + + try + { + EDTFluxResponseStatus Status; + ParsedResponse = MakeShared(RawJsonData, Status, false); + + if (Status == EDTFluxResponseStatus::Success) + { + bParsingSuccess = true; + } + else + { + ErrorMessage = FString::Printf(TEXT("Parsing failed with status: %s"), + *UEnum::GetValueAsString(Status)); + } + } + catch (const std::exception& e) + { + ErrorMessage = FString::Printf(TEXT("Exception during parsing: %s"), ANSI_TO_TCHAR(e.what())); + } + catch (...) + { + ErrorMessage = TEXT("Unknown exception during parsing"); + } + + const float ParsingTime = (FPlatformTime::Seconds() - StartTime) * 1000.0f; + FFunctionGraphTask::CreateAndDispatchWhenReady( + [RequestId, ParsedResponse, bParsingSuccess, ErrorMessage, OnCompleted, OnFailed]() + { + // Cette lambda s'exécute sur le main thread + if (bParsingSuccess && ParsedResponse.IsValid()) + { + OnCompleted.ExecuteIfBound(RequestId, ParsedResponse, true); + } + else + { + OnFailed.ExecuteIfBound(RequestId, ErrorMessage); + } + }, + TStatId(), + nullptr, + ENamedThreads::GameThread // Forcer main thread + ); + }, + TStatId(), + nullptr, + ENamedThreads::AnyBackgroundThreadNormalTask + ); + + // Tracker la tâche + { + FScopeLock Lock(&TasksLock); + ActiveTasks.Add(Task); + } + + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Queued async parsing task for request %s"), *RequestId.ToString()); +} + +TSharedPtr FDTFluxAsyncParser::ParseResponseSync( + const FString& RawJsonData, + float TimeoutSeconds) +{ + if (RawJsonData.IsEmpty()) + { + return nullptr; + } + + // Variables pour la synchronisation + TSharedPtr Result; + std::atomic bCompleted{false}; + + // Lancer le parsing async avec callback sync + FOnParsingCompleted OnCompleted = FOnParsingCompleted::CreateLambda( + [&Result, &bCompleted](const FGuid& RequestId, TSharedPtr ParsedResponse, bool bSuccess) + { + if (bSuccess) + { + Result = ParsedResponse; + } + bCompleted.store(true); + } + ); + + FOnParsingFailed OnFailed = FOnParsingFailed::CreateLambda( + [&bCompleted](const FGuid& RequestId, const FString& ErrorMessage) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Sync parsing failed: %s"), *ErrorMessage); + bCompleted.store(true); + } + ); + + FGuid TempId = FGuid::NewGuid(); + ParseResponseAsync(TempId, RawJsonData, OnCompleted, OnFailed); + + // Attendre avec timeout + const double StartTime = FPlatformTime::Seconds(); + while (!bCompleted.load() && (FPlatformTime::Seconds() - StartTime) < TimeoutSeconds) + { + FPlatformProcess::Sleep(0.001f); // 1ms + } + + return Result; +} + +void FDTFluxAsyncParser::CancelAllParsing() +{ + FScopeLock Lock(&TasksLock); + + for (const FGraphEventRef& Task : ActiveTasks) + { + // Note: On ne peut pas vraiment "cancel" une tâche TaskGraph en cours, + // mais on peut marquer qu'on ne veut plus les résultats + } + + ActiveTasks.Empty(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Cancelled all pending parsing tasks")); +} + +FDTFluxAsyncParser::FParsingStats FDTFluxAsyncParser::GetStats() const +{ + FScopeLock StatsLock_Local(&StatsLock); + FScopeLock TasksLock_Local(&TasksLock); + + FParsingStats Stats; + Stats.TasksInProgress = ActiveTasks.Num(); + Stats.TasksCompleted = TasksCompletedCount; + Stats.TasksFailed = TasksFailedCount; + + if (ParsingTimes.Num() > 0) + { + float Sum = 0.0f; + for (float Time : ParsingTimes) + { + Sum += Time; + } + Stats.AverageParsingTimeMs = Sum / ParsingTimes.Num(); + } + + return Stats; +} + +void FDTFluxAsyncParser::ResetStats() +{ + FScopeLock Lock(&StatsLock); + TasksCompletedCount = 0; + TasksFailedCount = 0; + ParsingTimes.Empty(); +} + +void FDTFluxAsyncParser::OnTaskCompleted(bool bSuccess, float ParsingTimeMs) +{ + FScopeLock Lock(&StatsLock); + + if (bSuccess) + { + TasksCompletedCount++; + } + else + { + TasksFailedCount++; + } + + ParsingTimes.Add(ParsingTimeMs); + + // Garder seulement les 100 derniers temps pour la moyenne + if (ParsingTimes.Num() > 100) + { + ParsingTimes.RemoveAt(0); + } +} + +void FDTFluxAsyncParser::CleanupCompletedTasks() +{ + FScopeLock Lock(&TasksLock); + + for (auto It = ActiveTasks.CreateIterator(); It; ++It) + { + const FGraphEventRef& Task = *It; + if (Task.IsValid() && Task->IsComplete()) + { + It.RemoveCurrent(); // Supprime l'élément actuel de manière sécurisée + } + } +} diff --git a/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp b/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp index aa742c8..bba7986 100644 --- a/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp +++ b/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp @@ -1,51 +1,93 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// ================================================================================================ +// DTFluxRequestManager.cpp - Implémentation du gestionnaire de requêtes +// ================================================================================================ #include "DTFluxQueuedManager.h" + +#include "DTFluxAsyncParser.h" #include "DTFluxNetworkModule.h" +#include "Struct/DTFluxServerResponseStruct.h" +#include "Struct/DTFluxRequestStructs.h" #include "JsonObjectConverter.h" +bool FDTFluxTrackedRequest::HasTimedOut() const +{ + if (State != EDTFluxRequestState::Pending && State != EDTFluxRequestState::Sent) + return false; + return (FDateTime::Now() - CreatedAt).GetTotalSeconds() > Config.TimeoutSeconds; +} -const FString FDTFluxQueuedRequest::Serialize() const +bool FDTFluxTrackedRequest::CanRetry() const +{ + return CurrentRetries < Config.MaxRetries && + (State == EDTFluxRequestState::Failed || State == EDTFluxRequestState::TimedOut); +} + +bool FDTFluxTrackedRequest::IsCacheValid() const +{ + if (State != EDTFluxRequestState::Cached) return false; + return (FDateTime::Now() - CompletedAt).GetTotalSeconds() < Config.CacheValiditySeconds; +} + +float FDTFluxTrackedRequest::GetRetryDelay() const +{ + return FMath::Pow(Config.RetryBackoffMultiplier, CurrentRetries); +} + +bool FDTFluxTrackedRequest::Matches(EDTFluxApiDataType InType, int32 InContestId, int32 InStageId, + int32 InSplitId) const +{ + return RequestType == InType && ContestId == InContestId && StageId == InStageId && SplitId == InSplitId; +} + +FString FDTFluxTrackedRequest::GetCacheKey() const +{ + return FString::Printf(TEXT("%s_%d_%d_%d"), + *UEnum::GetValueAsString(RequestType), ContestId, StageId, SplitId); +} + +void FDTFluxTrackedRequest::SetRawResponse(const FString& RawData) +{ + RawResponseData = RawData; + ParsedResponse.Reset(); + bIsResponseParsed = false; +} + +FString FDTFluxTrackedRequest::Serialize() const { FString JSONString; switch (RequestType) { - case EDTFluxRequestType::RaceData: - + case EDTFluxApiDataType::RaceData: { FDTFluxRaceDataRequest RaceData; FJsonObjectConverter::UStructToJsonObjectString(RaceData, JSONString); break; } - - case EDTFluxRequestType::TeamList: + case EDTFluxApiDataType::TeamList: { const FDTFluxTeamListRequest TeamList; FJsonObjectConverter::UStructToJsonObjectString(TeamList, JSONString); break; } - - case EDTFluxRequestType::ContestRanking: + case EDTFluxApiDataType::ContestRanking: { FDTFluxContestRankingRequest ContestRanking(ContestId); FJsonObjectConverter::UStructToJsonObjectString(ContestRanking, JSONString); break; } - - case EDTFluxRequestType::StageRanking: + case EDTFluxApiDataType::StageRanking: { FDTFluxStageRankingRequest StageRanking(ContestId, StageId); FJsonObjectConverter::UStructToJsonObjectString(StageRanking, JSONString); break; } - - case EDTFluxRequestType::SplitRanking: + case EDTFluxApiDataType::SplitRanking: { FDTFluxSplitRankingRequest SplitRanking(ContestId, StageId, SplitId); FJsonObjectConverter::UStructToJsonObjectString(SplitRanking, JSONString); break; } - default: JSONString = ""; break; @@ -53,387 +95,747 @@ const FString FDTFluxQueuedRequest::Serialize() const return JSONString; } -UDTFluxQueuedManager::UDTFluxQueuedManager() - : bIsInitialized(false) - , CheckInterval(0.5f) - , TimeSinceLastCheck(0.0f) +FDTFluxQueuedRequestManager::FDTFluxQueuedRequestManager() { + AsyncParser = MakeUnique(); + UE_LOG(logDTFluxNetwork, Verbose, TEXT("RequestManager created")); } -UDTFluxQueuedManager::~UDTFluxQueuedManager() +FDTFluxQueuedRequestManager::~FDTFluxQueuedRequestManager() { - ClearAllRequests(); + Shutdown(); + UE_LOG(logDTFluxNetwork, Verbose, TEXT("RequestManager destroyed")); } -void UDTFluxQueuedManager::Initialize() +void FDTFluxQueuedRequestManager::Initialize(const FDTFluxRequestConfig& InDefaultConfig) { - if (!bIsInitialized) + if (bIsInitialized.load()) { - UE_LOG(logDTFluxNetwork, Log, TEXT("Initializing DTFluxQueuedManager")); - bIsInitialized = true; + UE_LOG(logDTFluxNetwork, Warning, TEXT("RequestManager already initialized")); + return; } + + DefaultConfig = InDefaultConfig; + bIsInitialized.store(true); + + UE_LOG(logDTFluxNetwork, Log, TEXT("RequestManager initialized with timeout=%.1fs, cache=%.1fs"), + DefaultConfig.TimeoutSeconds, DefaultConfig.CacheValiditySeconds); } -FGuid UDTFluxQueuedManager::QueueRequest(EDTFluxRequestType RequestType, int32 ContestId, int32 StageId, int32 SplitId, - const FString& RawMessage) +void FDTFluxQueuedRequestManager::Shutdown() { - // Créer la requête avec les structs existants - FDTFluxQueuedRequest NewRequest(RequestType, ContestId, StageId, SplitId); - NewRequest.RawResponse = RawMessage; + if (!bIsInitialized.load()) + return; - // Ajouter à la queue des requêtes en attente - PendingRequestsQueue.Enqueue(NewRequest); - - UE_LOG(logDTFluxNetwork, Verbose, TEXT("Queued request %s: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"), - *NewRequest.RequestId.ToString(), (int32)RequestType, ContestId, StageId, SplitId); - - return NewRequest.RequestId; -} - -bool UDTFluxQueuedManager::MarkRequestAsError(const FGuid& TargetRequestGuid) -{ - // TODO: Implement a retry mechanism - // For now we simply suppress the request and log a message - bool bFoundMatch = false; - FDTFluxQueuedRequest Request; - TQueue TempQueue; - while (PendingRequestsQueue.Dequeue(Request)) + bIsInitialized.store(false); + // Nettoyer toutes les données { - if (Request.RequestId == TargetRequestGuid) + FScopeLock RequestsLock_Local(&RequestsLock); + FScopeLock CallbacksLock_Local(&CallbacksLock); + + AllRequests.Empty(); + CacheKeyToRequestId.Empty(); + SuccessCallbacks.Empty(); + ErrorCallbacks.Empty(); + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("RequestManager shutdown")); +} + +FGuid FDTFluxQueuedRequestManager::CreateTrackedRequest( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + const FDTFluxRequestConfig& CustomConfig) +{ + if (!bIsInitialized.load()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("RequestManager not initialized")); + return FGuid(); + } + + // Vérifier le cache d'abord + FString CachedResponse; + if (CustomConfig.bEnableCache && GetFromCache(RequestType, CachedResponse, ContestId, StageId, SplitId)) + { + UE_LOG(logDTFluxNetwork, Log, TEXT("Request served from cache: Type=%s"), + *UEnum::GetValueAsString(RequestType)); + + // Créer une "fausse" requête pour représenter le hit cache + auto CachedRequest = MakeShared(); + CachedRequest->RequestType = RequestType; + CachedRequest->ContestId = ContestId; + CachedRequest->StageId = StageId; + CachedRequest->SplitId = SplitId; + CachedRequest->Config = CustomConfig.bEnableCache ? CustomConfig : DefaultConfig; + CachedRequest->State = EDTFluxRequestState::Cached; + CachedRequest->RawResponseData = CachedResponse; + CachedRequest->CompletedAt = FDateTime::Now(); + + FGuid CacheRequestId = CachedRequest->RequestId; + { - UE_LOG(logDTFluxNetwork, Error, - TEXT("Marked request %s as error: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"), - *Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId, - Request.SplitId); + FScopeLock Lock(&RequestsLock); + AllRequests.Add(CacheRequestId, CachedRequest); } - else + + RecordCacheHit(); + return CacheRequestId; + } + + // Créer une nouvelle requête + auto NewRequest = MakeShared(); + NewRequest->RequestType = RequestType; + NewRequest->ContestId = ContestId; + NewRequest->StageId = StageId; + NewRequest->SplitId = SplitId; + NewRequest->Config = (CustomConfig.TimeoutSeconds > 0) ? CustomConfig : DefaultConfig; + + FGuid RequestId = NewRequest->RequestId; + + { + FScopeLock Lock(&RequestsLock); + AllRequests.Add(RequestId, NewRequest); + TotalRequests++; + } + + RecordCacheMiss(); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Created tracked request %s: Type=%s, Contest=%d, Stage=%d, Split=%d"), + *RequestId.ToString(), *UEnum::GetValueAsString(RequestType), ContestId, StageId, SplitId); + + return RequestId; +} + +FGuid FDTFluxQueuedRequestManager::CreateTrackedRequestWithCallbacks( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + FOnDTFluxRequestSuccess OnSuccess, + FOnDTFluxRequestError OnError, + const FDTFluxRequestConfig& CustomConfig) +{ + FGuid RequestId = CreateTrackedRequest(RequestType, ContestId, StageId, SplitId, CustomConfig); + + if (RequestId.IsValid()) + { + FScopeLock Lock(&CallbacksLock); + + if (OnSuccess.IsBound()) { - TempQueue.Enqueue(Request); + SuccessCallbacks.Add(RequestId, OnSuccess); + } + if (OnError.IsBound()) + { + ErrorCallbacks.Add(RequestId, OnError); } } - while (TempQueue.Dequeue(Request)) + + return RequestId; +} + +bool FDTFluxQueuedRequestManager::MarkRequestAsSent(const FGuid& RequestId) +{ + FScopeLock Lock(&RequestsLock); + + if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { - PendingRequestsQueue.Enqueue(Request); + TSharedPtr Request = *RequestPtr; + Request->SentAt = FDateTime::Now(); + Request->LastAttemptTime = FDateTime::Now(); + ChangeRequestState(Request, EDTFluxRequestState::Sent); + return true; } - if (bFoundMatch) + + return false; +} + +bool FDTFluxQueuedRequestManager::CompleteRequest(const FGuid& RequestId, const FString& RawResponseData, + bool bUseAsyncParsing) +{ + TSharedPtr Request; + { - UE_LOG(logDTFluxNetwork, Error, TEXT("No Request Found with GUID %s"), *TargetRequestGuid.ToString()); + FScopeLock Lock(&RequestsLock); + if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) + { + Request = *RequestPtr; + } } + + if (!Request.IsValid()) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Request %s not found"), *RequestId.ToString()); + return false; + } + + // Stocker la réponse brute + Request->SetRawResponse(RawResponseData); + Request->CompletedAt = FDateTime::Now(); + + // Décider du parsing selon les callbacks et la configuration + bool bHasCallbacks = false; + { + FScopeLock Lock(&CallbacksLock); + bHasCallbacks = SuccessCallbacks.Contains(RequestId) || ErrorCallbacks.Contains(RequestId); + } + + if (bHasCallbacks && bUseAsyncParsing && !RawResponseData.IsEmpty()) + { + // Parsing asynchrone pour les callbacks + FOnParsingCompleted OnCompleted = FOnParsingCompleted::CreateRaw( + this, &FDTFluxQueuedRequestManager::OnParsingCompleted + ); + + FOnParsingFailed OnFailed = FOnParsingFailed::CreateRaw( + this, &FDTFluxQueuedRequestManager::OnParsingFailed + ); + + AsyncParser->ParseResponseAsync(RequestId, RawResponseData, OnCompleted, OnFailed); + + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Started async parsing for request %s"), *RequestId.ToString()); + return true; + } + else + { + // Compléter immédiatement sans parsing ou avec parsing sync + EDTFluxRequestState NewState = Request->Config.bEnableCache + ? EDTFluxRequestState::Cached + : EDTFluxRequestState::Completed; + + ChangeRequestState(Request, NewState); + + if (Request->Config.bEnableCache) + { + FScopeLock Lock(&RequestsLock); + CacheKeyToRequestId.Add(Request->GetCacheKey(), RequestId); + } + + // Déclencher les callbacks avec les données brutes + TriggerCallbacks(*Request); + CleanupCallbacks(RequestId); + + return true; + } +} + +bool FDTFluxQueuedRequestManager::FailRequest(const FGuid& RequestId, const FString& ErrorMessage) +{ + TSharedPtr Request; + + { + FScopeLock Lock(&RequestsLock); + if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) + { + Request = *RequestPtr; + } + } + + if (!Request.IsValid()) + { + return false; + } + + Request->LastErrorMessage = ErrorMessage; + ChangeRequestState(Request, EDTFluxRequestState::Failed); + + // Déclencher les callbacks d'erreur + TriggerCallbacks(*Request); + CleanupCallbacks(RequestId); + + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed request %s: %s"), *RequestId.ToString(), *ErrorMessage); return true; } -bool UDTFluxQueuedManager::MarkRequestAsResponded(const FGuid& TargetRequestGuid) +bool FDTFluxQueuedRequestManager::RetryRequest(const FGuid& RequestId) { - TQueue TempQueue; - bool bFoundMatch = false; + TSharedPtr Request; - // Parcourir toutes les requêtes en attente - FDTFluxQueuedRequest Request; - while (PendingRequestsQueue.Dequeue(Request)) { - if (!bFoundMatch && Request.RequestId == TargetRequestGuid) + FScopeLock Lock(&RequestsLock); + if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) { - // Marquer comme ayant reçu une réponse - Request.bHasReceivedResponse = true; - bFoundMatch = true; - - // Ajouter à la queue des requêtes terminées - CompletedRequestsQueue.Enqueue(Request); - - UE_LOG(logDTFluxNetwork, Verbose, - TEXT("Marked request %s as responded: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"), - *Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId, - Request.SplitId); - } - else - { - // Remettre dans la queue temporaire - TempQueue.Enqueue(Request); + Request = *RequestPtr; } } - // Remettre les requêtes non traitées dans la queue principale - while (TempQueue.Dequeue(Request)) + if (!Request.IsValid() || !Request->CanRetry()) { - PendingRequestsQueue.Enqueue(Request); + return false; } - return bFoundMatch; + Request->CurrentRetries++; + Request->LastAttemptTime = FDateTime::Now(); + ChangeRequestState(Request, EDTFluxRequestState::Retrying); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Retrying request %s (attempt %d/%d)"), + *RequestId.ToString(), Request->CurrentRetries, Request->Config.MaxRetries); + + return true; } -bool UDTFluxQueuedManager::MarkRequestAsResponded(const FDTFluxQueuedRequest& TargetRequest) +bool FDTFluxQueuedRequestManager::FindPendingRequest( + FGuid& OutRequestId, + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId) const { - return MarkRequestAsResponded(TargetRequest.RequestId); -} + FScopeLock Lock(&RequestsLock); -bool UDTFluxQueuedManager::IsRequestPending(FGuid& OutRequestId, EDTFluxApiDataType RequestType, int32 ContestId, - int32 StageId, - int32 SplitId) -{ - TQueue TempQueue; - bool bFoundMatch = false; - - // Parcourir toutes les requêtes en attente - FDTFluxQueuedRequest Request; - while (PendingRequestsQueue.Dequeue(Request)) + for (const auto& [RequestId, Request] : AllRequests) { - // Vérifier si cette requête correspond - if (!bFoundMatch && Request.Matches(RequestType, ContestId, StageId, SplitId)) + if ((Request->State == EDTFluxRequestState::Pending || Request->State == EDTFluxRequestState::Sent) && + Request->Matches(RequestType, ContestId, StageId, SplitId)) { - bFoundMatch = true; - OutRequestId = Request.RequestId; - UE_LOG(logDTFluxNetwork, Verbose, - TEXT("Found pending request %s: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"), - *Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId, - Request.SplitId); + OutRequestId = RequestId; + return true; } - - // Remettre dans la queue temporaire - TempQueue.Enqueue(Request); } - // Remettre toutes les requêtes dans la queue principale - while (TempQueue.Dequeue(Request)) - { - PendingRequestsQueue.Enqueue(Request); - } - - return bFoundMatch; + return false; } -FDTFluxQueuedRequest* UDTFluxQueuedManager::GetRequestPending(EDTFluxRequestType RequestType, int32 ContestId, - int32 StageId, int32 SplitId) +bool FDTFluxQueuedRequestManager::GetFromCache( + EDTFluxApiDataType RequestType, + FString& OutRawResponse, + int32 ContestId, + int32 StageId, + int32 SplitId) const { - auto SearchInQueue = [&RequestType, ContestId, StageId, SplitId]( - TQueue& Queue) -> FDTFluxQueuedRequest* - { - // Copie temporaire de la queue pour la recherche - TQueue TempQueue; + FString CacheKey = GenerateCacheKey(RequestType, ContestId, StageId, SplitId); - FDTFluxQueuedRequest* FoundItem = nullptr; - FDTFluxQueuedRequest Item; - while (Queue.Dequeue(Item)) + FScopeLock Lock(&RequestsLock); + + if (const FGuid* RequestId = CacheKeyToRequestId.Find(CacheKey)) + { + if (const TSharedPtr* RequestPtr = AllRequests.Find(*RequestId)) { - if (Item.RequestType == RequestType && Item.ContestId == ContestId && Item.StageId == StageId && Item. - SplitId == SplitId) // Assuming RequestId is your GUID field + const TSharedPtr& CachedRequest = *RequestPtr; + + if (CachedRequest->IsCacheValid() && !CachedRequest->RawResponseData.IsEmpty()) { - FoundItem = &Item; + OutRawResponse = CachedRequest->RawResponseData; + return true; } - // Remettre dans la queue temporaire - TempQueue.Enqueue(Item); } - while (TempQueue.Dequeue(Item)) - { - Queue.Enqueue(Item); - } - return FoundItem; - }; - return SearchInQueue(PendingRequestsQueue); + } + + return false; } -const FDTFluxQueuedRequest* UDTFluxQueuedManager::GetRequest(const FGuid& SearchedGuid) +bool FDTFluxQueuedRequestManager::GetParsedFromCache( + EDTFluxApiDataType RequestType, + TSharedPtr& OutResponse, + int32 ContestId, + int32 StageId, + int32 SplitId) const { - auto SearchInQueue = [&SearchedGuid](TQueue& Queue) -> FDTFluxQueuedRequest* + FString CacheKey = GenerateCacheKey(RequestType, ContestId, StageId, SplitId); + + FScopeLock Lock(&RequestsLock); + + if (const FGuid* RequestId = CacheKeyToRequestId.Find(CacheKey)) { - // Copie temporaire de la queue pour la recherche - TQueue TempQueue; - - FDTFluxQueuedRequest* FoundItem = nullptr; - FDTFluxQueuedRequest Item; - while (Queue.Dequeue(Item)) + if (const TSharedPtr* RequestPtr = AllRequests.Find(*RequestId)) { - if (Item.RequestId == SearchedGuid) // Assuming RequestId is your GUID field + const TSharedPtr& CachedRequest = *RequestPtr; + + if (CachedRequest->IsCacheValid()) { - // Trouver l'élément dans la queue originale - // On doit refaire une copie car on ne peut pas retourner l'adresse de 'Item' - FoundItem = &Item; + // Parsing lazy si nécessaire + if (!CachedRequest->ParsedResponse.IsSet() && !CachedRequest->RawResponseData.IsEmpty()) + { + OutResponse = AsyncParser->ParseResponseSync(CachedRequest->RawResponseData, 1.0f); + if (OutResponse.IsValid()) + { + CachedRequest->ParsedResponse = OutResponse; + const_cast(CachedRequest.Get())->bIsResponseParsed = true; + } + } + else if (CachedRequest->ParsedResponse.IsSet()) + { + OutResponse = CachedRequest->ParsedResponse.GetValue(); + } + + return OutResponse.IsValid(); } - // Remettre dans la queue temporaire - TempQueue.Enqueue(Item); } - while (TempQueue.Dequeue(Item)) - { - Queue.Enqueue(Item); - } - return FoundItem; - }; + } - // Chercher dans chaque queue - if (FDTFluxQueuedRequest* Found = SearchInQueue(PendingRequestsQueue)) - return Found; + return false; +} - if (const FDTFluxQueuedRequest* Found = SearchInQueue(CompletedRequestsQueue)) - return Found; +// === ACCESSEURS === - if (const FDTFluxQueuedRequest* Found = SearchInQueue(TimedOutRequestsQueue)) - return Found; +bool FDTFluxQueuedRequestManager::GetRequest(const FGuid& RequestId, FDTFluxTrackedRequest& OutRequest) const +{ + FScopeLock Lock(&RequestsLock); + + if (const TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) + { + OutRequest = **RequestPtr; + return true; + } + + return false; +} + +const FDTFluxTrackedRequest* FDTFluxQueuedRequestManager::GetRequestPtr(const FGuid& RequestId) const +{ + FScopeLock Lock(&RequestsLock); + + if (const TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) + { + return RequestPtr->Get(); + } return nullptr; } -int32 UDTFluxQueuedManager::GetPendingRequestCount() +TArray FDTFluxQueuedRequestManager::GetRequestsByState(EDTFluxRequestState State) const { - TQueue TempQueue; - int32 Count = 0; + FScopeLock Lock(&RequestsLock); - // Compter les requêtes en attente - FDTFluxQueuedRequest Request; - while (PendingRequestsQueue.Dequeue(Request)) + TArray Results; + + for (const auto& [RequestId, Request] : AllRequests) { - Count++; - TempQueue.Enqueue(Request); + if (Request->State == State) + { + Results.Add(*Request); + } } - // Remettre toutes les requêtes dans la queue principale - while (TempQueue.Dequeue(Request)) + return Results; +} + +int32 FDTFluxQueuedRequestManager::GetRequestCount(EDTFluxRequestState State) const +{ + FScopeLock Lock(&RequestsLock); + + int32 Count = 0; + + for (const auto& [RequestId, Request] : AllRequests) { - PendingRequestsQueue.Enqueue(Request); + if (Request->State == State) + { + Count++; + } } return Count; } -int32 UDTFluxQueuedManager::CleanupTimedOutRequests() +FDTFluxQueuedRequestManager::FRequestStatistics FDTFluxQueuedRequestManager::GetStatistics() const { - TQueue TempQueue; - int32 TimeoutCount = 0; + FScopeLock RequestsLock_Local(&RequestsLock); + FScopeLock MetricsLock_Local(&MetricsLock); - // Parcourir toutes les requêtes en attente - FDTFluxQueuedRequest Request; - while (PendingRequestsQueue.Dequeue(Request)) + FRequestStatistics Stats; + + for (const auto& [RequestId, Request] : AllRequests) { - if (Request.HasTimedOut()) + switch (Request->State) { - // Ajouter à la queue des requêtes expirées - TimedOutRequestsQueue.Enqueue(Request); - TimeoutCount++; - - UE_LOG(logDTFluxNetwork, Warning, - TEXT("Request %s timed out: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"), - *Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId, - Request.SplitId); - } - else - { - // Remettre dans la queue temporaire - TempQueue.Enqueue(Request); + case EDTFluxRequestState::Pending: + case EDTFluxRequestState::Sent: + case EDTFluxRequestState::Retrying: + Stats.Pending++; + break; + case EDTFluxRequestState::Cached: + Stats.Cached++; + break; + case EDTFluxRequestState::Completed: + Stats.Completed++; + break; + case EDTFluxRequestState::Failed: + case EDTFluxRequestState::TimedOut: + Stats.Failed++; + break; } } - // Remettre les requêtes non expirées dans la queue principale - while (TempQueue.Dequeue(Request)) + Stats.TotalRequests = TotalRequests; + Stats.CacheHits = CacheHits; + Stats.CacheMisses = CacheMisses; + + if (Stats.TotalRequests > 0) { - PendingRequestsQueue.Enqueue(Request); + Stats.HitRate = ((float)Stats.CacheHits / (float)Stats.TotalRequests) * 100.0f; } - return TimeoutCount; + return Stats; } -int32 UDTFluxQueuedManager::CleanCashedRequests() +// === NETTOYAGE === + +int32 FDTFluxQueuedRequestManager::CleanupExpiredCache() { - int32 CleanedRequestsCount = 0; + FScopeLock Lock(&RequestsLock); - // Queue temporaire pour stocker les requêtes encore valides - TQueue ValidCompletedRequests; + TArray ExpiredRequests; - // Traiter toutes les requêtes terminées - FDTFluxQueuedRequest CompletedRequest; - while (CompletedRequestsQueue.Dequeue(CompletedRequest)) + for (const auto& [RequestId, Request] : AllRequests) { - // Vérifier si la requête est cacheable et a reçu une réponse - if (CompletedRequest.bIsCacheable && CompletedRequest.bHasReceivedResponse) + if (Request->State == EDTFluxRequestState::Cached && !Request->IsCacheValid()) { - // Calculer l'âge de la requête en secondes - float RequestAge = (FDateTime::Now() - CompletedRequest.CreatedAt).GetTotalSeconds(); - - // Vérifier si le cache est encore valide - if (RequestAge <= CompletedRequest.CachedValidity) - { - // Le cache est encore valide, conserver la requête - ValidCompletedRequests.Enqueue(CompletedRequest); - } - else - { - // Le cache a expiré, compter cette requête comme nettoyée - CleanedRequestsCount++; - - UE_LOG(LogTemp, Verbose, - TEXT("DTFluxQueuedManager: Cleaned expired cached request %s (Age: %.2fs, Validity: %.2fs)"), - *CompletedRequest.RequestId.ToString(), RequestAge, CompletedRequest.CachedValidity); - } - } - else - { - // Requête non cacheable ou sans réponse, la conserver - ValidCompletedRequests.Enqueue(CompletedRequest); + ExpiredRequests.Add(RequestId); } } - // Restaurer la queue avec uniquement les requêtes valides - while (ValidCompletedRequests.Dequeue(CompletedRequest)) + for (const FGuid& RequestId : ExpiredRequests) { - CompletedRequestsQueue.Enqueue(CompletedRequest); + if (TSharedPtr* RequestPtr = AllRequests.Find(RequestId)) + { + const TSharedPtr& Request = *RequestPtr; + CacheKeyToRequestId.Remove(Request->GetCacheKey()); + AllRequests.Remove(RequestId); + } } - // Log du résultat si des requêtes ont été nettoyées - if (CleanedRequestsCount > 0) - { - UE_LOG(LogTemp, Log, TEXT("DTFluxQueuedManager: Cleaned %d expired cached requests"), CleanedRequestsCount); - } - - return CleanedRequestsCount; + return ExpiredRequests.Num(); } -void UDTFluxQueuedManager::ClearAllRequests() +int32 FDTFluxQueuedRequestManager::CleanupCompletedRequests(float OlderThanSeconds) { - // Vider toutes les queues - FDTFluxQueuedRequest DummyRequest; - while (PendingRequestsQueue.Dequeue(DummyRequest)) - { - } - while (CompletedRequestsQueue.Dequeue(DummyRequest)) - { - } - while (TimedOutRequestsQueue.Dequeue(DummyRequest)) + FScopeLock Lock(&RequestsLock); + + TArray OldRequests; + const FDateTime Threshold = FDateTime::Now() - FTimespan::FromSeconds(OlderThanSeconds); + + for (const auto& [RequestId, Request] : AllRequests) { + if ((Request->State == EDTFluxRequestState::Completed || Request->State == EDTFluxRequestState::Failed || + Request->State == EDTFluxRequestState::TimedOut) && Request->CompletedAt < Threshold) + { + OldRequests.Add(RequestId); + } } - UE_LOG(logDTFluxNetwork, Log, TEXT("Cleared all pending requests")); + for (const FGuid& RequestId : OldRequests) + { + AllRequests.Remove(RequestId); + } + + return OldRequests.Num(); } - -void UDTFluxQueuedManager::Tick(float DeltaTime) +void FDTFluxQueuedRequestManager::ClearAllRequests() { - if (!bIsInitialized) - { + FScopeLock RequestsLock_Local(&RequestsLock); + FScopeLock CallbacksLock_Local(&CallbacksLock); + + AllRequests.Empty(); + CacheKeyToRequestId.Empty(); + SuccessCallbacks.Empty(); + ErrorCallbacks.Empty(); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Cleared all requests")); +} + +void FDTFluxQueuedRequestManager::Tick(float DeltaTime) +{ + if (!bIsInitialized.load()) return; - } - // Incrémenter le temps écoulé - TimeSinceLastCheck += DeltaTime; + // Mise à jour des timers + TimeSinceLastTimeoutCheck += DeltaTime; + TimeSinceLastCacheCleanup += DeltaTime; + TimeSinceLastRetryCheck += DeltaTime; - // Vérifier si c'est le moment de nettoyer les requêtes expirées - if (TimeSinceLastCheck >= CheckInterval) + // Vérifier les timeouts + if (TimeSinceLastTimeoutCheck >= TimeoutCheckInterval) { - TimeSinceLastCheck = 0.0f; - CleanupTimedOutRequests(); + ProcessTimeouts(); + TimeSinceLastTimeoutCheck = 0.0f; } - // Traiter les requêtes expirées - FDTFluxQueuedRequest TimedOutRequest; - while (TimedOutRequestsQueue.Dequeue(TimedOutRequest)) + // Vérifier les retries + if (TimeSinceLastRetryCheck >= RetryCheckInterval) { - // Déclencher l'événement pour chaque requête expirée - OnRequestTimedOut.Broadcast(TimedOutRequest); + ProcessRetries(); + TimeSinceLastRetryCheck = 0.0f; + } + + // Nettoyage du cache + if (TimeSinceLastCacheCleanup >= CacheCleanupInterval) + { + ProcessCacheCleanup(); + TimeSinceLastCacheCleanup = 0.0f; } } -bool UDTFluxQueuedManager::IsTickable() const +void FDTFluxQueuedRequestManager::ChangeRequestState(TSharedPtr Request, + EDTFluxRequestState NewState) { - return bIsInitialized; + if (!Request.IsValid()) + return; + + const EDTFluxRequestState OldState = Request->State; + Request->State = NewState; + + // Déclencher l'événement de changement d'état + OnRequestStateChanged.Broadcast(Request->RequestId, NewState); + + UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("DTFluxQueuedRequestManager: Request %s state changed from %s to %s"), + *Request->RequestId.ToString(), + *UEnum::GetValueAsString(OldState), + *UEnum::GetValueAsString(NewState)); } -TStatId UDTFluxQueuedManager::GetStatId() const +void FDTFluxQueuedRequestManager::ProcessTimeouts() { - RETURN_QUICK_DECLARE_CYCLE_STAT(UDTFluxQueuedManager, STATGROUP_Tickables); + FScopeLock Lock(&RequestsLock); + + TArray> TimedOutRequests; + + for (const auto& Pair : AllRequests) + { + const auto& Request = Pair.Value; + if (Request->HasTimedOut()) + { + TimedOutRequests.Add(Request); + } + } + + for (const auto& Request : TimedOutRequests) + { + Request->LastErrorMessage = FString::Printf( + TEXT("Request timed out after %.1f seconds"), Request->Config.TimeoutSeconds); + + if (Request->CanRetry()) + { + Request->CurrentRetries++; + ChangeRequestState(Request, EDTFluxRequestState::Retrying); + } + else + { + ChangeRequestState(Request, EDTFluxRequestState::TimedOut); + TriggerCallbacks(*Request); + OnRequestFailed.Broadcast(*Request); + } + } +} + +void FDTFluxQueuedRequestManager::ProcessRetries() +{ + FScopeLock Lock(&RequestsLock); + + const FDateTime Now = FDateTime::Now(); + TArray> ReadyToRetry; + + for (const auto& Pair : AllRequests) + { + const auto& Request = Pair.Value; + if (Request->State == EDTFluxRequestState::Retrying) + { + const float ElapsedSinceLastAttempt = (Now - Request->LastAttemptTime).GetTotalSeconds(); + if (ElapsedSinceLastAttempt >= Request->GetRetryDelay()) + { + ReadyToRetry.Add(Request); + } + } + } + + for (const auto& Request : ReadyToRetry) + { + Request->LastAttemptTime = Now; + ChangeRequestState(Request, EDTFluxRequestState::Pending); + } +} + +void FDTFluxQueuedRequestManager::ProcessCacheCleanup() +{ + CleanupExpiredCache(); + CleanupCompletedRequests(600.0f); +} + +void FDTFluxQueuedRequestManager::TriggerCallbacks(const FDTFluxTrackedRequest& Request) +{ + FScopeLock Lock(&CallbacksLock); + + if (Request.State == EDTFluxRequestState::Completed || Request.State == EDTFluxRequestState::Cached) + { + // Success Cb + const FOnDTFluxRequestSuccess* SuccessCallback = SuccessCallbacks.Find(Request.RequestId); + if (SuccessCallback && SuccessCallback->IsBound()) + { + SuccessCallback->Execute(Request); + } + } + else if (Request.State == EDTFluxRequestState::Failed || Request.State == EDTFluxRequestState::TimedOut) + { + // Error Cb + const FOnDTFluxRequestError* ErrorCallback = ErrorCallbacks.Find(Request.RequestId); + if (ErrorCallback && ErrorCallback->IsBound()) + { + ErrorCallback->Execute(Request, Request.LastErrorMessage); + } + } +} + +void FDTFluxQueuedRequestManager::CleanupCallbacks(const FGuid& RequestId) +{ + FScopeLock Lock(&CallbacksLock); + SuccessCallbacks.Remove(RequestId); + ErrorCallbacks.Remove(RequestId); +} + +void FDTFluxQueuedRequestManager::RecordCacheHit() const +{ + FScopeLock Lock(&MetricsLock); + CacheHits++; +} + +void FDTFluxQueuedRequestManager::RecordCacheMiss() const +{ + FScopeLock Lock(&MetricsLock); + CacheMisses++; +} + +void FDTFluxQueuedRequestManager::OnParsingCompleted(const FGuid& RequestId, + TSharedPtr ParsedResponse, bool bSuccess) +{ + FScopeLock Lock(&RequestsLock); + + auto* RequestPtr = AllRequests.Find(RequestId); + if (!RequestPtr || !RequestPtr->IsValid()) + return; + + auto Request = *RequestPtr; + + if (bSuccess && ParsedResponse.IsValid()) + { + Request->ParsedResponse = ParsedResponse; + Request->bIsResponseParsed = true; + + UE_LOG(logDTFluxNetwork, VeryVerbose, + TEXT("DTFluxQueuedRequestManager: Async parsing completed for request %s"), + *RequestId.ToString()); + } + else + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("DTFluxQueuedRequestManager: Async parsing failed for request %s"), + *RequestId.ToString()); + } +} + +void FDTFluxQueuedRequestManager::OnParsingFailed(const FGuid& RequestId, const FString& ErrorMessage) +{ + UE_LOG(logDTFluxNetwork, Error, TEXT("DTFluxQueuedRequestManager: Async parsing failed for request %s: %s"), + *RequestId.ToString(), + *ErrorMessage); +} + +FString FDTFluxQueuedRequestManager::GenerateCacheKey(EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, + int32 SplitId) +{ + return FString::Printf(TEXT("%s_%d_%d_%d"), + *UEnum::GetValueAsString(RequestType), + ContestId, + StageId, + SplitId); } diff --git a/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp b/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp index 22ee501..5b8b893 100644 --- a/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp +++ b/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp @@ -1,372 +1,395 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// ================================================================================================ +// DTFluxNetworkSubsystem.cpp - Implémentation complète du subsystem avec compatibilité +// ================================================================================================ #include "Subsystems/DTFluxNetworkSubsystem.h" - -#include "DTFluxCoreModule.h" +#include "DTFluxQueuedManager.h" #include "DTFluxNetworkModule.h" #include "DTFluxNetworkSettings.h" -#include "DTFluxQueuedManager.h" -#include "DTFluxQueuedManager.h" #include "JsonObjectConverter.h" -#include "Clients/DTFluxHttpClient.h" #include "Clients/DTFluxWebSocketClient.h" #include "Struct/DTFluxServerResponseStruct.h" #include "Struct/DTFluxRequestStructs.h" -#include "Struct/DTFluxRaceDataServerResponse.h" -#include "Struct/DTFluxRankingServerResponse.h" -#include "Struct/DTFluxSplitSensorServerResponse.h" -#include "Struct/DTFluxTeamListServerResponse.h" #include "Types/Objects/UDTFluxParticipantFactory.h" -#include "Types/Struct/DTFluxRaceDataStructs.h" -#include "Types/Struct/DTFluxSplitSensor.h" - -// === CONNEXION WEBSOCKET === -void UDTFluxNetworkSubsystem::Connect() -{ - WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port)); - WsClient->Connect(); -} - -void UDTFluxNetworkSubsystem::Disconnect() -{ - WsClient->Disconnect(); -} - -void UDTFluxNetworkSubsystem::Reconnect() -{ - ReconnectWs(FName("Ws_Client_0")); -} - -// === REQUÊTES AVEC TRACKING === - -FGuid UDTFluxNetworkSubsystem::SendTrackedRequest( - EDTFluxApiDataType RequestType, - int32 ContestId, - int32 StageId, - int32 SplitId, - float TimeoutSeconds) -{ - if (!QueueManager) - { - UE_LOG(logDTFluxNetwork, Error, TEXT("QueueManager is not initialized")); - return FGuid(); - } - - // Vérifier si une requête similaire est déjà en cours (optionnel) - if (IsRequestPending(RequestType, ContestId, StageId, SplitId)) - { - UE_LOG(logDTFluxNetwork, Warning, - TEXT("Similar request already pending: Type=%d, Contest=%d, Stage=%d, Split=%d"), - (int32)RequestType, ContestId, StageId, SplitId); - } - - // Créer et enqueue la requête - FGuid RequestId = QueueManager->QueueRequest(RequestType, ContestId, StageId, SplitId); - - // Envoyer immédiatement si possible (le QueueManager gère la queue) - if (const FDTFluxQueuedRequest* QueuedRequest = QueueManager->GetRequest(RequestId)) - { - SendQueuedRequest(*QueuedRequest); - } - - UE_LOG(logDTFluxNetwork, Log, TEXT("Queued tracked request %s: Type=%d, Contest=%d, Stage=%d, Split=%d"), - *RequestId.ToString(), (int32)RequestType, ContestId, StageId, SplitId); - - return RequestId; -} - -FGuid UDTFluxNetworkSubsystem::SendTrackedRequestWithCallback( - EDTFluxApiDataType RequestType, - int32 ContestId, - int32 StageId, - int32 SplitId, - FOnDTFluxTrackedRequestResponse OnCompleted, - FOnDTFluxTrackedRequestTimeout OnTimeout, - TOptional OnError, - float TimeoutSeconds) -{ - FGuid RequestId = SendTrackedRequest(RequestType, ContestId, StageId, SplitId, TimeoutSeconds); - - if (RequestId.IsValid()) - { - // Stocker les callbacks pour cette requête - if (OnCompleted.IsBound()) - { - PendingCallbacks.Add(RequestId, OnCompleted); - } - if (OnTimeout.IsBound()) - { - PendingTimeoutCallbacks.Add(RequestId, OnTimeout); - } - if (OnError.IsSet() && OnError.GetValue().IsBound()) - { - PendingErrorCallbacks.Add(RequestId, OnError.GetValue()); - } - } - - return RequestId; -} - - -bool UDTFluxNetworkSubsystem::GetTrackedRequest(const FGuid& RequestId, FDTFluxQueuedRequest& OutRequest) const -{ - if (!QueueManager) - { - return false; - } - - const FDTFluxQueuedRequest* Request = QueueManager->GetRequest(RequestId); - if (Request) - { - OutRequest = *Request; - return true; - } - return false; -} - -const FDTFluxQueuedRequest* UDTFluxNetworkSubsystem::GetTrackedRequestPtr(const FGuid& RequestId) const -{ - if (!QueueManager) - { - return nullptr; - } - return QueueManager->GetRequest(RequestId); -} - -bool UDTFluxNetworkSubsystem::HasRequestReceivedResponse(const FGuid& RequestId) const -{ - FDTFluxQueuedRequest Request; - if (GetTrackedRequest(RequestId, Request)) - { - return Request.bHasReceivedResponse; - } - return false; -} - -FString UDTFluxNetworkSubsystem::GetRequestResponseData(const FGuid& RequestId) const -{ - FDTFluxQueuedRequest Request; - if (GetTrackedRequest(RequestId, Request)) - { - return Request.RawResponse; - } - return FString(); -} - -bool UDTFluxNetworkSubsystem::IsRequestPending(EDTFluxRequestType RequestType, int32 ContestId, int32 StageId, - int32 SplitId) const -{ - if (!QueueManager) - { - return false; - } - FGuid OutRequestId; - return QueueManager->IsRequestPending(OutRequestId, RequestType, ContestId, StageId, SplitId); -} - -int32 UDTFluxNetworkSubsystem::GetPendingRequestCount() const -{ - if (!QueueManager) - { - return 0; - } - return QueueManager->GetPendingRequestCount(); -} - -UDTFluxQueuedManager* UDTFluxNetworkSubsystem::GetQueueManager() const -{ - return QueueManager; -} - - -void UDTFluxNetworkSubsystem::SendRequest(const EDTFluxRequestType RequestType, int InContestId, int InStageId, - int InSplitId) -{ - FString Message; - switch (RequestType) - { - case EDTFluxRequestType::ContestRanking: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxContestRankingRequest(InContestId), Message); - break; - case EDTFluxRequestType::StageRanking: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxStageRankingRequest(InContestId, InStageId), Message); - break; - case EDTFluxRequestType::SplitRanking: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxSplitRankingRequest(InContestId, InStageId, InSplitId), - Message); - break; - case EDTFluxRequestType::TeamList: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxTeamListRequest(), Message); - break; - case EDTFluxRequestType::RaceData: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxRaceDataRequest(), Message); - break; - default: - return; - } - //Dirty trick to fix Case - Message = Message.Replace(TEXT("Id"),TEXT("ID"), ESearchCase::CaseSensitive); - UE_LOG(logDTFluxCore, Warning, TEXT("Sending Request %s"), *Message); - SendMessage(Message); -} - -void UDTFluxNetworkSubsystem::SendMessage(const FString& Message) -{ - UE_LOG(logDTFluxCore, Warning, TEXT("Sending Message %s"), *Message); - - - if (WsClient.IsValid() && WsClient->CanSend()) - { - WsClient->Send(Message); - UE_LOG(logDTFluxNetwork, Log, TEXT("Can send request")); - } - else - { - UE_LOG(logDTFluxNetwork, Error, TEXT("[Websocket Not Connected]. Connect before sending requests...")); - } -} +// ================================================================================================ +// LIFECYCLE DU SUBSYSTEM +// ================================================================================================ void UDTFluxNetworkSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); - FDTFluxCoreModule& DTFluxCore = FModuleManager::Get().LoadModuleChecked("DTFluxCore"); + UE_LOG(logDTFluxNetwork, Log, TEXT("Initializing DTFluxNetworkSubsystem...")); + + // === CHARGER LES PARAMÈTRES === UDTFluxNetworkSettings* NetworkSettings = GetMutableDefault(); UDTFluxNetworkSettings::GetWebSocketSettings(NetworkSettings, WsSettings); - UDTFluxNetworkSettings::GetHTTPSettings(NetworkSettings, HttpSettings); + + // === CRÉER LES CLIENTS RÉSEAU === WsClient = MakeShareable(new FDTFluxWebSocketClient()); - HttpClient = MakeShareable(new FDTFluxHttpClient()); + + // === CRÉER LE GESTIONNAIRE DE REQUÊTES OPTIMISÉ === + RequestManager = MakeShared(); + + FDTFluxRequestConfig DefaultConfig; + DefaultConfig.TimeoutSeconds = 5.0f; + DefaultConfig.MaxRetries = 3; + DefaultConfig.RetryBackoffMultiplier = 1.5f; + DefaultConfig.bEnableCache = true; + DefaultConfig.CacheValiditySeconds = 60.0f; + + RequestManager->Initialize(DefaultConfig); + + // === CONNECTER LES ÉVÉNEMENTS === RegisterWebSocketEvents(); - RegisterHttpEvents(); + + // Connecter les événements du RequestManager aux delegates Blueprint + RequestManager->OnRequestCompleted.AddUObject(this, &UDTFluxNetworkSubsystem::OnRequestCompleted_Internal); + RequestManager->OnRequestFailed.AddUObject(this, &UDTFluxNetworkSubsystem::OnRequestFailed_Internal); + #if WITH_EDITOR + // Écouter les changements de configuration en éditeur NetworkSettings->OnDTFluxWebSocketSettingsChanged.AddUFunction(this, FName("WsSettingsChanged")); - NetworkSettings->OnDTFluxHttpSettingsChanged.AddUFunction(this, FName("HttpSettingsChanged")); #endif + + // === CONNEXION AUTOMATIQUE === if (WsSettings.bShouldConnectAtStartup) { WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port)); WsClient->Connect(); } - // Initialisation du Queue Manager - QueueManager = NewObject(this); - QueueManager->Initialize(); - - // Connexion au delegate de timeout du Queue Manager - QueueManager->OnRequestTimedOut.AddDynamic(this, &UDTFluxNetworkSubsystem::OnRequestTimedOut_Internal); + UE_LOG(logDTFluxNetwork, Log, + TEXT("DTFluxNetworkSubsystem initialized successfully with optimized RequestManager")); } void UDTFluxNetworkSubsystem::Deinitialize() { - Super::Deinitialize(); - // Nettoyer le Queue Manager - if (QueueManager) + UE_LOG(logDTFluxNetwork, Log, TEXT("Deinitializing DTFluxNetworkSubsystem...")); + + // === NETTOYER LE REQUEST MANAGER === + if (RequestManager.IsValid()) { - QueueManager->OnRequestTimedOut.RemoveDynamic(this, &UDTFluxNetworkSubsystem::OnRequestTimedOut_Internal); - QueueManager->ClearAllRequests(); + RequestManager->OnRequestCompleted.RemoveAll(this); + RequestManager->OnRequestFailed.RemoveAll(this); + RequestManager->Shutdown(); + RequestManager.Reset(); } - // Nettoyer les callbacks - PendingCallbacks.Empty(); - PendingTimeoutCallbacks.Empty(); - // Déconnexion des clients + // === DÉCONNECTER LES CLIENTS === UnregisterWebSocketEvents(); - UnregisterHttpEvents(); + + if (WsClient.IsValid()) + { + WsClient->Disconnect(); + WsClient.Reset(); + } + + + Super::Deinitialize(); + UE_LOG(logDTFluxNetwork, Log, TEXT("DTFluxNetworkSubsystem deinitialized")); } -void UDTFluxNetworkSubsystem::WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings) -{ - // TODO Implement a ClientSelector To retrieve impacted WsClients and populate changes or maybe create a delegate - bool bNeedsReload = WsSettings != NewWsSettings; +// ================================================================================================ +// CONNEXION WEBSOCKET +// ================================================================================================ - WsSettings = NewWsSettings; - if (bNeedsReload || WsSettings.bShouldConnectAtStartup) +void UDTFluxNetworkSubsystem::Connect() +{ + if (WsClient.IsValid()) { - UE_LOG(logDTFluxNetwork, Warning, TEXT("WSocket Settings needs Reloding client")) - ReconnectWs(FName("Ws_Client_0")); + WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port)); + WsClient->Connect(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Connecting to WebSocket: %s"), *WsClient->GetAddress()); } } - -void UDTFluxNetworkSubsystem::HttpSettingsChanged(const FDTFluxHttpSettings& NewHttpSettings) +void UDTFluxNetworkSubsystem::Disconnect() { - // TODO Implement a ClientSelector To retrieve impacted HttpClients and populate changes or maybe create a delegate - HttpSettings = NewHttpSettings; + if (WsClient.IsValid()) + { + WsClient->Disconnect(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Disconnecting from WebSocket")); + } } -void UDTFluxNetworkSubsystem::ReconnectWs(const FName WsClientId) +void UDTFluxNetworkSubsystem::Reconnect() { - FString NewAddress = ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port); - WsClient->SetAddress(NewAddress); - WsClient->Reconnect(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Reconnecting WebSocket...")); + ReconnectWs(FName("Ws_Client_0")); } -void UDTFluxNetworkSubsystem::ReconnectHttp(const FName WsClientId) +// ================================================================================================ +// REQUÊTES TRACKÉES (NOUVEAU SYSTÈME OPTIMISÉ) +// ================================================================================================ + +FGuid UDTFluxNetworkSubsystem::SendTrackedRequest( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + float TimeoutSeconds, + int32 MaxRetries, + bool bEnableCache) { + if (!RequestManager.IsValid()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("RequestManager not available")); + return FGuid(); + } + + // Configuration personnalisée pour cette requête + FDTFluxRequestConfig CustomConfig; + CustomConfig.TimeoutSeconds = TimeoutSeconds; + CustomConfig.MaxRetries = MaxRetries; + CustomConfig.bEnableCache = bEnableCache; + CustomConfig.CacheValiditySeconds = 60.0f; + CustomConfig.RetryBackoffMultiplier = 1.5f; + + FGuid RequestId = RequestManager->CreateTrackedRequest(RequestType, ContestId, StageId, SplitId, CustomConfig); + + if (RequestId.IsValid()) + { + // Récupérer la requête pour l'envoyer + if (const FDTFluxTrackedRequest* Request = RequestManager->GetRequestPtr(RequestId)) + { + // Si la requête est déjà en cache, pas besoin d'envoyer + if (Request->State != EDTFluxRequestState::Cached) + { + RequestManager->MarkRequestAsSent(RequestId); + SendQueuedRequest(*Request); + } + } + } + + return RequestId; } +FGuid UDTFluxNetworkSubsystem::SendTrackedRequestWithCallbacks( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + FOnDTFluxRequestSuccess& OnSuccess, + FOnDTFluxRequestError& OnError, + float TimeoutSeconds, + int32 MaxRetries, + bool bEnableCache) +{ + if (!RequestManager.IsValid()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("RequestManager not available")); + return FGuid(); + } + + FDTFluxRequestConfig CustomConfig; + CustomConfig.TimeoutSeconds = TimeoutSeconds; + CustomConfig.MaxRetries = MaxRetries; + CustomConfig.bEnableCache = bEnableCache; + CustomConfig.CacheValiditySeconds = 60.0f; + CustomConfig.RetryBackoffMultiplier = 1.5f; + + FGuid RequestId = RequestManager->CreateTrackedRequestWithCallbacks( + RequestType, ContestId, StageId, SplitId, OnSuccess, OnError, CustomConfig); + + if (RequestId.IsValid()) + { + if (const FDTFluxTrackedRequest* Request = RequestManager->GetRequestPtr(RequestId)) + { + if (Request->State != EDTFluxRequestState::Cached) + { + RequestManager->MarkRequestAsSent(RequestId); + SendQueuedRequest(*Request); + } + } + } + + return RequestId; +} + +// ================================================================================================ +// ACCESSEURS BLUEPRINT POUR LES REQUÊTES TRACKÉES +// ================================================================================================ + +bool UDTFluxNetworkSubsystem::GetTrackedRequest(const FGuid& RequestId, FDTFluxTrackedRequest& OutRequest) const +{ + if (RequestManager.IsValid()) + { + return RequestManager->GetRequest(RequestId, OutRequest); + } + return false; +} + +bool UDTFluxNetworkSubsystem::HasRequestReceivedResponse(const FGuid& RequestId) const +{ + FDTFluxTrackedRequest Request; + if (GetTrackedRequest(RequestId, Request)) + { + return Request.State == EDTFluxRequestState::Completed || + Request.State == EDTFluxRequestState::Cached || + !Request.RawResponseData.IsEmpty(); + } + return false; +} + +FString UDTFluxNetworkSubsystem::GetRequestResponseData(const FGuid& RequestId) const +{ + FDTFluxTrackedRequest Request; + if (GetTrackedRequest(RequestId, Request)) + { + return Request.RawResponseData; + } + return FString(); +} + +bool UDTFluxNetworkSubsystem::IsRequestPending(EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, + int32 SplitId) const +{ + if (RequestManager.IsValid()) + { + FGuid OutRequestId; + return RequestManager->FindPendingRequest(OutRequestId, RequestType, ContestId, StageId, SplitId); + } + return false; +} + +int32 UDTFluxNetworkSubsystem::GetPendingRequestCount() const +{ + if (RequestManager.IsValid()) + { + return RequestManager->GetRequestCount(EDTFluxRequestState::Pending) + + RequestManager->GetRequestCount(EDTFluxRequestState::Sent); + } + return 0; +} + +void UDTFluxNetworkSubsystem::GetRequestStatistics(int32& OutPending, int32& OutCached, int32& OutCompleted, + int32& OutFailed, float& OutHitRate) const +{ + if (RequestManager.IsValid()) + { + FDTFluxQueuedRequestManager::FRequestStatistics Stats = RequestManager->GetStatistics(); + OutPending = Stats.Pending; + OutCached = Stats.Cached; + OutCompleted = Stats.Completed; + OutFailed = Stats.Failed; + OutHitRate = Stats.HitRate; + } + else + { + OutPending = OutCached = OutCompleted = OutFailed = 0; + OutHitRate = 0.0f; + } +} + +// ================================================================================================ +// REQUÊTES LEGACY (COMPATIBILITÉ TOTALE) +// ================================================================================================ + +void UDTFluxNetworkSubsystem::SendRequest(const EDTFluxApiDataType RequestType, int InContestId, int InStageId, + int InSplitId) +{ + FString Message; + switch (RequestType) + { + case EDTFluxApiDataType::ContestRanking: + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxContestRankingRequest(InContestId), Message); + break; + case EDTFluxApiDataType::StageRanking: + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxStageRankingRequest(InContestId, InStageId), Message); + break; + case EDTFluxApiDataType::SplitRanking: + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxSplitRankingRequest(InContestId, InStageId, InSplitId), + Message); + break; + case EDTFluxApiDataType::TeamList: + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxTeamListRequest(), Message); + break; + case EDTFluxApiDataType::RaceData: + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxRaceDataRequest(), Message); + break; + default: + UE_LOG(logDTFluxNetwork, Warning, TEXT("Unknown request type: %s"), *UEnum::GetValueAsString(RequestType)); + return; + } + + // Dirty trick to fix Case (compatibilité avec l'ancienne API) + Message = Message.Replace(TEXT("Id"), TEXT("ID"), ESearchCase::CaseSensitive); + UE_LOG(logDTFluxNetwork, Warning, TEXT("Sending Legacy Request %s"), *Message); + SendMessage(Message); +} + +void UDTFluxNetworkSubsystem::SendMessage(const FString& Message) +{ + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Sending Message: %s"), *Message); + + if (WsClient.IsValid() && WsClient->CanSend()) + { + WsClient->Send(Message); + UE_LOG(logDTFluxNetwork, Log, TEXT("Message sent successfully")); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("WebSocket not connected. Connect before sending requests.")); + } +} + +// ================================================================================================ +// GESTION DES ÉVÉNEMENTS WEBSOCKET +// ================================================================================================ + void UDTFluxNetworkSubsystem::RegisterWebSocketEvents() { - OnWsConnectedEventDelegateHandle = - WsClient->RegisterConnectedEvent().AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketConnected_Subsystem); - OnWsConnectionErrorEventDelegateHandle = - WsClient->RegisterConnectionError() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem); - OnWsClosedEventDelegateHandle = - WsClient->RegisterClosedEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem); - OnWsMessageEventDelegateHandle = - WsClient->RegisterMessageEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem); - OnWsMessageSentEventDelegateHandle = - WsClient->RegisterMessageSentEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem); -} + if (!WsClient.IsValid()) return; -void UDTFluxNetworkSubsystem::RegisterHttpEvents() -{ + OnWsConnectedEventDelegateHandle = WsClient->RegisterConnectedEvent() + .AddUObject( + this, &UDTFluxNetworkSubsystem::OnWebSocketConnected_Subsystem); + OnWsConnectionErrorEventDelegateHandle = WsClient->RegisterConnectionError() + .AddUObject( + this, + &UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem); + OnWsClosedEventDelegateHandle = WsClient->RegisterClosedEvent() + .AddUObject( + this, &UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem); + OnWsMessageEventDelegateHandle = WsClient->RegisterMessageEvent() + .AddUObject( + this, &UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem); + OnWsMessageSentEventDelegateHandle = WsClient->RegisterMessageSentEvent() + .AddUObject( + this, + &UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem); } void UDTFluxNetworkSubsystem::UnregisterWebSocketEvents() { - if (OnWsConnectedEventDelegateHandle.IsValid()) - { - WsClient->UnregisterConnectedEvent().Remove(OnWsConnectedEventDelegateHandle); - } - if (OnWsConnectionErrorEventDelegateHandle.IsValid()) - { - WsClient->UnregisterConnectionError().Remove(OnWsConnectionErrorEventDelegateHandle); - } - if (OnWsClosedEventDelegateHandle.IsValid()) - { - WsClient->UnregisterClosedEvent().Remove(OnWsClosedEventDelegateHandle); - } - if (OnWsMessageEventDelegateHandle.IsValid()) - { - WsClient->UnregisterMessageEvent().Remove(OnWsMessageEventDelegateHandle); - } - if (OnWsMessageSentEventDelegateHandle.IsValid()) - { - WsClient->UnregisterRawMessageEvent().Remove(OnWsMessageSentEventDelegateHandle); - } -} + if (!WsClient.IsValid()) return; -void UDTFluxNetworkSubsystem::UnregisterHttpEvents() -{ + if (OnWsConnectedEventDelegateHandle.IsValid()) + WsClient->UnregisterConnectedEvent().Remove(OnWsConnectedEventDelegateHandle); + if (OnWsConnectionErrorEventDelegateHandle.IsValid()) + WsClient->UnregisterConnectionError().Remove(OnWsConnectionErrorEventDelegateHandle); + if (OnWsClosedEventDelegateHandle.IsValid()) + WsClient->UnregisterClosedEvent().Remove(OnWsClosedEventDelegateHandle); + if (OnWsMessageEventDelegateHandle.IsValid()) + WsClient->UnregisterMessageEvent().Remove(OnWsMessageEventDelegateHandle); + if (OnWsMessageSentEventDelegateHandle.IsValid()) + WsClient->UnregisterMessageSentEvent().Remove(OnWsMessageSentEventDelegateHandle); } void UDTFluxNetworkSubsystem::OnWebSocketConnected_Subsystem() { WsStatus = EDTFluxConnectionStatus::Connected; OnWebSocketConnected.Broadcast(); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws Is Connected with %s"), *WsClient->GetAddress()) + UE_LOG(logDTFluxNetwork, Warning, TEXT("WebSocket connected to %s"), *WsClient->GetAddress()); } void UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem(const FString& Error) { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws Error with %s : %s"), *WsClient->GetAddress(), *Error); + UE_LOG(logDTFluxNetwork, Warning, TEXT("WebSocket error with %s: %s"), *WsClient->GetAddress(), *Error); WsStatus = EDTFluxConnectionStatus::Error; + if (WsSettings.bShouldAutoReconnectOnError) { WsClient->Reconnect(); @@ -375,124 +398,303 @@ void UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem(const FString void UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem(int32 StatusCode, const FString& Reason, bool bWasClean) { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws Error with %s :\n Reason : %s \tStatusCode : %i, bWasClean : %s"), - *WsClient->GetAddress(), *Reason, StatusCode, bWasClean ? TEXT("True") : TEXT("False")); + UE_LOG(logDTFluxNetwork, Warning, TEXT("WebSocket closed: Reason=%s, StatusCode=%d, Clean=%s"), + *Reason, StatusCode, bWasClean ? TEXT("True") : TEXT("False")); WsStatus = EDTFluxConnectionStatus::Closed; } +void UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem(const FString& MessageString) +{ + UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("Received WebSocket message: %s"), *MessageString); + + // Essayer d'abord de matcher avec une requête trackée + if (!TryMatchResponseToQueuedRequest(MessageString)) + { + // Si pas de match, traiter en mode legacy + ProcessLegacyResponse(MessageString); + } +} + +void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent) +{ + UE_LOG(logDTFluxNetwork, VeryVerbose, TEXT("WebSocket message sent: %s"), *MessageSent); +} + +bool UDTFluxNetworkSubsystem::TryMatchResponseToQueuedRequest(const FString& MessageString) +{ + if (!RequestManager.IsValid()) + return false; + + // Parser rapidement pour identifier le type de réponse + EDTFluxResponseStatus Status; + FDTFluxServerResponse QuickResponse(MessageString, Status, false); + + if (Status != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Failed to parse response for matching: %s"), + *QuickResponse.GetErrorMessage()); + return false; + } + + // Chercher une requête correspondante + FGuid FoundRequestId; + if (RequestManager->FindPendingRequest(FoundRequestId, QuickResponse.GetResponseType(), + QuickResponse.ContestID, QuickResponse.StageID, QuickResponse.SplitID)) + { + // Utiliser le parsing asynchrone pour les réponses volumineuses + bool bUseAsyncParsing = ShouldUseAsyncParsing(MessageString); + + RequestManager->CompleteRequest(FoundRequestId, MessageString, bUseAsyncParsing); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Matched response to tracked request %s (async=%s)"), + *FoundRequestId.ToString(), bUseAsyncParsing ? TEXT("true") : TEXT("false")); + return true; + } + + return false; +} + +void UDTFluxNetworkSubsystem::ProcessLegacyResponse(const FString& MessageString) +{ + UE_LOG(logDTFluxNetwork, Log, TEXT("Processing legacy response")); + + // Parsing synchrone pour le mode legacy (pour simplicité) + EDTFluxResponseStatus Status; + TSharedPtr ParsedResponse = MakeShared(MessageString, Status); + if (Status == EDTFluxResponseStatus::Success) + { + ProcessParsedResponse(ParsedResponse); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse legacy response: %s"), + *ParsedResponse->GetErrorMessage()); + } +} + +void UDTFluxNetworkSubsystem::ProcessParsedResponse(TSharedPtr ParsedResponse) +{ + if (!ParsedResponse.IsValid()) + return; + + // Traiter les messages push (temps réel) d'abord + if (ProcessPushMessage(*ParsedResponse) == EDTFluxResponseStatus::Success) + { + return; // Message push traité avec succès + } + + // Traiter les réponses classiques selon le type + switch (ParsedResponse->GetResponseType()) + { + case EDTFluxApiDataType::RaceData: + ParseRaceData(*ParsedResponse); + break; + case EDTFluxApiDataType::TeamList: + ParseTeamListResponse(*ParsedResponse); + break; + case EDTFluxApiDataType::ContestRanking: + ParseContestRanking(*ParsedResponse); + break; + case EDTFluxApiDataType::StageRanking: + ParseStageRankingResponse(*ParsedResponse); + break; + case EDTFluxApiDataType::SplitRanking: + ParseSplitRankingResponse(*ParsedResponse); + break; + default: + UE_LOG(logDTFluxNetwork, Warning, TEXT("Unknown response type in legacy processing: %s"), + *UEnum::GetValueAsString(ParsedResponse->GetResponseType())); + break; + } +} + +// ================================================================================================ +// CALLBACKS POUR LE REQUEST MANAGER +// ================================================================================================ + +void UDTFluxNetworkSubsystem::OnRequestCompleted_Internal(const FDTFluxTrackedRequest& CompletedRequest) +{ + // Broadcaster l'événement Blueprint + OnTrackedRequestCompleted.Broadcast( + CompletedRequest.RequestId, + CompletedRequest.RequestType, + CompletedRequest.RawResponseData + ); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Tracked request completed: %s"), *CompletedRequest.RequestId.ToString()); +} + +void UDTFluxNetworkSubsystem::OnRequestFailed_Internal(const FDTFluxTrackedRequest& FailedRequest) +{ + // Broadcaster l'événement Blueprint + OnTrackedRequestFailed.Broadcast( + FailedRequest.RequestId, + FailedRequest.RequestType, + FailedRequest.LastErrorMessage + ); + + UE_LOG(logDTFluxNetwork, Warning, TEXT("Tracked request failed: %s - %s"), + *FailedRequest.RequestId.ToString(), *FailedRequest.LastErrorMessage); +} + +// ================================================================================================ +// CONFIGURATION DYNAMIQUE +// ================================================================================================ + +void UDTFluxNetworkSubsystem::WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings) +{ + bool bNeedsReload = WsSettings != NewWsSettings; + WsSettings = NewWsSettings; + + if (bNeedsReload || WsSettings.bShouldConnectAtStartup) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("WebSocket settings changed, reconnecting...")); + ReconnectWs(FName("Ws_Client_0")); + } +} + +void UDTFluxNetworkSubsystem::ReconnectWs(const FName WsClientId) +{ + if (WsClient.IsValid()) + { + FString NewAddress = ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port); + WsClient->SetAddress(NewAddress); + WsClient->Reconnect(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Reconnecting to: %s"), *NewAddress); + } +} + +// ================================================================================================ +// MÉTHODES DE PARSING LEGACY (COMPATIBILITÉ TOTALE) +// ================================================================================================ + void UDTFluxNetworkSubsystem::ParseTeamListResponse(FDTFluxServerResponse& Response) { FDTFluxTeamListDefinition TeamListDefinition; Response.ParseTeamListResponse(TeamListDefinition); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Parsing Team List Response")); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseTeamListResponse() for JSON Response : %s"), *Response.RawMessage); - return; + OnTeamListReceived.ExecuteIfBound(TeamListDefinition); + UE_LOG(logDTFluxNetwork, Log, TEXT("Team list parsed: %d participants"), TeamListDefinition.Participants.Num()); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse team list: %s"), *Response.GetErrorMessage()); } - UE_LOG(logDTFluxNetwork, Warning, TEXT("PArsing OK. Sending to Core...")); - const bool bIsSuccessfullyBounded = OnTeamListReceived.ExecuteIfBound(TeamListDefinition); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Inserting %i Participants [%s]"), TeamListDefinition.Participants.Num(), - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } void UDTFluxNetworkSubsystem::ParseRaceData(FDTFluxServerResponse& Response) { FDTFluxRaceData RaceData; Response.ParseRaceData(RaceData); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) - { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseRaceData() for JSON Response : %s"), *Response.RawMessage); - return; - } - const bool bIsSuccessfullyBounded = OnRaceDataReceived.ExecuteIfBound(RaceData); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Sending %i Contests, [%s]"), RaceData.Datas.Num(), - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) + { + OnRaceDataReceived.ExecuteIfBound(RaceData); + UE_LOG(logDTFluxNetwork, Log, TEXT("Race data parsed: %d contests"), RaceData.Datas.Num()); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse race data: %s"), *Response.GetErrorMessage()); + } } void UDTFluxNetworkSubsystem::ParseContestRanking(FDTFluxServerResponse& Response) { FDTFluxContestRankings ContestRankings; Response.ParseContestRanking(ContestRankings); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseContestRanking() for JSON Response : %s"), *Response.RawMessage); - return; + OnContestRankingReceived.ExecuteIfBound(ContestRankings); + UE_LOG(logDTFluxNetwork, Log, TEXT("Contest ranking parsed for Contest %d: %d entries"), + ContestRankings.ContestId, ContestRankings.Rankings.Num()); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse contest ranking: %s"), *Response.GetErrorMessage()); } - const bool bIsSuccessfullyBounded = OnContestRankingReceived.ExecuteIfBound(ContestRankings); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws ContestRanking Data Sent for Contest %i, [%s]"), - ContestRankings.ContestId, - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } void UDTFluxNetworkSubsystem::ParseStageRankingResponse(FDTFluxServerResponse& Response) { FDTFluxStageRankings StageRankings; Response.ParseStageRankingResponse(StageRankings); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStageRankingResponse() for JSON Response : %s"), - *Response.RawMessage); + OnStageRankingReceived.ExecuteIfBound(StageRankings); + UE_LOG(logDTFluxNetwork, Log, TEXT("Stage ranking parsed for Contest %d, Stage %d: %d entries"), + StageRankings.ContestId, StageRankings.StageId, StageRankings.Rankings.Num()); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse stage ranking: %s"), *Response.GetErrorMessage()); } - const bool bIsSuccessfullyBounded = OnStageRankingReceived.ExecuteIfBound(StageRankings); - UE_LOG(logDTFluxNetwork, Warning, TEXT("StageRanking Data Sent for Contest %i, Stage %i\n[Result] : %s"), - StageRankings.ContestId, StageRankings.StageId, - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } void UDTFluxNetworkSubsystem::ParseSplitRankingResponse(FDTFluxServerResponse& Response) { FDTFluxSplitRankings SplitRankings; Response.ParseSplitRankingResponse(SplitRankings); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitRankingResponse() for JSON Response : %s"), - *Response.RawMessage); + OnSplitRankingReceived.ExecuteIfBound(SplitRankings); + UE_LOG(logDTFluxNetwork, Log, TEXT("Split ranking parsed for Contest %d, Stage %d, Split %d: %d entries"), + SplitRankings.ContestId, SplitRankings.StageId, SplitRankings.SplitId, SplitRankings.Rankings.Num()); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse split ranking: %s"), *Response.GetErrorMessage()); } - const bool bIsSuccessfullyBounded = OnSplitRankingReceived.ExecuteIfBound(SplitRankings); - UE_LOG(logDTFluxNetwork, Warning, TEXT("SplitRanking Data Sent for Contest %i, Stage %i, Split %i\n[Result] : %s"), - SplitRankings.ContestId, SplitRankings.StageId, SplitRankings.SplitId, - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } void UDTFluxNetworkSubsystem::ParseStatusUpdateResponse(FDTFluxServerResponse& Response) { FDTFluxTeamStatusUpdate StatusUpdate; Response.ParseStatusUpdateResponse(StatusUpdate); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStatusUpdateResponse() for JSON Response : %s"), - *Response.RawMessage); + OnTeamStatusUpdateReceived.ExecuteIfBound(StatusUpdate); + UE_LOG(logDTFluxNetwork, Log, TEXT("Status update parsed for Bib %d: %s"), + StatusUpdate.Bib, *UEnum::GetValueAsString(StatusUpdate.Status)); + } + else + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse status update: %s"), *Response.GetErrorMessage()); } - const bool bIsSuccessfullyBounded = OnTeamStatusUpdateReceived.ExecuteIfBound(StatusUpdate); - UE_LOG(logDTFluxNetwork, Warning, TEXT("StatusUpdate Data Sent for Bib %i with new status %s\n[Result] : %s"), - StatusUpdate.Bib, *UEnum::GetValueAsString(StatusUpdate.Status), - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } void UDTFluxNetworkSubsystem::ParseSplitSensorResponse(FDTFluxServerResponse& Response) { - TArray SplitSensorInfos = TArray(); + TArray SplitSensorInfos; Response.ParseSplitSensorResponse(SplitSensorInfos); - if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + + if (Response.GetParsingStatus() == EDTFluxResponseStatus::Success) { - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitSensorResponse() for JSON Response : %s"), - *Response.RawMessage); + for (const auto& SplitSensorInfo : SplitSensorInfos) + { + OnSplitSensorReceived.ExecuteIfBound(SplitSensorInfo); + UE_LOG(logDTFluxNetwork, VeryVerbose, + TEXT("Split sensor parsed for Bib %d on Contest %d, Stage %d, Split %d"), + SplitSensorInfo.Bib, SplitSensorInfo.ContestId, SplitSensorInfo.StageId, SplitSensorInfo.SplitId); + } + UE_LOG(logDTFluxNetwork, Log, TEXT("Split sensor data parsed: %d entries"), SplitSensorInfos.Num()); } - for (auto& SplitSensorInfo : SplitSensorInfos) + else { - const bool bIsSuccessfullyBounded = OnSplitSensorReceived.ExecuteIfBound(SplitSensorInfo); - UE_LOG(logDTFluxNetwork, Warning, - TEXT("SplitSensor Data Sent for Bib %i on [Split %i] of [Stage %i] in [Contest %i]\n[Result] : %s"), - SplitSensorInfo.Bib, SplitSensorInfo.SplitId, SplitSensorInfo.StageId, SplitSensorInfo.ContestId, - bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse split sensor: %s"), *Response.GetErrorMessage()); } } EDTFluxResponseStatus UDTFluxNetworkSubsystem::ProcessPushMessage(FDTFluxServerResponse& Response) { + // Traiter les messages push (temps réel) qui ne correspondent pas à des requêtes EDTFluxResponseStatus ResponseStatus = EDTFluxResponseStatus::UnknownError; + if (DTFluxDataTypeUtils::IsPushOnly(Response.GetResponseType())) { switch (Response.GetResponseType()) @@ -536,248 +738,17 @@ EDTFluxResponseStatus UDTFluxNetworkSubsystem::ProcessPushMessage(FDTFluxServerR break; } } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Processed push message: %s"), + *UEnum::GetValueAsString(Response.GetResponseType())); } + return ResponseStatus; } -void UDTFluxNetworkSubsystem::Parse(FDTFluxServerResponse& Response) -{ - EDTFluxResponseStatus ResponseStatus = EDTFluxResponseStatus::Success; - switch (Response.GetResponseType()) - { - case EDTFluxApiDataType::RaceData: - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Legacy Parsing RaceData")); - ParseRaceData(Response); - ResponseStatus = Response.GetParsingStatus(); - break; - } - case EDTFluxApiDataType::TeamList: - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Legacy Parsing TeamList")); - ParseTeamListResponse(Response); - ResponseStatus = Response.GetParsingStatus(); - break; - } - case EDTFluxApiDataType::ContestRanking: - { - ParseContestRanking(Response); - ResponseStatus = Response.GetParsingStatus(); - break; - } - case EDTFluxApiDataType::StageRanking: - { - ParseStageRankingResponse(Response); - ResponseStatus = Response.GetParsingStatus(); - break; - } - case EDTFluxApiDataType::SplitRanking: - { - ParseSplitRankingResponse(Response); - ResponseStatus = Response.GetParsingStatus(); - break; - } - default: - { - UE_LOG(logDTFluxNetwork, Error, TEXT("Legacy Parsing Unknown")); - ResponseStatus = EDTFluxResponseStatus::UnknownError; - break; - } - } - if (ResponseStatus != EDTFluxResponseStatus::Success) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("UDTFluxNetworkSubsystem::Parse() Parsing failed")); - } -} - -void UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem(const FString& MessageString) -{ - // UE_LOG(logDTFluxNetwork, Warning, TEXT("Client %s :\nMessage Received : %s"), *WsClient->GetAddress(), *MessageString); - //Do Something With the message - EDTFluxResponseStatus ResponseStatus; - FDTFluxServerResponse Response(MessageString, ResponseStatus); - if (!TryMatchResponseToQueuedRequest(Response)) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Response %s does not match any queued request"), - *UEnum::GetValueAsString(Response.GetResponseType())); - if (ProcessPushMessage(Response) != EDTFluxResponseStatus::Success) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Not a push message")); - // Legacy - Parse(Response); - return; - } - } - // // if we are here we have a tracked Message - // QueueManager->MarkRequestAsResponded() -} - -void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent) -{ - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws %s :\nMessage Sent: %s"), *WsClient->GetAddress(), *MessageSent); -} - -bool UDTFluxNetworkSubsystem::CleanRequestCallbacks(const FGuid& RequestId) -{ - bool bCbSuppressSuccess = false; - bool bErrorCbSuppressSuccess = false; - bool bTimeoutCbSuppressSuccess = false; - if (PendingCallbacks.Contains(RequestId)) - { - PendingCallbacks.Remove(RequestId); - bCbSuppressSuccess = true; - } - if (PendingTimeoutCallbacks.Contains(RequestId)) - { - PendingTimeoutCallbacks.Remove(RequestId); - bTimeoutCbSuppressSuccess = true; - } - if (PendingTimeoutCallbacks.Contains(RequestId)) - { - PendingTimeoutCallbacks.Remove(RequestId); - bErrorCbSuppressSuccess = true; - } - return bCbSuppressSuccess && bErrorCbSuppressSuccess && bTimeoutCbSuppressSuccess; -} - -void UDTFluxNetworkSubsystem::OnRequestTimedOut_Internal(const FDTFluxQueuedRequest& TimedOutRequest) -{ - UE_LOG(logDTFluxNetwork, Warning, TEXT("Request %s timed out: Type=%d, Contest=%d, Stage=%d, Split=%d"), - *TimedOutRequest.RequestId.ToString(), - (int32)TimedOutRequest.RequestType, - TimedOutRequest.ContestId, - TimedOutRequest.StageId, - TimedOutRequest.SplitId); - - // Appeler le callback de timeout si présent - if (FOnDTFluxTrackedRequestTimeout* TimeoutCallback = PendingTimeoutCallbacks.Find(TimedOutRequest.RequestId)) - { - if (TimeoutCallback->IsBound()) - { - TimeoutCallback->Execute(TimedOutRequest.RequestId, TEXT("Request timeout")); - } - PendingTimeoutCallbacks.Remove(TimedOutRequest.RequestId); - } - - // Nettoyer les callbacks de succès aussi - PendingCallbacks.Remove(TimedOutRequest.RequestId); - - // Broadcaster l'événement Blueprint - OnTrackedRequestFailed.Broadcast(TimedOutRequest.RequestId, TimedOutRequest.RequestType, TEXT("Request timeout")); -} - -bool UDTFluxNetworkSubsystem::TryMatchResponseToQueuedRequest(FDTFluxServerResponse& Response) -{ - if (!QueueManager) - { - return false; - } - FGuid FoundRequestId; - if (QueueManager->IsRequestPending(FoundRequestId, Response.GetResponseType(), Response.ContestID, Response.StageID, - Response.SplitID)) - { - UE_LOG(logDTFluxNetwork, Log, - TEXT("Matched response to queued request: Type=%s, Contest=%d, Stage=%d, Split=%d"), - *UEnum::GetValueAsString(Response.GetResponseType()), Response.ContestID, Response.StageID, - Response.SplitID); - if (PendingCallbacks.Contains(FoundRequestId)) - { - FOnDTFluxTrackedRequestResponse* SuccessCallback = PendingCallbacks.Find(FoundRequestId); - SuccessCallback->ExecuteIfBound(FoundRequestId, Response); - //Suppress Callback; - return CleanRequestCallbacks(FoundRequestId); - } - return QueueManager->MarkRequestAsResponded(FoundRequestId); - } - - return false; -} - -void UDTFluxNetworkSubsystem::CompleteTrackedRequest(const FGuid& RequestId, const FString& ResponseData, - EDTFluxRequestType RequestType) -{ - // Marquer la requête comme ayant reçu une réponse - if (QueueManager) - { - QueueManager->MarkRequestAsResponded(RequestId); - } - - // Appeler le callback de succès si présent - if (FOnDTFluxTrackedRequestResponse* SuccessCallback = PendingCallbacks.Find(RequestId)) - { - if (SuccessCallback->IsBound()) - { - EDTFluxResponseStatus ResponseStatus; - FDTFluxServerResponse Response(ResponseData, ResponseStatus); - if (ResponseStatus == EDTFluxResponseStatus::Success) - { - SuccessCallback->Execute(RequestId, Response); - QueueManager->MarkRequestAsResponded(RequestId); - PendingCallbacks.Remove(RequestId); - PendingTimeoutCallbacks.Remove(RequestId); - } - else - { - QueueManager->MarkRequestAsError(RequestId); - // Fail - // FailTrackedRequest() - } - } - } - - // Nettoyer le callback de timeout - PendingTimeoutCallbacks.Remove(RequestId); - - // Broadcaster l'événement Blueprint - OnTrackedRequestCompleted.Broadcast(RequestId, RequestType, ResponseData); - - UE_LOG(logDTFluxNetwork, Log, TEXT("Completed tracked request %s"), *RequestId.ToString()); -} - -void UDTFluxNetworkSubsystem::FailTrackedRequest(const FGuid& RequestId, const FString& ErrorMessage, - EDTFluxRequestType RequestType) -{ - // Appeler le callback d'erreur si présent - if (FOnDTFluxTrackedRequestTimeout* ErrorCallback = PendingTimeoutCallbacks.Find(RequestId)) - { - if (ErrorCallback->IsBound()) - { - ErrorCallback->ExecuteIfBound(RequestId, ErrorMessage); - } - PendingTimeoutCallbacks.Remove(RequestId); - } - - // Nettoyer les callbacks - PendingCallbacks.Remove(RequestId); - - // Broadcaster l'événement Blueprint - OnTrackedRequestFailed.Broadcast(RequestId, RequestType, ErrorMessage); - - UE_LOG(logDTFluxNetwork, Error, TEXT("Failed tracked request %s: %s"), *RequestId.ToString(), *ErrorMessage); -} - -void UDTFluxNetworkSubsystem::SendQueuedRequest(const FDTFluxQueuedRequest& QueuedRequest) -{ - // Générer le message JSON à partir de la requête - FString Message = QueuedRequest.Serialize(); - - if (Message.IsEmpty()) - { - UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to serialize queued request %s"), - *QueuedRequest.RequestId.ToString()); - FailTrackedRequest(QueuedRequest.RequestId, TEXT("Serialization failed"), QueuedRequest.RequestType); - return; - } - - // Dirty trick to fix Case (comme dans l'original) - Message = Message.Replace(TEXT("Id"), TEXT("ID"), ESearchCase::CaseSensitive); - - UE_LOG(logDTFluxNetwork, Log, TEXT("Sending queued request %s: %s"), *QueuedRequest.RequestId.ToString(), *Message); - - // Envoyer via WebSocket - SendMessage(Message); -} - +// ================================================================================================ +// UTILITAIRES +// ================================================================================================ FString UDTFluxNetworkSubsystem::ConstructWsAddress(const FString& Address, const FString& Path, const int& Port) { @@ -788,5 +759,39 @@ FString UDTFluxNetworkSubsystem::ConstructWsAddress(const FString& Address, cons } NewAddress += Address + FString(":") + FString::FromInt(Port) + Path; return NewAddress; - // UE_LOG(logDTFluxNetwork, Log, TEXT("NewAddress : %s"), *NewAddress); +} + +void UDTFluxNetworkSubsystem::SendQueuedRequest(const FDTFluxTrackedRequest& QueuedRequest) +{ + FString Message = QueuedRequest.Serialize(); + + if (Message.IsEmpty()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to serialize request %s"), *QueuedRequest.RequestId.ToString()); + + if (RequestManager.IsValid()) + { + RequestManager->FailRequest(QueuedRequest.RequestId, TEXT("Serialization failed")); + } + return; + } + + // Dirty trick to fix Case (compatibilité avec l'ancienne API) + Message = Message.Replace(TEXT("Id"), TEXT("ID"), ESearchCase::CaseSensitive); + + UE_LOG(logDTFluxNetwork, Log, TEXT("Sending tracked request %s: %s"), + *QueuedRequest.RequestId.ToString(), *Message); + + SendMessage(Message); +} + +bool UDTFluxNetworkSubsystem::ShouldUseAsyncParsing(const FString& JsonData) const +{ + // Critères pour décider du parsing asynchrone : + // - Taille des données (> 1KB par défaut) + // - Charge actuelle du système + // - Type de données (certains types sont plus complexes à parser) + + const int32 AsyncThreshold = 1024; // 1KB + return JsonData.Len() > AsyncThreshold; } diff --git a/Source/DTFluxNetwork/Public/DTFluxAsyncParser.h b/Source/DTFluxNetwork/Public/DTFluxAsyncParser.h new file mode 100644 index 0000000..9fb0495 --- /dev/null +++ b/Source/DTFluxNetwork/Public/DTFluxAsyncParser.h @@ -0,0 +1,171 @@ +// ================================================================================================ +// DTFluxAsyncParser.h - Async Response Parser +// ================================================================================================ +#pragma once + +#include "CoreMinimal.h" +#include "HAL/CriticalSection.h" +#include "Async/TaskGraphInterfaces.h" +#include "Struct/DTFluxServerResponseStruct.h" + +// Forward declarations + +// ================================================================================================ +// DELEGATES POUR LE PARSING ASYNCHRONE +// ================================================================================================ + +DECLARE_DELEGATE_ThreeParams(FOnParsingCompleted, const FGuid& /*RequestId*/, + TSharedPtr /*ParsedResponse*/, bool /*bSuccess*/); +DECLARE_DELEGATE_TwoParams(FOnParsingFailed, const FGuid& /*RequestId*/, const FString& /*ErrorMessage*/); + +// ================================================================================================ +// ASYNC PARSER - Délégation du parsing avec TaskGraph +// ================================================================================================ + +/** + * Gestionnaire centralisé pour le parsing asynchrone des réponses JSON + * Utilise le TaskGraph d'Unreal Engine pour déléguer le parsing sur des worker threads + */ +class DTFLUXNETWORK_API FDTFluxAsyncParser +{ +public: + FDTFluxAsyncParser(); + ~FDTFluxAsyncParser(); + + // === INTERFACE PUBLIQUE === + + /** + * Lancer le parsing asynchrone d'une réponse JSON + * @param RequestId - ID de la requête pour le suivi + * @param RawJsonData - Données JSON brutes à parser + * @param OnCompleted - Callback appelé en cas de succès (sur main thread) + * @param OnFailed - Callback appelé en cas d'échec (sur main thread) + */ + void ParseResponseAsync( + const FGuid& RequestId, + const FString& RawJsonData, + FOnParsingCompleted OnCompleted, + FOnParsingFailed OnFailed = FOnParsingFailed() + ); + + /** + * Parsing synchrone avec timeout (pour les cas urgents) + * @param RawJsonData - Données JSON à parser + * @param TimeoutSeconds - Timeout maximum pour le parsing + * @return Réponse parsée ou nullptr en cas d'échec + */ + TSharedPtr ParseResponseSync( + const FString& RawJsonData, + float TimeoutSeconds = 1.0f + ); + + /** + * Annuler toutes les tâches de parsing en attente + */ + void CancelAllParsing(); + + // === STATISTIQUES === + + /** + * Statistiques de performance du parsing + */ + struct FParsingStats + { + int32 TasksInProgress = 0; // Tâches actuellement en cours + int32 TasksCompleted = 0; // Tâches terminées avec succès + int32 TasksFailed = 0; // Tâches échouées + float AverageParsingTimeMs = 0.0f; // Temps moyen de parsing en ms + }; + + /** + * Obtenir les statistiques de parsing + */ + FParsingStats GetStats() const; + + /** + * Réinitialiser les statistiques + */ + void ResetStats(); + +private: + // === TRACKING DES TÂCHES === + mutable FCriticalSection TasksLock; + TSet ActiveTasks; + + // === STATISTIQUES === + mutable FCriticalSection StatsLock; + mutable int32 TasksCompletedCount = 0; + mutable int32 TasksFailedCount = 0; + mutable TArray ParsingTimes; // Historique des temps de parsing + + // === MÉTHODES PRIVÉES === + + /** + * Callback appelé quand une tâche se termine + * @param bSuccess - Succès ou échec de la tâche + * @param ParsingTimeMs - Temps de parsing en millisecondes + */ + void OnTaskCompleted(bool bSuccess, float ParsingTimeMs); + + /** + * Nettoyer les tâches terminées de la liste active + */ + void CleanupCompletedTasks(); +}; + +// ================================================================================================ +// TÂCHE DE PARSING POUR LE TASKGRAPH +// ================================================================================================ + +/** + * Tâche de parsing JSON exécutée sur un thread worker + * Compatible avec le TaskGraph d'Unreal Engine + */ +class FDTFluxParsingTask +{ +public: + FDTFluxParsingTask( + const FGuid& InRequestId, + const FString& InRawJsonData, + FOnParsingCompleted InOnCompleted, + FOnParsingFailed InOnFailed + ); + + // === INTERFACE TASK GRAPH === + + FORCEINLINE TStatId GetStatId() const + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FDTFluxParsingTask, STATGROUP_TaskGraphTasks); + } + + static FORCEINLINE TStatId GetStatId_DoWork() + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FDTFluxParsingTask_DoWork, STATGROUP_TaskGraphTasks); + } + + static FORCEINLINE ENamedThreads::Type GetDesiredThread() + { + // Exécuter sur un thread worker (pas le main thread) + return ENamedThreads::AnyBackgroundThreadNormalTask; + } + + static FORCEINLINE ESubsequentsMode::Type GetSubsequentsMode() + { + return ESubsequentsMode::TrackSubsequents; + } + + // === EXÉCUTION DE LA TÂCHE === + + /** + * Méthode principale d'exécution de la tâche + * Appelée automatiquement par le TaskGraph sur un worker thread + */ + void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent); + +private: + FGuid RequestId; + FString RawJsonData; + FOnParsingCompleted OnCompleted; + FOnParsingFailed OnFailed; + double StartTime; +}; diff --git a/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h b/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h index 50a78d8..d299bb3 100644 --- a/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h +++ b/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h @@ -1,66 +1,444 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// ================================================================================================ +// DTFluxRequestManager.h - Gestionnaire C++ optimisé avec cache, timeout et retry +// ================================================================================================ #pragma once #include "CoreMinimal.h" -#include "UObject/Object.h" -#include "Containers/Queue.h" #include "Tickable.h" -#include "Struct/DTFluxRequestStructs.h" +#include "HAL/CriticalSection.h" #include "Struct/DTFluxServerResponseStruct.h" #include "Types/Enum/DTFluxCoreEnum.h" #include "DTFluxQueuedManager.generated.h" -DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRequestTimedOut, const FDTFluxQueuedRequest&, TimedOutRequest); +class FDTFluxAsyncParser; -/** - * @brief Classe Tickable gérant les requêtes WebSockets qui ne sont pas traçables nativement par l'API. - * Cette classe utilise TQueue pour gérer efficacement les requêtes en attente et vérifie leur état dans le tick. - */ -UCLASS() -class DTFLUXNETWORK_API UDTFluxQueuedManager : public UObject, public FTickableGameObject + +// ================================================================================================ +// ENUMS ET STRUCTURES POUR LES REQUÊTES +// ================================================================================================ + +UENUM(BlueprintType) +enum class EDTFluxRequestState : uint8 +{ + Pending UMETA(DisplayName = "Pending"), + Sent UMETA(DisplayName = "Sent"), + Completed UMETA(DisplayName = "Completed"), + Failed UMETA(DisplayName = "Failed"), + TimedOut UMETA(DisplayName = "TimedOut"), + Cached UMETA(DisplayName = "Cached"), + Retrying UMETA(DisplayName = "Retrying") +}; + +USTRUCT(BlueprintType) +struct DTFLUXNETWORK_API FDTFluxRequestConfig { GENERATED_BODY() -public: - /** Constructeur par défaut */ - UDTFluxQueuedManager(); - virtual ~UDTFluxQueuedManager() override; - void Initialize(); - FGuid QueueRequest(EDTFluxRequestType RequestType, int32 ContestId = -1, int32 StageId = -1, int32 SplitId = -1, - const FString& RawMessage = ""); - bool MarkRequestAsError(const FGuid& TargetRequestGuid); - bool MarkRequestAsResponded(const FGuid& TargetRequestGuid); - bool MarkRequestAsResponded(const FDTFluxQueuedRequest& TargetRequest); - bool IsRequestPending(FGuid& OutRequestId, EDTFluxRequestType RequestType, int32 ContestId = -1, int32 StageId = -1, - int32 SplitId = -1); - FDTFluxQueuedRequest* GetRequestPending(EDTFluxRequestType RequestType, int32 ContestId = -1, int32 StageId = -1, - int32 SplitId = -1); - const FDTFluxQueuedRequest* GetRequest(const FGuid& SearchedGuid); - int32 GetPendingRequestCount(); - int32 CleanupTimedOutRequests(); - int32 CleanCashedRequests(); - void ClearAllRequests(); - // bool TryProcessResponse(const FDTFluxServerResponse& Response); + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float TimeoutSeconds = 5.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + int32 MaxRetries = 3; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float RetryBackoffMultiplier = 1.5f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + bool bEnableCache = true; + + UPROPERTY(EditAnywhere, BlueprintReadWrite) + float CacheValiditySeconds = 60.0f; +}; + +USTRUCT(BlueprintType) +struct DTFLUXNETWORK_API FDTFluxTrackedRequest +{ + GENERATED_BODY() + + // === IDENTIFICATION === + UPROPERTY(BlueprintReadOnly) + FGuid RequestId = FGuid::NewGuid(); + + UPROPERTY(BlueprintReadOnly) + EDTFluxApiDataType RequestType = EDTFluxApiDataType::None; + + UPROPERTY(BlueprintReadOnly) + int32 ContestId = -1; + + UPROPERTY(BlueprintReadOnly) + int32 StageId = -1; + + UPROPERTY(BlueprintReadOnly) + int32 SplitId = -1; + + // === ÉTAT ET TIMING === + UPROPERTY(BlueprintReadOnly) + EDTFluxRequestState State = EDTFluxRequestState::Pending; + + UPROPERTY(BlueprintReadOnly) + FDateTime CreatedAt = FDateTime::Now(); + + UPROPERTY(BlueprintReadOnly) + FDateTime SentAt = FDateTime::MinValue(); + + UPROPERTY(BlueprintReadOnly) + FDateTime CompletedAt = FDateTime::MinValue(); + + UPROPERTY(BlueprintReadOnly) + FDateTime LastAttemptTime = FDateTime::Now(); + + // === CONFIGURATION === + FDTFluxRequestConfig Config; + + // === RETRY LOGIC === + UPROPERTY(BlueprintReadOnly) + int32 CurrentRetries = 0; + + // === DONNÉES DE RÉPONSE === + UPROPERTY(BlueprintReadOnly) + FString RawResponseData; + + // Réponse parsée (lazy loading) + mutable TOptional> ParsedResponse; + + UPROPERTY(BlueprintReadOnly) + bool bIsResponseParsed = false; + + UPROPERTY(BlueprintReadOnly) + FString LastErrorMessage; + + // === MÉTHODES UTILITAIRES === + + bool HasTimedOut() const; + bool CanRetry() const; + bool IsCacheValid() const; + float GetRetryDelay() const; + bool Matches(EDTFluxApiDataType InType, int32 InContestId = -1, int32 InStageId = -1, int32 InSplitId = -1) const; + FString GetCacheKey() const; + void SetRawResponse(const FString& RawData); + FString Serialize() const; +}; + +// ================================================================================================ +// DELEGATES POUR LES CALLBACKS +// ================================================================================================ + +DECLARE_DELEGATE_OneParam(FOnDTFluxRequestSuccess, const FDTFluxTrackedRequest&); +DECLARE_DELEGATE_TwoParams(FOnDTFluxRequestError, const FDTFluxTrackedRequest&, const FString& /*ErrorMessage*/); + +DECLARE_MULTICAST_DELEGATE_TwoParams(FOnRequestStateChangedNative, const FGuid& /*RequestId*/, + EDTFluxRequestState& /*NewState*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnRequestCompletedNative, const FDTFluxTrackedRequest& /*CompletedRequest*/); +DECLARE_MULTICAST_DELEGATE_OneParam(FOnRequestFailedNative, const FDTFluxTrackedRequest& /*FailedRequest*/); + +// ================================================================================================ +// REQUEST MANAGER - Classe C++ principale avec SmartPointers +// ================================================================================================ + +/** + * Gestionnaire de requêtes trackées avec cache, timeout, retry et parsing asynchrone + * Implémentation C++ pure avec SmartPointers pour des performances optimales + */ +class DTFLUXNETWORK_API FDTFluxQueuedRequestManager : public FTickableGameObject +{ +public: + FDTFluxQueuedRequestManager(); + virtual ~FDTFluxQueuedRequestManager(); + + // === LIFECYCLE === + + /** + * Initialiser le gestionnaire de requêtes + * @param DefaultConfig Configuration par défaut pour les nouvelles requêtes + */ + void Initialize(const FDTFluxRequestConfig& DefaultConfig = FDTFluxRequestConfig()); + + /** + * Arrêter le gestionnaire et nettoyer toutes les ressources + */ + void Shutdown(); + + /** + * Vérifier si le gestionnaire est initialisé + */ + bool IsInitialized() const { return bIsInitialized.load(); } + + // === CRÉATION DE REQUÊTES === + + /** + * Créer une nouvelle requête trackée + * @param RequestType Type de requête (ContestRanking, StageRanking, etc.) + * @param ContestId ID du contest (-1 si non applicable) + * @param StageId ID du stage (-1 si non applicable) + * @param SplitId ID du split (-1 si non applicable) + * @param CustomConfig Configuration spécifique pour cette requête + * @return GUID de la requête créée + */ + FGuid CreateTrackedRequest( + EDTFluxApiDataType RequestType, + int32 ContestId = -1, + int32 StageId = -1, + int32 SplitId = -1, + const FDTFluxRequestConfig& CustomConfig = FDTFluxRequestConfig() + ); + + /** + * Créer une requête trackée avec callbacks C++ + * @param RequestType Type de requête + * @param ContestId ID du contest + * @param StageId ID du stage + * @param SplitId ID du split + * @param OnSuccess Callback appelé en cas de succès + * @param OnError Callback appelé en cas d'erreur + * @param CustomConfig Configuration spécifique + * @return GUID de la requête créée + */ + FGuid CreateTrackedRequestWithCallbacks( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + FOnDTFluxRequestSuccess OnSuccess, + FOnDTFluxRequestError OnError, + const FDTFluxRequestConfig& CustomConfig = FDTFluxRequestConfig() + ); + + // === GESTION DES REQUÊTES === + + /** + * Marquer une requête comme envoyée + */ + bool MarkRequestAsSent(const FGuid& RequestId); + + /** + * Compléter une requête avec la réponse reçue + * @param RequestId ID de la requête + * @param RawResponseData Données JSON brutes de la réponse + * @param bUseAsyncParsing Utiliser le parsing asynchrone (recommandé) + */ + bool CompleteRequest(const FGuid& RequestId, const FString& RawResponseData, bool bUseAsyncParsing = true); + + /** + * Marquer une requête comme échouée + */ + bool FailRequest(const FGuid& RequestId, const FString& ErrorMessage); + + /** + * Relancer une requête (si retry possible) + */ + bool RetryRequest(const FGuid& RequestId); + + // === RECHERCHE ET CACHE === + + /** + * Chercher une requête en attente correspondant aux critères + */ + bool FindPendingRequest( + FGuid& OutRequestId, + EDTFluxApiDataType RequestType, + int32 ContestId = -1, + int32 StageId = -1, + int32 SplitId = -1 + ) const; + + /** + * Récupérer une réponse depuis le cache (données brutes) + */ + bool GetFromCache( + EDTFluxApiDataType RequestType, + FString& OutRawResponse, + int32 ContestId = -1, + int32 StageId = -1, + int32 SplitId = -1 + ) const; + + /** + * Récupérer une réponse parsée depuis le cache + */ + bool GetParsedFromCache( + EDTFluxApiDataType RequestType, + TSharedPtr& OutResponse, + int32 ContestId = -1, + int32 StageId = -1, + int32 SplitId = -1 + ) const; + + // === ACCESSEURS === + + /** + * Récupérer une requête par son ID + */ + bool GetRequest(const FGuid& RequestId, FDTFluxTrackedRequest& OutRequest) const; + + /** + * Récupérer un pointeur vers une requête (plus efficace) + */ + const FDTFluxTrackedRequest* GetRequestPtr(const FGuid& RequestId) const; + + /** + * Récupérer toutes les requêtes dans un état donné + */ + TArray GetRequestsByState(EDTFluxRequestState State) const; + + /** + * Compter les requêtes dans un état donné + */ + int32 GetRequestCount(EDTFluxRequestState State = EDTFluxRequestState::Pending) const; + + // === STATISTIQUES === + + /** + * Statistiques complètes du gestionnaire de requêtes + */ + struct FRequestStatistics + { + int32 Pending = 0; + int32 Cached = 0; + int32 Completed = 0; + int32 Failed = 0; + int32 TotalRequests = 0; + int32 CacheHits = 0; + int32 CacheMisses = 0; + float HitRate = 0.0f; + }; + + FRequestStatistics GetStatistics() const; + + // === NETTOYAGE === + + /** + * Nettoyer les entrées de cache expirées + * @return Nombre d'entrées supprimées + */ + int32 CleanupExpiredCache(); + + /** + * Nettoyer les requêtes terminées anciennes + * @param OlderThanSeconds Supprimer les requêtes plus anciennes que ce délai + * @return Nombre de requêtes supprimées + */ + int32 CleanupCompletedRequests(float OlderThanSeconds = 300.0f); + + /** + * Vider toutes les requêtes et le cache + */ + void ClearAllRequests(); + + // === EVENTS === + + FOnRequestStateChangedNative OnRequestStateChanged; + FOnRequestCompletedNative OnRequestCompleted; + FOnRequestFailedNative OnRequestFailed; + + // === INTERFACE TICKABLE === - // Interface FTickableGameObject virtual void Tick(float DeltaTime) override; - virtual bool IsTickable() const override; - virtual TStatId GetStatId() const override; + virtual bool IsTickable() const override { return true; }; + + virtual TStatId GetStatId() const override + { + RETURN_QUICK_DECLARE_CYCLE_STAT(FDTFluxQueuedRequestManager, STATGROUP_Tickables); + }; virtual bool IsTickableWhenPaused() const override { return true; } virtual bool IsTickableInEditor() const override { return true; } - // Interface ~FTickableGameObject - - UPROPERTY(BlueprintAssignable, Category = "DTFlux|Network") - FOnRequestTimedOut OnRequestTimedOut; + // === ACCESSEUR POUR LE PARSER (debug/stats) === + const FDTFluxAsyncParser* GetAsyncParser() const { return AsyncParser.Get(); } private: - TQueue PendingRequestsQueue; - TQueue CompletedRequestsQueue; - TQueue TimedOutRequestsQueue; + // === CONFIGURATION === + FDTFluxRequestConfig DefaultConfig; + std::atomic bIsInitialized{false}; - bool bIsInitialized; - float CheckInterval; - float TimeSinceLastCheck; + // === TIMING POUR LE TICK === + float TimeSinceLastTimeoutCheck = 0.0f; + float TimeSinceLastCacheCleanup = 0.0f; + float TimeSinceLastRetryCheck = 0.0f; + + static constexpr float TimeoutCheckInterval = 1.0f; + static constexpr float CacheCleanupInterval = 30.0f; + static constexpr float RetryCheckInterval = 0.5f; + + // === STOCKAGE THREAD-SAFE === + mutable FCriticalSection RequestsLock; + TMap> AllRequests; + TMap CacheKeyToRequestId; + + // === CALLBACKS C++ === + mutable FCriticalSection CallbacksLock; + TMap SuccessCallbacks; + TMap ErrorCallbacks; + + // === MÉTRIQUES === + mutable FCriticalSection MetricsLock; + mutable int32 TotalRequests = 0; + mutable int32 CacheHits = 0; + mutable int32 CacheMisses = 0; + + + // === PARSER ASYNCHRONE === + TUniquePtr AsyncParser; + + // === MÉTHODES PRIVÉES === + + /** + * Changer l'état d'une requête et notifier les observers + */ + void ChangeRequestState(TSharedPtr Request, EDTFluxRequestState NewState); + + /** + * Traiter les requêtes en timeout (appelé périodiquement) + */ + void ProcessTimeouts(); + + /** + * Traiter les requêtes à relancer (appelé périodiquement) + */ + void ProcessRetries(); + + /** + * Nettoyer le cache périodiquement + */ + void ProcessCacheCleanup(); + + /** + * Déclencher les callbacks pour une requête + */ + void TriggerCallbacks(const FDTFluxTrackedRequest& Request); + + /** + * Nettoyer les callbacks d'une requête + */ + void CleanupCallbacks(const FGuid& RequestId); + + /** + * Enregistrer un hit cache dans les métriques + */ + void RecordCacheHit() const; + + /** + * Enregistrer un miss cache dans les métriques + */ + void RecordCacheMiss() const; + + + // === CALLBACKS POUR LE PARSING ASYNCHRONE === + + /** + * Callback appelé quand le parsing asynchrone réussit + */ + void OnParsingCompleted(const FGuid& RequestId, TSharedPtr ParsedResponse, bool bSuccess); + + /** + * Callback appelé quand le parsing asynchrone échoue + */ + void OnParsingFailed(const FGuid& RequestId, const FString& ErrorMessage); + + // === UTILITAIRES STATIQUES === + + /** + * Générer une clé de cache unique pour une requête + */ + static FString GenerateCacheKey(EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId); }; diff --git a/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h b/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h index 3752f58..4d9a365 100644 --- a/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h +++ b/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h @@ -1,41 +1,54 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// ================================================================================================ +// DTFluxNetworkSubsystem.h - Interface UObject avec compatibilité Blueprint +// ================================================================================================ #pragma once #include "CoreMinimal.h" -#include "DTFluxQueuedManager.h" -#include "Struct/DTFluxServerResponseStruct.h" #include "Subsystems/EngineSubsystem.h" #include "Types/DTFluxNetworkSettingsTypes.h" #include "Types/Enum/DTFluxCoreEnum.h" -#include "Types/Struct/DTFluxRaceDataStructs.h" -#include "Types/Struct/DTFluxRankingStructs.h" -#include "Types/Struct/DTFluxSplitSensor.h" -#include "Types/Struct/DTFluxTeamListStruct.h" +#include "DTFluxQueuedManager.h" #include "DTFluxNetworkSubsystem.generated.h" - +// Forward declarations class FDTFluxWebSocketClient; -class UDTFluxQueuedManager; +class FDTFluxQueuedRequestManager; + typedef TSharedPtr FDTFluxWebSocketClientSP; -class FDTFluxHttpClient; -typedef TSharedPtr FDTFluxHttpClientSP; +// ================================================================================================ +// DELEGATES BLUEPRINT POUR LES REQUÊTES TRACKÉES +// ================================================================================================ -// Delegates pour les requêtes avec callback -DECLARE_DELEGATE_TwoParams(FOnDTFluxRequestResponseError, const FGuid&, const FString&); -DECLARE_DELEGATE_TwoParams(FOnDTFluxTrackedRequestResponse, const FGuid&, FDTFluxServerResponse&); -DECLARE_DELEGATE_TwoParams(FOnDTFluxTrackedRequestTimeout, const FGuid&, const FString& /*ErrorMessage*/); -// Delegates Blueprint pour les requêtes avec tracking DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDTFluxTrackedRequestCompleted, const FGuid&, RequestId, EDTFluxApiDataType, RequestType, const FString&, ResponseData); DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDTFluxTrackedRequestFailed, const FGuid&, RequestId, EDTFluxApiDataType, RequestType, const FString&, ErrorMessage); +// ================================================================================================ +// DELEGATES LEGACY POUR LA COMPATIBILITÉ +// ================================================================================================ + +DECLARE_DELEGATE_OneParam(FOnRaceDataReceived, const FDTFluxRaceData& /*RaceDataDefinition*/); +DECLARE_DELEGATE_OneParam(FOnTeamListReceived, const FDTFluxTeamListDefinition& /*TeamListDefinition*/); +DECLARE_DELEGATE_OneParam(FOnStageRankingReceived, const FDTFluxStageRankings& /*StageRankings*/); +DECLARE_DELEGATE_OneParam(FOnSplitRankingReceived, const FDTFluxSplitRankings& /*SplitRankings*/); +DECLARE_DELEGATE_OneParam(FOnContestRankingReceived, const FDTFluxContestRankings& /*ContestRankings*/); +DECLARE_DELEGATE_OneParam(FOnSplitSensorReceived, const FDTFluxSplitSensorInfo& /*SplitSensorInfo*/); +DECLARE_DELEGATE_OneParam(FOnTeamUpdateReceived, const FDTFluxTeamListDefinition& /*ParticipantToUpdate*/); +DECLARE_DELEGATE_OneParam(FOnTeamStatusUpdateReceived, const FDTFluxTeamStatusUpdate& /*TeamToUpdate*/); + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWebSocketConnected); + +// ================================================================================================ +// NETWORK SUBSYSTEM - Interface UObject avec compatibilité Blueprint +// ================================================================================================ /** - * + * Subsystem réseau DTFlux avec support complet des requêtes trackées et compatibilité legacy + * Combine l'efficacité du RequestManager C++ avec l'interface Blueprint UObject */ UCLASS(Blueprintable) class DTFLUXNETWORK_API UDTFluxNetworkSubsystem : public UEngineSubsystem @@ -43,180 +56,236 @@ class DTFLUXNETWORK_API UDTFluxNetworkSubsystem : public UEngineSubsystem GENERATED_BODY() public: - UPROPERTY() + // === ÉTAT DE CONNEXION === + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Network") EDTFluxConnectionStatus WsStatus = EDTFluxConnectionStatus::Unset; - DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWebSocketConnected); + // === CONNEXION WEBSOCKET (Legacy) === - UPROPERTY(BlueprintAssignable, Category="DTFlux|Network") - FOnWebSocketConnected OnWebSocketConnected; - - DECLARE_DELEGATE_OneParam(FOnRaceDataReceived, const FDTFluxRaceData& /*RaceDataDefinition*/); - FOnRaceDataReceived OnRaceDataReceived; - - FOnRaceDataReceived& OnReceivedRaceData() - { - return OnRaceDataReceived; - }; - - // === DELEGATES POUR LES DONNÉES REÇUES (PUSH) === - DECLARE_DELEGATE_OneParam(FOnTeamListReceived, const FDTFluxTeamListDefinition& /*TeamListDefinition*/); - FOnTeamListReceived OnTeamListReceived; - - FOnTeamListReceived& OnReceivedTeamList() - { - return OnTeamListReceived; - }; - - DECLARE_DELEGATE_OneParam(FOnStageRankingReceived, const FDTFluxStageRankings& /*StageRankings*/); - FOnStageRankingReceived OnStageRankingReceived; - - FOnStageRankingReceived& OnReceivedStageRanking() - { - return OnStageRankingReceived; - } - - DECLARE_DELEGATE_OneParam(FOnSplitRankingReceived, const FDTFluxSplitRankings& /*SplitRankings*/); - FOnSplitRankingReceived OnSplitRankingReceived; - - FOnSplitRankingReceived& OnReceivedSplitRanking() - { - return OnSplitRankingReceived; - } - - DECLARE_DELEGATE_OneParam(FOnContestRankingReceived, const FDTFluxContestRankings& /*ContestRankings*/); - FOnContestRankingReceived OnContestRankingReceived; - - FOnContestRankingReceived& OnReceivedContestRanking() - { - return OnContestRankingReceived; - }; - - // === DELEGATES POUR LES DONNÉES REÇUES (PULL) === - DECLARE_DELEGATE_OneParam(FOnSplitSensorReceived, const FDTFluxSplitSensorInfo& /*ContestRankings*/); - FOnSplitSensorReceived OnSplitSensorReceived; - - FOnSplitSensorReceived& OnReceivedSplitSensor() - { - return OnSplitSensorReceived; - }; - - DECLARE_DELEGATE_OneParam(FOnTeamUpdateReceived, const FDTFluxTeamListDefinition& /*ParticipantToUpdate*/); - FOnTeamUpdateReceived OnTeamUpdateReceived; - - FOnTeamUpdateReceived& OnReceivedTeamUpdate() - { - return OnTeamUpdateReceived; - }; - - DECLARE_DELEGATE_OneParam(FOnTeamStatusUpdateReceived, const FDTFluxTeamStatusUpdate& /*TeamToUpdate*/); - FOnTeamStatusUpdateReceived OnTeamStatusUpdateReceived; - - FOnTeamStatusUpdateReceived& OnReceivedTeamStatusUpdate() - { - return OnTeamStatusUpdateReceived; - }; - - UFUNCTION(BlueprintCallable, Category="DTFlux|Network") + /** + * Se connecter au serveur WebSocket + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Network") void Connect(); - UFUNCTION(BlueprintCallable, Category="DTFlux|Network") + + /** + * Se déconnecter du serveur WebSocket + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Network") void Disconnect(); - UFUNCTION(BlueprintCallable, Category="DTFlux|Network") + + /** + * Reconnecter au serveur WebSocket + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Network") void Reconnect(); + // === REQUÊTES TRACKÉES (Nouveau système optimisé) === - // === REQUÊTES AVEC QUEUE ET TRACKING === - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") - FGuid SendTrackedRequest(EDTFluxApiDataType RequestType, int32 ContestId = -1, int32 StageId = -1, - int32 SplitId = -1, float TimeoutSeconds = 30.0f); + /** + * Envoyer une requête trackée avec cache, timeout et retry + * @param RequestType Type de requête (ContestRanking, StageRanking, etc.) + * @param ContestId ID du contest (-1 si non applicable) + * @param StageId ID du stage (-1 si non applicable) + * @param SplitId ID du split (-1 si non applicable) + * @param TimeoutSeconds Timeout en secondes + * @param MaxRetries Nombre maximum de tentatives + * @param bEnableCache Activer le cache pour cette requête + * @return GUID de la requête pour le suivi + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") + FGuid SendTrackedRequest( + EDTFluxApiDataType RequestType, + int32 ContestId = -1, + int32 StageId = -1, + int32 SplitId = -1, + float TimeoutSeconds = 5.0f, + int32 MaxRetries = 3, + bool bEnableCache = true + ); - FGuid SendTrackedRequestWithCallback(EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId, - FOnDTFluxTrackedRequestResponse OnCompleted, - FOnDTFluxTrackedRequestTimeout OnTimeout, - TOptional OnError = TOptional< - FOnDTFluxRequestResponseError>(), - float TimeoutSeconds = 30.0f); + /** + * Envoyer une requête trackée avec callbacks C++ (non Blueprint) + * @param RequestType Type de requête + * @param ContestId ID du contest + * @param StageId ID du stage + * @param SplitId ID du split + * @param OnSuccess Callback appelé en cas de succès + * @param OnError Callback appelé en cas d'erreur + * @param TimeoutSeconds Timeout en secondes + * @param MaxRetries Nombre maximum de tentatives + * @param bEnableCache Activer le cache + * @return GUID de la requête + */ + FGuid SendTrackedRequestWithCallbacks( + EDTFluxApiDataType RequestType, + int32 ContestId, + int32 StageId, + int32 SplitId, + FOnDTFluxRequestSuccess& OnSuccess, + FOnDTFluxRequestError& OnError, + float TimeoutSeconds = 5.0f, + int32 MaxRetries = 3, + bool bEnableCache = true + ); - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") - bool GetTrackedRequest(const FGuid& RequestId, FDTFluxQueuedRequest& OutRequest) const; - const FDTFluxQueuedRequest* GetTrackedRequestPtr(const FGuid& RequestId) const; - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests", CallInEditor) + // === ACCESSEURS BLUEPRINT POUR LES REQUÊTES TRACKÉES === + + /** + * Récupérer une requête trackée par son ID + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") + bool GetTrackedRequest(const FGuid& RequestId, FDTFluxTrackedRequest& OutRequest) const; + + /** + * Vérifier si une requête a reçu une réponse + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") bool HasRequestReceivedResponse(const FGuid& RequestId) const; - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") + + /** + * Récupérer les données de réponse d'une requête + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") FString GetRequestResponseData(const FGuid& RequestId) const; - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") + + /** + * Vérifier si une requête similaire est en attente + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") bool IsRequestPending(EDTFluxApiDataType RequestType, int32 ContestId = -1, int32 StageId = -1, int32 SplitId = -1) const; - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") + + /** + * Compter le nombre de requêtes en attente + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") int32 GetPendingRequestCount() const; - UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") - UDTFluxQueuedManager* GetQueueManager() const; - // === EVENTS BLUEPRINT POUR LE TRACKING === - UPROPERTY(BlueprintAssignable, Category="DTFlux|Tracked Requests") - FOnDTFluxTrackedRequestCompleted OnTrackedRequestCompleted; - UPROPERTY(BlueprintAssignable, Category="DTFlux|Tracked Requests") - FOnDTFluxTrackedRequestFailed OnTrackedRequestFailed; - UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") + /** + * Récupérer les statistiques du gestionnaire de requêtes + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Tracked Requests") + void GetRequestStatistics(int32& OutPending, int32& OutCached, int32& OutCompleted, int32& OutFailed, + float& OutHitRate) const; - // === REQUÊTES DIRECTES (LEGACY) === + // === REQUÊTES LEGACY (Compatibilité totale) === + + /** + * Envoyer une requête en mode legacy (pour compatibilité) + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Legacy") void SendRequest(const EDTFluxApiDataType RequestType, int InContestId = -1, int InStageId = -1, int InSplitId = -1); - UFUNCTION(BlueprintCallable, Category="DTFlux|Network") + + /** + * Envoyer un message brut via WebSocket + */ + UFUNCTION(BlueprintCallable, Category = "DTFlux|Network") void SendMessage(const FString& Message); + // === EVENTS BLUEPRINT === + + /** + * Event déclenché lors de la connexion WebSocket + */ + UPROPERTY(BlueprintAssignable, Category = "DTFlux|Network") + FOnWebSocketConnected OnWebSocketConnected; + + /** + * Event déclenché quand une requête trackée se termine avec succès + */ + UPROPERTY(BlueprintAssignable, Category = "DTFlux|Tracked Requests") + FOnDTFluxTrackedRequestCompleted OnTrackedRequestCompleted; + + /** + * Event déclenché quand une requête trackée échoue + */ + UPROPERTY(BlueprintAssignable, Category = "DTFlux|Tracked Requests") + FOnDTFluxTrackedRequestFailed OnTrackedRequestFailed; + + // === DELEGATES LEGACY (Compatibilité totale) === + + FOnRaceDataReceived OnRaceDataReceived; + FOnTeamListReceived OnTeamListReceived; + FOnStageRankingReceived OnStageRankingReceived; + FOnSplitRankingReceived OnSplitRankingReceived; + FOnContestRankingReceived OnContestRankingReceived; + FOnSplitSensorReceived OnSplitSensorReceived; + FOnTeamUpdateReceived OnTeamUpdateReceived; + FOnTeamStatusUpdateReceived OnTeamStatusUpdateReceived; + + // Accesseurs pour la compatibilité legacy + FOnRaceDataReceived& OnReceivedRaceData() { return OnRaceDataReceived; } + FOnTeamListReceived& OnReceivedTeamList() { return OnTeamListReceived; } + FOnStageRankingReceived& OnReceivedStageRanking() { return OnStageRankingReceived; } + FOnSplitRankingReceived& OnReceivedSplitRanking() { return OnSplitRankingReceived; } + FOnContestRankingReceived& OnReceivedContestRanking() { return OnContestRankingReceived; } + FOnSplitSensorReceived& OnReceivedSplitSensor() { return OnSplitSensorReceived; } + FOnTeamUpdateReceived& OnReceivedTeamUpdate() { return OnTeamUpdateReceived; } + FOnTeamStatusUpdateReceived& OnReceivedTeamStatusUpdate() { return OnTeamStatusUpdateReceived; } + + // === ACCESSEUR PUBLIC POUR LE REQUEST MANAGER === + + /** + * Accéder au gestionnaire de requêtes (pour usage avancé) + */ + TSharedPtr GetRequestManager() const { return RequestManager; } + protected: - // ~Subsystem Interface + // === LIFECYCLE DU SUBSYSTEM === virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; - // ~Subsystem Interface - private: // === CONFIGURATION === FDTFluxWsSettings WsSettings; - FDTFluxHttpSettings HttpSettings; - - UPROPERTY() - UDTFluxQueuedManager* QueueManager; - - // === MAPPING DES CALLBACKS C++ === - TMap PendingCallbacks; - TMap PendingTimeoutCallbacks; - TMap PendingErrorCallbacks; // === CLIENTS RÉSEAU === FDTFluxWebSocketClientSP WsClient = nullptr; - FDTFluxHttpClientSP HttpClient = nullptr; - // === MÉTHODES DE CONFIGURATION === - UFUNCTION() - void WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings); - UFUNCTION() - void HttpSettingsChanged(const FDTFluxHttpSettings& NewHttpSettings); - void ReconnectWs(const FName WsClientId); - void ReconnectHttp(const FName WsClientId); + // === REQUEST MANAGER C++ === + TSharedPtr RequestManager; // === GESTION DES ÉVÉNEMENTS WEBSOCKET === void RegisterWebSocketEvents(); void UnregisterWebSocketEvents(); - void OnWebSocketConnected_Subsystem(); void OnWebSocketConnectionError_Subsystem(const FString& Error); void OnWebSocketClosedEvent_Subsystem(int32 StatusCode, const FString& Reason, bool bWasClean); + void OnWebSocketMessageEvent_Subsystem(const FString& MessageString); + void OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent); + // Handles pour les événements WebSocket FDelegateHandle OnWsConnectedEventDelegateHandle; FDelegateHandle OnWsConnectionErrorEventDelegateHandle; FDelegateHandle OnWsClosedEventDelegateHandle; FDelegateHandle OnWsMessageEventDelegateHandle; FDelegateHandle OnWsMessageSentEventDelegateHandle; - // === GESTION DES ÉVÉNEMENTS HTTP === - void RegisterHttpEvents(); - void UnregisterHttpEvents(); + // === PARSING ET TRAITEMENT DES RÉPONSES === - // === PARSING DES RÉPONSES === - void ParseTeamListResponse(FDTFluxServerResponse& ServerResponse); + /** + * Essayer de matcher une réponse à une requête trackée + * @param MessageString Message JSON reçu + * @return true si la réponse correspond à une requête trackée + */ + bool TryMatchResponseToQueuedRequest(const FString& MessageString); + + /** + * Traiter une réponse en mode legacy + * @param MessageString Message JSON à traiter + */ + void ProcessLegacyResponse(const FString& MessageString); + + /** + * Traiter une réponse déjà parsée + * @param ParsedResponse Réponse parsée à traiter + */ + void ProcessParsedResponse(TSharedPtr ParsedResponse); + + // === MÉTHODES DE PARSING LEGACY (pour compatibilité) === + void ParseTeamListResponse(FDTFluxServerResponse& Response); void ParseRaceData(FDTFluxServerResponse& Response); void ParseContestRanking(FDTFluxServerResponse& Response); void ParseStageRankingResponse(FDTFluxServerResponse& Response); @@ -225,19 +294,45 @@ private: void ParseSplitSensorResponse(FDTFluxServerResponse& Response); EDTFluxResponseStatus ProcessPushMessage(FDTFluxServerResponse& Response); - void Parse(FDTFluxServerResponse& Response); - void OnWebSocketMessageEvent_Subsystem(const FString& MessageString); - void OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent); - bool CleanRequestCallbacks(const FGuid& RequestId); + // === CALLBACKS POUR LE REQUEST MANAGER === - // === GESTION DES REQUÊTES TRACKÉES === + /** + * Callback appelé quand une requête trackée se termine + */ + void OnRequestCompleted_Internal(const FDTFluxTrackedRequest& CompletedRequest); + + /** + * Callback appelé quand une requête trackée échoue + */ + void OnRequestFailed_Internal(const FDTFluxTrackedRequest& FailedRequest); + + // === CONFIGURATION DYNAMIQUE === + + /** + * Callback appelé quand les paramètres WebSocket changent + */ UFUNCTION() - void OnRequestTimedOut_Internal(const FDTFluxQueuedRequest& TimedOutRequest); - bool TryMatchResponseToQueuedRequest(FDTFluxServerResponse& Response); - void CompleteTrackedRequest(const FGuid& RequestId, const FString& ResponseData, EDTFluxRequestType RequestType); - void FailTrackedRequest(const FGuid& RequestId, const FString& ErrorMessage, EDTFluxRequestType RequestType); - void SendQueuedRequest(const FDTFluxQueuedRequest& QueuedRequest); + void WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings); + + /** + * Reconnecter le client WebSocket + */ + void ReconnectWs(const FName WsClientId); // === UTILITAIRES === + + /** + * Construire une adresse WebSocket complète + */ static FString ConstructWsAddress(const FString& Address, const FString& Path, const int& Port); + + /** + * Envoyer une requête trackée via le réseau + */ + void SendQueuedRequest(const FDTFluxTrackedRequest& QueuedRequest); + + /** + * Déterminer si on doit utiliser le parsing asynchrone + */ + bool ShouldUseAsyncParsing(const FString& JsonData) const; };