diff --git a/Source/DTFluxAPIStatus/DTFluxAPIStatus.Build.cs b/Source/DTFluxAPIStatus/DTFluxAPIStatus.Build.cs index 8c74166..eaaa0e8 100644 --- a/Source/DTFluxAPIStatus/DTFluxAPIStatus.Build.cs +++ b/Source/DTFluxAPIStatus/DTFluxAPIStatus.Build.cs @@ -2,33 +2,36 @@ public class DTFluxAPIStatus : ModuleRules { - public DTFluxAPIStatus(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxAPIStatus(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", "DTFluxCoreSubsystem", + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "Projects", - "DTFluxNetwork", - "DTFluxProjectSettings", - "DTFluxCore", - "EditorStyle", - "ToolWidgets", // Nécessaire pour FSlimHorizontalToolBarBuilder - "UnrealEd", - "Settings" - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "Projects", + "DTFluxNetwork", + "DTFluxProjectSettings", + "DTFluxCore", + "EditorStyle", + "ToolWidgets", // Nécessaire pour FSlimHorizontalToolBarBuilder + "UnrealEd", + "Settings", + "DTFluxCoreSubsystem", + "InputCore", + "OutputLog", + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp b/Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp index 3b2e089..05df0b8 100644 --- a/Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp +++ b/Source/DTFluxAPIStatus/Private/DTFluxStatusWidget.cpp @@ -5,12 +5,11 @@ #include "SlateOptMacros.h" #include "DTFluxAPIStatusModule.h" -#include "EditorStyleSet.h" -#include "ISettingsCategory.h" -#include "ISettingsContainer.h" +#include "DTFluxCoreSubsystem.h" #include "ISettingsModule.h" -#include "ISettingsSection.h" -#include "Styling/SlateIconFinder.h" +#include "MovieSceneSequenceID.h" +#include "OutputLogCreationParams.h" +#include "OutputLogModule.h" #include "Types/Enum/DTFluxCoreEnum.h" #include "Subsystems/DTFluxNetworkSubsystem.h" #include "Framework/MultiBox/MultiBoxBuilder.h" @@ -27,32 +26,32 @@ void SDTFluxStatusWidget::OnOpenSettingsClicked() FReply SDTFluxStatusWidget::OnRaceDatasClicked() { - DTFlux->SendRequest(EDTFluxRequestType::RaceData); + DTFluxNetwork->SendRequest(EDTFluxRequestType::RaceData); return FReply::Handled(); } FReply SDTFluxStatusWidget::OnTeamListClicked() { - DTFlux->SendRequest(EDTFluxRequestType::TeamList); + DTFluxNetwork->SendRequest(EDTFluxRequestType::TeamList); return FReply::Handled(); } void SDTFluxStatusWidget::Construct(const FArguments& InArgs) { - - DTFlux = - GEngine->GetEngineSubsystem(); + DTFluxNetwork = + GEngine->GetEngineSubsystem(); + DTFluxCore = + GEngine->GetEngineSubsystem(); ConnectionActionButtonText.Set( - DTFlux->WsStatus != EDTFluxConnectionStatus::Connected ? - FText::FromString("Connect") : - FText::FromString("Disconnect") - ); - + DTFluxNetwork->WsStatus != EDTFluxConnectionStatus::Connected + ? FText::FromString("Connect") + : FText::FromString("Disconnect") + ); + FOutputLogModule& OutputLogModule = FModuleManager::LoadModuleChecked("OutputLog"); + const FOutputLogCreationParams Params; bCanSupportFocus = true; + PopulateComboBoxItems(); - - - FSlimHorizontalToolBarBuilder ToolBarBuilder( nullptr, FMultiBoxCustomization::None, @@ -72,234 +71,200 @@ void SDTFluxStatusWidget::Construct(const FArguments& InArgs) } ToolBarBuilder.EndSection(); - - + FSlateFontInfo TitleTextFont = FCoreStyle::Get().GetFontStyle(FName("EmbossedText")); TitleTextFont.Size = 15; ChildSlot [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + [ #pragma region ToolBarSection - SNew(SVerticalBox) - +SVerticalBox::Slot() - .AutoHeight() - - [ - SNew(SBox) - [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .FillWidth(2.0) + SNew(SBox) [ - SNew(SSpacer) - ] - +SHorizontalBox::Slot() - .AutoWidth() - .FillWidth(1.0) - .VAlign(VAlign_Center) - .HAlign(HAlign_Right) - [ - ToolBarBuilder.MakeWidget() + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .FillWidth(2.0) + [ + SNew(SSpacer) + ] + + SHorizontalBox::Slot() + .AutoWidth() + .FillWidth(1.0) + .VAlign(VAlign_Center) + .HAlign(HAlign_Right) + [ + ToolBarBuilder.MakeWidget() + ] ] +#pragma endregion ] - ] -#pragma endregion -#pragma region WebsocketStatusSection - // Main VerticalBox - +SVerticalBox::Slot() - .AutoHeight() - [ - SNew(SBorder) - .Padding(6.0f) + + SVerticalBox::Slot() + .AutoHeight() [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() - .VAlign(VAlign_Center) - .HAlign(HAlign_Center) - .MaxWidth(175.0) - .MinWidth(150.0) +#pragma region WebsocketStatusSection + SNew(SBorder) + .Padding(6.0f) [ - SNew(STextBlock ) - .Text(FText::FromString(TEXT("Websocket connection :"))) - .Justification(ETextJustify::Left) - ] - +SHorizontalBox::Slot() - .VAlign(VAlign_Center) - .MinWidth(50.0) - .MaxWidth(100.0) - [ - SAssignNew( WsStatusText, STextBlock) - .Text(this, &SDTFluxStatusWidget::GetWebSocketStatusText) - .Justification(ETextJustify::Left) - .ColorAndOpacity(this, &SDTFluxStatusWidget::GetWebSocketStatusColor) - ] - +SHorizontalBox::Slot() - .MaxWidth(100.0) - .MinWidth(30.0) - [ - SAssignNew(ConnectionActionButton, SButton) - .Text(this, &SDTFluxStatusWidget::GetWebConnectActionButtonText) - .ForegroundColor_Raw(this, &SDTFluxStatusWidget::GetWebConnectActionButtonColor) - .OnClicked(this,&SDTFluxStatusWidget::OnConnectionActionButtonClicked) + SNew(SHorizontalBox) + + SHorizontalBox::Slot() .VAlign(VAlign_Center) .HAlign(HAlign_Center) - .ContentPadding(1.5f) + .MaxWidth(175.0) + .MinWidth(150.0) + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Websocket connection :"))) + .Justification(ETextJustify::Left) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .MinWidth(50.0) + .MaxWidth(100.0) + [ + SAssignNew(WsStatusText, STextBlock) + .Text(this, &SDTFluxStatusWidget::GetWebSocketStatusText) + .Justification(ETextJustify::Left) + .ColorAndOpacity(this, &SDTFluxStatusWidget::GetWebSocketStatusColor) + ] + + SHorizontalBox::Slot() + .MaxWidth(100.0) + .MinWidth(30.0) + [ + SAssignNew(ConnectionActionButton, SButton) + .Text(this, &SDTFluxStatusWidget::GetWebConnectActionButtonText) + .ForegroundColor_Raw(this, &SDTFluxStatusWidget::GetWebConnectActionButtonColor) + .OnClicked(this, &SDTFluxStatusWidget::OnConnectionActionButtonClicked) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + .ContentPadding(1.5f) + ] ] - ] - ] #pragma endregion -#pragma region DataModelControlSection - +SVerticalBox::Slot() - .VAlign(VAlign_Fill) - .HAlign(HAlign_Fill) - .AutoHeight() - [ - SNew(SBox) + ] + + SVerticalBox::Slot() + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + .AutoHeight() [ - SNew(SHorizontalBox) - +SHorizontalBox::Slot() +#pragma region DataModelControlSection + SNew(SBorder) + .Padding(6.0f) + .BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder")) [ - SNew(SButton) - .Text(FText::FromString("Get RaceDatas")) - .OnClicked(this, &SDTFluxStatusWidget::OnRaceDatasClicked) - ] - +SHorizontalBox::Slot() - [ - SNew(SButton) - .Text(FText::FromString("Get TeamList")) - .OnClicked(this, &SDTFluxStatusWidget::OnTeamListClicked) - ] - ] - ] -#pragma endregion -#pragma region HTTPStatusSection -// +SVerticalBox::Slot() -// .AutoHeight() -// [ -// SNew(SHorizontalBox) -// +SHorizontalBox::Slot() -// [ -// SNew(STextBlock) -// .Text(FText::FromString(TEXT("HTTP connection :"))) -// .Justification(ETextJustify::Left) -// ] -// +SHorizontalBox::Slot() -// [ -// SNew(STextBlock) -// .Text(FText::FromString(TEXT("invalid"))) -// .Justification(ETextJustify::Center) -// .ColorAndOpacity(FColor::Red) -// ] -// +SHorizontalBox::Slot() -// [ -// SNew(SButton) -// .Text(FText::FromString(TEXT("Connection test"))) -// ] -// ] -#pragma endregion -#pragma region ContestsDataSection - // +SVerticalBox::Slot() - // .AutoHeight() - // .VAlign(VAlign_Fill) - // .HAlign(HAlign_Fill) - // [ - // SNew(SBorder) - // .Padding(1.5f) - // .VAlign(VAlign_Fill) - // .HAlign(HAlign_Fill) - // [ - // SNew(STextBlock) - // .Justification(ETextJustify::Left) - // .Text(FText::FromString("Contest :")) - // .ColorAndOpacity(FColor::White) - // ] - // ] - // +SVerticalBox::Slot() - // .AutoHeight() - // .VAlign(VAlign_Center) - // .HAlign(HAlign_Fill) - // [ - // SNew(SBorder) - // .Padding(1.0f) - // .VAlign(VAlign_Center) - // .HAlign(HAlign_Fill) - // [ - // SNew(SDatastorageView, DTFlux) - // ] - // ] -#pragma endregion -#pragma region ParticipantsDataSection - // +SVerticalBox::Slot() - // .AutoHeight() - // [ - // SNew(SHorizontalBox) - // +SHorizontalBox::Slot() - // [ - // SNew(STextBlock) - // .Text(FText::FromString(TEXT("Participants"))) - // .Justification(ETextJustify::Left) - // ] - // +SHorizontalBox::Slot() - // [ - // SNew(SButton) - // .Text(FText::FromString(TEXT("Show"))) - // ] - // ] -#pragma endregion -#pragma region EventsSection - // +SVerticalBox::Slot() - // .AutoHeight() - // [ - // SNew(SHorizontalBox) - // +SHorizontalBox::Slot() - // [ - // SNew(STextBlock) - // .Text(FText::FromString(TEXT("Future Events"))) - // .Justification(ETextJustify::Left) - // ] - // +SHorizontalBox::Slot() - // [ - // SNew(SButton) - // .Text(FText::FromString(TEXT("Show"))) - // ] - // ] -#pragma endregion -#pragma region SequencesSection - // +SVerticalBox::Slot() - // .AutoHeight() - // [ - // SNew(SHorizontalBox) - // +SHorizontalBox::Slot() - // [ - // SNew(STextBlock) - // .Text(FText::FromString(TEXT("Sequence On Air"))) - // .Justification(ETextJustify::Left) - // ] - // +SHorizontalBox::Slot() - // [ - // SNew(SButton) - // .Text(FText::FromString(TEXT("Show"))) - // ] - // ] -#pragma endregion + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(SButton) + .Text(FText::FromString("Get RaceDatas")) + .OnClicked(this, &SDTFluxStatusWidget::OnRaceDatasClicked) + ] + + SHorizontalBox::Slot() + [ + SNew(SButton) + .Text(FText::FromString("Get TeamList")) + .OnClicked(this, &SDTFluxStatusWidget::OnTeamListClicked) + ] - + ] +#pragma endregion + ] + + SVerticalBox::Slot() + .VAlign(VAlign_Fill) + .HAlign(HAlign_Fill) + .AutoHeight() + [ +#pragma region RankingSection + SNew(SBorder) + .Padding(6.0f) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + [ + SNew(SBorder) + .Padding(6.0f) + .BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder")) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .HAlign(HAlign_Left) + .AutoWidth() + [ + SNew(STextBlock) + .Text(FText::FromString(TEXT("Contest Rankings:"))) + .Margin(FMargin(0.0f, 0.0f, 8.0f, 0.0f)) + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .FillWidth(1.0f) + .MaxWidth(200.0f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + [ + SAssignNew(ContestComboBox, SComboBox>) + .OptionsSource(&ContestComboBoxItems) + .OnGenerateWidget(this, &SDTFluxStatusWidget::OnGeneRankingComboWidget) + .OnSelectionChanged(this, &SDTFluxStatusWidget::OnComboContestRankingChanged) + .Content() + [ + SNew(STextBlock) + .Text(this, &SDTFluxStatusWidget::GetCurrContestComboBoxValue) + ] + ] + ] + ] + + SHorizontalBox::Slot() + .VAlign(VAlign_Center) + .AutoWidth() + .Padding(FMargin(8.0f, 0.0f, 0.0f, 0.0f)) + [ + SNew(SButton) + .Text(FText::FromString(TEXT("RequestRanking"))) + .OnClicked(this, &SDTFluxStatusWidget::OnRankingButtonClicked) + .ContentPadding(FMargin(12.0f, 4.0f)) + .VAlign(VAlign_Center) + .HAlign(HAlign_Center) + ] + ] + + ] + ] +#pragma endregion + ] + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(3.0f) + [ + OutputLogModule.MakeOutputLogWidget(Params) + ] + + ] ]; } FText SDTFluxStatusWidget::GetWebSocketStatusText() const { - FString Status = - UEnum::GetDisplayValueAsText(DTFlux->WsStatus).ToString(); + UEnum::GetDisplayValueAsText(DTFluxNetwork->WsStatus).ToString(); return - FText::FromString(Status); + FText::FromString(Status); // FText::FromString("Unknown"); - } FText SDTFluxStatusWidget::GetWebConnectActionButtonText() const { - switch (DTFlux->WsStatus) + switch (DTFluxNetwork->WsStatus) { case EDTFluxConnectionStatus::Connected: return FText::FromString("Disconnect"); @@ -310,39 +275,40 @@ FText SDTFluxStatusWidget::GetWebConnectActionButtonText() const FReply SDTFluxStatusWidget::OnConnectionActionButtonClicked() { - if(DTFlux) + if (DTFluxNetwork) { - switch (DTFlux->WsStatus) + switch (DTFluxNetwork->WsStatus) { - case EDTFluxConnectionStatus::Connected: - DTFlux->Reconnect(); - break; - default: - DTFlux->Connect(); - break; + case EDTFluxConnectionStatus::Connected: + DTFluxNetwork->Reconnect(); + break; + default: + DTFluxNetwork->Connect(); + break; } } return FReply::Handled(); } + FSlateColor SDTFluxStatusWidget::GetWebSocketStatusColor() const { FColor Color; - switch (DTFlux->WsStatus) + switch (DTFluxNetwork->WsStatus) { case EDTFluxConnectionStatus::Unset: Color = FColor::Orange; break; case EDTFluxConnectionStatus::Connected: - Color = FColor::Green; + Color = FColor::Green; break; case EDTFluxConnectionStatus::NotConnected: - Color = FColor::Orange; + Color = FColor::Orange; break; case EDTFluxConnectionStatus::Closed: - Color = FColor::Magenta; + Color = FColor::Magenta; break; default: - Color = FColor::Red; + Color = FColor::Red; break; } return FSlateColor(Color); @@ -350,8 +316,8 @@ FSlateColor SDTFluxStatusWidget::GetWebSocketStatusColor() const FSlateColor SDTFluxStatusWidget::GetWebConnectActionButtonColor() const { - FColor Color= FColor::Green; - switch (DTFlux->WsStatus) + FColor Color = FColor::Green; + switch (DTFluxNetwork->WsStatus) { case EDTFluxConnectionStatus::Connected: Color = FColor::Red; @@ -364,4 +330,119 @@ FSlateColor SDTFluxStatusWidget::GetWebConnectActionButtonColor() const } +void SDTFluxStatusWidget::PopulateComboBoxItems() +{ + ContestComboBoxItems.Empty(); + + if (DTFluxNetwork) + { + FString Separator = " | "; + FString RootSeparator = " -> "; + TArray DataFromSubsystem = DTFluxCore->GetContests(); + + for (const auto& Contest : DataFromSubsystem) + { + ContestComboBoxItems.Add( + FComboBoxItem::CreateItem(EComboBoxItemType::Contest, Contest.Name + RootSeparator, Contest.ContestId)); + + + for (const auto& Stage : Contest.Stages) + { + int StageId = Stage.StageId; + FString StageDisplayName = Contest.Name + Separator + Stage.Name; + ContestComboBoxItems.Add(FComboBoxItem::CreateItem(EComboBoxItemType::Stage, + StageDisplayName + RootSeparator, Contest.ContestId, + StageId)); + for (const auto& Split : Contest.Splits) + { + ContestComboBoxItems.Add(FComboBoxItem::CreateItem(EComboBoxItemType::Split, + StageDisplayName + Separator + Split.Name, + Contest.ContestId, StageId, Split.SplitId)); + } + } + } + } + if (ContestComboBox.IsValid()) + { + ContestComboBox->RefreshOptions(); + if (ContestComboBoxItems.Num() > 0) + { + ContestComboBox->SetSelectedItem(ContestComboBoxItems[0]); + SelectedContestComboBoxItem = ContestComboBoxItems[0]; + } + } +} + +TSharedRef SDTFluxStatusWidget::OnGeneRankingComboWidget(TSharedPtr InItem) +{ + return SNew(STextBlock) + .Text(FText::FromString(InItem->DisplayText)) + .ColorAndOpacity(GetComboItemRankingColor(InItem)) + .Margin(FMargin(2.0f, 1.0f)); +} + +void SDTFluxStatusWidget::OnComboContestRankingChanged(TSharedPtr NewSelection, + ESelectInfo::Type SelectInfo) +{ + SelectedContestComboBoxItem = NewSelection; +} + +FText SDTFluxStatusWidget::GetCurrContestComboBoxValue() const +{ + if (SelectedContestComboBoxItem.IsValid()) + { + return FText::FromString(SelectedContestComboBoxItem->DisplayText); + } + return FText::FromString("None"); +} + +FSlateColor SDTFluxStatusWidget::GetComboItemRankingColor(const TSharedPtr Item) +{ + switch (Item->Type) + { + case EComboBoxItemType::Contest: + return FSlateColor(FLinearColor(0.2f, 0.6f, 1.0f)); + case EComboBoxItemType::Stage: + return FSlateColor(FLinearColor(0.2f, 0.8f, 0.2f)); + case EComboBoxItemType::Split: + return FSlateColor(FLinearColor(1.0f, 0.8f, 0.2f)); + case EComboBoxItemType::None: + return FSlateColor(FLinearColor(0.6f, 0.28f, 0.28f)); + default: + return FSlateColor(FLinearColor::White); + } +} + + +FReply SDTFluxStatusWidget::OnRankingButtonClicked() const +{ + if (DTFluxNetwork) + { + // Exemple d'envoi de requête basée sur la sélection + int ForContest = SelectedContestComboBoxItem.IsValid() ? SelectedContestComboBoxItem->ContestId : -1; + int ForStage = SelectedContestComboBoxItem.IsValid() ? SelectedContestComboBoxItem->StageId : -1; + int ForSplit = SelectedContestComboBoxItem.IsValid() ? SelectedContestComboBoxItem->SplitId : -1; + + EDTFluxApiDataType RequestType = EDTFluxApiDataType::None; + if (ForContest == -1) + { + UE_LOG(logDTFluxStatus, Error, TEXT("Contest not selected !!!!")); + return FReply::Handled(); + } + if (ForStage == -1) + { + UE_LOG(logDTFluxStatus, Warning, TEXT("Stage not selected !!!! Requesting contest Ranking")); + RequestType = EDTFluxApiDataType::ContestRanking; + DTFluxNetwork->SendRequest(RequestType, ForContest); + return FReply::Handled(); + } + RequestType = ForSplit == -1 ? EDTFluxApiDataType::StageRanking : EDTFluxApiDataType::SplitRanking; + UE_LOG(logDTFluxStatus, Warning, TEXT("Requesting %s Ranking"), *UEnum::GetValueAsString(RequestType)); + DTFluxNetwork->SendRequest(RequestType, ForContest, ForStage, ForSplit); + } + + return FReply::Handled(); +} + + END_SLATE_FUNCTION_BUILD_OPTIMIZATION diff --git a/Source/DTFluxAPIStatus/Public/widgets/DTFluxStatusWidget.h b/Source/DTFluxAPIStatus/Public/widgets/DTFluxStatusWidget.h index ce16311..904860b 100644 --- a/Source/DTFluxAPIStatus/Public/widgets/DTFluxStatusWidget.h +++ b/Source/DTFluxAPIStatus/Public/widgets/DTFluxStatusWidget.h @@ -10,14 +10,16 @@ * */ class UDTFluxNetworkSubsystem; +class UDTFluxCoreSubsystem; class SSuperListView; + class DTFLUXAPISTATUS_API SDTFluxStatusWidget : public SCompoundWidget { public: SLATE_BEGIN_ARGS(SDTFluxStatusWidget) - { - } + { + } SLATE_END_ARGS() @@ -28,9 +30,43 @@ public: void Construct(const FArguments& InArgs); TAttribute ConnectionActionButtonText; FReply OnConnectionActionButtonClicked(); - + + enum class EComboBoxItemType + { + Contest, + Stage, + Split, + None + }; + + struct FComboBoxItem + { + FString DisplayText; + int ContestId = -1; + int StageId = -1; + int SplitId = -1; + EComboBoxItemType Type = EComboBoxItemType::None; + + + FComboBoxItem(const EComboBoxItemType InType, const FString& InDisplayText, const int InContestId, + const int InStageId = -1, const int InSplitId = -1) + : DisplayText(InDisplayText), ContestId(InContestId), StageId(InStageId), SplitId(InSplitId), Type(InType) + { + } + + static TSharedPtr CreateItem(const EComboBoxItemType& InType, const FString& InDisplayText, + const int InContestId, const int InStageId = -1, + const int InSplitId = -1) + { + TSharedPtr Item = MakeShareable( + new FComboBoxItem(InType, InDisplayText, InContestId, InStageId, InSplitId)); + return Item; + } + }; + private: - UDTFluxNetworkSubsystem* DTFlux = nullptr; + UDTFluxNetworkSubsystem* DTFluxNetwork = nullptr; + UDTFluxCoreSubsystem* DTFluxCore = nullptr; // // TODO make a struct FText GetWebSocketStatusText() const; FText GetWebConnectActionButtonText() const; @@ -39,6 +75,18 @@ private: TSharedPtr WsStatusText; TSharedPtr ConnectionActionButton; + TSharedPtr>> ContestComboBox; + TArray> ContestComboBoxItems; + TSharedPtr SelectedContestComboBoxItem; - + + // Méthodes pour le ComboBox + void PopulateComboBoxItems(); + TSharedRef OnGeneRankingComboWidget(TSharedPtr InItem); + void OnComboContestRankingChanged(TSharedPtr NewSelection, ESelectInfo::Type SelectInfo); + FText GetCurrContestComboBoxValue() const; + + static FSlateColor GetComboItemRankingColor(const TSharedPtr Item); + + FReply OnRankingButtonClicked() const; }; diff --git a/Source/DTFluxAssetsEditor/DTFluxAssetsEditor.Build.cs b/Source/DTFluxAssetsEditor/DTFluxAssetsEditor.Build.cs index 030ed63..acf0480 100644 --- a/Source/DTFluxAssetsEditor/DTFluxAssetsEditor.Build.cs +++ b/Source/DTFluxAssetsEditor/DTFluxAssetsEditor.Build.cs @@ -2,28 +2,37 @@ public class DTFluxAssetsEditor : ModuleRules { - public DTFluxAssetsEditor(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxAssetsEditor(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core" + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "AssetTools", - "SlateCore", - "UnrealEd", - "DTFluxCore", - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "SlateCore", + "Slate", + "AssetTools", + "UnrealEd", + "DTFluxCore", + "ToolMenus", + "EditorWidgets", + "EditorStyle", + "PropertyEditor", + "SharedSettingsWidgets", + "PropertyEditor", + "DesktopWidgets", + "ApplicationCore", + "InputCore" + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxAssetsEditor/Private/DTFluxAssetsEditorModule.cpp b/Source/DTFluxAssetsEditor/Private/DTFluxAssetsEditorModule.cpp index b43b4a9..8bdfe44 100644 --- a/Source/DTFluxAssetsEditor/Private/DTFluxAssetsEditorModule.cpp +++ b/Source/DTFluxAssetsEditor/Private/DTFluxAssetsEditorModule.cpp @@ -3,28 +3,60 @@ #include "DTFluxAssetModelTypeActions.h" #include "IAssetTools.h" #include "AssetToolsModule.h" +#include "DTFluxModelAssetDetailCustomization.h" +#include "Assets/DTFluxModelAsset.h" #define LOCTEXT_NAMESPACE "FDTFluxAssetsEditorModule" DTFLUXASSETSEDITOR_API DEFINE_LOG_CATEGORY(logDTFluxAssetEditor) + void FDTFluxAssetsEditorModule::StartupModule() { IAssetTools& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools").Get(); EAssetTypeCategories::Type Category = AssetToolsModule.RegisterAdvancedAssetCategory("DTFlux", INVTEXT("DTFlux")); - DTFluxAssetModelActions = MakeShareable(new FDTFluxAssetModelTypeActions(Category)); + DTFluxAssetModelActions = MakeShareable(new FDTFluxAssetModelTypeActions(Category)); AssetToolsModule.RegisterAssetTypeActions(DTFluxAssetModelActions.ToSharedRef()); + RegisterCustomizations(); } void FDTFluxAssetsEditorModule::ShutdownModule() { - if(DTFluxAssetModelActions.IsValid() && FModuleManager::Get().IsModuleLoaded("AssetTools")) + if (DTFluxAssetModelActions.IsValid() && FModuleManager::Get().IsModuleLoaded("AssetTools")) { IAssetTools& AssetToolsModule = FModuleManager::GetModuleChecked("AssetTools").Get(); AssetToolsModule.UnregisterAssetTypeActions(DTFluxAssetModelActions.ToSharedRef()); } + UnregisterCustomizations(); +} + + +void FDTFluxAssetsEditorModule::RegisterCustomizations() +{ + // Enregistrer la customization pour DTFluxModelAsset + FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); + + PropertyModule.RegisterCustomClassLayout( + UDTFluxModelAsset::StaticClass()->GetFName(), + FOnGetDetailCustomizationInstance::CreateStatic(&FDTFluxModelAssetCustomization::MakeInstance) + ); + + PropertyModule.NotifyCustomizationModuleChanged(); +} + +void FDTFluxAssetsEditorModule::UnregisterCustomizations() +{ + if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) + { + FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked( + "PropertyEditor"); + + PropertyModule.UnregisterCustomClassLayout(UDTFluxModelAsset::StaticClass()->GetFName()); + + PropertyModule.NotifyCustomizationModuleChanged(); + } } #undef LOCTEXT_NAMESPACE - -IMPLEMENT_MODULE(FDTFluxAssetsEditorModule, DTFluxAssetsEditor) \ No newline at end of file + +IMPLEMENT_MODULE(FDTFluxAssetsEditorModule, DTFluxAssetsEditor) diff --git a/Source/DTFluxAssetsEditor/Private/DTFluxModelAssetDetailCustomization.cpp b/Source/DTFluxAssetsEditor/Private/DTFluxModelAssetDetailCustomization.cpp new file mode 100644 index 0000000..66f42ca --- /dev/null +++ b/Source/DTFluxAssetsEditor/Private/DTFluxModelAssetDetailCustomization.cpp @@ -0,0 +1,217 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "DTFluxModelAssetDetailCustomization.h" +#include "DetailLayoutBuilder.h" +#include "DetailCategoryBuilder.h" +#include "DetailWidgetRow.h" +#include "Widgets/Layout/SBox.h" +#include "Assets/DTFluxModelAsset.h" + + +TSharedRef FDTFluxModelAssetCustomization::MakeInstance() +{ + return MakeShareable(new FDTFluxModelAssetCustomization); +} + +EActiveTimerReturnType SDTFluxAssetModelDetailsWidget::ForceInitialLayout(double InCurrentTime, float InDeltaTime) +{ + // Forcer la mise à jour des TreeViews + if (ContestTreeView.IsValid()) + { + ContestTreeView->RequestTreeRefresh(); + } + + if (ParticipantTreeView.IsValid()) + { + ParticipantTreeView->RequestTreeRefresh(); + } + + // Forcer l'invalidation du layout + Invalidate(EInvalidateWidget::Layout); + + UE_LOG(LogTemp, Log, TEXT("Forced initial layout refresh")); + + // Arrêter le timer (exécution unique) + return EActiveTimerReturnType::Stop; +} + +void FDTFluxModelAssetCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) +{ + // Edit object + TArray> ObjectsBeingCustomized; + DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized); + + if (ObjectsBeingCustomized.Num() > 0) + { + ModelAsset = Cast(ObjectsBeingCustomized[0].Get()); + } + + if (!ModelAsset.IsValid()) + { + UE_LOG(LogTemp, Error, TEXT("No valid DTFluxModelAsset found")); + return; + } + + // ===== Hiding Categories/Props ===== + DetailBuilder.HideCategory("General"); + DetailBuilder.HideCategory("Default"); + DetailBuilder.HideCategory("Transform"); + DetailBuilder.HideCategory("Rendering"); + DetailBuilder.HideCategory("Input"); + DetailBuilder.HideCategory("Actor"); + DetailBuilder.HideCategory("Advanced"); + // Hide individual Props + DetailBuilder.HideProperty("EventName"); + DetailBuilder.HideProperty("Persons"); + DetailBuilder.HideProperty("Participants"); + DetailBuilder.HideProperty("Contests"); + DetailBuilder.HideProperty("ContestRankings"); + DetailBuilder.HideProperty("StageRankings"); + DetailBuilder.HideProperty("SplitRankings"); + + IDetailCategoryBuilder& MainCategory = DetailBuilder.EditCategory( + "DTFlux Model Explorer", + FText::FromString("DTFlux Model Explorer"), + ECategoryPriority::Important + ); + + // Créer le widget hiérarchique + DetailsWidget = SNew(SDTFluxAssetModelDetailsWidget) + .ModelAsset(ModelAsset.Get()); + + MainCategory.AddCustomRow(FText::FromString("Data Explorer")) + .WholeRowContent() + [ + SNew(SBox) + .MinDesiredHeight(800.0f) + [ + DetailsWidget.ToSharedRef() + ] + ]; + + UE_LOG(LogTemp, Log, TEXT("DTFluxModelAsset custom-only interface applied")); +} + + +void FDTFluxModelAssetCustomization::CustomizeDetailsWithRawDataAccess(IDetailLayoutBuilder& DetailBuilder) +{ + // ===== WIDGET PRINCIPAL ===== + IDetailCategoryBuilder& MainCategory = DetailBuilder.EditCategory( + "DTFlux Model Explorer", + FText::FromString("DTFlux Model Explorer"), + ECategoryPriority::Important + ); + + DetailsWidget = SNew(SDTFluxAssetModelDetailsWidget) + .ModelAsset(ModelAsset.Get()); + + MainCategory.AddCustomRow(FText::FromString("Data Explorer")) + .WholeRowContent() + [ + SNew(SBox) + .MinDesiredHeight(650.0f) + + [ + DetailsWidget.ToSharedRef() + ] + ]; + IDetailCategoryBuilder& QuickActionsCategory = DetailBuilder.EditCategory( + "Quick Actions", + FText::FromString("Quick Actions"), + ECategoryPriority::Default + ); + + QuickActionsCategory.AddCustomRow(FText::FromString("Raw Data Access")) + .NameContent() + [ + SNew(STextBlock) + .Text(FText::FromString("Raw Data Access")) + .Font(FAppStyle::GetFontStyle("NormalText")) + ] + .ValueContent() + [ + SNew(SHorizontalBox) + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 5, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "Button") + .Text(FText::FromString("Edit Raw Properties")) + .ToolTipText(FText::FromString("Temporarily show standard properties for advanced editing")) + .OnClicked_Lambda([this, &DetailBuilder]() -> FReply + { + // Forcer le rafraîchissement du DetailsPanel pour montrer les propriétés standard + DetailBuilder.ForceRefreshDetails(); + UE_LOG(LogTemp, Warning, + TEXT( + "Tip: To edit raw data, right-click the asset and choose 'Edit' or use the Content Browser" + )); + return FReply::Handled(); + }) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 5, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "Button") + .Text(FText::FromString("Log All Data")) + .ToolTipText(FText::FromString("Print all data to Output Log")) + .OnClicked_Lambda([this]() -> FReply + { + if (ModelAsset.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("=== DTFLUX MODEL DUMP ===")); + UE_LOG(LogTemp, Warning, TEXT("Event: %s"), *ModelAsset->EventName); + + UE_LOG(LogTemp, Warning, TEXT("--- CONTESTS (%d) ---"), ModelAsset->Contests.Num()); + for (const auto& Contest : ModelAsset->Contests) + { + UE_LOG(LogTemp, Warning, TEXT("Contest '%s' (ID: %d) - %d stages, %d participants"), + *Contest.Key, Contest.Value.ContestId, + Contest.Value.Stages.Num(), Contest.Value.ParticipantsBib.Num()); + } + + UE_LOG(LogTemp, Warning, TEXT("--- PARTICIPANTS (%d) ---"), ModelAsset->Participants.Num()); + for (const auto& Participant : ModelAsset->Participants) + { + UE_LOG(LogTemp, Warning, TEXT("Bib %d: %s (%s) - %d teammates"), + Participant.Value.Bib, *Participant.Value.Team, + *Participant.Value.Category, Participant.Value.GetTeammate().Num()); + } + + UE_LOG(LogTemp, Warning, TEXT("--- PERSONS (%d) ---"), ModelAsset->Persons.Num()); + for (int32 i = 0; i < ModelAsset->Persons.Num(); ++i) + { + const auto& Person = ModelAsset->Persons[i]; + UE_LOG(LogTemp, Warning, TEXT("Person %d: %s %s (%s)"), + i, *Person.FirstName, *Person.LastName, *Person.Gender); + } + + UE_LOG(LogTemp, Warning, TEXT("=======================")); + } + return FReply::Handled(); + }) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "PrimaryButton") + .Text(FText::FromString("Refresh")) + .ToolTipText(FText::FromString("Refresh the hierarchy view")) + .OnClicked_Lambda([this]() -> FReply + { + if (DetailsWidget.IsValid()) + { + DetailsWidget->RefreshData(); + } + return FReply::Handled(); + }) + ] + ]; +} diff --git a/Source/DTFluxAssetsEditor/Private/Widget/DTFluxAssetModelDetailsWidget.cpp b/Source/DTFluxAssetsEditor/Private/Widget/DTFluxAssetModelDetailsWidget.cpp new file mode 100644 index 0000000..ccc1403 --- /dev/null +++ b/Source/DTFluxAssetsEditor/Private/Widget/DTFluxAssetModelDetailsWidget.cpp @@ -0,0 +1,645 @@ +// Fill out your copyright notice in the Description page of Project Settings. +#include "Widget/DTFluxAssetModelDetailsWidget.h" + +#include "Widgets/Layout/SBorder.h" +#include "Widgets/Layout/SBox.h" +#include "Widgets/Text/STextBlock.h" +#include "Widgets/Input/SButton.h" +#include "Widgets/Views/STableRow.h" +#include "Widgets/Views/SHeaderRow.h" + +void SHierarchicalTreeItemRow::Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView) +{ + Item = InArgs._Item; + ParentWidget = InArgs._ParentWidget; + + SMultiColumnTableRow>::Construct( + SMultiColumnTableRow>::FArguments(), + InOwnerTableView + ); +} + +TSharedRef SHierarchicalTreeItemRow::GenerateWidgetForColumn(const FName& ColumnName) +{ + if (!Item.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("GenerateWidgetForColumn: Invalid item for column %s"), *ColumnName.ToString()); + return SNew(STextBlock).Text(FText::FromString("Invalid Item")); + } + + if (!ParentWidget) + { + UE_LOG(LogTemp, Warning, TEXT("GenerateWidgetForColumn: Invalid ParentWidget for column %s"), + *ColumnName.ToString()); + return SNew(STextBlock).Text(FText::FromString("Invalid Parent")); + } + + UE_LOG(LogTemp, VeryVerbose, TEXT("GenerateWidgetForColumn: %s for item %s"), + *ColumnName.ToString(), *Item->Name); + + if (ColumnName == "Name") + { + return SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .VAlign(VAlign_Center) + .Padding(0, 0, 5, 0) + [ + SNew(SImage) + .Image(ParentWidget->GetItemIcon(Item->Type)) + .ColorAndOpacity(ParentWidget->GetItemTypeColor(Item->Type)) + ] + + SHorizontalBox::Slot() + .FillWidth(1.0f) + .VAlign(VAlign_Center) + [ + SNew(STextBlock) + .Text(FText::FromString(Item->Name)) + .ColorAndOpacity(ParentWidget->GetItemTypeColor(Item->Type)) + .Font_Lambda([this]() -> FSlateFontInfo + { + return Item->Type == FHierarchicalTreeItem::EItemType::Contest + ? FAppStyle::GetFontStyle("HeadingText") + : FAppStyle::GetFontStyle("NormalText"); + }) + ]; + } + else if (ColumnName == "ID") + { + return SNew(STextBlock) + .Text(FText::FromString(Item->ID)) + .Font(FAppStyle::GetFontStyle("NormalText")) + .Justification(ETextJustify::Center); + } + else if (ColumnName == "Details") + { + return SNew(STextBlock) + .Text(FText::FromString(Item->Details)) + .Font(FAppStyle::GetFontStyle("NormalText")) + .OverflowPolicy(ETextOverflowPolicy::Ellipsis); + } + else if (ColumnName == "Status") + { + return SNew(STextBlock) + .Text(FText::FromString(Item->Status)) + .Font(FAppStyle::GetFontStyle("NormalText")) + .OverflowPolicy(ETextOverflowPolicy::Ellipsis); + } + else if (ColumnName == "Extra") + { + return SNew(STextBlock) + .Text(FText::FromString(Item->Extra)) + .Font(FAppStyle::GetFontStyle("NormalText")) + .OverflowPolicy(ETextOverflowPolicy::Ellipsis); + } + + UE_LOG(LogTemp, Warning, TEXT("GenerateWidgetForColumn: Unknown column %s"), *ColumnName.ToString()); + return SNew(STextBlock).Text(FText::FromString(FString::Printf(TEXT("Unknown: %s"), *ColumnName.ToString()))); +} + +void SDTFluxAssetModelDetailsWidget::Construct(const FArguments& InArgs) +{ + ModelAsset = InArgs._ModelAsset; + + ChildSlot + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SVerticalBox) + + // === SECTION STATISTIQUES === + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + .Padding(10) + [ + SAssignNew(StatsText, STextBlock) + .Text(this, &SDTFluxAssetModelDetailsWidget::GetStatsText) + .Font(FAppStyle::GetFontStyle("HeadingText")) + .Justification(ETextJustify::Center) + ] + ] + + // === SECTION BOUTONS DE NAVIGATION === + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 5, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .Text(FText::FromString("Expand All Contests")) + .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnExpandAllClicked) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + .Padding(0, 0, 10, 0) + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "SimpleButton") + .Text(FText::FromString("Collapse All Contests")) + .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnCollapseAllClicked) + ] + + + SHorizontalBox::Slot() + .FillWidth(1.0f) + [ + SNew(SSpacer) + ] + + + SHorizontalBox::Slot() + .AutoWidth() + [ + SNew(SButton) + .ButtonStyle(FAppStyle::Get(), "PrimaryButton") + .Text(FText::FromString("Refresh")) + .OnClicked(this, &SDTFluxAssetModelDetailsWidget::OnRefreshClicked) + ] + ] + ] + +#pragma region ListView + + SVerticalBox::Slot() + .FillHeight(1.0f) + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + .Padding(0) + [ + SNew(SBox) + [ +#pragma region ScrollBox + SNew(SScrollBox) + .Orientation(Orient_Vertical) + .ScrollBarVisibility(EVisibility::Visible) + .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) + .ScrollBarAlwaysVisible(true) // Force la scrollbar à être toujours visible + +#pragma region ListView.Contest + + SScrollBox::Slot() + .Padding(0, 0, 0, 10) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + [ + SNew(SVerticalBox) + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + .Padding(10, 5) + [ + SNew(STextBlock) + .Text(FText::FromString("CONTESTS HIERARCHY")) + .Font(FAppStyle::GetFontStyle("HeadingText")) + .ColorAndOpacity(FLinearColor(0.2f, 0.6f, 1.0f)) + ] + ] + + // TreeView Contests + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + [ + SAssignNew(ContestTreeView, STreeView) + .TreeItemsSource(&RootItems) + .OnGenerateRow(this, &SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree) + .OnGetChildren(this, &SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree) + .OnSelectionChanged( + this, &SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged) + .OnSetExpansionRecursive( + this, &SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive) + .SelectionMode(ESelectionMode::Single) + .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) + .HeaderRow + ( + SNew(SHeaderRow) + .CanSelectGeneratedColumn(true) + + + SHeaderRow::Column("Name") + .DefaultLabel(FText::FromString("Contest / Stage / Split")) + .SortMode(EColumnSortMode::None) + + + SHeaderRow::Column("ID") + .DefaultLabel(FText::FromString("ID")) + .SortMode(EColumnSortMode::None) + .FillWidth(.02f) + + + + SHeaderRow::Column("Details") + .DefaultLabel(FText::FromString("Details")) + .SortMode(EColumnSortMode::None) + .FillWidth(.3f) + + + SHeaderRow::Column("Status") + .DefaultLabel(FText::FromString("Status / Time")) + .SortMode(EColumnSortMode::None) + .FillWidth(.1f) + + + + SHeaderRow::Column("Extra") + .DefaultLabel(FText::FromString("Extra Info")) + .FillWidth(.2f) + + .SortMode(EColumnSortMode::None) + ) + ] + ] + ] + ] +#pragma endregion + +#pragma region ListView.Participant + + SScrollBox::Slot() + .Padding(0, 10, 0, 0) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + [ + SNew(SVerticalBox) + + // Header "Participants" + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + .Padding(10, 5) + [ + SNew(STextBlock) + .Text(FText::FromString("PARTICIPANTS LIST")) + .Font(FAppStyle::GetFontStyle("HeadingText")) + .ColorAndOpacity(FLinearColor(0.2f, 0.8f, 0.8f)) + ] + ] + + // TreeView Participants + + SVerticalBox::Slot() + .AutoHeight() + [ + SNew(SBox) + [ + SAssignNew(ParticipantTreeView, STreeView) + .TreeItemsSource(&ParticipantItems) + .OnGenerateRow(this, &SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree) + .OnGetChildren(this, &SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree) + .OnSelectionChanged( + this, &SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged) + .OnSetExpansionRecursive( + this, &SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive) + .SelectionMode(ESelectionMode::Single) + .ConsumeMouseWheel(EConsumeMouseWheel::WhenScrollingPossible) + .HeaderRow + ( + SNew(SHeaderRow) + .CanSelectGeneratedColumn(true) + + + SHeaderRow::Column("Name") + .DefaultLabel(FText::FromString("Participant")) + .SortMode(EColumnSortMode::None) + + + SHeaderRow::Column("ID") + .DefaultLabel(FText::FromString("Bib")) + .SortMode(EColumnSortMode::None) + .FillWidth(.08f) + + + SHeaderRow::Column("Details") + .DefaultLabel(FText::FromString("Category & Teammates")) + .SortMode(EColumnSortMode::None) + .FillWidth(.2f) + + + SHeaderRow::Column("Status") + .DefaultLabel(FText::FromString("Status")) + .FillWidth(.1f) + .SortMode(EColumnSortMode::None) + + + SHeaderRow::Column("Extra") + .DefaultLabel(FText::FromString("Club")) + .FillWidth(.2f) + .SortMode(EColumnSortMode::None) + ) + ] + ] + ] + ] +#pragma endregion + ] +#pragma endregion + ] +#pragma endregion + ] + ] + +#pragma region DetailView.Participant + + SVerticalBox::Slot() + .AutoHeight() + .Padding(5) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("DetailsView.CategoryTop")) + .Padding(10) + [ + SAssignNew(SelectionText, STextBlock) + .Text(FText::FromString("Select an item to see details. Use expand/collapse arrows in the tree.")) + .Font(FAppStyle::GetFontStyle("NormalText")) + .AutoWrapText(true) + ] + ] +#pragma endregion + ] + + ]; + + RefreshData(); + + RegisterActiveTimer( + 0.1f, FWidgetActiveTimerDelegate::CreateSP(this, &SDTFluxAssetModelDetailsWidget::ForceInitialLayout)); +} + +// ===== CONSTRUCTION DE LA HIÉRARCHIE ===== + +void SDTFluxAssetModelDetailsWidget::BuildContestHierarchy() +{ + RootItems.Empty(); + + if (!ModelAsset) + return; + + // Construire la hiérarchie Contest → Stages → Splits + for (const auto& ContestPair : ModelAsset->Contests) + { + const FString& ContestName = ContestPair.Key; + const FDTFluxContest& Contest = ContestPair.Value; + + // Créer l'élément Contest racine + auto ContestItem = FHierarchicalTreeItem::CreateContest(ContestName, Contest); + + // Ajouter les Stages comme enfants + for (const FDTFluxStage& Stage : Contest.Stages) + { + auto StageItem = FHierarchicalTreeItem::CreateStage(Stage, Contest.ContestId); + ContestItem->AddChild(StageItem); + } + + // Ajouter les Splits comme enfants directs du Contest + for (const FDTFluxSplit& Split : Contest.Splits) + { + auto SplitItem = FHierarchicalTreeItem::CreateSplit(Split, Contest.ContestId); + ContestItem->AddChild(SplitItem); + } + + RootItems.Add(ContestItem); + } + + UE_LOG(LogTemp, Log, TEXT("Built contest hierarchy with %d root contests"), RootItems.Num()); +} + +void SDTFluxAssetModelDetailsWidget::BuildParticipantList() +{ + ParticipantItems.Empty(); + + if (!ModelAsset) + { + UE_LOG(LogTemp, Warning, TEXT("BuildParticipantList: ModelAsset is null!")); + return; + } + + UE_LOG(LogTemp, Log, TEXT("BuildParticipantList: ModelAsset has %d participants"), ModelAsset->Participants.Num()); + + // Créer la liste des participants (pas de hiérarchie pour les participants) + for (const auto& ParticipantPair : ModelAsset->Participants) + { + const FDTFluxParticipant& Participant = ParticipantPair.Value; + auto ParticipantItem = FHierarchicalTreeItem::CreateParticipant(Participant); + ParticipantItems.Add(ParticipantItem); + + UE_LOG(LogTemp, Log, TEXT("BuildParticipantList: Added participant %s (Bib: %d)"), + *ParticipantItem->Name, ParticipantItem->Bib); + } + + UE_LOG(LogTemp, Log, TEXT("BuildParticipantList: Built participant list with %d participants"), + ParticipantItems.Num()); +} + +// ===== CALLBACKS TREEVIEW ===== + +TSharedRef SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree( + FHierarchicalTreeItemPtr Item, const TSharedRef& OwnerTable) +{ + if (!Item.IsValid()) + { + UE_LOG(LogTemp, Warning, TEXT("OnGenerateRowForTree: Invalid item!")); + return SNew(STableRow, OwnerTable); + } + + UE_LOG(LogTemp, Log, TEXT("OnGenerateRowForTree: Generating row for %s (Type: %d)"), + *Item->Name, (int32)Item->Type); + + return SNew(SHierarchicalTreeItemRow, OwnerTable) + .Item(Item) + .ParentWidget(this); +} + +void SDTFluxAssetModelDetailsWidget::OnGetChildrenForTree(FHierarchicalTreeItemPtr Item, + TArray& OutChildren) +{ + if (Item.IsValid()) + { + OutChildren = Item->Children; + } +} + +void SDTFluxAssetModelDetailsWidget::OnTreeSelectionChanged(FHierarchicalTreeItemPtr SelectedItem, + ESelectInfo::Type SelectInfo) +{ + if (SelectionText.IsValid()) + { + if (SelectedItem.IsValid()) + { + FString TypeString; + switch (SelectedItem->Type) + { + case FHierarchicalTreeItem::EItemType::Contest: + TypeString = "Contest"; + break; + case FHierarchicalTreeItem::EItemType::Stage: + TypeString = "Stage"; + break; + case FHierarchicalTreeItem::EItemType::Split: + TypeString = "Split"; + break; + case FHierarchicalTreeItem::EItemType::Participant: + TypeString = "Participant"; + break; + } + FString SelectionInfo = FString::Printf( + TEXT("📋 Selected: %s (%s)\n🔢 ID: %s\n📄 Details: %s\n📊 Status: %s\n➕ Extra: %s\n🌟 Children: %d"), + *SelectedItem->Name, + *TypeString, + *SelectedItem->ID, + *SelectedItem->Details, + *SelectedItem->Status, + *SelectedItem->Extra, + SelectedItem->Children.Num()); + SelectionText->SetText(FText::FromString(SelectionInfo)); + } + else + { + SelectionText->SetText(FText::FromString("Select an item to see details")); + } + } +} + +void SDTFluxAssetModelDetailsWidget::OnSetExpansionRecursive(FHierarchicalTreeItemPtr Item, bool bIsExpanded) +{ + if (Item.IsValid() && ContestTreeView.IsValid()) + { + ContestTreeView->SetItemExpansion(Item, bIsExpanded); + + // Expansion récursive des enfants + for (auto Child : Item->Children) + { + OnSetExpansionRecursive(Child, bIsExpanded); + } + } +} + +// ===== CALLBACKS DES BOUTONS ===== + +FReply SDTFluxAssetModelDetailsWidget::OnExpandAllClicked() +{ + if (ContestTreeView.IsValid()) + { + for (auto& RootItem : RootItems) + { + OnSetExpansionRecursive(RootItem, true); + } + } + + UE_LOG(LogTemp, Log, TEXT("Expanded all contests")); + return FReply::Handled(); +} + +FReply SDTFluxAssetModelDetailsWidget::OnCollapseAllClicked() +{ + if (ContestTreeView.IsValid()) + { + for (auto& RootItem : RootItems) + { + OnSetExpansionRecursive(RootItem, false); + } + } + + UE_LOG(LogTemp, Log, TEXT("Collapsed all contests")); + return FReply::Handled(); +} + +FReply SDTFluxAssetModelDetailsWidget::OnRefreshClicked() +{ + RefreshData(); + UE_LOG(LogTemp, Log, TEXT("Data refreshed")); + return FReply::Handled(); +} + +// ===== REFRESHDATA ===== + +void SDTFluxAssetModelDetailsWidget::RefreshData() +{ + if (!ModelAsset) + { + UE_LOG(LogTemp, Warning, TEXT("ModelAsset is null!")); + return; + } + + UE_LOG(LogTemp, Log, TEXT("RefreshData: Starting refresh for ModelAsset %s"), *ModelAsset->GetName()); + + // Nettoyer les données existantes + RootItems.Empty(); + ParticipantItems.Empty(); + + // Construire la hiérarchie + BuildContestHierarchy(); + BuildParticipantList(); + + // Refresh les vues + if (ContestTreeView.IsValid()) + { + ContestTreeView->RequestTreeRefresh(); + } + + if (ParticipantTreeView.IsValid()) + { + ParticipantTreeView->RequestTreeRefresh(); + } + + UE_LOG(LogTemp, Log, TEXT("RefreshData: Completed successfully - %d contests, %d participants"), RootItems.Num(), + ParticipantItems.Num()); +} + +// ===== MÉTHODES UTILITAIRES ===== + +FSlateColor SDTFluxAssetModelDetailsWidget::GetItemTypeColor(FHierarchicalTreeItem::EItemType Type) const +{ + switch (Type) + { + case FHierarchicalTreeItem::EItemType::Contest: + return FSlateColor(FLinearColor(0.2f, 0.6f, 1.0f)); + case FHierarchicalTreeItem::EItemType::Stage: + return FSlateColor(FLinearColor(0.2f, 0.8f, 0.2f)); + case FHierarchicalTreeItem::EItemType::Split: + return FSlateColor(FLinearColor(1.0f, 0.8f, 0.2f)); + case FHierarchicalTreeItem::EItemType::Participant: + return FSlateColor(FLinearColor(0.2f, 0.8f, 0.8f)); + default: + return FSlateColor(FLinearColor::White); + } +} + +const FSlateBrush* SDTFluxAssetModelDetailsWidget::GetItemIcon(FHierarchicalTreeItem::EItemType Type) const +{ + switch (Type) + { + case FHierarchicalTreeItem::EItemType::Contest: + return FAppStyle::GetBrush("TreeArrow_Collapsed"); + case FHierarchicalTreeItem::EItemType::Stage: + case FHierarchicalTreeItem::EItemType::Split: + return FAppStyle::GetBrush("TreeArrow_Expanded"); + case FHierarchicalTreeItem::EItemType::Participant: + return FAppStyle::GetBrush("Icons.User"); + default: + return FAppStyle::GetBrush("Icons.Help"); + } +} + +FText SDTFluxAssetModelDetailsWidget::GetStatsText() const +{ + if (!ModelAsset) + return FText::FromString("No data"); + + return FText::FromString(FString::Printf( + TEXT("Contests: [%d] Participants: [%d] Persons: [%d]"), + ModelAsset->Contests.Num(), + ModelAsset->Participants.Num(), + ModelAsset->Persons.Num() + )); +} diff --git a/Source/DTFluxAssetsEditor/Public/DTFluxAssetsEditorModule.h b/Source/DTFluxAssetsEditor/Public/DTFluxAssetsEditorModule.h index 95f75f5..415a9b4 100644 --- a/Source/DTFluxAssetsEditor/Public/DTFluxAssetsEditorModule.h +++ b/Source/DTFluxAssetsEditor/Public/DTFluxAssetsEditorModule.h @@ -21,9 +21,11 @@ DTFLUXASSETSEDITOR_API DECLARE_LOG_CATEGORY_EXTERN(logDTFluxAssetEditor, Log, Al class FDTFluxAssetsEditorModule : public IModuleInterface { public: - virtual void StartupModule() override; - virtual void ShutdownModule() override; + virtual void StartupModule() override; + virtual void ShutdownModule() override; private: TSharedPtr DTFluxAssetModelActions; + void RegisterCustomizations(); + void UnregisterCustomizations(); }; diff --git a/Source/DTFluxAssetsEditor/Public/DTFluxModelAssetDetailCustomization.h b/Source/DTFluxAssetsEditor/Public/DTFluxModelAssetDetailCustomization.h new file mode 100644 index 0000000..28aebce --- /dev/null +++ b/Source/DTFluxAssetsEditor/Public/DTFluxModelAssetDetailCustomization.h @@ -0,0 +1,26 @@ +// Fill out your copyright notice in the Description page of Project Settings. +#pragma once + +#include "CoreMinimal.h" +#include "IDetailCustomization.h" +#include "Widget/DTFluxAssetModelDetailsWidget.h" + +class FDTFluxModelAssetCustomization : public IDetailCustomization +{ +public: + // IDetailCustomization interface + virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; + + void CustomizeDetailsWithRawDataAccess(IDetailLayoutBuilder& DetailBuilder); + + // Crée une instance de cette customization + static TSharedRef MakeInstance(); + +private: + // Handle vers l'objet en cours d'édition + TWeakObjectPtr ModelAsset; + + + // Widget personnalisé + TSharedPtr DetailsWidget; +}; diff --git a/Source/DTFluxAssetsEditor/Public/Widget/DTFluxAssetModelDetailsWidget.h b/Source/DTFluxAssetsEditor/Public/Widget/DTFluxAssetModelDetailsWidget.h new file mode 100644 index 0000000..9ec71e1 --- /dev/null +++ b/Source/DTFluxAssetsEditor/Public/Widget/DTFluxAssetModelDetailsWidget.h @@ -0,0 +1,191 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Types/Struct/DTFluxRaceDataStructs.h" +#include "Widgets/Views/STreeView.h" +#include "Widgets/Views/SHeaderRow.h" +#include "Assets/DTFluxModelAsset.h" + + +struct FHierarchicalTreeItem; +class SDTFluxAssetModelDetailsWidget; + +// ✅ SOUS-CLASSE DE SMultiColumnTableRow +class SHierarchicalTreeItemRow : public SMultiColumnTableRow> +{ +public: + SLATE_BEGIN_ARGS(SHierarchicalTreeItemRow) + { + } + + SLATE_ARGUMENT(TSharedPtr, Item) + SLATE_ARGUMENT(SDTFluxAssetModelDetailsWidget*, ParentWidget) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs, const TSharedRef& InOwnerTableView); + +protected: + virtual TSharedRef GenerateWidgetForColumn(const FName& ColumnName) override; + +private: + TSharedPtr Item; + SDTFluxAssetModelDetailsWidget* ParentWidget; +}; + +// ✅ STRUCTURE SIMPLE POUR TREEVIEW +struct FHierarchicalTreeItem +{ + enum class EItemType + { + Contest, + Stage, + Split, + Participant + }; + + EItemType Type; + FString Name; + FString ID; + FString Details; + FString Status; + FString Extra; + + // Données pour retrouver l'élément original + int32 ContestId = -1; + int32 StageId = -1; + int32 SplitId = -1; + int32 Bib = -1; + + // Enfants pour la hiérarchie + TArray> Children; + + FHierarchicalTreeItem(EItemType InType, const FString& InName) + : Type(InType), Name(InName) + { + } + + void AddChild(TSharedPtr Child) + { + if (Child.IsValid()) + { + Children.Add(Child); + } + } + + // Factory methods pour créer les éléments + static TSharedPtr CreateContest(const FString& ContestName, const FDTFluxContest& Contest) + { + TSharedPtr Item = MakeShareable( + new FHierarchicalTreeItem(EItemType::Contest, ContestName)); + Item->ContestId = Contest.ContestId; + Item->ID = FString::Printf(TEXT("%d"), Contest.ContestId); + Item->Details = FString::Printf( + TEXT("%d stages, %d participants"), Contest.Stages.Num(), Contest.ParticipantsBib.Num()); + Item->Status = Contest.Date.ToString(TEXT("%Y-%m-%d")); + Item->Extra = Contest.IsFinished() ? TEXT("Finished") : TEXT("Active"); + return Item; + } + + static TSharedPtr CreateStage(const FDTFluxStage& Stage, int32 InContestId) + { + FString StageName = Stage.Name.IsEmpty() ? FString::Printf(TEXT("Stage %d"), Stage.StageId) : Stage.Name; + TSharedPtr Item = MakeShareable(new FHierarchicalTreeItem(EItemType::Stage, StageName)); + Item->ContestId = InContestId; + Item->StageId = Stage.StageId; + Item->ID = FString::Printf(TEXT("%d"), Stage.StageId); + Item->Details = FString::Printf(TEXT("Start: %s"), *Stage.StartTime.ToString(TEXT("%H:%M"))); + Item->Status = FString::Printf(TEXT("End: %s"), *Stage.EndTime.ToString(TEXT("%H:%M"))); + Item->Extra = FString::Printf(TEXT("Cutoff: %s"), *Stage.CutOff.ToString(TEXT("%H:%M"))); + return Item; + } + + static TSharedPtr CreateSplit(const FDTFluxSplit& Split, int32 InContestId) + { + FString SplitName = Split.Name.IsEmpty() ? FString::Printf(TEXT("Split %d"), Split.SplitId) : Split.Name; + TSharedPtr Item = MakeShareable(new FHierarchicalTreeItem(EItemType::Split, SplitName)); + Item->ContestId = InContestId; + Item->SplitId = Split.SplitId; + Item->ID = FString::Printf(TEXT("%d"), Split.SplitId); + Item->Details = FString::Printf(TEXT("%d rankings"), Split.SplitRankings.Num()); + Item->Status = TEXT("-"); + Item->Extra = TEXT("-"); + return Item; + } + + static TSharedPtr CreateParticipant(const FDTFluxParticipant& Participant, + UDTFluxModelAsset* InModelAsset = nullptr) + { + FString ParticipantName = Participant.GetConcatFormattedName(); + TSharedPtr Item = MakeShareable( + new FHierarchicalTreeItem(EItemType::Participant, ParticipantName)); + Item->Bib = Participant.Bib; + Item->ContestId = Participant.ContestId; + Item->ID = FString::Printf(TEXT("%d"), Participant.Bib); + Item->Details = FString::Printf(TEXT("%s - %d teammates"), *Participant.Category, + Participant.GetTeammate().Num()); + FString Status = UEnum::GetValueAsString(Participant.Status); + TArray StatusArray; + Status.ParseIntoArray(StatusArray, TEXT("::")); + Item->Status = StatusArray.Last(); + Item->Extra = Participant.Club; + return Item; + } +}; + +typedef TSharedPtr FHierarchicalTreeItemPtr; + +/** + * Widget avec STreeView simple et efficace + */ +class DTFLUXASSETSEDITOR_API SDTFluxAssetModelDetailsWidget : public SCompoundWidget +{ +public: + SLATE_BEGIN_ARGS(SDTFluxAssetModelDetailsWidget) + { + } + + SLATE_ARGUMENT(UDTFluxModelAsset*, ModelAsset) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs); + void RefreshData(); + + // Méthodes publiques pour la sous-classe Row + FSlateColor GetItemTypeColor(FHierarchicalTreeItem::EItemType Type) const; + const FSlateBrush* GetItemIcon(FHierarchicalTreeItem::EItemType Type) const; + +private: + // Données + UDTFluxModelAsset* ModelAsset = nullptr; + TArray RootItems; // Contests racines avec hiérarchie + TArray ParticipantItems; // Participants séparés + + // Widgets - TreeView simple + TSharedPtr> ContestTreeView; + TSharedPtr> ParticipantTreeView; + TSharedPtr StatsText; + TSharedPtr SelectionText; + + // Méthodes de construction + void BuildContestHierarchy(); + void BuildParticipantList(); + + // Callbacks TreeView + TSharedRef OnGenerateRowForTree(FHierarchicalTreeItemPtr Item, + const TSharedRef& OwnerTable); + void OnGetChildrenForTree(FHierarchicalTreeItemPtr Item, TArray& OutChildren); + void OnTreeSelectionChanged(FHierarchicalTreeItemPtr SelectedItem, ESelectInfo::Type SelectInfo); + void OnSetExpansionRecursive(FHierarchicalTreeItemPtr Item, bool bIsExpanded); + + // Callbacks des boutons + FReply OnRefreshClicked(); + FReply OnExpandAllClicked(); + FReply OnCollapseAllClicked(); + + EActiveTimerReturnType ForceInitialLayout(double InCurrentTime, float InDeltaTime); + + // Utilitaires + FText GetStatsText() const; +}; diff --git a/Source/DTFluxCore/Private/Assets/DTFluxModelAsset.cpp b/Source/DTFluxCore/Private/Assets/DTFluxModelAsset.cpp index 1579405..683c765 100644 --- a/Source/DTFluxCore/Private/Assets/DTFluxModelAsset.cpp +++ b/Source/DTFluxCore/Private/Assets/DTFluxModelAsset.cpp @@ -10,21 +10,31 @@ UDTFluxModelAsset::UDTFluxModelAsset(const FObjectInitializer& ObjectInitializer { } -void UDTFluxModelAsset::AddContest(const FDTFluxContest &Contest) +void UDTFluxModelAsset::AddContest(const FDTFluxContest& Contest) { Contests.Add(Contest.Name, Contest); } bool UDTFluxModelAsset::GetContestById(const int InContestId, FDTFluxContest& OutContest) { - for(auto& ContestItem : Contests) + for (auto& ContestItem : Contests) { - if(ContestItem.Value.ContestId == InContestId) + if (ContestItem.Value.ContestId == InContestId) { OutContest = ContestItem.Value; return true; } + } + return false; +} +bool UDTFluxModelAsset::GetStage(FDTFluxStageKey StageKey, FDTFluxStage& OutStage) +{ + FDTFluxContest TargetContest; + int TargetStageId = StageKey.StageId; + if (GetContestById(StageKey.ContestId, TargetContest)) + { + return TargetContest.GetStage(TargetStageId, OutStage); } return false; } @@ -36,19 +46,20 @@ void UDTFluxModelAsset::AddPerson(const FDTFluxPerson& InPerson) void UDTFluxModelAsset::AddParticipant(const FDTFluxParticipant& InParticipant, const int ContestId) { - UE_LOG(logDTFluxCore, Error, TEXT("%i Person in Participant %i"), InParticipant.GetTeammateNum(), InParticipant.Bib); + UE_LOG(logDTFluxCore, Error, TEXT("%i Person in Participant %i"), InParticipant.GetTeammateNum(), + InParticipant.Bib); FDTFluxContest TargetContest; - if(GetContestById(ContestId, TargetContest)) + if (GetContestById(ContestId, TargetContest)) { TArray Teammate = InParticipant.Teammate; - for(auto& Person : InParticipant.Teammate) + for (auto& Person : InParticipant.Teammate) { UE_LOG(logDTFluxCore, Error, TEXT("AddParticipant() DTFlux Person %s %s %s"), - *Person.FirstName, *Person.LastName, *Person.Gender); - if(!PersonExists(Person)) + *Person.FirstName, *Person.LastName, *Person.Gender); + if (!PersonExists(Person)) { UE_LOG(logDTFluxCore, Error, TEXT("AddParticipant() DTFlux Person %s %s %s doesnot exists, adding..."), - *Person.FirstName, *Person.LastName, *Person.Gender); + *Person.FirstName, *Person.LastName, *Person.Gender); AddPerson(Person); } } @@ -68,10 +79,11 @@ bool UDTFluxModelAsset::PersonExists(const FDTFluxPerson& InPerson) const FString UDTFluxModelAsset::GetContestNameForId(const int InContestID) { FDTFluxContest Contest; - if(!GetContestById(InContestID, Contest)) + if (!GetContestById(InContestID, Contest)) { - UE_LOG(logDTFluxCore, Warning, TEXT("GetContestNameForId(%i) [unable to find a contest] result will be empty !!!"), - InContestID); + UE_LOG(logDTFluxCore, Warning, + TEXT("GetContestNameForId(%i) [unable to find a contest] result will be empty !!!"), + InContestID); } return Contest.Name; } @@ -85,7 +97,7 @@ void UDTFluxModelAsset::UpdateParticipant(const FDTFluxParticipant& Participant) { // TODO : If update is on Bib we are totally lost as we search by bib. int Bib = Participant.Bib; - if(Participants.Contains(Bib)) + if (Participants.Contains(Bib)) { TArray InTeammate = Participant.Teammate; Participants[Bib].Elite = Participant.Elite; @@ -95,7 +107,7 @@ void UDTFluxModelAsset::UpdateParticipant(const FDTFluxParticipant& Participant) Participants[Bib].Team = Participant.Team; Participants[Bib].Status = Participant.Status; //TODO : Update Person - for(const auto& Person : InTeammate) + for (const auto& Person : InTeammate) { //Don't know what to do... } @@ -104,12 +116,23 @@ void UDTFluxModelAsset::UpdateParticipant(const FDTFluxParticipant& Participant) void UDTFluxModelAsset::UpdateParticipantStatus(const FDTFluxTeamStatusUpdate& NewParticipantStatus) { - if(Participants.Contains(NewParticipantStatus.Bib)) + if (Participants.Contains(NewParticipantStatus.Bib)) { Participants[NewParticipantStatus.Bib].Status = NewParticipantStatus.Status; } } +bool UDTFluxModelAsset::GetParticipantByBib(int Bib, FDTFluxParticipant& OutParticipant) +{ + if (Participants.Contains(Bib)) + { + OutParticipant = Participants[Bib]; + return true; + } + return false; +} + + void UDTFluxModelAsset::UpdateOrCreateStageRanking(const FDTFluxStageRankings& InStageRankings) { FDTFluxStageKey StageKey = InStageRankings.GetCompositeKey(); diff --git a/Source/DTFluxCore/Private/Types/Objects/UDTFluxParticipantFactory.cpp b/Source/DTFluxCore/Private/Types/Objects/UDTFluxParticipantFactory.cpp index 7535cff..945a62e 100644 --- a/Source/DTFluxCore/Private/Types/Objects/UDTFluxParticipantFactory.cpp +++ b/Source/DTFluxCore/Private/Types/Objects/UDTFluxParticipantFactory.cpp @@ -18,8 +18,9 @@ bool UDTFluxParticipantFactory::CreateParticipantFomJson(const FString& JsonStri } } -bool UDTFluxParticipantFactory::CreateFromJsonCpp(const TSharedPtr JsonObject, FDTFluxParticipant& OutParticipant) +bool UDTFluxParticipantFactory::CreateFromJsonCpp(const TSharedPtr JsonObject, + FDTFluxParticipant& OutParticipant) { OutParticipant = FDTFluxParticipant::CreateFromJson(JsonObject); - return OutParticipant == 0; + return !OutParticipant.IsDefault(); } diff --git a/Source/DTFluxCore/Private/Types/Struct/DTFluxTeamListStruct.cpp b/Source/DTFluxCore/Private/Types/Struct/DTFluxTeamListStruct.cpp index 0f09e35..49f6cdf 100644 --- a/Source/DTFluxCore/Private/Types/Struct/DTFluxTeamListStruct.cpp +++ b/Source/DTFluxCore/Private/Types/Struct/DTFluxTeamListStruct.cpp @@ -4,8 +4,6 @@ #include "Types/Struct/DTFluxTeamListStruct.h" - - void FDTFluxParticipant::AddTeammate(const FDTFluxPerson& Person) { Teammate.Add(Person); @@ -15,19 +13,24 @@ void FDTFluxParticipant::AddTeammate(const FString LastName, const FString First { } -FText FDTFluxParticipant::GetFormattedName(const int MaxChar, const FString OverflowChars) +FString FDTFluxParticipant::GetFormattedName(const int MaxChar, const FString OverflowChars) const { // Vérifie les cas limites if (MaxChar <= 0) { - return FText::GetEmpty(); + return ""; } FString FirstName; FString LastName; - if(IsTeam()) + if (IsTeam()) { LastName = Team; } + else + { + FirstName = Teammate[0].FirstName; + LastName = Teammate[0].LastName; + } // Récupère la première lettre du prénom en majuscule FString Initial; if (!FirstName.IsEmpty()) @@ -40,6 +43,7 @@ FText FDTFluxParticipant::GetFormattedName(const int MaxChar, const FString Over // Construction du nom final FString FullName = Initial + FormattedLastName; + UE_LOG(logDTFluxCore, Error, TEXT("FullName for Bib %i is %s"), Bib, *FullName); // Tronque si nécessaire if (FullName.Len() > MaxChar) @@ -48,8 +52,7 @@ FText FDTFluxParticipant::GetFormattedName(const int MaxChar, const FString Over const int32 AvailableLength = MaxChar - Initial.Len(); if (AvailableLength <= 0) { - // Pas assez de place pour le nom → juste l'initiale ? - return FText::FromString(Initial); + return Initial; } // Coupe le nom pour qu’il rentre dans la limite @@ -83,41 +86,41 @@ FText FDTFluxParticipant::GetFormattedName(const int MaxChar, const FString Over } } - return FText::FromString(FullName); + return FullName; } -FText FDTFluxParticipant::GetConcatFormattedName(const int MaxChar, const FString OverflowChar) +FString FDTFluxParticipant::GetConcatFormattedName(const int MaxChar, const FString OverflowChar) const { FString BibText = FString::FromInt(Bib) + " "; - FText FormattedName = GetFormattedName(MaxChar - BibText.Len(), OverflowChar ); - return FText::FromString(BibText + FormattedName.ToString()); + FString FormattedName = GetFormattedName(MaxChar - BibText.Len(), OverflowChar); + return BibText + FormattedName; } // Constructeur privé depuis JSON FDTFluxParticipant::FDTFluxParticipant(const TSharedPtr& JsonObject) : Bib(JsonObject->GetIntegerField(TEXT("bib"))) - , ContestId(JsonObject->GetIntegerField(TEXT("contestId"))) - , Category(JsonObject->GetStringField(TEXT("category"))) - , Club(JsonObject->GetStringField(TEXT("club"))) - , Elite(JsonObject->GetBoolField(TEXT("elite"))) - , Status(static_cast(JsonObject->GetIntegerField(TEXT("status")))) - , Team(JsonObject->GetStringField(TEXT("team"))) - , bIsMassStartParticipant(false) - , CurrentSplit(-1) + , ContestId(JsonObject->GetIntegerField(TEXT("contestId"))) + , Category(JsonObject->GetStringField(TEXT("category"))) + , Club(JsonObject->GetStringField(TEXT("club"))) + , Elite(JsonObject->GetBoolField(TEXT("elite"))) + , Status(static_cast(JsonObject->GetIntegerField(TEXT("status")))) + , Team(JsonObject->GetStringField(TEXT("team"))) + , bIsMassStartParticipant(false) + , CurrentSplit(-1) { UE_LOG(logDTFluxCore, Error, TEXT("Ctor with JSON Object")) - for(uint8 Index = 1; ; Index++) + for (uint8 Index = 1; ; Index++) { FString FirstNameKey = Index == 1 ? "firstName" : FString::Printf(TEXT("firstName%i"), Index); FString LastNameKey = Index == 1 ? "lastName" : FString::Printf(TEXT("lastName%i"), Index); FString GenderKey = Index == 1 ? "gender" : FString::Printf(TEXT("gender%i"), Index); // max 10 Persons - if(Index >= 10) + if (Index >= 10) { break; } if (!JsonObject->HasField(FirstNameKey) && !JsonObject->HasField(LastNameKey) - && !JsonObject->HasField(GenderKey)) + && !JsonObject->HasField(GenderKey)) { UE_LOG(logDTFluxCore, Error, TEXT("No Corresponding Field!!!")) break; @@ -134,7 +137,6 @@ FDTFluxParticipant::FDTFluxParticipant(const TSharedPtr& JsonObject Teammate.Add(Person); } UE_LOG(logDTFluxCore, Error, TEXT("Ctor with JSON Object Teammate is now %i long"), Teammate.Num()); - } FDTFluxParticipant FDTFluxParticipant::CreateFromJson(const TSharedPtr& JsonObject) @@ -147,7 +149,7 @@ int FDTFluxParticipant::GetTeammateNum() const return Teammate.Num(); } -bool FDTFluxParticipant::IsTeam() +bool FDTFluxParticipant::IsTeam() const { return Teammate.Num() < 1; -} \ No newline at end of file +} diff --git a/Source/DTFluxCore/Private/Types/Struct/FDTFluxPursuitInfo.cpp b/Source/DTFluxCore/Private/Types/Struct/FDTFluxPursuitInfo.cpp new file mode 100644 index 0000000..990fb77 --- /dev/null +++ b/Source/DTFluxCore/Private/Types/Struct/FDTFluxPursuitInfo.cpp @@ -0,0 +1,4 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Types/Struct/FDTFluxPursuitInfo.h" diff --git a/Source/DTFluxCore/Public/Assets/DTFluxModelAsset.h b/Source/DTFluxCore/Public/Assets/DTFluxModelAsset.h index c135617..8cac78d 100644 --- a/Source/DTFluxCore/Public/Assets/DTFluxModelAsset.h +++ b/Source/DTFluxCore/Public/Assets/DTFluxModelAsset.h @@ -7,6 +7,7 @@ #include "Dom/JsonObject.h" #include "Types/Struct/DTFluxCompositeKey.h" #include "Types/Struct/DTFluxRaceDataStructs.h" +#include "Types/Struct/DTFluxSplitSensor.h" #include "DTFluxModelAsset.generated.h" @@ -19,8 +20,8 @@ class DTFLUXCORE_API UDTFluxModelAsset : public UObject { GENERATED_BODY() UDTFluxModelAsset(const FObjectInitializer& ObjectInitializer); -public: +public: UPROPERTY(BlueprintReadWrite, EditAnywhere) FString EventName = "MyEvent"; @@ -30,7 +31,7 @@ public: UPROPERTY(BlueprintReadWrite, EditAnywhere) TMap Participants; - + UPROPERTY(BlueprintReadOnly, EditAnywhere) TMap Contests; @@ -44,11 +45,15 @@ public: TMap SplitRankings; UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|ModelAsset") - void AddContest(const FDTFluxContest &Contest); + void AddContest(const FDTFluxContest& Contest); UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|Contest") bool GetContestById(const int InContestId, FDTFluxContest& OutContest); - + + UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|Contest") + bool GetStage(FDTFluxStageKey StageKey, FDTFluxStage& OutStage); + + UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|Person") void AddPerson(const FDTFluxPerson& InPerson); @@ -81,4 +86,7 @@ public: UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|Participant") void UpdateParticipantStatus(const FDTFluxTeamStatusUpdate& NewParticipantStatus); + + UFUNCTION(BlueprintCallable, CallInEditor, Category="DTFlux|Participant") + bool GetParticipantByBib(int Bib, FDTFluxParticipant& OutParticipant); }; diff --git a/Source/DTFluxCore/Public/Types/Enum/DTFluxCoreEnum.h b/Source/DTFluxCore/Public/Types/Enum/DTFluxCoreEnum.h index db5e7c2..28f3b88 100644 --- a/Source/DTFluxCore/Public/Types/Enum/DTFluxCoreEnum.h +++ b/Source/DTFluxCore/Public/Types/Enum/DTFluxCoreEnum.h @@ -7,23 +7,68 @@ UENUM(BlueprintType) -enum class EDTFluxRequestType : uint8 +enum class EDTFluxApiDataType : uint8 { - None = 0 UMETA(DisplayName="None"), - ContestRanking = 1 UMETA(DisplayName="contest-ranking"), - StageRanking = 2 UMETA(DisplayName="stage-ranking"), - SplitRanking = 3 UMETA(DisplayName="split-ranking"), - TeamList = 4 UMETA(DisplayName="team-list"), - RaceData = 5 UMETA(DisplayName="race-data"), + None = 0 UMETA(DisplayName="None"), + + // Types bidirectionnels (requête/réponse) + ContestRanking = 1 UMETA(DisplayName="contest-ranking"), + StageRanking = 2 UMETA(DisplayName="stage-ranking"), + SplitRanking = 3 UMETA(DisplayName="split-ranking"), + TeamList = 4 UMETA(DisplayName="team-list"), + RaceData = 5 UMETA(DisplayName="race-data"), + + // Types uniquement réponse (push du serveur) + TeamUpdate = 10 UMETA(DisplayName="team-update"), + StatusUpdate = 11 UMETA(DisplayName="status-update"), + SplitSensor = 12 UMETA(DisplayName="split-sensor"), + + // Types système + Error = 99 UMETA(DisplayName="error"), }; +// Alias pour clarifier l'usage +using EDTFluxRequestType = EDTFluxApiDataType; +using EDTFluxResponseType = EDTFluxApiDataType; + +// Utilitaires pour valider les usages +namespace DTFluxDataTypeUtils +{ + inline bool CanBeRequested(EDTFluxApiDataType Type) + { + return static_cast(Type) >= 1 && static_cast(Type) <= 5; + } + + inline bool IsPushOnly(EDTFluxApiDataType Type) + { + return static_cast(Type) >= 10 && static_cast(Type) <= 12; + } + + inline bool IsValidResponseType(EDTFluxApiDataType Type) + { + return Type != EDTFluxApiDataType::None; + } +} + +// +// UENUM(BlueprintType) +// enum class EDTFluxRequestType : uint8 +// { +// None = 0 UMETA(DisplayName="None"), +// ContestRanking = 1 UMETA(DisplayName="contest-ranking"), +// StageRanking = 2 UMETA(DisplayName="stage-ranking"), +// SplitRanking = 3 UMETA(DisplayName="split-ranking"), +// TeamList = 4 UMETA(DisplayName="team-list"), +// RaceData = 5 UMETA(DisplayName="race-data"), +// }; + UENUM(BlueprintType) enum class EDTFluxConnectionStatus : uint8 { - Unset = 0 UMETA(DisplayName="Unset"), - Connected = 1 << 0 UMETA(DisplayName="Connected"), - Error = 1 << 1 UMETA(DisplayName="Error"), - Closed = 1 << 2 UMETA(DisplayName="Closed"), - NotConnected= 1 << 3 UMETA(DisplayName="NotConnected") + Unset = 0 UMETA(DisplayName="Unset"), + Connected = 1 << 0 UMETA(DisplayName="Connected"), + Error = 1 << 1 UMETA(DisplayName="Error"), + Closed = 1 << 2 UMETA(DisplayName="Closed"), + NotConnected = 1 << 3 UMETA(DisplayName="NotConnected") }; diff --git a/Source/DTFluxCore/Public/Types/Struct/DTFluxCompositeKey.h b/Source/DTFluxCore/Public/Types/Struct/DTFluxCompositeKey.h index 7e3ccec..1c39471 100644 --- a/Source/DTFluxCore/Public/Types/Struct/DTFluxCompositeKey.h +++ b/Source/DTFluxCore/Public/Types/Struct/DTFluxCompositeKey.h @@ -6,95 +6,106 @@ #include "UObject/Object.h" #include "DTFluxCompositeKey.generated.h" -/** - * - */ -USTRUCT(BlueprintType) -struct DTFLUXCORE_API FDTFluxStageKey +USTRUCT() +struct FDTFluxCompositeKey { - GENERATED_BODY() - FDTFluxStageKey() = default; - FDTFluxStageKey(const int InContestId, const int InStageId ) - :ContestId(InContestId) - , StageId(InStageId){}; - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") - int ContestId = -1; - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") - int StageId = -1; - - friend uint32 GetTypeHash(const FDTFluxStageKey& Key) - { - return HashCombine( - GetTypeHash(Key.ContestId), - GetTypeHash(Key.StageId) - ); - } - - bool operator==(const FDTFluxStageKey& Other) const - { - return ContestId == Other.ContestId && StageId == Other.StageId; - } - FString GetDisplayName() const - { - return FString::Printf(TEXT("Contest%i -| Stage%i"), ContestId, StageId); - } - - FText GetTooltipText() const - { - return FText::Format(INVTEXT("Contest{0}|Stage{1}"), - FText::AsNumber(ContestId), - FText::AsNumber(StageId)); - } - + GENERATED_BODY() }; /** * */ USTRUCT(BlueprintType) -struct DTFLUXCORE_API FDTFluxSplitKey +struct DTFLUXCORE_API FDTFluxStageKey : public FDTFluxCompositeKey { - GENERATED_BODY() - FDTFluxSplitKey() = default; - FDTFluxSplitKey(const int InContestId, const int InStageId, const int InSplitId ) - :ContestId(InContestId) - , StageId(InStageId) - , SplitId(InSplitId){}; - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") - int ContestId = -1; - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") - int StageId = -1; - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") - int SplitId = -1; + GENERATED_BODY() + FDTFluxStageKey() = default; - friend uint32 GetTypeHash(const FDTFluxSplitKey& Key) - { - return HashCombine( - GetTypeHash(Key.ContestId), - GetTypeHash(Key.StageId), - GetTypeHash(Key.SplitId) - ); - } - - bool operator==(const FDTFluxSplitKey& Other) const - { - return ContestId == Other.ContestId && StageId == Other.StageId && SplitId == Other.SplitId; - } - FString GetDisplayName() const - { - return FString::Printf(TEXT("Contest%i | Stage%i | Split%i"), ContestId, StageId, SplitId); - } + FDTFluxStageKey(const int InContestId, const int InStageId) + : ContestId(InContestId) + , StageId(InStageId) + { + }; - FText GetTooltipText() const - { - return FText::Format(INVTEXT("Contest{0}|Stage{1}|Split{2}"), - FText::AsNumber(ContestId), - FText::AsNumber(StageId), - FText::AsNumber(SplitId) - ); - } - + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") + int ContestId = -1; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") + int StageId = -1; + + friend uint32 GetTypeHash(const FDTFluxStageKey& Key) + { + return HashCombine( + GetTypeHash(Key.ContestId), + GetTypeHash(Key.StageId) + ); + } + + bool operator==(const FDTFluxStageKey& Other) const + { + return ContestId == Other.ContestId && StageId == Other.StageId; + } + + FString GetDisplayName() const + { + return FString::Printf(TEXT("Contest%i -| Stage%i"), ContestId, StageId); + } + + FText GetTooltipText() const + { + return FText::Format(INVTEXT("Contest{0}|Stage{1}"), + FText::AsNumber(ContestId), + FText::AsNumber(StageId)); + } }; +/** + * + */ +USTRUCT(BlueprintType) +struct DTFLUXCORE_API FDTFluxSplitKey : public FDTFluxCompositeKey +{ + GENERATED_BODY() + FDTFluxSplitKey() = default; + + FDTFluxSplitKey(const int InContestId, const int InStageId, const int InSplitId) + : ContestId(InContestId) + , StageId(InStageId) + , SplitId(InSplitId) + { + }; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") + int ContestId = -1; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") + int StageId = -1; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model") + int SplitId = -1; + + friend uint32 GetTypeHash(const FDTFluxSplitKey& Key) + { + return HashCombine( + GetTypeHash(Key.ContestId), + GetTypeHash(Key.StageId), + GetTypeHash(Key.SplitId) + ); + } + + bool operator==(const FDTFluxSplitKey& Other) const + { + return ContestId == Other.ContestId && StageId == Other.StageId && SplitId == Other.SplitId; + } + + FString GetDisplayName() const + { + return FString::Printf(TEXT("Contest%i | Stage%i | Split%i"), ContestId, StageId, SplitId); + } + + FText GetTooltipText() const + { + return FText::Format(INVTEXT("Contest{0}|Stage{1}|Split{2}"), + FText::AsNumber(ContestId), + FText::AsNumber(StageId), + FText::AsNumber(SplitId) + ); + } +}; diff --git a/Source/DTFluxCore/Public/Types/Struct/DTFluxRaceDataStructs.h b/Source/DTFluxCore/Public/Types/Struct/DTFluxRaceDataStructs.h index 55765a0..8117276 100644 --- a/Source/DTFluxCore/Public/Types/Struct/DTFluxRaceDataStructs.h +++ b/Source/DTFluxCore/Public/Types/Struct/DTFluxRaceDataStructs.h @@ -17,6 +17,7 @@ USTRUCT(BlueprintType, Category="DTFlux|RaceData") struct DTFLUXCORE_API FDTFluxSplit { GENERATED_BODY() + public: UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model", EditAnywhere) int SplitId = -1; @@ -28,7 +29,6 @@ public: // // void InsertOrReplace(const FDTFluxStageRankingResponseItem& SplitRankingItemResp); // void SortByRank(); // TArray GetSplitRanking(const int From = 0, const int DisplayNumber = 0); - }; /** @@ -39,6 +39,7 @@ USTRUCT(BlueprintType, Category="DTFlux|RaceData") struct DTFLUXCORE_API FDTFluxStage { GENERATED_BODY() + public: UPROPERTY(BlueprintReadWrite, Category="DTFlux|model", EditAnywhere) int StageId; @@ -60,6 +61,7 @@ USTRUCT(BlueprintType, Category="DTFlux|RaceData") struct DTFLUXCORE_API FDTFluxContest { GENERATED_BODY() + public: UPROPERTY(BlueprintReadWrite, Category="DTFlux|model", EditAnywhere) int ContestId = -1; @@ -79,20 +81,22 @@ public: int LastStageId = -1; - bool IsFinished(); + bool IsFinished() const; inline void UpdateEndTime(); int GetLastStageId(); void UpdateLastStageId(); + FDTFluxStage& GetLastStage() const; + bool GetStage(const int StageID, FDTFluxStage& OutStage) const; }; -inline bool FDTFluxContest::IsFinished() +inline bool FDTFluxContest::IsFinished() const { return EndTime <= FDateTime::Now(); } inline void FDTFluxContest::UpdateEndTime() { - TArray TempStages; + TArray TempStages = Stages; TempStages.Sort([](const FDTFluxStage& A, const FDTFluxStage& B) { return A.EndTime < B.EndTime; @@ -102,7 +106,7 @@ inline void FDTFluxContest::UpdateEndTime() inline int FDTFluxContest::GetLastStageId() { - if(LastStageId <= 0) + if (LastStageId <= 0) { UpdateLastStageId(); } @@ -111,12 +115,39 @@ inline int FDTFluxContest::GetLastStageId() inline void FDTFluxContest::UpdateLastStageId() { - TArray TempStages = Stages; - TempStages.Sort([](const FDTFluxStage&A , const FDTFluxStage& B) + TArray TempStages = Stages; + TempStages.Sort([](const FDTFluxStage& A, const FDTFluxStage& B) + { + return A.StageId < B.StageId; + }); + LastStageId = TempStages.Last().StageId; +} + +inline FDTFluxStage& FDTFluxContest::GetLastStage() const +{ + TArray TempStages = Stages; + TempStages.Sort([](const FDTFluxStage& A, const FDTFluxStage& B) + { + return A.StageId < B.StageId; + }); + return TempStages.Last(); +} + +inline bool FDTFluxContest::GetStage(const int StageID, FDTFluxStage& OutStage) const +{ + if (Stages.Num() == 0) + { + return false; + } + for (const FDTFluxStage& Stage : Stages) + { + if (Stage.StageId == StageID) { - return A.StageId < B.StageId; - }); - LastStageId = TempStages.Last().StageId; + OutStage = Stage; + return true; + } + } + return false; } @@ -124,14 +155,9 @@ USTRUCT() struct DTFLUXCORE_API FDTFluxRaceData { GENERATED_BODY() + public: - UPROPERTY() // ReSharper disable once IdentifierTypo TArray Datas; - - }; - - - diff --git a/Source/DTFluxCore/Public/Types/Struct/DTFluxRankingStructs.h b/Source/DTFluxCore/Public/Types/Struct/DTFluxRankingStructs.h index 15a2fcb..cdbddaf 100644 --- a/Source/DTFluxCore/Public/Types/Struct/DTFluxRankingStructs.h +++ b/Source/DTFluxCore/Public/Types/Struct/DTFluxRankingStructs.h @@ -16,6 +16,7 @@ USTRUCT(BlueprintType, Category="DTFlux|Model") struct DTFLUXCORE_API FDTFluxContestRanking { GENERATED_BODY() + public: UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) int Bib; @@ -31,7 +32,7 @@ public: FString SpeedRunningAverage; UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere); FString SpeedTotalAverage; - void Dump () const; + void Dump() const; }; @@ -39,13 +40,14 @@ USTRUCT(BlueprintType) struct FDTFluxContestRankings { GENERATED_BODY() + public: - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) TArray Rankings; - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) int ContestId; //TODO check if necessary ??? - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) FString ContestName; void SetName(const FString Name) @@ -62,6 +64,7 @@ USTRUCT(BlueprintType, Category="DTFlux|Model") struct DTFLUXCORE_API FDTFluxDetailedRankingItem { GENERATED_BODY() + public: UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) int Bib; @@ -82,23 +85,23 @@ public: UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) FDateTime StartTime; UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) - float SpeedRunning; + float SpeedRunning; UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) - float SpeedTotal; + float SpeedTotal; UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere) float SpeedSwim; void Dump() const; - }; USTRUCT(BlueprintType) struct FDTFluxDetailedRankings { GENERATED_BODY() + public: - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) int ContestId = -1; - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) int StageId = -1; TArray Rankings; }; @@ -139,6 +142,7 @@ struct FDTFluxStageRankings : public FDTFluxDetailedRankings { return FDTFluxStageKey(InRankings.ContestId, InRankings.StageId); } + inline FDTFluxStageKey GetCompositeKey() const { return FDTFluxStageKey(ContestId, StageId); @@ -146,54 +150,53 @@ struct FDTFluxStageRankings : public FDTFluxDetailedRankings inline bool IsInitialized() const { - return ContestId > 0 && StageId > 0; } void Initialize() { - for(auto& Ranking : Rankings) + for (auto& Ranking : Rankings) { FDateTime RankingStartTime; - if(Ranking.TimeStart != "") + if (Ranking.TimeStart != "") { TArray Exploded; Ranking.TimeStart.ParseIntoArray(Exploded, TEXT(":"), true); - if(Exploded.Num() == 3) + if (Exploded.Num() == 3) { - RankingStartTime = FDateTime(0,0,0, - FCString::Atoi(*Exploded[0]), FCString::Atoi(*Exploded[1]), - FCString::Atoi(*Exploded[2])); + FDateTime Now = FDateTime::Now(); + RankingStartTime = FDateTime(Now.GetYear(), Now.GetMonth(), Now.GetDay(), + FCString::Atoi(*Exploded[0]), FCString::Atoi(*Exploded[1]), + FCString::Atoi(*Exploded[2])); } } Ranking.StartTime = RankingStartTime; } } - }; - USTRUCT(BlueprintType) struct FDTFluxSplitRankings : public FDTFluxDetailedRankings { GENERATED_BODY() + public: - UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere) + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere) int SplitId; + inline static FDTFluxSplitKey GetKeyFrom(const FDTFluxSplitRankings& InRankings) { return FDTFluxSplitKey(InRankings.ContestId, InRankings.StageId, InRankings.SplitId); } + inline FDTFluxSplitKey GetCompositeKey() const { return FDTFluxSplitKey(ContestId, StageId, SplitId); } + inline bool IsInitialized() const { - - return ContestId > 0 && StageId > 0 && SplitId >0 ; + return ContestId > 0 && StageId > 0 && SplitId > 0; } }; - - diff --git a/Source/DTFluxCore/Public/Types/Struct/DTFluxTeamListStruct.h b/Source/DTFluxCore/Public/Types/Struct/DTFluxTeamListStruct.h index 262e320..d93bd3d 100644 --- a/Source/DTFluxCore/Public/Types/Struct/DTFluxTeamListStruct.h +++ b/Source/DTFluxCore/Public/Types/Struct/DTFluxTeamListStruct.h @@ -4,7 +4,6 @@ #include "CoreMinimal.h" #include "UObject/Object.h" -#include "DTFluxCoreModule.h" #include "Types/Enum/DTFluxModelEnums.h" #include "DTFluxTeamListStruct.generated.h" @@ -12,6 +11,7 @@ USTRUCT() struct DTFLUXCORE_API FDTFluxTeamListItemDefinition { GENERATED_BODY() + public: UPROPERTY() FString Type = "team-list-item"; @@ -44,13 +44,11 @@ public: }; - - - USTRUCT(BlueprintType, Category="DTFlux|Model") struct DTFLUXCORE_API FDTFluxPerson { GENERATED_BODY() + public: UPROPERTY(BlueprintReadWrite, Category="DTFlux|model", EditAnywhere) FString FirstName; @@ -66,20 +64,23 @@ public: bool operator==(const FDTFluxPerson& Right) const { return FirstName.ToLower() + LastName.ToLower() + Gender.ToLower() - == Right.FirstName.ToLower() + Right.LastName.ToLower() + Right.Gender.ToLower(); + == Right.FirstName.ToLower() + Right.LastName.ToLower() + Right.Gender.ToLower(); } + bool operator==(const int Length) const { return (FirstName.ToLower() + LastName.ToLower() + Gender.ToLower()).Len() == Length; } + bool operator!=(const int Length) const { return !(*this == Length); } + bool operator!=(const FDTFluxPerson& Right) const { return FirstName.ToLower() + LastName.ToLower() + Gender.ToLower() - != Right.FirstName.ToLower() + Right.LastName.ToLower() + Right.Gender.ToLower(); + != Right.FirstName.ToLower() + Right.LastName.ToLower() + Right.Gender.ToLower(); } }; @@ -91,30 +92,42 @@ struct DTFLUXCORE_API FDTFluxParticipant friend class UDTFluxModelAsset; friend class UDTFluxParticipantFactory; + public: // Constructeur public par défaut requis par Unreal FDTFluxParticipant() : Bib(-1) - ,ContestId(-1) - , Elite(false) - , Status(static_cast(0)) - , bIsMassStartParticipant(false) - , CurrentSplit(-1) + , ContestId(-1) + , Elite(false) + , Status(static_cast(0)) + , bIsMassStartParticipant(false) + , CurrentSplit(-1) { Teammate.Reset(); } - bool operator == ( int Rhs) const + /** + * Vérifie si le participant est dans son état par défaut (non initialisé) + * @return True si tous les champs sont à leur valeur par défaut + */ + bool IsDefault() const { - return Rhs == 0 && Bib == -1 && Team.IsEmpty() && Club.IsEmpty() && ContestId == -1 - && Teammate.IsEmpty(); + return Bib == -1 + && ContestId == -1 + && Category.IsEmpty() + && Club.IsEmpty() + && !Elite + && Status == static_cast(0) + && Team.IsEmpty() + && !bIsMassStartParticipant + && CurrentSplit == -1 + && Teammate.IsEmpty(); } - UPROPERTY(BlueprintReadOnly, Category="DTFlux|model", EditAnywhere) - int Bib = -1; + int Bib = -1; UPROPERTY(BlueprintReadOnly, Category="DTFlux|model", EditAnywhere) int ContestId = -1; UPROPERTY(BlueprintReadOnly, Category="DTFlux|model", EditAnywhere) @@ -135,21 +148,55 @@ public: // void Dump() const; void AddTeammate(const FDTFluxPerson& Person); void AddTeammate(const FString LastName, const FString FirstName, const FString Gender); - FText GetFormattedName(const int MaxChar = 15, const FString OverflowChar = FString("...")); - FText GetConcatFormattedName(const int MaxChar = 20, const FString OverflowChar = FString("...")); + + FText GetFormattedNameText(const int MaxChar = 15, const FString OverflowChar = FString("...")) const + { + return FText::FromString(GetFormattedName(MaxChar, OverflowChar)); + }; + + FText GetConcatFormattedNameText(const int MaxChar = 20, const FString OverflowChar = FString("...")) const + { + return FText::FromString(GetConcatFormattedName(MaxChar, OverflowChar)); + }; + FString GetFormattedName(const int MaxChar = 15, const FString OverflowChar = FString("...")) const; + FString GetConcatFormattedName(const int MaxChar = 20, const FString OverflowChar = FString("...")) const; + + static FString GetFormattedName(const FDTFluxParticipant& Participant, const int MaxChar = 15, + const FString OverflowChar = FString("...")) + { + return Participant.GetFormattedName(MaxChar, OverflowChar); + }; + + static FString GetConcatFormattedName(const FDTFluxParticipant& Participant, const int MaxChar = 15, + const FString OverflowChar = FString("...")) + { + return Participant.GetConcatFormattedName(MaxChar, OverflowChar); + }; + + static FText GetFormattedNameText(const FDTFluxParticipant& Participant, const int MaxChar = 15, + const FString OverflowChar = FString("...")) + { + return Participant.GetFormattedNameText(); + }; + + static FText GetConcatFormattedNameText(const FDTFluxParticipant& Participant, const int MaxChar = 15, + const FString OverflowChar = FString("...")) + { + return Participant.GetConcatFormattedNameText(); + }; + const TArray GetTeammate() const { return Teammate; } private: - // --- Constructeur privé --- explicit FDTFluxParticipant(const TSharedPtr& JsonObject); - + protected: UPROPERTY(Category="DTFlux|model", VisibleAnywhere) TArray Teammate; // Méthode publique pour construire à partir d'un JSON (utilisée par la factory) static FDTFluxParticipant CreateFromJson(const TSharedPtr& JsonObject); int GetTeammateNum() const; - bool IsTeam(); + bool IsTeam() const; }; @@ -162,10 +209,11 @@ USTRUCT(BlueprintType) struct DTFLUXCORE_API FDTFluxTeamListDefinition { GENERATED_BODY() + public: UPROPERTY() // ReSharper disable once IdentifierTypo - TArray Participants; + TArray Participants; }; USTRUCT(BlueprintType) @@ -175,11 +223,14 @@ struct FDTFluxTeamStatusUpdate public: FDTFluxTeamStatusUpdate() = default; - FDTFluxTeamStatusUpdate(const int InBib, const int InStatus) - :Bib(InBib) - , Status(static_cast(InStatus)){}; - + FDTFluxTeamStatusUpdate(const int InBib, const int InStatus) + : Bib(InBib) + , Status(static_cast(InStatus)) + { + }; + + UPROPERTY(BlueprintReadWrite, Category="DTFlux|Participant") int Bib = -1; UPROPERTY(BlueprintReadWrite, Category="DTFlux|Participant") diff --git a/Source/DTFluxCore/Public/Types/Struct/FDTFluxPursuitInfo.h b/Source/DTFluxCore/Public/Types/Struct/FDTFluxPursuitInfo.h new file mode 100644 index 0000000..e5f397c --- /dev/null +++ b/Source/DTFluxCore/Public/Types/Struct/FDTFluxPursuitInfo.h @@ -0,0 +1,32 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "DTFluxTeamListStruct.h" +#include "UObject/Object.h" +#include "FDTFluxPursuitInfo.generated.h" + +USTRUCT(Blueprintable, BlueprintType) +struct FDTFluxPursuitInfo +{ + GENERATED_BODY() + + FDTFluxPursuitInfo() = default; + + FDTFluxPursuitInfo(int InBib, FDateTime InStartTime, bool InbIsMassStart = false) : + bIsMassStart(InbIsMassStart), + Bib(InBib), + StartTime(InStartTime) + { + }; + + UPROPERTY(BlueprintReadOnly, VisibleAnywhere) + bool bIsMassStart = false; + + UPROPERTY(BlueprintReadOnly, VisibleAnywhere) + int Bib = -1; + + UPROPERTY(BlueprintReadOnly, VisibleAnywhere) + FDateTime StartTime; +}; diff --git a/Source/DTFluxCoreSubsystem/DTFluxCoreSubsystem.Build.cs b/Source/DTFluxCoreSubsystem/DTFluxCoreSubsystem.Build.cs index 01644db..89e7397 100644 --- a/Source/DTFluxCoreSubsystem/DTFluxCoreSubsystem.Build.cs +++ b/Source/DTFluxCoreSubsystem/DTFluxCoreSubsystem.Build.cs @@ -2,31 +2,31 @@ public class DTFluxCoreSubsystem : ModuleRules { - public DTFluxCoreSubsystem(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxCoreSubsystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", "DTFluxCore", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "UnrealEd", - "DTFluxNetwork", - "DTFluxProjectSettings", - "DTFluxCore", - "JsonUtilities", - "Json" - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "UnrealEd", + "DTFluxNetwork", + "DTFluxProjectSettings", + "DTFluxCore", + "JsonUtilities", + "Json" + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp index d8ac024..f3d9a8c 100644 --- a/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp +++ b/Source/DTFluxCoreSubsystem/Private/DTFluxCoreSubsystem.cpp @@ -15,20 +15,19 @@ void UDTFluxCoreSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("[UDTFluxCoreSubsystem] Initializing...")); - if(!DataStorage) + if (!DataStorage) { const UDTFluxGeneralSettings* GeneralSettings = GetDefault(); TSoftObjectPtr ModelAsset = GeneralSettings->ModelAsset; DataStorage = ModelAsset.LoadSynchronous(); - if(!DataStorage) + if (!DataStorage) { - UE_LOG(logDTFluxCore, Error, TEXT("DataStorage Not Valid")); + UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("DataStorage Not Valid")); } - } //TODO REMOVE This as it's only for testing purpose NetworkSubsystem = GEngine->GetEngineSubsystem(); - if(NetworkSubsystem->WsStatus != EDTFluxConnectionStatus::Connected) + if (NetworkSubsystem->WsStatus != EDTFluxConnectionStatus::Connected) { RegisterDelegates(); } @@ -41,11 +40,11 @@ void UDTFluxCoreSubsystem::Deinitialize() void UDTFluxCoreSubsystem::SaveDataStorage() { - if(!DataStorage->MarkPackageDirty()) + if (!DataStorage->MarkPackageDirty()) { UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Unable to make package dirty !!!")) } - if(DataStorage) + if (DataStorage) { UEditorLoadingAndSavingUtils::SavePackages({DataStorage->GetPackage()}, true); } @@ -53,31 +52,66 @@ void UDTFluxCoreSubsystem::SaveDataStorage() void UDTFluxCoreSubsystem::RegisterDelegates() { - if(NetworkSubsystem) + if (NetworkSubsystem) { - NetworkSubsystem->OnReceivedRaceData().BindUFunction(this, "ProcessRaceData"); - NetworkSubsystem->OnReceivedTeamList().BindUFunction(this, "ProcessTeamList"); - NetworkSubsystem->OnReceivedContestRanking().BindUFunction(this, "ProcessContestRanking"); - NetworkSubsystem->OnReceivedStageRanking().BindUFunction(this, "ProcessStageRanking"); - NetworkSubsystem->OnReceivedSplitRanking().BindUFunction(this, "ProcessSplitRanking"); - NetworkSubsystem->OnReceivedTeamUpdate().BindUFunction(this, "ProcessTeamList"); - NetworkSubsystem->OnReceivedTeamStatusUpdate().BindUFunction(this, "ProcessTeamStatusUpdate"); - NetworkSubsystem->OnReceivedTeamUpdate().BindUFunction(this, "ProcessTeamUpdate"); - NetworkSubsystem->OnReceivedSplitSensor().BindUFunction(this, "ProcessSplitSensor"); - + // ✅ Binding avec vérification automatique des signatures + // Si la signature ne correspond pas, erreur de compilation ! + + NetworkSubsystem->OnReceivedRaceData().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessRaceData + ); + + NetworkSubsystem->OnReceivedTeamList().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessTeamList + ); + + NetworkSubsystem->OnReceivedContestRanking().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessContestRanking + ); + + NetworkSubsystem->OnReceivedStageRanking().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessStageRanking + ); + + NetworkSubsystem->OnReceivedSplitRanking().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessSplitRanking + ); + + // ⚠️ ATTENTION : Vous avez un doublon ici ! + // NetworkSubsystem->OnReceivedTeamUpdate().BindUFunction(this, "ProcessTeamList"); + + NetworkSubsystem->OnReceivedTeamStatusUpdate().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessTeamStatusUpdate + ); + + NetworkSubsystem->OnReceivedTeamUpdate().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessTeamUpdate + ); + + NetworkSubsystem->OnReceivedSplitSensor().BindUObject( + this, + &UDTFluxCoreSubsystem::ProcessSplitSensor + ); } } void UDTFluxCoreSubsystem::ProcessRaceData(const FDTFluxRaceData& RaceDataDefinition) { - - if( RaceDataDefinition.Datas.Num() > 0 ) + if (RaceDataDefinition.Datas.Num() > 0) { - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Receiving RaceDataDefinition [%s]"), *RaceDataDefinition.Datas[0].Name); - if(DataStorage != nullptr) + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Receiving RaceDataDefinition [%s]"), + *RaceDataDefinition.Datas[0].Name); + if (DataStorage != nullptr) { UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("DataStorage Name %s"), *DataStorage->EventName); - for(auto Contest : RaceDataDefinition.Datas) + for (auto Contest : RaceDataDefinition.Datas) { DataStorage->AddContest(Contest); } @@ -90,32 +124,31 @@ void UDTFluxCoreSubsystem::ProcessRaceData(const FDTFluxRaceData& RaceDataDefini return; } UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("RaceDataDefinition is empty !!!")); - - } void UDTFluxCoreSubsystem::ProcessTeamList(const FDTFluxTeamListDefinition& TeamListDefinition) { - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Received TeamList with %i Items"), TeamListDefinition.Participants.Num()); - for(const auto& Participant : TeamListDefinition.Participants) + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Received TeamList with %i Items"), + TeamListDefinition.Participants.Num()); + for (const auto& Participant : TeamListDefinition.Participants) { - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Add Participant %i in %i ContestId"), - Participant.Bib, Participant.ContestId ); + Participant.Bib, Participant.ContestId); DataStorage->AddParticipant(Participant, Participant.ContestId); } - - + SaveDataStorage(); } void UDTFluxCoreSubsystem::ProcessContestRanking(const FDTFluxContestRankings& ContestRankings) { - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Received ContestRankings with %i Items"), ContestRankings.Rankings.Num()); + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Received ContestRankings with %i Items"), + ContestRankings.Rankings.Num()); FDTFluxContestRankings NewContestRankings = ContestRankings; - NewContestRankings.SetName( DataStorage->GetContestNameForId(ContestRankings.ContestId)); + NewContestRankings.SetName(DataStorage->GetContestNameForId(ContestRankings.ContestId)); DataStorage->AddContestRanking(NewContestRankings); - UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("ContestRankings added for Contest %s"), *NewContestRankings.ContestName); + UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("ContestRankings added for Contest %s"), + *NewContestRankings.ContestName); SaveDataStorage(); } @@ -125,12 +158,12 @@ void UDTFluxCoreSubsystem::ProcessStageRanking(const FDTFluxStageRankings& Stage DataStorage->UpdateOrCreateStageRanking(StageRankings); SaveDataStorage(); } + void UDTFluxCoreSubsystem::ProcessSplitRanking(const FDTFluxSplitRankings& SplitRankings) { UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Received SplitRanking with %i Items"), SplitRankings.Rankings.Num()); DataStorage->UpdateOrCreateSplitRanking(SplitRankings); SaveDataStorage(); - } void UDTFluxCoreSubsystem::ProcessTeamStatusUpdate(const FDTFluxTeamStatusUpdate& NewParticipantStatus) @@ -138,25 +171,33 @@ void UDTFluxCoreSubsystem::ProcessTeamStatusUpdate(const FDTFluxTeamStatusUpdate DataStorage->UpdateParticipantStatus(NewParticipantStatus); } -void UDTFluxCoreSubsystem::ProcessTeamUpdate(const FDTFluxParticipant& Participant) +void UDTFluxCoreSubsystem::ProcessTeamUpdate(const FDTFluxTeamListDefinition& TeamListDefinitiont) { - DataStorage->UpdateParticipant(Participant); + for (const auto& Participant : TeamListDefinitiont.Participants) + { + DataStorage->UpdateParticipant(Participant); + } } void UDTFluxCoreSubsystem::ProcessSplitSensor(const FDTFluxSplitSensorInfo& SplitSensorInfo) { FDTFluxContest Contest; FDTFluxStageKey StageKey(SplitSensorInfo.ContestId, SplitSensorInfo.StageId); + FDTFluxStage Stage; + DataStorage->GetStage(StageKey, Stage); + FDTFluxParticipant Participant; + DataStorage->GetParticipantByBib(SplitSensorInfo.Bib, Participant); + DataStorage->GetContestById(SplitSensorInfo.ContestId, Contest); - UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("%s %s Split %i Sensor for Participant [Bib] %i "), - *Contest.Name, *Contest.Stages[SplitSensorInfo.StageId].Name, - SplitSensorInfo.SplitId , SplitSensorInfo.Bib); + UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("%s|%s Split %i Sensor for Participant [Bib] %i [FullName] %s"), + *Contest.Name, *Stage.Name, + SplitSensorInfo.SplitId, SplitSensorInfo.Bib, *Participant.GetFormattedName()); } void UDTFluxCoreSubsystem::SendRequest(const FString& Message) { - if(NetworkSubsystem) + if (NetworkSubsystem) { NetworkSubsystem->SendMessage(Message); } @@ -192,7 +233,7 @@ void UDTFluxCoreSubsystem::SendStageRankingRequest(int InContestId, int InStageI } void UDTFluxCoreSubsystem::RequestAllStageRankingOfContest(int InContestId, int InStageId, - bool bShouldIncludeSplitRanking) + bool bShouldIncludeSplitRanking) { // TODO Implement this } @@ -209,7 +250,7 @@ void UDTFluxCoreSubsystem::RequestAllSplitRankingOfContest(int InContestId, int FDTFluxStageRankings UDTFluxCoreSubsystem::GetStageRankings(FDTFluxStageKey StageKey) { - if(DataStorage->StageRankings.Contains(StageKey)) + if (DataStorage->StageRankings.Contains(StageKey)) { return DataStorage->StageRankings[StageKey]; } @@ -240,28 +281,44 @@ TArray UDTFluxCoreSubsystem::GetCurrentContests() TArray UDTFluxCoreSubsystem::GetContestsIdForTime(const FDateTime Time) { TArray Contests; - for(const auto& Pair : DataStorage->Contests) + for (const auto& Pair : DataStorage->Contests) { FDTFluxContest Contest = Pair.Value; int ContestId = Contest.ContestId; - if(Contest.Date < Time && Contest.EndTime > Time) + if (Contest.Date < Time && Contest.EndTime > Time) { Contests.Add(ContestId); } } return Contests; } + TArray UDTFluxCoreSubsystem::GetContestsForTime(const FDateTime Time) { TArray Contests; - for(const auto& Pair : DataStorage->Contests) + for (const auto& Pair : DataStorage->Contests) { FDTFluxContest Contest = Pair.Value; int ContestId = Contest.ContestId; - if(Contest.Date < Time && Contest.EndTime > Time) + if (Contest.Date < Time && Contest.EndTime > Time) { Contests.Add(Contest); } } return Contests; } + +void UDTFluxCoreSubsystem::RequestRankingsForStages(TArray RequestedStages) const +{ +} + +TArray UDTFluxCoreSubsystem::GetContests() +{ + if (DataStorage) + { + TArray OutContests; + DataStorage->Contests.GenerateValueArray(OutContests); + return OutContests; + } + return TArray(); +} diff --git a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h index 7c8e100..124e02a 100644 --- a/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h +++ b/Source/DTFluxCoreSubsystem/Public/DTFluxCoreSubsystem.h @@ -3,9 +3,8 @@ #pragma once #include "CoreMinimal.h" -#include "DTFluxPursuitSystem/Public/Types/Objects/DTFluxPursuitManager.h" +#include "Containers/Deque.h" #include "Subsystems/EngineSubsystem.h" -#include "Types/Enum/DTfluxCoreEnum.h" #include "Types/Struct/DTFluxRaceDataStructs.h" #include "Types/Struct/DTFluxTeamListStruct.h" #include "Types/Struct/DTFluxRankingStructs.h" @@ -13,8 +12,6 @@ #include "DTFluxCoreSubsystem.generated.h" - - class UDTFluxNetworkSubsystem; /** Forward Decl */ class UDTFluxModelAsset; @@ -27,52 +24,58 @@ class DTFLUXCORESUBSYSTEM_API UDTFluxCoreSubsystem : public UEngineSubsystem { GENERATED_BODY() - public: - DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSplitRankings, FDTFluxSplitRankings&, SplitRankings); + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnSplitRankings OnSplitRankings; - + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStageRankings, FDTFluxStageRankings&, StageRankings); + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnStageRankings OnStageRankings; - + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContestRankings, FDTFluxContestRankings&, ContestRankings); + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnContestRankings OnContestRankings; DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTeamList); + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnTeamList OnTeamList; - + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTeamStatusUpdate, FDTFluxParticipant, TeamUpdated); + UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") FOnTeamStatusUpdate OnTeamStatusUpdate; + + DECLARE_DELEGATE_TwoParams(FOnRequestedStageRankings, const FDTFluxStageKey&, const FDTFluxContestRankings&); + FOnRequestedStageRankings OnRequestedStageRankings; // // DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTeamUpdate, FDateTime, ReceivedAt, FDTFluxParticipant, TeamUpdatedList); // UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem") // FOnTeamUpdate OnTeamUpdate; - + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - void SendTeamListRequest(); - + void SendTeamListRequest(); + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - void SendRaceDataRequest(); - + void SendRaceDataRequest(); + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - void SendContestRankingRequest(int InContestId); - + void SendContestRankingRequest(int InContestId); + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void SendStageRankingRequest(int InContestId, int InStageId, bool bShouldIncludeSplitRanking = true); UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") - void RequestAllStageRankingOfContest(int InContestId, int InStageId, bool bShouldIncludeSplitRanking = true); - + void RequestAllStageRankingOfContest(int InContestId, int InStageId, bool bShouldIncludeSplitRanking = true); + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void SendSplitRankingRequest(int InContestId, int InStageId, int InSplitId); - + UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem") void RequestAllSplitRankingOfContest(int InContestId, int InStageId); @@ -94,14 +97,21 @@ public: UFUNCTION() TArray GetContestsForTime(const FDateTime Time); + UFUNCTION() + void RequestRankingsForStages(const TArray RequestedStages) const; + UFUNCTION() + TArray GetContests(); protected: // ~Subsystem Interface virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; // ~Subsystem Interface + + UFUNCTION() void SaveDataStorage(); + private: UDTFluxNetworkSubsystem* NetworkSubsystem = nullptr; @@ -116,16 +126,17 @@ private: UFUNCTION() void ProcessSplitRanking(const FDTFluxSplitRankings& SplitRankings); UFUNCTION() - void ProcessTeamStatusUpdate(const FDTFluxTeamStatusUpdate& NewParticipantStatus); + void ProcessTeamStatusUpdate(const FDTFluxTeamStatusUpdate& NewParticipantStatus); UFUNCTION() void ProcessSplitSensor(const FDTFluxSplitSensorInfo& SplitSensorInfo); UFUNCTION() - void ProcessTeamUpdate(const FDTFluxParticipant& Participant); + void ProcessTeamUpdate(const FDTFluxTeamListDefinition& TeamListDefinition); UFUNCTION() void SendRequest(const FString& Message); UFUNCTION() void RegisterDelegates(); + UPROPERTY() UDTFluxModelAsset* DataStorage = nullptr; }; diff --git a/Source/DTFluxNetwork/DTFluxNetwork.Build.cs b/Source/DTFluxNetwork/DTFluxNetwork.Build.cs index 52b292f..0a49554 100644 --- a/Source/DTFluxNetwork/DTFluxNetwork.Build.cs +++ b/Source/DTFluxNetwork/DTFluxNetwork.Build.cs @@ -2,31 +2,31 @@ public class DTFluxNetwork : ModuleRules { - public DTFluxNetwork(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxNetwork(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core" + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "WebSockets", - "HTTP", - "DTFluxCore", - "DTFluxProjectSettings", - "JsonUtilities", - "Json", - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "WebSockets", + "HTTP", + "DTFluxCore", + "DTFluxProjectSettings", + "JsonUtilities", + "Json", + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp b/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp new file mode 100644 index 0000000..86f3596 --- /dev/null +++ b/Source/DTFluxNetwork/Private/DTFluxQueuedManager.cpp @@ -0,0 +1,401 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#include "DTFluxQueuedManager.h" +#include "DTFluxNetworkModule.h" +#include "JsonObjectConverter.h" + + +const FString FDTFluxQueuedRequest::Serialize() const +{ + FString JSONString; + switch (RequestType) + { + case EDTFluxRequestType::RaceData: + + { + FDTFluxRaceDataRequest RaceData; + FJsonObjectConverter::UStructToJsonObjectString(RaceData, JSONString); + break; + } + + case EDTFluxRequestType::TeamList: + { + const FDTFluxTeamListRequest TeamList; + FJsonObjectConverter::UStructToJsonObjectString(TeamList, JSONString); + break; + } + + case EDTFluxRequestType::ContestRanking: + { + FDTFluxContestRankingRequest ContestRanking(ContestId); + FJsonObjectConverter::UStructToJsonObjectString(ContestRanking, JSONString); + break; + } + + case EDTFluxRequestType::StageRanking: + { + FDTFluxStageRankingRequest StageRanking(ContestId, StageId); + FJsonObjectConverter::UStructToJsonObjectString(StageRanking, JSONString); + break; + } + + case EDTFluxRequestType::SplitRanking: + { + FDTFluxSplitRankingRequest SplitRanking(ContestId, StageId, SplitId); + FJsonObjectConverter::UStructToJsonObjectString(SplitRanking, JSONString); + break; + } + + default: + JSONString = ""; + break; + } + return JSONString; +} + +UDTFluxQueuedManager::UDTFluxQueuedManager() + : bIsInitialized(false) + , CheckInterval(0.5f) + , TimeSinceLastCheck(0.0f) +{ +} + +UDTFluxQueuedManager::~UDTFluxQueuedManager() +{ + ClearAllRequests(); +} + +void UDTFluxQueuedManager::Initialize() +{ + if (!bIsInitialized) + { + UE_LOG(logDTFluxNetwork, Log, TEXT("Initializing DTFluxQueuedManager")); + bIsInitialized = true; + } +} + +FGuid UDTFluxQueuedManager::QueueRequest(EDTFluxRequestType RequestType, int32 ContestId, int32 StageId, int32 SplitId, + const FString& RawMessage) +{ + // Créer la requête avec les structs existants + FDTFluxQueuedRequest NewRequest(RequestType, ContestId, StageId, SplitId); + NewRequest.RawResponse = RawMessage; + + // 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::MarkRequestAsResponded(const FGuid& TargetRequestGuid) +{ + TQueue TempQueue; + bool bFoundMatch = false; + + // Parcourir toutes les requêtes en attente + FDTFluxQueuedRequest Request; + while (PendingRequestsQueue.Dequeue(Request)) + { + if (!bFoundMatch && Request.RequestId == TargetRequestGuid) + { + // 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); + } + } + + // Remettre les requêtes non traitées dans la queue principale + while (TempQueue.Dequeue(Request)) + { + PendingRequestsQueue.Enqueue(Request); + } + + return bFoundMatch; +} + +bool UDTFluxQueuedManager::MarkRequestAsResponded(const FDTFluxQueuedRequest& TargetRequest) +{ + return MarkRequestAsResponded(TargetRequest.RequestId); +} + +bool UDTFluxQueuedManager::IsRequestPending(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)) + { + // Vérifier si cette requête correspond + if (!bFoundMatch && Request.Matches(RequestType, ContestId, StageId, SplitId)) + { + bFoundMatch = 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; +} + +FDTFluxQueuedRequest* UDTFluxQueuedManager::GetRequestPending(EDTFluxRequestType RequestType, int32 ContestId, + int32 StageId, int32 SplitId) +{ + auto SearchInQueue = [&RequestType, ContestId, StageId, SplitId]( + TQueue& Queue) -> FDTFluxQueuedRequest* + { + // Copie temporaire de la queue pour la recherche + TQueue TempQueue; + + FDTFluxQueuedRequest* FoundItem = nullptr; + FDTFluxQueuedRequest Item; + while (Queue.Dequeue(Item)) + { + if (Item.RequestType == RequestType && Item.ContestId == ContestId && Item.StageId == StageId && Item. + SplitId == SplitId) // Assuming RequestId is your GUID field + { + FoundItem = &Item; + } + // Remettre dans la queue temporaire + TempQueue.Enqueue(Item); + } + while (TempQueue.Dequeue(Item)) + { + Queue.Enqueue(Item); + } + return FoundItem; + }; + return SearchInQueue(PendingRequestsQueue); +} + +const FDTFluxQueuedRequest* UDTFluxQueuedManager::GetRequest(const FGuid& SearchedGuid) +{ + auto SearchInQueue = [&SearchedGuid](TQueue& Queue) -> FDTFluxQueuedRequest* + { + // Copie temporaire de la queue pour la recherche + TQueue TempQueue; + + FDTFluxQueuedRequest* FoundItem = nullptr; + FDTFluxQueuedRequest Item; + while (Queue.Dequeue(Item)) + { + if (Item.RequestId == SearchedGuid) // Assuming RequestId is your GUID field + { + // 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; + } + // 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; + + if (const FDTFluxQueuedRequest* Found = SearchInQueue(CompletedRequestsQueue)) + return Found; + + if (const FDTFluxQueuedRequest* Found = SearchInQueue(TimedOutRequestsQueue)) + return Found; + + return nullptr; +} + +int32 UDTFluxQueuedManager::GetPendingRequestCount() +{ + TQueue TempQueue; + int32 Count = 0; + + // Compter les requêtes en attente + FDTFluxQueuedRequest Request; + while (PendingRequestsQueue.Dequeue(Request)) + { + Count++; + TempQueue.Enqueue(Request); + } + + // Remettre toutes les requêtes dans la queue principale + while (TempQueue.Dequeue(Request)) + { + PendingRequestsQueue.Enqueue(Request); + } + + return Count; +} + +int32 UDTFluxQueuedManager::CleanupTimedOutRequests() +{ + TQueue TempQueue; + int32 TimeoutCount = 0; + + // Parcourir toutes les requêtes en attente + FDTFluxQueuedRequest Request; + while (PendingRequestsQueue.Dequeue(Request)) + { + if (Request.HasTimedOut()) + { + // 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); + } + } + + // Remettre les requêtes non expirées dans la queue principale + while (TempQueue.Dequeue(Request)) + { + PendingRequestsQueue.Enqueue(Request); + } + + return TimeoutCount; +} + +int32 UDTFluxQueuedManager::CleanCashedRequests() +{ + int32 CleanedRequestsCount = 0; + + // Queue temporaire pour stocker les requêtes encore valides + TQueue ValidCompletedRequests; + + // Traiter toutes les requêtes terminées + FDTFluxQueuedRequest CompletedRequest; + while (CompletedRequestsQueue.Dequeue(CompletedRequest)) + { + // Vérifier si la requête est cacheable et a reçu une réponse + if (CompletedRequest.bIsCacheable && CompletedRequest.bHasReceivedResponse) + { + // 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); + } + } + + // Restaurer la queue avec uniquement les requêtes valides + while (ValidCompletedRequests.Dequeue(CompletedRequest)) + { + CompletedRequestsQueue.Enqueue(CompletedRequest); + } + + // 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; +} + +void UDTFluxQueuedManager::ClearAllRequests() +{ + // Vider toutes les queues + FDTFluxQueuedRequest DummyRequest; + while (PendingRequestsQueue.Dequeue(DummyRequest)) + { + } + while (CompletedRequestsQueue.Dequeue(DummyRequest)) + { + } + while (TimedOutRequestsQueue.Dequeue(DummyRequest)) + { + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Cleared all pending requests")); +} + + +void UDTFluxQueuedManager::Tick(float DeltaTime) +{ + if (!bIsInitialized) + { + return; + } + + // Incrémenter le temps écoulé + TimeSinceLastCheck += DeltaTime; + + // Vérifier si c'est le moment de nettoyer les requêtes expirées + if (TimeSinceLastCheck >= CheckInterval) + { + TimeSinceLastCheck = 0.0f; + CleanupTimedOutRequests(); + } + + // Traiter les requêtes expirées + FDTFluxQueuedRequest TimedOutRequest; + while (TimedOutRequestsQueue.Dequeue(TimedOutRequest)) + { + // Déclencher l'événement pour chaque requête expirée + OnRequestTimedOut.Broadcast(TimedOutRequest); + } +} + +bool UDTFluxQueuedManager::IsTickable() const +{ + return bIsInitialized; +} + +TStatId UDTFluxQueuedManager::GetStatId() const +{ + RETURN_QUICK_DECLARE_CYCLE_STAT(UDTFluxQueuedManager, STATGROUP_Tickables); +} diff --git a/Source/DTFluxNetwork/Private/Struct/DTFluxServerResponseStruct.cpp b/Source/DTFluxNetwork/Private/Struct/DTFluxServerResponseStruct.cpp new file mode 100644 index 0000000..c39b476 --- /dev/null +++ b/Source/DTFluxNetwork/Private/Struct/DTFluxServerResponseStruct.cpp @@ -0,0 +1,530 @@ +#pragma once + +#include "Struct/DTFluxServerResponseStruct.h" + +// === IMPLÉMENTATION DES CONSTRUCTEURS === + +FDTFluxServerResponse::FDTFluxServerResponse() +{ + ReceivedAt = FDateTime::Now(); + ApiDataType = EDTFluxApiDataType::None; + ParsingStatus = EDTFluxResponseStatus::Unset; +} + +FDTFluxServerResponse::FDTFluxServerResponse(const FString& JsonMessage, EDTFluxResponseStatus& OutStatus, + bool bLogErrors) +{ + ReceivedAt = FDateTime::Now(); + RawMessage = JsonMessage; + + ParsingStatus = InitializeFromJson(JsonMessage, bLogErrors); + OutStatus = ParsingStatus; + + if (bLogErrors && ParsingStatus != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to create DTFluxServerResponse: %s"), *GetErrorMessage()); + } +} + +FDTFluxServerResponse FDTFluxServerResponse::CreateFromJson(const FString& JsonMessage, bool bLogErrors) +{ + EDTFluxResponseStatus Status; + FDTFluxServerResponse Response(JsonMessage, Status, bLogErrors); + + if (bLogErrors) + { + if (Status == EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Successfully created DTFluxServerResponse: %s"), + *Response.ToDebugString()); + } + else + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Created DTFluxServerResponse with issues: %s"), + *Response.GetErrorMessage()); + } + } + + return Response; +} + +EDTFluxResponseStatus FDTFluxServerResponse::TryParse(bool bLogErrors) +{ + // Vérifier que le type est présent + if (Type.IsEmpty()) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response missing 'type' field")); + ApiDataType = EDTFluxApiDataType::None; + ParsingStatus = EDTFluxResponseStatus::MissingData; + return ParsingStatus; + } + } + + ParsingStatus = EDTFluxResponseStatus::UnknownError; + // Validation supplémentaire selon le type + if (ContainsDataType("race-data")) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Parsing race-data response")); + } + ApiDataType = EDTFluxApiDataType::RaceData; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + if (ContainsDataType("team-list")) + { + ApiDataType = EDTFluxApiDataType::TeamList; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + if (ContainsDataType("contest-ranking")) + { + if (ContestID == -1) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Contest-ranking missing ContestID")); + } + ParsingStatus = EDTFluxResponseStatus::DataError; + return ParsingStatus; + } + ApiDataType = EDTFluxApiDataType::ContestRanking; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + else if (ContainsDataType("stage-ranking")) + { + if (ContestID == -1 || StageID == -1) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Stage-ranking missing ContestID or StageID")); + } + ParsingStatus = EDTFluxResponseStatus::DataError; + return ParsingStatus; + } + if (SplitID != -1) + { + ApiDataType = EDTFluxApiDataType::SplitRanking; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + ApiDataType = EDTFluxApiDataType::StageRanking; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + if (ContainsDataType("status-update")) + { + ApiDataType = EDTFluxApiDataType::StatusUpdate; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + if (ContainsDataType("split-sensor")) + { + ApiDataType = EDTFluxApiDataType::SplitSensor; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + if (ContainsDataType("team-update")) + { + ApiDataType = EDTFluxApiDataType::TeamUpdate; + ParsingStatus = EDTFluxResponseStatus::Success; + return ParsingStatus; + } + return EDTFluxResponseStatus::UnknownError; +} + +EDTFluxResponseStatus FDTFluxServerResponse::InitializeFromJson(const FString& JsonMessage, bool bLogErrors) +{ + // Parser le JSON de base + if (!FJsonObjectConverter::JsonObjectStringToUStruct(JsonMessage, this, 0, 0, false, &FailureReason)) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("JSON parse error: %s\nMessage: %s"), *FailureReason.ToString(), + *JsonMessage); + } + return EDTFluxResponseStatus::JsonParseError; + } + // Vérifier si c'est une erreur du serveur + if (Code != -1) + { + if (bLogErrors) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Server error response: Code=%d, Message=%s"), Code, *Message); + } + return EDTFluxResponseStatus::ServerError; + } + return TryParse(); +} + + +FString FDTFluxServerResponse::GetErrorMessage() const +{ + switch (ParsingStatus) + { + case EDTFluxResponseStatus::Success: + return TEXT("No error"); + + case EDTFluxResponseStatus::JsonParseError: + return FString::Printf(TEXT("JSON parsing failed: %s"), *FailureReason.ToString()); + + case EDTFluxResponseStatus::ServerError: + return FString::Printf(TEXT("Server error %d: %s"), Code, *Message); + + case EDTFluxResponseStatus::InvalidType: + return FString::Printf(TEXT("Invalid or missing response type: '%s'"), *Type); + + case EDTFluxResponseStatus::MissingData: + return FString::Printf(TEXT("Missing required data fields for type '%s'"), *Type); + + case EDTFluxResponseStatus::UnknownError: + default: + return TEXT("Unknown error occurred during parsing"); + } +} + +FString FDTFluxServerResponse::ToDebugString() const +{ + return FString::Printf( + TEXT("DTFluxServerResponse[Status=%s, Type=%s, Code=%d, Contest=%d, Stage=%d, Split=%d, ReceivedAt=%s]"), + *UEnum::GetValueAsString(ParsingStatus), *Type, Code, ContestID, StageID, SplitID, *ReceivedAt.ToString()); +} + +bool FDTFluxServerResponse::ParseTeamListResponse(FDTFluxTeamListDefinition& OutTeamList) +{ + ParsingStatus = EDTFluxResponseStatus::Unset; + if (!ValidateResponseType(TEXT("team-list"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a team-list type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + TSharedPtr JsonObject; + if (!ParseJsonObject(JsonObject)) + { + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + const TArray>* DataArray; + if (!JsonObject->TryGetArrayField(TEXT("datas"), DataArray)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("No 'datas' array found in team-list response")); + ParsingStatus = EDTFluxResponseStatus::MissingData; + return false; + } + + OutTeamList.Participants.Empty(); + for (const TSharedPtr& Value : *DataArray) + { + if (Value->Type == EJson::Object) + { + const TSharedPtr Item = Value->AsObject(); + FDTFluxParticipant Participant; + if (UDTFluxParticipantFactory::CreateFromJsonCpp(Item, Participant)) + { + OutTeamList.Participants.Add(Participant); + ParsingStatus = EDTFluxResponseStatus::Success; + } + else + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("Failed to parse participant from JSON")); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + } + } + } + ParsingStatus = GetParsingStatus(); + if (ParsingStatus == EDTFluxResponseStatus::Success && OutTeamList.Participants.Num() != 0) + { + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed %d participants from team-list"), + OutTeamList.Participants.Num()); + return true; + } + if (OutTeamList.Participants.Num() == 0) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("No Participant Added")); + } + if (ParsingStatus != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse team-list Error : %s"), *GetErrorMessage()); + } + return false; +} + +bool FDTFluxServerResponse::ParseTeamUpdateResponse(FDTFluxTeamListDefinition& OutTeamUpdate) +{ + return ParseTeamListResponse(OutTeamUpdate); +} + +bool FDTFluxServerResponse::ParseRaceData(FDTFluxRaceData& OutRaceData) +{ + ParsingStatus = EDTFluxResponseStatus::Unset; + if (!ValidateResponseType(TEXT("race-data"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a race-data type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + FDTFluxRaceDataResponse RaceDataResponse; + if (!FJsonObjectConverter::JsonObjectStringToUStruct(RawMessage, &RaceDataResponse)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse race-data JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + OutRaceData.Datas.Empty(); + for (const auto& Contest : RaceDataResponse.Datas) + { + FDTFluxContest NewContest; + NewContest.Name = Contest.Name; + NewContest.ContestId = Contest.Id; + NewContest.Date = Contest.Date; + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Processing Contest %d [%s] Starting at %s"), + Contest.Id, *Contest.Name, *Contest.Date.ToString()); + // Satges + for (const auto& Stage : Contest.Stages) + { + FDTFluxStage NewStage; + NewStage.StageId = Stage.Id; + NewStage.Name = Stage.Name; + + // Construct full Timestamps strings + FString StartTimeFString = FString::Printf(TEXT("%s %s"), + *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), + *Stage.StartTime); + FString EndTimeFString = FString::Printf(TEXT("%s %s"), + *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), + *Stage.EndTime); + FString CutOffFString = FString::Printf(TEXT("%s %s"), + *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), + *Stage.CutOff); + FDateTime::Parse(StartTimeFString, NewStage.StartTime); + FDateTime::Parse(EndTimeFString, NewStage.EndTime); + FDateTime::Parse(CutOffFString, NewStage.CutOff); + NewContest.Stages.Add(NewStage); + + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Stage %d [%s]: Start[%s], CutOff[%s], End[%s]"), + Stage.Id, *Stage.Name, *NewStage.StartTime.ToString(), + *NewStage.CutOff.ToString(), *NewStage.EndTime.ToString()); + } + + // Traiter les splits + for (const auto& Split : Contest.Splits) + { + FDTFluxSplit NewSplit; + NewSplit.SplitId = Split.Id; + NewSplit.Name = Split.Name; + NewContest.Splits.Add(NewSplit); + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Split %d [%s]"), Split.Id, *Split.Name); + } + // Update Contest metadata + NewContest.UpdateEndTime(); + NewContest.UpdateLastStageId(); + + OutRaceData.Datas.Add(NewContest); + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed %d contests from race-data"), + OutRaceData.Datas.Num()); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +bool FDTFluxServerResponse::ParseContestRanking(FDTFluxContestRankings& OutContestRankings) +{ + if (!ValidateResponseType(TEXT("contest-ranking"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a contest-ranking type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + FDTFluxContestRankingResponse ContestRankingResponse; + if (!FJsonObjectConverter::JsonObjectStringToUStruct( + RawMessage, &ContestRankingResponse)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse contest-ranking JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + OutContestRankings.ContestId = ContestRankingResponse.ContestID; + OutContestRankings.Rankings.Empty(); + for (const auto& RankingItem : ContestRankingResponse.Datas) + { + FDTFluxContestRanking Ranking = RankingItem; + OutContestRankings.Rankings.Add(Ranking); + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed contest ranking for Contest %d with %d entries"), + OutContestRankings.ContestId, OutContestRankings.Rankings.Num()); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +bool FDTFluxServerResponse::ParseStageRankingResponse(FDTFluxStageRankings& OutStageRankings) +{ + if (!ValidateResponseType(TEXT("stage-ranking"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a stage-ranking type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + FDTFluxStageRankingResponse RankingResponse; + if (!FJsonObjectConverter::JsonObjectStringToUStruct(RawMessage, &RankingResponse)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse stage-ranking JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + OutStageRankings.ContestId = ContestID; + OutStageRankings.StageId = StageID; + OutStageRankings.Rankings = static_cast>(RankingResponse.Datas); + OutStageRankings.Initialize(); + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed stage ranking for Contest %d, Stage %d with %d entries"), + OutStageRankings.ContestId, OutStageRankings.StageId, OutStageRankings.Rankings.Num()); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +bool FDTFluxServerResponse::ParseSplitRankingResponse(FDTFluxSplitRankings& OutSplitRankings) +{ + if (!ValidateResponseType(TEXT("stage-ranking")) || SplitID == -1) + { + UE_LOG(logDTFluxNetwork, Error, + TEXT("Response is not a split-ranking type (stage-ranking with SplitID != -1)")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + FDTFluxSplitRankingResponse SplitRankingResponse; + if (!FJsonObjectConverter::JsonObjectStringToUStruct< + FDTFluxSplitRankingResponse>(RawMessage, &SplitRankingResponse)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse split-ranking JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + OutSplitRankings.ContestId = ContestID; + OutSplitRankings.StageId = StageID; + OutSplitRankings.SplitId = SplitID; + OutSplitRankings.Rankings = static_cast>(SplitRankingResponse.Datas); + UE_LOG(logDTFluxNetwork, Log, + TEXT("Successfully parsed split ranking for Contest %d, Stage %d, Split %d with %d entries"), + OutSplitRankings.ContestId, OutSplitRankings.StageId, OutSplitRankings.SplitId, + OutSplitRankings.Rankings.Num()); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +bool FDTFluxServerResponse::ParseStatusUpdateResponse(FDTFluxTeamStatusUpdate& OutStatusUpdate) +{ + if (!ValidateResponseType(TEXT("status-update"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a status-update type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + if (!FJsonObjectConverter::JsonObjectStringToUStruct(RawMessage, &OutStatusUpdate)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse status-update JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed status update for bib %d"), OutStatusUpdate.Bib); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +bool FDTFluxServerResponse::ParseSplitSensorResponse(TArray& OutSplitSensorInfos) +{ + if (!ValidateResponseType(TEXT("split-sensor"))) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Response is not a split-sensor type")); + ParsingStatus = EDTFluxResponseStatus::InvalidType; + return false; + } + + FDTFluxSplitSensorResponse SplitSensorResponse; + if (!FJsonObjectConverter::JsonObjectStringToUStruct(RawMessage, &SplitSensorResponse)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse split-sensor JSON: %s"), *RawMessage); + ParsingStatus = EDTFluxResponseStatus::JsonParseError; + return false; + } + + OutSplitSensorInfos.Empty(); + for (const auto& SplitSensorInfoResponse : SplitSensorResponse.Datas) + { + FDTFluxSplitSensorInfo NewSplitSensorInfo; + NewSplitSensorInfo.Bib = SplitSensorInfoResponse.Bib; + NewSplitSensorInfo.ContestId = SplitSensorInfoResponse.ContestID; + NewSplitSensorInfo.StageId = SplitSensorInfoResponse.StageID; + NewSplitSensorInfo.SplitId = SplitSensorInfoResponse.SplitID; + NewSplitSensorInfo.Time = SplitSensorInfoResponse.Time; + + OutSplitSensorInfos.Add(NewSplitSensorInfo); + + UE_LOG(logDTFluxNetwork, Verbose, TEXT("Split sensor info for bib %d in Contest %d, Stage %d, Split %d"), + NewSplitSensorInfo.Bib, NewSplitSensorInfo.ContestId, NewSplitSensorInfo.StageId, + NewSplitSensorInfo.SplitId); + } + + UE_LOG(logDTFluxNetwork, Log, TEXT("Successfully parsed %d split sensor entries"), OutSplitSensorInfos.Num()); + ParsingStatus = EDTFluxResponseStatus::Success; + return true; +} + +void FDTFluxServerResponse::ShowDebug(const bool bShouldPrintRawMessage) const +{ + FString DebugMsg = FString::Printf( + TEXT("DTFluxServerResponse[Type=%s, Code=%d, Contest=%d, Stage=%d, Split=%d, ReceivedAt=%s]"), + *Type, Code, ContestID, StageID, SplitID, *ReceivedAt.ToString()); + if (bShouldPrintRawMessage) + { + UE_LOG(logDTFluxNetwork, Log, TEXT("%s\nRawMessage: \"%s\""), *DebugMsg, *RawMessage); + } +} + +// === MÉTHODES PRIVÉES === + +bool FDTFluxServerResponse::ParseJsonObject(TSharedPtr& OutJsonObject) const +{ + TSharedRef> Reader = TJsonReaderFactory<>::Create(RawMessage); + + if (!FJsonSerializer::Deserialize(Reader, OutJsonObject) || !OutJsonObject.IsValid()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse JSON: %s"), *RawMessage); + return false; + } + + return true; +} + +bool FDTFluxServerResponse::ValidateResponseType(const FString& ExpectedType) const +{ + if (!IsValidResponse()) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Invalid server response: Code=%d, Message=%s"), Code, *Message); + return false; + } + + if (!ContainsDataType(ExpectedType)) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("Expected type '%s' but got '%s'"), *ExpectedType, *Type); + return false; + } + + return true; +} diff --git a/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp b/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp index 4883f8a..0c77df4 100644 --- a/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp +++ b/Source/DTFluxNetwork/Private/Subsystems/DTFluxNetworkSubsystem.cpp @@ -5,6 +5,8 @@ #include "DTFluxCoreModule.h" #include "DTFluxNetworkModule.h" #include "DTFluxNetworkSettings.h" +#include "DTFluxQueuedManager.h" +#include "DTFluxQueuedManager.h" #include "JsonObjectConverter.h" #include "Clients/DTFluxHttpClient.h" #include "Clients/DTFluxWebSocketClient.h" @@ -19,10 +21,11 @@ #include "Types/Struct/DTFluxSplitSensor.h" +// === CONNEXION WEBSOCKET === void UDTFluxNetworkSubsystem::Connect() { - WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port)); - WsClient->Connect(); + WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port)); + WsClient->Connect(); } void UDTFluxNetworkSubsystem::Disconnect() @@ -35,6 +38,141 @@ 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, + FOnDTFluxRequestResponse OnCompleted, + FOnDTFluxRequestTimeout OnTimeout, + 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); + } + } + + 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; + } + return QueueManager->IsRequestPending(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) { @@ -48,7 +186,8 @@ void UDTFluxNetworkSubsystem::SendRequest(const EDTFluxRequestType RequestType, FJsonObjectConverter::UStructToJsonObjectString(FDTFluxStageRankingRequest(InContestId, InStageId), Message); break; case EDTFluxRequestType::SplitRanking: - FJsonObjectConverter::UStructToJsonObjectString(FDTFluxSplitRankingRequest(InContestId, InStageId, InSplitId), Message); + FJsonObjectConverter::UStructToJsonObjectString(FDTFluxSplitRankingRequest(InContestId, InStageId, InSplitId), + Message); break; case EDTFluxRequestType::TeamList: FJsonObjectConverter::UStructToJsonObjectString(FDTFluxTeamListRequest(), Message); @@ -60,7 +199,7 @@ void UDTFluxNetworkSubsystem::SendRequest(const EDTFluxRequestType RequestType, return; } //Dirty trick to fix Case - Message = Message.Replace(TEXT("Id"),TEXT( "ID"), ESearchCase::CaseSensitive); + Message = Message.Replace(TEXT("Id"),TEXT("ID"), ESearchCase::CaseSensitive); UE_LOG(logDTFluxCore, Warning, TEXT("Sending Request %s"), *Message); SendMessage(Message); } @@ -70,11 +209,10 @@ void UDTFluxNetworkSubsystem::SendMessage(const FString& Message) UE_LOG(logDTFluxCore, Warning, TEXT("Sending Message %s"), *Message); - if(WsClient.IsValid() && WsClient->CanSend()) + if (WsClient.IsValid() && WsClient->CanSend()) { WsClient->Send(Message); UE_LOG(logDTFluxNetwork, Log, TEXT("Can send request")); - } else { @@ -86,8 +224,7 @@ void UDTFluxNetworkSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); FDTFluxCoreModule& DTFluxCore = FModuleManager::Get().LoadModuleChecked("DTFluxCore"); - FString StatusString = UEnum::GetValueAsString(WsStatus); - UE_LOG(logDTFluxNetwork, Log, TEXT("Status is %s"), *StatusString); + UDTFluxNetworkSettings* NetworkSettings = GetMutableDefault(); UDTFluxNetworkSettings::GetWebSocketSettings(NetworkSettings, WsSettings); UDTFluxNetworkSettings::GetHTTPSettings(NetworkSettings, HttpSettings); @@ -99,16 +236,36 @@ void UDTFluxNetworkSubsystem::Initialize(FSubsystemCollectionBase& Collection) NetworkSettings->OnDTFluxWebSocketSettingsChanged.AddUFunction(this, FName("WsSettingsChanged")); NetworkSettings->OnDTFluxHttpSettingsChanged.AddUFunction(this, FName("HttpSettingsChanged")); #endif - if(WsSettings.bShouldConnectAtStartup) + 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); } void UDTFluxNetworkSubsystem::Deinitialize() { Super::Deinitialize(); + // Nettoyer le Queue Manager + if (QueueManager) + { + QueueManager->OnRequestTimedOut.RemoveDynamic(this, &UDTFluxNetworkSubsystem::OnRequestTimedOut_Internal); + QueueManager->ClearAllRequests(); + } + + // Nettoyer les callbacks + PendingCallbacks.Empty(); + PendingTimeoutCallbacks.Empty(); + // Déconnexion des clients + UnregisterWebSocketEvents(); + UnregisterHttpEvents(); } void UDTFluxNetworkSubsystem::WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings) @@ -117,7 +274,7 @@ void UDTFluxNetworkSubsystem::WsSettingsChanged(const FDTFluxWsSettings& NewWsSe bool bNeedsReload = WsSettings != NewWsSettings; WsSettings = NewWsSettings; - if( bNeedsReload || WsSettings.bShouldConnectAtStartup) + if (bNeedsReload || WsSettings.bShouldConnectAtStartup) { UE_LOG(logDTFluxNetwork, Warning, TEXT("WSocket Settings needs Reloding client")) ReconnectWs(FName("Ws_Client_0")); @@ -137,9 +294,9 @@ void UDTFluxNetworkSubsystem::ReconnectWs(const FName WsClientId) WsClient->SetAddress(NewAddress); WsClient->Reconnect(); } + void UDTFluxNetworkSubsystem::ReconnectHttp(const FName WsClientId) { - } void UDTFluxNetworkSubsystem::RegisterWebSocketEvents() @@ -148,16 +305,16 @@ void UDTFluxNetworkSubsystem::RegisterWebSocketEvents() WsClient->RegisterConnectedEvent().AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketConnected_Subsystem); OnWsConnectionErrorEventDelegateHandle = WsClient->RegisterConnectionError() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem); - OnWsClosedEventDelegateHandle = + .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem); + OnWsClosedEventDelegateHandle = WsClient->RegisterClosedEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem); + .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem); OnWsMessageEventDelegateHandle = WsClient->RegisterMessageEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem); + .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem); OnWsMessageSentEventDelegateHandle = - WsClient->RegisterMessageSentEvent() - .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem); + WsClient->RegisterMessageSentEvent() + .AddUObject(this, &UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem); } void UDTFluxNetworkSubsystem::RegisterHttpEvents() @@ -166,25 +323,25 @@ void UDTFluxNetworkSubsystem::RegisterHttpEvents() void UDTFluxNetworkSubsystem::UnregisterWebSocketEvents() { - if(OnWsConnectedEventDelegateHandle.IsValid()) + if (OnWsConnectedEventDelegateHandle.IsValid()) { WsClient->UnregisterConnectedEvent().Remove(OnWsConnectedEventDelegateHandle); } - if(OnWsConnectionErrorEventDelegateHandle.IsValid()) + if (OnWsConnectionErrorEventDelegateHandle.IsValid()) { - WsClient->UnregisterConnectionError(); + WsClient->UnregisterConnectionError().Remove(OnWsConnectionErrorEventDelegateHandle); } - if(OnWsClosedEventDelegateHandle.IsValid()) + if (OnWsClosedEventDelegateHandle.IsValid()) { - WsClient->UnregisterClosedEvent(); + WsClient->UnregisterClosedEvent().Remove(OnWsClosedEventDelegateHandle); } - if(OnWsMessageEventDelegateHandle.IsValid()) + if (OnWsMessageEventDelegateHandle.IsValid()) { - WsClient->UnregisterMessageEvent(); + WsClient->UnregisterMessageEvent().Remove(OnWsMessageEventDelegateHandle); } - if(OnWsMessageSentEventDelegateHandle.IsValid()) + if (OnWsMessageSentEventDelegateHandle.IsValid()) { - WsClient->UnregisterRawMessageEvent(); + WsClient->UnregisterRawMessageEvent().Remove(OnWsMessageSentEventDelegateHandle); } } @@ -203,7 +360,7 @@ void UDTFluxNetworkSubsystem::OnWebSocketConnectionError_Subsystem(const FString { UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws Error with %s : %s"), *WsClient->GetAddress(), *Error); WsStatus = EDTFluxConnectionStatus::Error; - if(WsSettings.bShouldAutoReconnectOnError) + if (WsSettings.bShouldAutoReconnectOnError) { WsClient->Reconnect(); } @@ -212,261 +369,239 @@ 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")); + *WsClient->GetAddress(), *Reason, StatusCode, bWasClean ? TEXT("True") : TEXT("False")); WsStatus = EDTFluxConnectionStatus::Closed; } -void UDTFluxNetworkSubsystem::ParseTeamListResponse(const FDTFluxServerResponse& ServerResponse) +void UDTFluxNetworkSubsystem::ParseTeamListResponse(FDTFluxServerResponse& Response) { - TSharedPtr JsonObject; - TSharedRef> Reader = TJsonReaderFactory<>::Create(ServerResponse.RawMessage); - - if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid()) - { - UE_LOG(logDTFluxNetwork, Error, TEXT("JSON invalide : %s"), *ServerResponse.RawMessage); - return; - } - const TArray>* DataArray; - if (!JsonObject->TryGetArrayField(TEXT("datas"), DataArray)) - { - UE_LOG(logDTFluxNetwork, Error, TEXT("Aucun champ 'datas' trouvé dans le team-list")); - return; - } FDTFluxTeamListDefinition TeamListDefinition; - for (const TSharedPtr& Value : *DataArray) + Response.ParseTeamListResponse(TeamListDefinition); + UE_LOG(logDTFluxNetwork, Warning, TEXT("Parsing Team List Response")); + if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) { - if (Value->Type == EJson::Object) - { - const TSharedPtr Item = Value->AsObject(); - - FDTFluxParticipant Participant; - UDTFluxParticipantFactory::CreateFromJsonCpp(Item, Participant); - TeamListDefinition.Participants.Add(Participant); - } + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseTeamListResponse() for JSON Response : %s"), *Response.RawMessage); + return; } - + 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(), - OnTeamListReceived.ExecuteIfBound(TeamListDefinition) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); + bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); } -void UDTFluxNetworkSubsystem::ParseRaceData(const FDTFluxServerResponse& Response) +void UDTFluxNetworkSubsystem::ParseRaceData(FDTFluxServerResponse& Response) { - FDTFluxRaceDataResponse RaceData; - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &RaceData)) + FDTFluxRaceData RaceData; + Response.ParseRaceData(RaceData); + if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) { - //convert - FDTFluxRaceData RaceDataDefinition; - for(auto Contest : RaceData.Datas) + 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")); +} + +void UDTFluxNetworkSubsystem::ParseContestRanking(FDTFluxServerResponse& Response) +{ + FDTFluxContestRankings ContestRankings; + Response.ParseContestRanking(ContestRankings); + if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseContestRanking() for JSON Response : %s"), *Response.RawMessage); + return; + } + 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) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStageRankingResponse() for JSON Response : %s"), + *Response.RawMessage); + } + 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) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitRankingResponse() for JSON Response : %s"), + *Response.RawMessage); + } + 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) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStatusUpdateResponse() for JSON Response : %s"), + *Response.RawMessage); + } + 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(); + Response.ParseSplitSensorResponse(SplitSensorInfos); + if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitSensorResponse() for JSON Response : %s"), + *Response.RawMessage); + } + for (auto& SplitSensorInfo : SplitSensorInfos) + { + 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")); + } +} + +EDTFluxResponseStatus UDTFluxNetworkSubsystem::ProcessPushMessage(FDTFluxServerResponse& Response) +{ + EDTFluxResponseStatus ResponseStatus = EDTFluxResponseStatus::UnknownError; + if (DTFluxDataTypeUtils::IsPushOnly(Response.GetResponseType())) + { + switch (Response.GetResponseType()) { - FDTFluxContest NewContest; - NewContest.Name = Contest.Name; - NewContest.ContestId = Contest.Id; - NewContest.Date = Contest.Date; - UE_LOG(logDTFluxNetwork, Warning, TEXT("Contest %i [%s] Starting at %s \nStages: \n"), Contest.Id, *Contest.Date.ToString(),*Contest.Name); - for(auto Stage : Contest.Stages) + case EDTFluxApiDataType::SplitSensor: { - FDTFluxStage NewStage; - NewStage.StageId = Stage.Id; - NewStage.Name = Stage.Name; - FString StartTimeFString = FString::Printf(TEXT("%s %s"), - *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), - *Stage.StartTime - ); - FString EndTimeFString = FString::Printf(TEXT("%s %s"), - *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), - *Stage.EndTime - ); - FString CutOffFString = FString::Printf(TEXT("%s %s"), - *NewContest.Date.ToFormattedString(TEXT("%Y-%m-%d")), - *Stage.CutOff - ); - FDateTime::Parse(StartTimeFString, NewStage.StartTime); - FDateTime::Parse(EndTimeFString, NewStage.EndTime); - FDateTime::Parse(CutOffFString, NewStage.CutOff); - NewContest.Stages.Add(NewStage); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Stage %i [%s]: \nSTartTime Received [%s] -> Datetime[%s], CutOff [%s], EndTime [%s] \n"), Stage.Id, *Stage.Name, - *Stage.StartTime, *NewStage.StartTime.ToString(), *NewStage.CutOff.ToString(), *NewStage.EndTime.ToString()); + TArray SplitSensorInfos; + if (Response.ParseSplitSensorResponse(SplitSensorInfos)) + { + for (const auto& SplitSensorInfo : SplitSensorInfos) + { + OnSplitSensorReceived.ExecuteIfBound(SplitSensorInfo); + } + } + ResponseStatus = Response.GetParsingStatus(); + break; } - NewContest.UpdateEndTime(); - NewContest.UpdateLastStageId(); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Contest %i [%s]\nSplits: \n"), Contest.Id, *Contest.Name); - for(auto Split: Contest.Splits) + case EDTFluxApiDataType::StatusUpdate: { - FDTFluxSplit NewSplit; - NewSplit.SplitId = Split.Id; - NewSplit.Name = Split.Name; - NewContest.Splits.Add(NewSplit); - UE_LOG(logDTFluxNetwork, Warning, TEXT("Split %i [%s]: \n"), Split.Id, *Split.Name); + FDTFluxTeamStatusUpdate StatusUpdate; + if (Response.ParseStatusUpdateResponse(StatusUpdate)) + { + OnTeamStatusUpdateReceived.ExecuteIfBound(StatusUpdate); + } + ResponseStatus = Response.GetParsingStatus(); + break; + } + case EDTFluxApiDataType::TeamUpdate: + { + FDTFluxTeamListDefinition TeamUpdateList; + if (Response.ParseTeamUpdateResponse(TeamUpdateList)) + { + OnTeamUpdateReceived.ExecuteIfBound(TeamUpdateList); + } + ResponseStatus = Response.GetParsingStatus(); + break; + } + default: + { + ResponseStatus = EDTFluxResponseStatus::UnknownError; + break; } - RaceDataDefinition.Datas.Add(NewContest); } - UE_LOG(logDTFluxNetwork, Warning, TEXT("Sending %i Contests, [%s]"), RaceDataDefinition.Datas.Num(), - OnRaceDataReceived.ExecuteIfBound(RaceDataDefinition) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); - return; } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseRaceData() for JSON Response : %s"), *Response.RawMessage); + return ResponseStatus; } -void UDTFluxNetworkSubsystem::ParseContestRanking(const FDTFluxServerResponse& Response) +void UDTFluxNetworkSubsystem::Parse(FDTFluxServerResponse& Response) { - FDTFluxContestRankingResponse ContestRankingResponse; - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &ContestRankingResponse)) + EDTFluxResponseStatus ResponseStatus = EDTFluxResponseStatus::Success; + switch (Response.GetResponseType()) { - FDTFluxContestRankings ContestRankings; - ContestRankings.ContestId = ContestRankingResponse.ContestID; - for(auto& RankingItem : ContestRankingResponse.Datas) + case EDTFluxApiDataType::RaceData: { - FDTFluxContestRanking Temp = RankingItem; - ContestRankings.Rankings.Add(Temp); + UE_LOG(logDTFluxNetwork, Warning, TEXT("Legacy Parsing RaceData")); + ParseRaceData(Response); + ResponseStatus = Response.GetParsingStatus(); + break; } - UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws ContestRanking Data Sent for Contest %i, [%s]"), ContestRankings.ContestId, - OnContestRankingReceived.ExecuteIfBound(ContestRankings) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); - return; - } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseContestRanking() for JSON Response : %s"), *Response.RawMessage); - -} - -void UDTFluxNetworkSubsystem::ParseStageRankingResponse(const FDTFluxServerResponse& Response) -{ - FDTFluxStageRankingResponse RankingResponse; - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &RankingResponse)) - { - FDTFluxStageRankings NewRankings; - NewRankings.ContestId = Response.ContestID; - NewRankings.StageId = Response.StageID; - NewRankings.Rankings = static_cast>(RankingResponse.Datas); - NewRankings.Initialize(); - UE_LOG(logDTFluxNetwork, Warning, TEXT("StageRanking Data Sent for Contest %i, Stage %i\n[Result] : %s"), - NewRankings.ContestId, NewRankings.StageId, - OnStageRankingReceived.ExecuteIfBound(NewRankings) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED") - ); - return; - } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStageRankingResponse() for JSON Response : %s"), *Response.RawMessage); -} - -void UDTFluxNetworkSubsystem::ParseSplitRankingResponse(const FDTFluxServerResponse& Response) -{ - FDTFluxSplitRankingResponse SplitRankingResponse; - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &SplitRankingResponse)) - { - FDTFluxSplitRankings NewSplitRankings; - NewSplitRankings.ContestId = Response.ContestID; - NewSplitRankings.StageId = Response.StageID; - NewSplitRankings.SplitId = Response.SplitID; - NewSplitRankings.Rankings = static_cast>(SplitRankingResponse.Datas); - UE_LOG(logDTFluxNetwork, Warning, TEXT("SplitRanking Data Sent for Contest %i, Stage %i and Split %i\n[Result] : %s"), - NewSplitRankings.ContestId, NewSplitRankings.StageId, NewSplitRankings.SplitId, - OnSplitRankingReceived.ExecuteIfBound(NewSplitRankings) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); - return; - } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitRankingResponse() for JSON Response : %s"), *Response.RawMessage); - -} - -void UDTFluxNetworkSubsystem::ParseStatusUpdateResponse(const FDTFluxServerResponse& Response) -{ - FDTFluxTeamStatusUpdate StatusUpdateResponse; - - if (FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &StatusUpdateResponse)) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Status Update for bib %i \n[Result] : %s\n"), - StatusUpdateResponse.Bib, - OnTeamStatusUpdateReceived.ExecuteIfBound(StatusUpdateResponse) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); - return; - } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseStatusUpdateResponse() for JSON Response : %s"), *Response.RawMessage); -} - -void UDTFluxNetworkSubsystem::ParseSplitSensorResponse(const FDTFluxServerResponse& Response) -{ - FDTFluxSplitSensorResponse SplitSensorResponse; - if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &SplitSensorResponse)) - { - for(const auto& SplitSensorInfoResponse : SplitSensorResponse.Datas) + case EDTFluxApiDataType::TeamList: { - FDTFluxSplitSensorInfo NewSplitSensorInfo; - NewSplitSensorInfo.Bib = SplitSensorInfoResponse.Bib; - NewSplitSensorInfo.ContestId = SplitSensorInfoResponse.ContestID; - NewSplitSensorInfo.StageId = SplitSensorInfoResponse.StageID; - NewSplitSensorInfo.SplitId = SplitSensorInfoResponse.SplitID; - NewSplitSensorInfo.Time = SplitSensorInfoResponse.Time; - UE_LOG(logDTFluxNetwork, Warning, TEXT("Status Update for bib %i in Contest %i, Stage %i in split %i\n[Result] : %s\n"), - NewSplitSensorInfo.Bib, NewSplitSensorInfo.ContestId, NewSplitSensorInfo.StageId, NewSplitSensorInfo.SplitId, - OnSplitSensorReceived.ExecuteIfBound(NewSplitSensorInfo) ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED")); + 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; } } - UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitSensorResponse() failed for JSON Response : %s"), *Response.RawMessage); + if (ResponseStatus != EDTFluxResponseStatus::Success) + { + UE_LOG(logDTFluxNetwork, Warning, TEXT("UDTFluxNetworkSubsystem::Parse() Parsing failed")); + } } + //TODO reforge API to keep track of Requests void UDTFluxNetworkSubsystem::OnWebSocketMessageEvent_Subsystem(const FString& MessageString) { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Client %s :\nMessage Received : %s"), *WsClient->GetAddress(), *MessageString); + // UE_LOG(logDTFluxNetwork, Warning, TEXT("Client %s :\nMessage Received : %s"), *WsClient->GetAddress(), *MessageString); //Do Something With the message - FDTFluxServerResponse Response; - Response.ReceivedAt = FDateTime::Now(); - Response.RawMessage = MessageString; - Response.FailureReason = FText::FromString("--"); - if(FJsonObjectConverter::JsonObjectStringToUStruct(MessageString, &Response, 0, 0, false, &(Response.FailureReason))) + EDTFluxResponseStatus ResponseStatus; + FDTFluxServerResponse Response(MessageString, ResponseStatus); + if (!TryMatchResponseToQueuedRequest(Response)) { - if(Response.Code == -1) + UE_LOG(logDTFluxNetwork, Warning, TEXT("Response %s does not match any queued request"), + *UEnum::GetValueAsString(Response.GetResponseType())); + if (ProcessPushMessage(Response) != EDTFluxResponseStatus::Success) { - // return DataReceived.Broadcast(Response); - if(Response.Type.Contains("race-data")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Race-Data Received")); - return ParseRaceData(Response); - } - if(Response.Type.Contains("team-list")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Team-List Received")); - return ParseTeamListResponse(Response); - } - if(Response.Type.Contains("contest-ranking")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("Contest-Ranking Received")); - return ParseContestRanking(Response); - } - if(Response.Type.Contains("stage-ranking") ) - { - if(Response.SplitID == -1) - { - // StageRanking - UE_LOG(logDTFluxNetwork, Warning, TEXT("Stage-Ranking Data")); - ParseStageRankingResponse(Response); - } - else - { - // StageRanking - UE_LOG(logDTFluxNetwork, Warning, TEXT("Split-Ranking Data")); - return ParseSplitRankingResponse(Response); - } - } - if(Response.Type.Contains("split-sensor")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("split-sensor Data")); - ParseSplitSensorResponse(Response); - - } - if(Response.Type.Contains("status-update")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("status-update Data")); - ParseStatusUpdateResponse(Response); - - } - if(Response.Type.Contains("team-update")) - { - UE_LOG(logDTFluxNetwork, Warning, TEXT("team-update Data")); - ParseTeamListResponse(Response); - } + UE_LOG(logDTFluxNetwork, Warning, TEXT("Not a push message")); + // Legacy + Parse(Response); } - } - UE_LOG(logDTFluxNetwork, Error, TEXT("Ws %s :\nMessage Received : %s Cannot be Parsed"), *WsClient->GetAddress(), *MessageString); - // return DataReceived.Broadcast(Response); - } void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent) @@ -474,15 +609,144 @@ void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FStrin UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws %s :\nMessage Sent: %s"), *WsClient->GetAddress(), *MessageSent); } +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 (FOnDTFluxRequestTimeout* 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(const FDTFluxServerResponse& Response) +{ + if (!QueueManager) + { + return false; + } + // Essayer de trouver une requête correspondante + // Note: Cette méthode nécessiterait une modification de UDTFluxQueuedManager pour supporter le matching par type et paramètres + + // Pour l'instant, on utilise une approche simple : chercher la première requête du bon type + // Vous devrez probablement modifier UDTFluxQueuedManager pour ajouter une méthode comme FindMatchingRequest() + + + // Implémentation temporaire : on assume qu'il n'y a qu'une requête de chaque type en cours + if (QueueManager->IsRequestPending(Response.GetResponseType(), Response.ContestID, Response.StageID, + Response.SplitID)) + { + // Marquer comme répondu - vous devrez adapter cette méthode selon votre logique de matching + // Pour l'instant, on va faire un workaround simple + + 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); + + return true; + } + + 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 (FOnDTFluxRequestResponse* SuccessCallback = PendingCallbacks.Find(RequestId)) + { + if (SuccessCallback->IsBound()) + { + SuccessCallback->Execute(RequestId, ResponseData); + } + PendingCallbacks.Remove(RequestId); + } + + // 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 (FOnDTFluxRequestTimeout* ErrorCallback = PendingTimeoutCallbacks.Find(RequestId)) + { + if (ErrorCallback->IsBound()) + { + ErrorCallback->Execute(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); +} + + FString UDTFluxNetworkSubsystem::ConstructWsAddress(const FString& Address, const FString& Path, const int& Port) { FString NewAddress; - if( !Address.Contains("ws://") && !Address.Contains("wss://")) + if (!Address.Contains("ws://") && !Address.Contains("wss://")) { NewAddress += FString("ws://"); } - NewAddress +=Address + FString(":") + FString::FromInt(Port) + Path; + NewAddress += Address + FString(":") + FString::FromInt(Port) + Path; return NewAddress; // UE_LOG(logDTFluxNetwork, Log, TEXT("NewAddress : %s"), *NewAddress); - } diff --git a/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h b/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h new file mode 100644 index 0000000..ef61f99 --- /dev/null +++ b/Source/DTFluxNetwork/Public/DTFluxQueuedManager.h @@ -0,0 +1,166 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UObject/Object.h" +#include "Containers/Queue.h" +#include "Tickable.h" +#include "Struct/DTFluxRequestStructs.h" +#include "Struct/DTFluxServerResponseStruct.h" +#include "Types/Enum/DTFluxCoreEnum.h" +#include "DTFluxQueuedManager.generated.h" + +/** + * @brief Structure représentant une requête en file d'attente avec ses métadonnées + */ +USTRUCT(BlueprintType) +struct FDTFluxQueuedRequest : public FDTFluxRequestBase +{ + GENERATED_BODY() + + /** L'identifiant unique de la requête */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + FGuid RequestId; + + /** L'heure à laquelle la requête a été envoyée */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + FDateTime CreatedAt; + + /** Le type de requête */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + EDTFluxApiDataType RequestType = EDTFluxRequestType::None; + + /** Identifiant de la compétition (ContestId) */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + int32 ContestId = -1; + + /** Identifiant de l'étape (StageId) */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + int32 StageId = -1; + + /** Identifiant du split (SplitId) */ + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + int32 SplitId = -1; + + UPROPERTY(BlueprintReadOnly, Category = "DTFlux|Request") + FString RawResponse = ""; + + /** Délai maximum avant que la requête soit considérée comme expirée (en secondes) */ + UPROPERTY(BlueprintReadWrite, Category = "DTFlux|Request") + float TimeoutSeconds = 2.0f; + + /** Determine si la requête peut être mise en cache */ + UPROPERTY(BlueprintReadWrite, Category = "DTFlux|Request") + bool bIsCacheable = false; + + /** Validité du cache si bIsCacheable est mis à true après reception de la réponse (en secondes) */ + UPROPERTY(BlueprintReadWrite, Category = "DTFlux|Request") + float CachedValidity = 50.0f; + + /** Indicateur si la requête a reçu une réponse */ + UPROPERTY(BlueprintReadWrite, Category = "DTFlux|Request") + bool bHasReceivedResponse = false; + + /** Constructeur par défaut */ + FDTFluxQueuedRequest() + { + RequestId = FGuid::NewGuid(); + CreatedAt = FDateTime::Now(); + } + + /** Constructeur avec paramètres */ + FDTFluxQueuedRequest(EDTFluxRequestType InRequestType, int32 InContestId = -1, int32 InStageId = -1, + int32 InSplitId = -1) + : RequestType(InRequestType) + , ContestId(InContestId) + , StageId(InStageId) + , SplitId(InSplitId) + { + RequestId = FGuid::NewGuid(); + CreatedAt = FDateTime::Now(); + } + + bool operator==(const FDTFluxQueuedRequest& Left) const + { + return RequestId == Left.RequestId; + } + + bool operator!=(const FDTFluxQueuedRequest& Left) const + { + return RequestId != Left.RequestId; + } + + + const FString Serialize() const; + + /** Vérifie si la requête a expiré */ + bool HasTimedOut() const + { + return (FDateTime::Now() - CreatedAt).GetTotalSeconds() > TimeoutSeconds; + } + + + /** Vérifie si cette requête correspond aux paramètres spécifiés */ + bool Matches(EDTFluxRequestType InRequestType, int32 InContestId = -1, int32 InStageId = -1, + int32 InSplitId = -1) const + { + return RequestType == InRequestType && + (InContestId == -1 || ContestId == InContestId) && + (InStageId == -1 || StageId == InStageId) && + (InSplitId == -1 || SplitId == InSplitId); + } +}; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnRequestTimedOut, const FDTFluxQueuedRequest&, TimedOutRequest); + +/** + * @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 +{ + 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 MarkRequestAsResponded(const FGuid& TargetRequestGuid); + bool MarkRequestAsResponded(const FDTFluxQueuedRequest& TargetRequest); + bool IsRequestPending(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); + + + // Interface FTickableGameObject + virtual void Tick(float DeltaTime) override; + virtual bool IsTickable() const override; + virtual TStatId GetStatId() const override; + virtual bool IsTickableWhenPaused() const override { return true; } + virtual bool IsTickableInEditor() const override { return true; } + // Interface ~FTickableGameObject + + + UPROPERTY(BlueprintAssignable, Category = "DTFlux|Network") + FOnRequestTimedOut OnRequestTimedOut; + +private: + TQueue PendingRequestsQueue; + TQueue CompletedRequestsQueue; + TQueue TimedOutRequestsQueue; + + bool bIsInitialized; + float CheckInterval; + float TimeSinceLastCheck; +}; diff --git a/Source/DTFluxNetwork/Public/Struct/DTFluxRequestStructs.h b/Source/DTFluxNetwork/Public/Struct/DTFluxRequestStructs.h index a47d352..7d2020f 100644 --- a/Source/DTFluxNetwork/Public/Struct/DTFluxRequestStructs.h +++ b/Source/DTFluxNetwork/Public/Struct/DTFluxRequestStructs.h @@ -15,9 +15,13 @@ USTRUCT() struct FDTFluxRequestBase { GENERATED_BODY() + public: UPROPERTY() FString Path = ""; + + FDateTime CreatedAt = FDateTime::Now(); + FGuid RequestId = FGuid::NewGuid(); }; /** @@ -25,11 +29,13 @@ public: * RaceData represents all data concerning the Race and its different Contests, Stages and Splits. */ USTRUCT() -struct FDTFluxRaceDataRequest: public FDTFluxRequestBase +struct FDTFluxRaceDataRequest : public FDTFluxRequestBase { GENERATED_BODY() + public: - FDTFluxRaceDataRequest(){ + FDTFluxRaceDataRequest() + { Path = "race-datas"; } }; @@ -39,11 +45,13 @@ public: * TeamList is the list of participants of the events */ USTRUCT() -struct FDTFluxTeamListRequest: public FDTFluxRequestBase +struct FDTFluxTeamListRequest : public FDTFluxRequestBase { GENERATED_BODY() + public: - FDTFluxTeamListRequest(){ + FDTFluxTeamListRequest() + { Path = "team-list"; } }; @@ -52,16 +60,17 @@ public: * Struct representing a Ranking json request object for a specific to the server */ USTRUCT() -struct FDTFluxContestRankingRequest: public FDTFluxRequestBase +struct FDTFluxContestRankingRequest : public FDTFluxRequestBase { GENERATED_BODY() - + public: FDTFluxContestRankingRequest() { Path = "contest-ranking"; ContestID = -1; } + FDTFluxContestRankingRequest(int InContestID) { Path = "contest-ranking"; @@ -76,10 +85,10 @@ public: * Struct representing a Ranking json request object for a specific Stage to the server */ USTRUCT() -struct FDTFluxStageRankingRequest: public FDTFluxRequestBase +struct FDTFluxStageRankingRequest : public FDTFluxRequestBase { GENERATED_BODY() - + public: FDTFluxStageRankingRequest() { @@ -88,6 +97,7 @@ public: StageID = -1; SplitID = -1; } + FDTFluxStageRankingRequest(int InContestID, int InStageId) { Path = "stage-ranking"; @@ -102,18 +112,16 @@ public: int StageID; UPROPERTY() int SplitID; - - }; /** * Struct representing a Ranking json request object for a specific Split to the server */ USTRUCT() -struct FDTFluxSplitRankingRequest: public FDTFluxStageRankingRequest +struct FDTFluxSplitRankingRequest : public FDTFluxStageRankingRequest { GENERATED_BODY() - + public: FDTFluxSplitRankingRequest() { @@ -122,6 +130,7 @@ public: StageID = -1; SplitID = -1; } + FDTFluxSplitRankingRequest(int InContestID, int InStageId, int InSplitId) { Path = "stage-ranking"; @@ -129,5 +138,4 @@ public: StageID = InStageId; SplitID = InSplitId; } - }; diff --git a/Source/DTFluxNetwork/Public/Struct/DTFluxServerResponseStruct.h b/Source/DTFluxNetwork/Public/Struct/DTFluxServerResponseStruct.h index b60c29e..22ca8e0 100644 --- a/Source/DTFluxNetwork/Public/Struct/DTFluxServerResponseStruct.h +++ b/Source/DTFluxNetwork/Public/Struct/DTFluxServerResponseStruct.h @@ -4,13 +4,38 @@ #include "CoreMinimal.h" #include "UObject/Object.h" +#include "DTFluxNetworkModule.h" +#include "DTFluxRaceDataServerResponse.h" +#include "DTFluxRankingServerResponse.h" +#include "DTFluxSplitSensorServerResponse.h" +#include "JsonObjectConverter.h" +#include "Types/Enum/DTFluxCoreEnum.h" +#include "Types/Objects/UDTFluxParticipantFactory.h" +#include "Types/Struct/DTFluxRaceDataStructs.h" +#include "Types/Struct/DTFluxRankingStructs.h" +#include "Types/Struct/DTFluxSplitSensor.h" #include "DTFluxServerResponseStruct.generated.h" +/** + * Enum pour indiquer le statut du parsing + */ +UENUM(BlueprintType) +enum class EDTFluxResponseStatus : uint8 +{ + Unset = 0b00000000 UMETA(DisplayName="Unset"), + Success = 0b10000000 UMETA(DisplayName="Success"), + JsonParseError = 0b00000001 UMETA(DisplayName="JsonParseError"), + ServerError = 0b00000010 UMETA(DisplayName="ServerError"), + InvalidType = 0b00000100 UMETA(DisplayName="InvalidType"), + MissingData = 0b00001000 UMETA(DisplayName="MissingData"), + DataError = 0b00010000 UMETA(DisplayName="MissingData"), + UnknownError = 0b00100000 UMETA(DisplayName="UnknownError") +}; /** - * Struct representing a mixed root json server response + * Struct representing a mixed root json server response with integrated parsing capabilities */ USTRUCT() struct DTFLUXNETWORK_API FDTFluxServerResponse @@ -20,35 +45,76 @@ struct DTFLUXNETWORK_API FDTFluxServerResponse public: UPROPERTY() FString Type = ""; + UPROPERTY() int Code = -1; + UPROPERTY() FString Message = ""; + UPROPERTY() FString Trigger = ""; + UPROPERTY() int ContestID = -1; + UPROPERTY() int StageID = -1; + UPROPERTY() int SplitID = -1; + UPROPERTY() FDateTime ReceivedAt; + UPROPERTY() FString RawMessage; + UPROPERTY() FName RequestId = FName(""); + UPROPERTY() FText FailureReason; - + + // === CONSTRUCTEURS === + FDTFluxServerResponse(); + FDTFluxServerResponse(const FString& JsonMessage, EDTFluxResponseStatus& OutStatus, bool bLogErrors = true); + static FDTFluxServerResponse CreateFromJson(const FString& JsonMessage, bool bLogErrors = true); + + // === MÉTHODES DE PARSING === + + EDTFluxResponseStatus TryParse(bool bLogErrors = true); + + bool ParseTeamListResponse(FDTFluxTeamListDefinition& OutTeamList); + bool ParseTeamUpdateResponse(FDTFluxTeamListDefinition& OutTeamUpdate); + bool ParseRaceData(FDTFluxRaceData& OutRaceData); + bool ParseContestRanking(FDTFluxContestRankings& OutContestRankings); + bool ParseStageRankingResponse(FDTFluxStageRankings& OutStageRankings); + bool ParseSplitRankingResponse(FDTFluxSplitRankings& OutSplitRankings); + bool ParseStatusUpdateResponse(FDTFluxTeamStatusUpdate& OutStatusUpdate); + bool ParseSplitSensorResponse(TArray& OutSplitSensorInfos); + + + // === MÉTHODES UTILITAIRES === + + bool IsValidResponse() const { return Code == -1; } + bool IsSuccessfullyParsed() const { return ParsingStatus == EDTFluxResponseStatus::Success; } + EDTFluxResponseStatus GetParsingStatus() const { return ParsingStatus; } + EDTFluxApiDataType GetResponseType() const { return ApiDataType; } + FString GetDataType() const { return Type; } + bool ContainsDataType(const FString& DataType) const { return Type.Contains(DataType); } + FString ToDebugString() const; + void ShowDebug(const bool bShouldPrintRawMessage = false) const; + FString GetErrorMessage() const; + +private: + // === DONNÉES INTERNES === + EDTFluxApiDataType ApiDataType; + // Statut du parsing initial + EDTFluxResponseStatus ParsingStatus = EDTFluxResponseStatus::Unset; + + // === MÉTHODES PRIVÉES DE PARSING === + bool ParseJsonObject(TSharedPtr& OutJsonObject) const; + bool ValidateResponseType(const FString& ExpectedType) const; + EDTFluxResponseStatus InitializeFromJson(const FString& JsonMessage, bool bLogErrors); }; - - - - - - - - - - diff --git a/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h b/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h index b0caa60..81a358a 100644 --- a/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h +++ b/Source/DTFluxNetwork/Public/Subsystems/DTFluxNetworkSubsystem.h @@ -3,6 +3,7 @@ #pragma once #include "CoreMinimal.h" +#include "DTFluxQueuedManager.h" #include "Struct/DTFluxServerResponseStruct.h" #include "Subsystems/EngineSubsystem.h" #include "Types/DTFluxNetworkSettingsTypes.h" @@ -14,12 +15,24 @@ #include "DTFluxNetworkSubsystem.generated.h" - - class FDTFluxWebSocketClient; +class UDTFluxQueuedManager; typedef TSharedPtr FDTFluxWebSocketClientSP; class FDTFluxHttpClient; typedef TSharedPtr FDTFluxHttpClientSP; + + +// Delegates pour les requêtes avec callback +DECLARE_DELEGATE_TwoParams(FOnDTFluxRequestResponse, const FGuid&, const FString&); +DECLARE_DELEGATE_TwoParams(FOnDTFluxRequestTimeout, const FGuid&, const FString&); +// 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); + + /** * */ @@ -27,25 +40,28 @@ UCLASS(Blueprintable) class DTFLUXNETWORK_API UDTFluxNetworkSubsystem : public UEngineSubsystem { GENERATED_BODY() - -public: +public: UPROPERTY() EDTFluxConnectionStatus WsStatus = EDTFluxConnectionStatus::Unset; - + DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnWebSocketConnected); + 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; @@ -53,32 +69,40 @@ public: 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 FDTFluxParticipant& /*ParticipantToUpdate*/); + DECLARE_DELEGATE_OneParam(FOnTeamUpdateReceived, const FDTFluxTeamListDefinition& /*ParticipantToUpdate*/); FOnTeamUpdateReceived OnTeamUpdateReceived; + FOnTeamUpdateReceived& OnReceivedTeamUpdate() { return OnTeamUpdateReceived; @@ -86,6 +110,7 @@ public: DECLARE_DELEGATE_OneParam(FOnTeamStatusUpdateReceived, const FDTFluxTeamStatusUpdate& /*TeamToUpdate*/); FOnTeamStatusUpdateReceived OnTeamStatusUpdateReceived; + FOnTeamStatusUpdateReceived& OnReceivedTeamStatusUpdate() { return OnTeamStatusUpdateReceived; @@ -99,13 +124,41 @@ public: void Reconnect(); + // === 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); + FGuid SendTrackedRequestWithCallback(EDTFluxApiDataType RequestType, int32 ContestId, int32 StageId, int32 SplitId, + FOnDTFluxRequestResponse OnCompleted, FOnDTFluxRequestTimeout OnTimeout, + float TimeoutSeconds = 30.0f); + 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) + bool HasRequestReceivedResponse(const FGuid& RequestId) const; + UFUNCTION(BlueprintCallable, Category="DTFlux|Tracked Requests") + FString GetRequestResponseData(const FGuid& RequestId) const; + 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") + 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") - void SendRequest(const EDTFluxRequestType RequestType, int InContestId = -1, int InStageId = -1, int InSplitId = -1); - - - UFUNCTION(BlueprintCallable, Category="DTFlux|Network") + + // === REQUÊTES DIRECTES (LEGACY) === + void SendRequest(const EDTFluxApiDataType RequestType, int InContestId = -1, int InStageId = -1, + int InSplitId = -1); + UFUNCTION(BlueprintCallable, Category="DTFlux|Network") void SendMessage(const FString& Message); - + protected: // ~Subsystem Interface virtual void Initialize(FSubsystemCollectionBase& Collection) override; @@ -114,9 +167,22 @@ protected: private: + // === CONFIGURATION === FDTFluxWsSettings WsSettings; FDTFluxHttpSettings HttpSettings; + UPROPERTY() + UDTFluxQueuedManager* QueueManager; + + // === MAPPING DES CALLBACKS C++ === + TMap PendingCallbacks; + TMap PendingTimeoutCallbacks; + + // === CLIENTS RÉSEAU === + FDTFluxWebSocketClientSP WsClient = nullptr; + FDTFluxHttpClientSP HttpClient = nullptr; + + // === MÉTHODES DE CONFIGURATION === UFUNCTION() void WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings); UFUNCTION() @@ -124,40 +190,46 @@ private: void ReconnectWs(const FName WsClientId); void ReconnectHttp(const FName WsClientId); + // === GESTION DES ÉVÉNEMENTS WEBSOCKET === void RegisterWebSocketEvents(); - void RegisterHttpEvents(); void UnregisterWebSocketEvents(); - void UnregisterHttpEvents(); void OnWebSocketConnected_Subsystem(); void OnWebSocketConnectionError_Subsystem(const FString& Error); - void OnWebSocketClosedEvent_Subsystem(int32 StatusCode , const FString& Reason, bool bWasClean); - void ParseTeamListResponse(const FDTFluxServerResponse& ServerResponse); - void ParseRaceData(const FDTFluxServerResponse& Response); - void ParseContestRanking(const FDTFluxServerResponse& Response); - void ParseStageRankingResponse(const FDTFluxServerResponse& Response); - void ParseSplitRankingResponse(const FDTFluxServerResponse& Response); - void ParseStatusUpdateResponse(const FDTFluxServerResponse& Response); - void ParseSplitSensorResponse(const FDTFluxServerResponse& Response); - void OnWebSocketMessageEvent_Subsystem(const FString& MessageString); - void OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent); - - // TODO : Allow multiple instances of network clients. - // // For Future use of Multi-Connections - // TArray WsClients; - // // For Future use of Multi-Connections - // TArray HttpClient; - // Fo now we jest stick to only one client for each protocol + void OnWebSocketClosedEvent_Subsystem(int32 StatusCode, const FString& Reason, bool bWasClean); FDelegateHandle OnWsConnectedEventDelegateHandle; FDelegateHandle OnWsConnectionErrorEventDelegateHandle; FDelegateHandle OnWsClosedEventDelegateHandle; FDelegateHandle OnWsMessageEventDelegateHandle; FDelegateHandle OnWsMessageSentEventDelegateHandle; - - FDTFluxWebSocketClientSP WsClient = nullptr; - FDTFluxHttpClientSP HttpClient = nullptr; + // === GESTION DES ÉVÉNEMENTS HTTP === + void RegisterHttpEvents(); + void UnregisterHttpEvents(); + // === PARSING DES RÉPONSES === + void ParseTeamListResponse(FDTFluxServerResponse& ServerResponse); + void ParseRaceData(FDTFluxServerResponse& Response); + void ParseContestRanking(FDTFluxServerResponse& Response); + void ParseStageRankingResponse(FDTFluxServerResponse& Response); + void ParseSplitRankingResponse(FDTFluxServerResponse& Response); + void ParseStatusUpdateResponse(FDTFluxServerResponse& Response); + void ParseSplitSensorResponse(FDTFluxServerResponse& Response); + EDTFluxResponseStatus ProcessPushMessage(FDTFluxServerResponse& Response); + + void Parse(FDTFluxServerResponse& Response); + void OnWebSocketMessageEvent_Subsystem(const FString& MessageString); + void OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent); + + // === GESTION DES REQUÊTES TRACKÉES === + UFUNCTION() + void OnRequestTimedOut_Internal(const FDTFluxQueuedRequest& TimedOutRequest); + bool TryMatchResponseToQueuedRequest(const 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); + + // === UTILITAIRES === static FString ConstructWsAddress(const FString& Address, const FString& Path, const int& Port); }; diff --git a/Source/DTFluxPursuitSystem/DTFluxPursuitSystem.Build.cs b/Source/DTFluxPursuitSystem/DTFluxPursuitSystem.Build.cs index 5626d0e..7114ac1 100644 --- a/Source/DTFluxPursuitSystem/DTFluxPursuitSystem.Build.cs +++ b/Source/DTFluxPursuitSystem/DTFluxPursuitSystem.Build.cs @@ -2,27 +2,27 @@ public class DTFluxPursuitSystem : ModuleRules { - public DTFluxPursuitSystem(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxPursuitSystem(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core" + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore", - "DTFluxCore", - "DTFluxCoreSubsystem" - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "DTFluxCore", + "DTFluxCoreSubsystem" + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxPursuitSystem/Private/Types/Objects/DTFluxPursuitManager.cpp b/Source/DTFluxPursuitSystem/Private/Types/Objects/DTFluxPursuitManager.cpp deleted file mode 100644 index 24fb604..0000000 --- a/Source/DTFluxPursuitSystem/Private/Types/Objects/DTFluxPursuitManager.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - - -#include "Types/Objects/DTFluxPursuitManager.h" - -#include "DTFluxCoreSubsystem.h" -#include "DTFluxPursuitSystemModule.h" -#include "K2Node_GetSubsystem.h" - - -UDTFluxPursuitManager::UDTFluxPursuitManager(const FObjectInitializer& ObjectInitializer):Super(ObjectInitializer) -{ - if(!FModuleManager::Get().IsModuleLoaded("DTFluxCoreSubsystem")) - { - FModuleManager::LoadModuleChecked("DTFluxCoreSubsystem"); - } - CoreSubsystem = GEngine->GetEngineSubsystem(); - if(!CoreSubsystem) - { - UE_LOG(logDTFluxPursuitSystem, Error, TEXT("Cannot Get DTFluxCoreSubsystem")); - } -} - -void UDTFluxPursuitManager::InitForTime(const FDateTime InFromTime) -{ - FromTime = InFromTime; - //Get the stage rankings - TArray TargetContests = CoreSubsystem->GetContestsForTime(FromTime); - TArray PursuitParticipantContainer; - for(auto& Contest : TargetContests) - { - int LastStage = Contest.GetLastStageId(); - FDTFluxStageRankings CurrentRanking = CoreSubsystem->GetStageRankings(FDTFluxStageKey(Contest.ContestId, LastStage)); - if(CurrentRanking.IsInitialized()) - { - TArray Rankings = CurrentRanking.Rankings; - for(auto& Ranking : Rankings) - { - Ranking.TimeStart - } - } - // - } -} - -bool UDTFluxPursuitManager::GetNextPursuits(TArray& OutFocusNext, - TArray& OutNext, int MaxPoursuit) -{ - // TODO: Implement UDTFluxPursuitManager::GetNextPursuits(TArray& ,TArray& , int) - return true; -} - -bool UDTFluxPursuitManager::GetPursuits(const FDateTime& InFromTime, TArray& OutFocusNext, - TArray& OutNext, int MaxPoursuit) -{ - // TODO: Implement UDTFluxPursuitManager::GetPursuits(const FDateTime&, TArray& ,TArray& , int) - return true; -} - -FDateTime UDTFluxPursuitManager::GetMassStart() -{ - //TODO : Implement me !!! - return MassStart; -} - -FText UDTFluxPursuitManager::GetFormattedName(FDTFluxPursuit& InPursuit, const int MaxChar, - const FString OverflowChar) -{ - return InPursuit.GetFormattedName(MaxChar, OverflowChar); -} - -FText UDTFluxPursuitManager::DisplayPursuit(FDTFluxPursuit& InPursuit, const int MaxWidth, - const FString NameOverflowChar) -{ - return InPursuit.DisplayPursuit(MaxWidth, NameOverflowChar); -} - -bool UDTFluxPursuitManager::IsUnique(const FDTFluxPursuit& InPursuit) -{ - return InPursuit.IsUnique(); -} \ No newline at end of file diff --git a/Source/DTFluxPursuitSystem/Private/Types/Structs/DTFluxPursuitStructs.cpp b/Source/DTFluxPursuitSystem/Private/Types/Structs/DTFluxPursuitStructs.cpp deleted file mode 100644 index c9d5f90..0000000 --- a/Source/DTFluxPursuitSystem/Private/Types/Structs/DTFluxPursuitStructs.cpp +++ /dev/null @@ -1,30 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - - -#include "Types/Struct/DTFluxPursuitStructs.h" - - -FDTFluxPursuit::FDTFluxPursuit() -{ -} - -FDTFluxPursuit::~FDTFluxPursuit() -{ -} - -FText FDTFluxPursuit::GetFormattedName(const int MaxChar, const FString OverflowChar) -{ - //TODO: Implement Me !!! - return Participants[0].GetConcatFormattedName(MaxChar, OverflowChar); -} - -FText FDTFluxPursuit::DisplayPursuit(const int MaxWidth, const FString NameOverflowChar) -{ - //TODO: Implement Me !!! - return Participants[0].GetConcatFormattedName(MaxWidth, NameOverflowChar); -} - -bool FDTFluxPursuit::IsUnique() const -{ - return Participants.Num() == 1; -} diff --git a/Source/DTFluxPursuitSystem/Public/Types/Objects/DTFluxPursuitManager.h b/Source/DTFluxPursuitSystem/Public/Types/Objects/DTFluxPursuitManager.h deleted file mode 100644 index 9c74342..0000000 --- a/Source/DTFluxPursuitSystem/Public/Types/Objects/DTFluxPursuitManager.h +++ /dev/null @@ -1,70 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Types/Struct/DTFluxRankingStructs.h" -#include "Types/Structs/DTFluxPursuitStructs.h" -#include "UObject/Object.h" -#include "DTFluxPursuitManager.generated.h" - -class UDTFluxCoreSubsystem; -/** - * - */ -UCLASS(BlueprintType) -class DTFLUXPURSUITSYSTEM_API UDTFluxPursuitManager : public UObject -{ - GENERATED_BODY() - - DTFLUXPURSUITSYSTEM_API UDTFluxPursuitManager(const FObjectInitializer& ObjectInitializer ); - -public: - - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"), Transient) - TArray PursuitParticipants; - - UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - FDateTime MassStart; - - UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - FDateTime FromTime; - - - UFUNCTION() - DTFLUXPURSUITSYSTEM_API void InitForTime(const FDateTime InFromTime); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API bool GetNextPursuits(TArray& OutFocusNext, TArray& OutNext, int MaxPoursuit = 8); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API bool GetPursuits(const FDateTime& InFromTime, TArray& OutFocusNext, TArray& OutNext, int MaxPoursuit = 8 ); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API FDateTime GetMassStart(); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API static FText GetFormattedName(FDTFluxPursuit& InPursuit, const int MaxChar = 10, const FString OverflowChar = FString(TEXT("..."))); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API static FText DisplayPursuit(FDTFluxPursuit& InPursuit, const int MaxWidth = 14, const FString NameOverflowChar = FString(TEXT("..."))); - - UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - DTFLUXPURSUITSYSTEM_API static bool IsUnique(const FDTFluxPursuit& InPursuit); - -protected: - - -private: - - UPROPERTY(VisibleAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit")) - int CurrentIndex; - - UPROPERTY() - TArray TargetStages; - - UPROPERTY() - UDTFluxCoreSubsystem* CoreSubsystem = nullptr; -}; - - diff --git a/Source/DTFluxPursuitSystem/Public/Types/Structs/DTFluxPursuitStructs.h b/Source/DTFluxPursuitSystem/Public/Types/Structs/DTFluxPursuitStructs.h deleted file mode 100644 index 8c41ce0..0000000 --- a/Source/DTFluxPursuitSystem/Public/Types/Structs/DTFluxPursuitStructs.h +++ /dev/null @@ -1,58 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "DTFluxTeamListStruct.h" -#include "DTFluxPursuitStructs.generated.h" - -/** - * - */ -USTRUCT(BlueprintType, Blueprintable) -struct DTFLUXCORE_API FDTFluxPursuit -{ - GENERATED_BODY() - -public: - FDTFluxPursuit(); - FDTFluxPursuit(const TArray& InParticipants) : Participants(InParticipants){}; - - ~FDTFluxPursuit(); - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - TArray Participants; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - FDateTime StartTime; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - int IndexMultiple = 0; - - FText GetFormattedName(const int MaxChar = 15, const FString OverflowChar = FString(TEXT("..."))); - FText DisplayPursuit(const int MaxWidth = 20, const FString NameOverflowChar = FString(TEXT("..."))); - bool IsUnique() const; - -}; - - -USTRUCT(BlueprintType, Blueprintable) -struct DTFLUXCORE_API FDTFluxPursuitInfo -{ - GENERATED_BODY() - -public: - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - int Bib = -1; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - FString Name; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - bool bIsMassStart; - - UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit") - FDateTime StartTime; -}; - diff --git a/Source/DTFluxUtilities/DTFluxUtilities.Build.cs b/Source/DTFluxUtilities/DTFluxUtilities.Build.cs index eae16ed..d63fbca 100644 --- a/Source/DTFluxUtilities/DTFluxUtilities.Build.cs +++ b/Source/DTFluxUtilities/DTFluxUtilities.Build.cs @@ -2,25 +2,26 @@ public class DTFluxUtilities : ModuleRules { - public DTFluxUtilities(ReadOnlyTargetRules Target) : base(Target) - { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; + public DTFluxUtilities(ReadOnlyTargetRules Target) : base(Target) + { + PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange( - new string[] - { - "Core", - } - ); + PublicDependencyModuleNames.AddRange( + new string[] + { + "Core", + } + ); - PrivateDependencyModuleNames.AddRange( - new string[] - { - "CoreUObject", - "Engine", - "Slate", - "SlateCore" - } - ); - } + PrivateDependencyModuleNames.AddRange( + new string[] + { + "CoreUObject", + "Engine", + "Slate", + "SlateCore", + "DTFluxCore" + } + ); + } } \ No newline at end of file diff --git a/Source/DTFluxUtilities/Private/FTDFluxUtils.cpp b/Source/DTFluxUtilities/Private/FTDFluxUtils.cpp new file mode 100644 index 0000000..4d60f77 --- /dev/null +++ b/Source/DTFluxUtilities/Private/FTDFluxUtils.cpp @@ -0,0 +1,9 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "FTDFluxUtils.h" + +FText UFTDFluxUtils::GetFormatedName(FDTFluxParticipant& Participant, const int MaxChar, const FString OverFlowChar) +{ + return Participant.GetFormattedNameText(MaxChar, OverFlowChar); +} diff --git a/Source/DTFluxUtilities/Public/FTDFluxUtils.h b/Source/DTFluxUtilities/Public/FTDFluxUtils.h new file mode 100644 index 0000000..e70bb5e --- /dev/null +++ b/Source/DTFluxUtilities/Public/FTDFluxUtils.h @@ -0,0 +1,22 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "DTFluxCore/Public/Types/Struct/DTFluxTeamListStruct.h" +#include "Kismet/BlueprintFunctionLibrary.h" +#include "FTDFluxUtils.generated.h" + +/** + * + */ +UCLASS() +class DTFLUXUTILITIES_API UFTDFluxUtils : public UBlueprintFunctionLibrary +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category="DTFlux|Utils", Meta=(Keywords="name, concat, participant")) + static FText GetFormatedName(FDTFluxParticipant& Participant, const int MaxChar = 10, + const FString OverFlowChar = "..."); +};