Compare commits

..

11 Commits

56 changed files with 5514 additions and 1210 deletions

9
Notes.md Normal file
View File

@ -0,0 +1,9 @@
# DEV NOTES
# TO THINK
- [ ] Team-Update : il faut trouver une stratégie pour les cas ou Participant supprimé avec réaffectation de son Bib
- [ ] Team-Update : il faut trouver une stratégie pour les cas de réaffectation de Bib d'un Concurrent.
__Solutions Possibles__:
- Si le Bib de l'update n'existe pas alors, le participant à changer de Bib. ça signifie qu'il faut que les organisateur ne réaffecte pas un Bib déja existant.~~
- [ ] Extraire Stages et Splits des Contest Pour les Mettre à plat.

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:export-filename="RaceResult.icone16x16.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
sodipodi:docname="RaceResult.icone16x16.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="px" /><defs
id="defs1" /><g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(3.0517578e-5)"><rect
style="fill:#e22c2b;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
id="rect1"
width="512"
height="512"
x="-3.0517578e-05"
y="0" /><path
style="fill:none;stroke:#ffffff;stroke-width:15;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:markers fill stroke"
d="m 154.64305,277.08769 -1.3605,-151.92206 146.48007,103.3977 v 184.12046 l 54.87334,-50.79185 V 199.53942 L 215.41187,92.967228 V 328.78654 Z"
id="path1" /></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -9,7 +9,7 @@ public class DTFluxAPIStatus : ModuleRules
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Core", "DTFluxCoreSubsystem",
}
);
@ -27,7 +27,10 @@ public class DTFluxAPIStatus : ModuleRules
"EditorStyle",
"ToolWidgets", // Nécessaire pour FSlimHorizontalToolBarBuilder
"UnrealEd",
"Settings"
"Settings",
"DTFluxCoreSubsystem",
"InputCore",
"OutputLog",
}
);
}

View File

@ -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,31 +26,31 @@ 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 =
DTFluxNetwork =
GEngine->GetEngineSubsystem<UDTFluxNetworkSubsystem>();
DTFluxCore =
GEngine->GetEngineSubsystem<UDTFluxCoreSubsystem>();
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<FOutputLogModule>("OutputLog");
const FOutputLogCreationParams Params;
bCanSupportFocus = true;
PopulateComboBoxItems();
FSlimHorizontalToolBarBuilder ToolBarBuilder(
nullptr,
@ -73,26 +72,27 @@ void SDTFluxStatusWidget::Construct(const FArguments& InArgs)
ToolBarBuilder.EndSection();
FSlateFontInfo TitleTextFont = FCoreStyle::Get().GetFontStyle(FName("EmbossedText"));
TitleTextFont.Size = 15;
ChildSlot
[
#pragma region ToolBarSection
SNew(SVerticalBox)
+SVerticalBox::Slot()
.AutoHeight()
SNew(SBorder)
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
#pragma region ToolBarSection
SNew(SBox)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
.FillWidth(2.0)
[
SNew(SSpacer)
]
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
.AutoWidth()
.FillWidth(1.0)
.VAlign(VAlign_Center)
@ -101,205 +101,170 @@ void SDTFluxStatusWidget::Construct(const FArguments& InArgs)
ToolBarBuilder.MakeWidget()
]
]
]
#pragma endregion
#pragma region WebsocketStatusSection
// Main VerticalBox
+SVerticalBox::Slot()
]
+ SVerticalBox::Slot()
.AutoHeight()
[
#pragma region WebsocketStatusSection
SNew(SBorder)
.Padding(6.0f)
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
.MaxWidth(175.0)
.MinWidth(150.0)
[
SNew(STextBlock )
SNew(STextBlock)
.Text(FText::FromString(TEXT("Websocket connection :")))
.Justification(ETextJustify::Left)
]
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
.VAlign(VAlign_Center)
.MinWidth(50.0)
.MaxWidth(100.0)
[
SAssignNew( WsStatusText, STextBlock)
SAssignNew(WsStatusText, STextBlock)
.Text(this, &SDTFluxStatusWidget::GetWebSocketStatusText)
.Justification(ETextJustify::Left)
.ColorAndOpacity(this, &SDTFluxStatusWidget::GetWebSocketStatusColor)
]
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
.MaxWidth(100.0)
.MinWidth(30.0)
[
SAssignNew(ConnectionActionButton, SButton)
.Text(this, &SDTFluxStatusWidget::GetWebConnectActionButtonText)
.ForegroundColor_Raw(this, &SDTFluxStatusWidget::GetWebConnectActionButtonColor)
.OnClicked(this,&SDTFluxStatusWidget::OnConnectionActionButtonClicked)
.OnClicked(this, &SDTFluxStatusWidget::OnConnectionActionButtonClicked)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
.ContentPadding(1.5f)
]
]
]
#pragma endregion
#pragma region DataModelControlSection
+SVerticalBox::Slot()
]
+ SVerticalBox::Slot()
.VAlign(VAlign_Fill)
.HAlign(HAlign_Fill)
.AutoHeight()
[
SNew(SBox)
#pragma region DataModelControlSection
SNew(SBorder)
.Padding(6.0f)
.BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder"))
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
+ SHorizontalBox::Slot()
[
SNew(SButton)
.Text(FText::FromString("Get RaceDatas"))
.OnClicked(this, &SDTFluxStatusWidget::OnRaceDatasClicked)
]
+SHorizontalBox::Slot()
+ 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<TSharedPtr<FComboBoxItem>>)
.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
#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
]
+ 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("Unknown");
}
FText SDTFluxStatusWidget::GetWebConnectActionButtonText() const
{
switch (DTFlux->WsStatus)
switch (DTFluxNetwork->WsStatus)
{
case EDTFluxConnectionStatus::Connected:
return FText::FromString("Disconnect");
@ -310,24 +275,25 @@ FText SDTFluxStatusWidget::GetWebConnectActionButtonText() const
FReply SDTFluxStatusWidget::OnConnectionActionButtonClicked()
{
if(DTFlux)
if (DTFluxNetwork)
{
switch (DTFlux->WsStatus)
switch (DTFluxNetwork->WsStatus)
{
case EDTFluxConnectionStatus::Connected:
DTFlux->Reconnect();
DTFluxNetwork->Reconnect();
break;
default:
DTFlux->Connect();
DTFluxNetwork->Connect();
break;
}
}
return FReply::Handled();
}
FSlateColor SDTFluxStatusWidget::GetWebSocketStatusColor() const
{
FColor Color;
switch (DTFlux->WsStatus)
switch (DTFluxNetwork->WsStatus)
{
case EDTFluxConnectionStatus::Unset:
Color = FColor::Orange;
@ -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<FDTFluxContest> 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<SWidget> SDTFluxStatusWidget::OnGeneRankingComboWidget(TSharedPtr<FComboBoxItem> InItem)
{
return SNew(STextBlock)
.Text(FText::FromString(InItem->DisplayText))
.ColorAndOpacity(GetComboItemRankingColor(InItem))
.Margin(FMargin(2.0f, 1.0f));
}
void SDTFluxStatusWidget::OnComboContestRankingChanged(TSharedPtr<FComboBoxItem> 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<FComboBoxItem> 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

View File

@ -10,8 +10,10 @@
*
*/
class UDTFluxNetworkSubsystem;
class UDTFluxCoreSubsystem;
class SSuperListView;
class DTFLUXAPISTATUS_API SDTFluxStatusWidget : public SCompoundWidget
{
public:
@ -29,8 +31,42 @@ public:
TAttribute<FText> 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<FComboBoxItem> CreateItem(const EComboBoxItemType& InType, const FString& InDisplayText,
const int InContestId, const int InStageId = -1,
const int InSplitId = -1)
{
TSharedPtr<FComboBoxItem> 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<STextBlock> WsStatusText;
TSharedPtr<SButton> ConnectionActionButton;
TSharedPtr<SComboBox<TSharedPtr<FComboBoxItem>>> ContestComboBox;
TArray<TSharedPtr<FComboBoxItem>> ContestComboBoxItems;
TSharedPtr<FComboBoxItem> SelectedContestComboBoxItem;
// Méthodes pour le ComboBox
void PopulateComboBoxItems();
TSharedRef<SWidget> OnGeneRankingComboWidget(TSharedPtr<FComboBoxItem> InItem);
void OnComboContestRankingChanged(TSharedPtr<FComboBoxItem> NewSelection, ESelectInfo::Type SelectInfo);
FText GetCurrContestComboBoxValue() const;
static FSlateColor GetComboItemRankingColor(const TSharedPtr<FComboBoxItem> Item);
FReply OnRankingButtonClicked() const;
};

View File

@ -9,7 +9,7 @@ public class DTFluxAssetsEditor : ModuleRules
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Core"
}
);
@ -18,11 +18,20 @@ public class DTFluxAssetsEditor : ModuleRules
{
"CoreUObject",
"Engine",
"SlateCore",
"Slate",
"AssetTools",
"SlateCore",
"UnrealEd",
"DTFluxCore",
"ToolMenus",
"EditorWidgets",
"EditorStyle",
"PropertyEditor",
"SharedSettingsWidgets",
"PropertyEditor",
"DesktopWidgets",
"ApplicationCore",
"InputCore"
}
);
}

View File

@ -3,26 +3,58 @@
#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<FAssetToolsModule>("AssetTools").Get();
EAssetTypeCategories::Type Category = AssetToolsModule.RegisterAdvancedAssetCategory("DTFlux", INVTEXT("DTFlux"));
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<FAssetToolsModule>("AssetTools").Get();
AssetToolsModule.UnregisterAssetTypeActions(DTFluxAssetModelActions.ToSharedRef());
}
UnregisterCustomizations();
}
void FDTFluxAssetsEditorModule::RegisterCustomizations()
{
// Enregistrer la customization pour DTFluxModelAsset
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
PropertyModule.RegisterCustomClassLayout(
UDTFluxModelAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FDTFluxModelAssetCustomization::MakeInstance)
);
PropertyModule.NotifyCustomizationModuleChanged();
}
void FDTFluxAssetsEditorModule::UnregisterCustomizations()
{
if (FModuleManager::Get().IsModuleLoaded("PropertyEditor"))
{
FPropertyEditorModule& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(
"PropertyEditor");
PropertyModule.UnregisterCustomClassLayout(UDTFluxModelAsset::StaticClass()->GetFName());
PropertyModule.NotifyCustomizationModuleChanged();
}
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,222 @@
// 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<IDetailCustomization> 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)
{
CustomizeDetailsWithRawDataAccess(DetailBuilder);
}
void FDTFluxModelAssetCustomization::CustomizeDetailsWithoutRawDataAsset(IDetailLayoutBuilder& DetailBuilder)
{
// Edit object
TArray<TWeakObjectPtr<UObject>> ObjectsBeingCustomized;
DetailBuilder.GetObjectsBeingCustomized(ObjectsBeingCustomized);
if (ObjectsBeingCustomized.Num() > 0)
{
ModelAsset = Cast<UDTFluxModelAsset>(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();
})
]
];
}

View File

@ -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<STableViewBase>& InOwnerTableView)
{
Item = InArgs._Item;
ParentWidget = InArgs._ParentWidget;
SMultiColumnTableRow<TSharedPtr<FHierarchicalTreeItem>>::Construct(
SMultiColumnTableRow<TSharedPtr<FHierarchicalTreeItem>>::FArguments(),
InOwnerTableView
);
}
TSharedRef<SWidget> 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<FHierarchicalTreeItemPtr>)
.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<FHierarchicalTreeItemPtr>)
.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<ITableRow> SDTFluxAssetModelDetailsWidget::OnGenerateRowForTree(
FHierarchicalTreeItemPtr Item, const TSharedRef<STableViewBase>& OwnerTable)
{
if (!Item.IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("OnGenerateRowForTree: Invalid item!"));
return SNew(STableRow<FHierarchicalTreeItemPtr>, 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<FHierarchicalTreeItemPtr>& 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()
));
}

View File

@ -26,4 +26,6 @@ public:
private:
TSharedPtr<FDTFluxAssetModelTypeActions> DTFluxAssetModelActions;
void RegisterCustomizations();
void UnregisterCustomizations();
};

View File

@ -0,0 +1,27 @@
// 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 CustomizeDetailsWithoutRawDataAsset(IDetailLayoutBuilder& DetailBuilder);
void CustomizeDetailsWithRawDataAccess(IDetailLayoutBuilder& DetailBuilder);
// Crée une instance de cette customization
static TSharedRef<IDetailCustomization> MakeInstance();
private:
// Handle vers l'objet en cours d'édition
TWeakObjectPtr<UDTFluxModelAsset> ModelAsset;
// Widget personnalisé
TSharedPtr<SDTFluxAssetModelDetailsWidget> DetailsWidget;
};

View File

@ -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<TSharedPtr<FHierarchicalTreeItem>>
{
public:
SLATE_BEGIN_ARGS(SHierarchicalTreeItemRow)
{
}
SLATE_ARGUMENT(TSharedPtr<FHierarchicalTreeItem>, Item)
SLATE_ARGUMENT(SDTFluxAssetModelDetailsWidget*, ParentWidget)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, const TSharedRef<STableViewBase>& InOwnerTableView);
protected:
virtual TSharedRef<SWidget> GenerateWidgetForColumn(const FName& ColumnName) override;
private:
TSharedPtr<FHierarchicalTreeItem> 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<TSharedPtr<FHierarchicalTreeItem>> Children;
FHierarchicalTreeItem(EItemType InType, const FString& InName)
: Type(InType), Name(InName)
{
}
void AddChild(TSharedPtr<FHierarchicalTreeItem> Child)
{
if (Child.IsValid())
{
Children.Add(Child);
}
}
// Factory methods pour créer les éléments
static TSharedPtr<FHierarchicalTreeItem> CreateContest(const FString& ContestName, const FDTFluxContest& Contest)
{
TSharedPtr<FHierarchicalTreeItem> 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<FHierarchicalTreeItem> CreateStage(const FDTFluxStage& Stage, int32 InContestId)
{
FString StageName = Stage.Name.IsEmpty() ? FString::Printf(TEXT("Stage %d"), Stage.StageId) : Stage.Name;
TSharedPtr<FHierarchicalTreeItem> 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<FHierarchicalTreeItem> CreateSplit(const FDTFluxSplit& Split, int32 InContestId)
{
FString SplitName = Split.Name.IsEmpty() ? FString::Printf(TEXT("Split %d"), Split.SplitId) : Split.Name;
TSharedPtr<FHierarchicalTreeItem> 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<FHierarchicalTreeItem> CreateParticipant(const FDTFluxParticipant& Participant,
UDTFluxModelAsset* InModelAsset = nullptr)
{
FString ParticipantName = Participant.GetConcatFormattedName();
TSharedPtr<FHierarchicalTreeItem> 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<FString> StatusArray;
Status.ParseIntoArray(StatusArray, TEXT("::"));
Item->Status = StatusArray.Last();
Item->Extra = Participant.Club;
return Item;
}
};
typedef TSharedPtr<FHierarchicalTreeItem> 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<FHierarchicalTreeItemPtr> RootItems; // Contests racines avec hiérarchie
TArray<FHierarchicalTreeItemPtr> ParticipantItems; // Participants séparés
// Widgets - TreeView simple
TSharedPtr<STreeView<FHierarchicalTreeItemPtr>> ContestTreeView;
TSharedPtr<STreeView<FHierarchicalTreeItemPtr>> ParticipantTreeView;
TSharedPtr<STextBlock> StatsText;
TSharedPtr<STextBlock> SelectionText;
// Méthodes de construction
void BuildContestHierarchy();
void BuildParticipantList();
// Callbacks TreeView
TSharedRef<ITableRow> OnGenerateRowForTree(FHierarchicalTreeItemPtr Item,
const TSharedRef<STableViewBase>& OwnerTable);
void OnGetChildrenForTree(FHierarchicalTreeItemPtr Item, TArray<FHierarchicalTreeItemPtr>& 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;
};

View File

@ -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,16 +46,17 @@ 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<FDTFluxPerson> 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))
if (!PersonExists(Person))
{
UE_LOG(logDTFluxCore, Error, TEXT("AddParticipant() DTFlux Person %s %s %s doesnot exists, adding..."),
*Person.FirstName, *Person.LastName, *Person.Gender);
@ -68,9 +79,10 @@ 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 !!!"),
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<FDTFluxPerson> 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();

View File

@ -1,43 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Types/Objects/DTFluxPursuitManager.h"
void UDTFluxPursuitManager::InitForStage(const FDTFluxStageRankings& StageRankings)
{
}
TArray<FDTFluxPursuit> UDTFluxPursuitManager::GetNextPursuits(int MaxPursuit)
{
//TODO : Implement me !!!
return PursuitParticipants;
}
TArray<FDTFluxPursuit> UDTFluxPursuitManager::GetPursuits(int FromIndex, int MaxPursuit)
{
//TODO : Implement me !!!
return PursuitParticipants;
}
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();
}

View File

@ -18,8 +18,9 @@ bool UDTFluxParticipantFactory::CreateParticipantFomJson(const FString& JsonStri
}
}
bool UDTFluxParticipantFactory::CreateFromJsonCpp(const TSharedPtr<FJsonObject> JsonObject, FDTFluxParticipant& OutParticipant)
bool UDTFluxParticipantFactory::CreateFromJsonCpp(const TSharedPtr<FJsonObject> JsonObject,
FDTFluxParticipant& OutParticipant)
{
OutParticipant = FDTFluxParticipant::CreateFromJson(JsonObject);
return OutParticipant == 0;
return !OutParticipant.IsDefault();
}

View File

@ -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;
}

View File

@ -7,23 +7,5 @@ void FDTFluxContestRanking::Dump() const
{
UE_LOG(logDTFluxCore, Log,
TEXT("FDTFluxContestRanking ->> \n \"rank\" : %d, Participant with Bib %d \"Gap\" : %s, \"Time\" : %s "),
Rank, Bib, *Gap, *Time );
Rank, Bib, *Gap, *Time);
};
// void FDTFluxStageRanking::Dump() const
// {
// UE_LOG(logDTFluxCore, Log, TEXT("RANKING : %02d. Participant bib %d %s %s %s %s %s"),
// Rank, Bib, *Gap, *TimeSwim,
// *TimeTransition, *TimeRun, *StartTime.ToString());
// }
//
// void FDTFluxSplitRanking::Dump() const
// {
// UE_LOG(logDTFluxCore, Log, TEXT("SplitGapItem"))
// // Participant.Dump();
// UE_LOG(logDTFluxCore, Log, TEXT("Bib %02d Rank %02d Gap %s Time %s"), Bib, Rank, *Gap, *Time);
// }

View File

@ -3,7 +3,7 @@
#include "Types/Struct/DTFluxTeamListStruct.h"
#include "DTFluxCoreModule.h"
void FDTFluxParticipant::AddTeammate(const FDTFluxPerson& Person)
@ -15,82 +15,76 @@ 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;
}
// Récupère la première lettre du prénom en majuscule
else
{
FirstName = Teammate[0].FirstName;
LastName = Teammate[0].LastName;
}
FString Initial;
if (!FirstName.IsEmpty())
{
Initial = FirstName.Left(1).ToUpper() + " ";
}
// Nom complet en majuscules
FString FormattedLastName = LastName.ToUpper();
// 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)
if (FullName.Len() <= MaxChar)
{
// On essaie de garder autant de caractères que possible
const int32 AvailableLength = MaxChar - Initial.Len();
if (AvailableLength <= 0)
{
// Pas assez de place pour le nom → juste l'initiale ?
return FText::FromString(Initial);
return FullName;
}
// Coupe le nom pour quil rentre dans la limite
const int32 TruncateLength = FMath::Min(AvailableLength, FormattedLastName.Len());
FullName = Initial + FormattedLastName.Left(TruncateLength);
// Si on a coupé trop court, on ajoute le suffixe
if (FormattedLastName.Len() > TruncateLength)
{
// On vérifie qu'il reste de la place pour le suffixe
const int32 CurrentLength = FullName.Len();
const int32 OverflowLength = OverflowChars.Len();
if (CurrentLength + OverflowLength <= MaxChar)
if (OverflowLength > MaxChar)
{
FullName += OverflowChars;
}
else
{
// Il faut tronquer davantage pour faire de la place au suffixe
const int32 RemainingSpace = MaxChar - CurrentLength;
if (RemainingSpace > 0)
{
FullName = FullName.Left(MaxChar - OverflowLength) + OverflowChars;
}
else
{
FullName = FullName.Left(MaxChar);
}
}
}
return FullName.Left(MaxChar);
}
return FText::FromString(FullName);
if (Initial.Len() + OverflowLength > MaxChar)
{
return FullName.Left(MaxChar);
}
const int32 AvailableForLastName = MaxChar - Initial.Len() - OverflowLength;
if (AvailableForLastName <= 0)
{
return FullName.Left(MaxChar);
}
FString TruncatedName = Initial + FormattedLastName.Left(AvailableForLastName) + OverflowChars;
if (TruncatedName.Len() > MaxChar)
{
return TruncatedName.Left(MaxChar);
}
return TruncatedName;
}
}
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
@ -106,13 +100,13 @@ FDTFluxParticipant::FDTFluxParticipant(const TSharedPtr<FJsonObject>& JsonObject
, 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;
}
@ -134,7 +128,6 @@ FDTFluxParticipant::FDTFluxParticipant(const TSharedPtr<FJsonObject>& 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<FJsonObject>& JsonObject)
@ -147,7 +140,7 @@ int FDTFluxParticipant::GetTeammateNum() const
return Teammate.Num();
}
bool FDTFluxParticipant::IsTeam()
bool FDTFluxParticipant::IsTeam() const
{
return Teammate.Num() < 1;
}

View File

@ -1,9 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Types/Struct/FDTFluxPoursuiteStruct.h"
FText FDTFluxPoursuite::GetParticipantFormatedName() const
{
return FText();
}

View File

@ -0,0 +1,4 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Types/Struct/FDTFluxPursuitInfo.h"

View File

@ -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";
@ -44,11 +45,15 @@ public:
TMap<FDTFluxSplitKey, FDTFluxSplitRankings> 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);
};

View File

@ -7,16 +7,61 @@
UENUM(BlueprintType)
enum class EDTFluxRequestType : uint8
enum class EDTFluxApiDataType : uint8
{
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<uint8>(Type) >= 1 && static_cast<uint8>(Type) <= 5;
}
inline bool IsPushOnly(EDTFluxApiDataType Type)
{
return static_cast<uint8>(Type) >= 10 && static_cast<uint8>(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
@ -25,5 +70,5 @@ enum class EDTFluxConnectionStatus : uint8
Connected = 1 << 0 UMETA(DisplayName="Connected"),
Error = 1 << 1 UMETA(DisplayName="Error"),
Closed = 1 << 2 UMETA(DisplayName="Closed"),
NotConnected= 1 << 3 UMETA(DisplayName="NotConnected")
NotConnected = 1 << 3 UMETA(DisplayName="NotConnected")
};

View File

@ -56,7 +56,7 @@ ENUM_CLASS_FLAGS(EDTFluxSplitType);
UENUM(BlueprintType, meta=(Bitflags, UseEnumValuesAsMaskValuesInEditor=true))
enum EDTFluxSortingFilter : uint8
enum class EDTFluxSortingFilter : uint8
{
None = 0b00000000 UMETA(DisplayName="No Sorting"),
IgnoreStatusOut = 0b00000001 UMETA(DisplayName="IgnoreStatusOut"),
@ -70,3 +70,20 @@ enum EDTFluxSortingFilter : uint8
DescendingByRank= Descending | ByRank UMETA(DisplayName="DescendingByRank")
};
ENUM_CLASS_FLAGS(EDTFluxSortingFilter);
UENUM(BlueprintType)
enum class EDTFluxSortingRankingType: uint8
{
Rank = 0b00000000 UMETA(DisplayName="Rank (Default)"),
Name = 0b00000001 UMETA(DisplayName="Name"),
Bib = 0b00000010 UMETA(DisplayName="Bib"),
TimeSwim = 0b00000100 UMETA(DisplayName="Swimming Time"),
TimeTransition = 0b00001000 UMETA(DisplayName="Transition Time"),
TimeRun = TimeSwim|TimeTransition UMETA(DisplayName="Running Time"),
StartTime = 0b00001110 UMETA(DisplayName="StartTime"),
Gap = 0b00010000 UMETA(DisplayName="StartTime"),
SwimSpeed = 0b00100000 UMETA(DisplayName="StartTime"),
RunningSpeed = 0b01000000 UMETA(DisplayName="StartTime"),
TotalSpeed = 0b10000000 UMETA(DisplayName="StartTime"),
};

View File

@ -1,63 +0,0 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Types/Struct/DTFluxPursuitStructs.h"
#include "Types/Struct/DTFluxRankingStructs.h"
#include "UObject/Object.h"
#include "DTFluxPursuitManager.generated.h"
/**
*
*/
UCLASS(BlueprintType)
class DTFLUXCORE_API UDTFluxPursuitManager : public UObject
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"), Transient)
TArray<FDTFluxPursuit> 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"))
int ContestId;
UPROPERTY(BlueprintReadOnly, EditAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
int StageId;
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
void InitForStage(const FDTFluxStageRankings& StageRankings);
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
TArray<FDTFluxPursuit> GetNextPursuits(int MaxPursuit);
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
TArray<FDTFluxPursuit> GetPursuits(int FromIndex = 0, int MaxPursuit=10);
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
FDateTime GetMassStart();
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
static FText GetFormattedName(FDTFluxPursuit& InPursuit, const int MaxChar = 10, const FString OverflowChar = FString(TEXT("...")));
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
static FText DisplayPursuit(FDTFluxPursuit& InPursuit, const int MaxWidth = 14, const FString NameOverflowChar = FString(TEXT("...")));
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
static bool IsUnique(const FDTFluxPursuit& InPursuit);
protected:
private:
UPROPERTY(VisibleAnywhere, Category="DTFlux|Pursuit", meta=(Keywords="Poursuit pursuit poursuit"))
int CurrentIndex;
};

View File

@ -6,17 +6,26 @@
#include "UObject/Object.h"
#include "DTFluxCompositeKey.generated.h"
USTRUCT()
struct FDTFluxCompositeKey
{
GENERATED_BODY()
};
/**
*
*/
USTRUCT(BlueprintType)
struct DTFLUXCORE_API FDTFluxStageKey
struct DTFLUXCORE_API FDTFluxStageKey : public FDTFluxCompositeKey
{
GENERATED_BODY()
FDTFluxStageKey() = default;
FDTFluxStageKey(const int InContestId, const int InStageId )
:ContestId(InContestId)
, StageId(InStageId){};
FDTFluxStageKey(const int InContestId, const int InStageId)
: ContestId(InContestId)
, StageId(InStageId)
{
};
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model")
int ContestId = -1;
@ -35,6 +44,7 @@ struct DTFLUXCORE_API FDTFluxStageKey
{
return ContestId == Other.ContestId && StageId == Other.StageId;
}
FString GetDisplayName() const
{
return FString::Printf(TEXT("Contest%i -| Stage%i"), ContestId, StageId);
@ -46,21 +56,23 @@ struct DTFLUXCORE_API FDTFluxStageKey
FText::AsNumber(ContestId),
FText::AsNumber(StageId));
}
};
/**
*
*/
USTRUCT(BlueprintType)
struct DTFLUXCORE_API FDTFluxSplitKey
struct DTFLUXCORE_API FDTFluxSplitKey : public FDTFluxCompositeKey
{
GENERATED_BODY()
FDTFluxSplitKey() = default;
FDTFluxSplitKey(const int InContestId, const int InStageId, const int InSplitId )
:ContestId(InContestId)
FDTFluxSplitKey(const int InContestId, const int InStageId, const int InSplitId)
: ContestId(InContestId)
, StageId(InStageId)
, SplitId(InSplitId){};
, SplitId(InSplitId)
{
};
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="DTFlux|Model")
int ContestId = -1;
@ -82,6 +94,7 @@ struct DTFLUXCORE_API FDTFluxSplitKey
{
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);
@ -95,6 +108,4 @@ struct DTFLUXCORE_API FDTFluxSplitKey
FText::AsNumber(SplitId)
);
}
};

View File

@ -1,38 +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<FDTFluxParticipant>& InParticipants) : Participants(InParticipants){};
~FDTFluxPursuit();
UPROPERTY(BlueprintReadOnly, VisibleAnywhere, Category="DTFlux|Pursuit")
TArray<FDTFluxParticipant> 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;
};

View File

@ -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<FDTFluxSplitRanking> 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;
@ -73,18 +75,89 @@ public:
TArray<FDTFluxSplit> Splits;
UPROPERTY(BlueprintReadWrite, Category="DTFlux|model", EditAnywhere)
FDateTime Date;
UPROPERTY(BlueprintReadWrite, Category="DTFlux|model", EditAnywhere)
FDateTime EndTime;
UPROPERTY()
int LastStageId = -1;
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() const
{
return EndTime <= FDateTime::Now();
}
inline void FDTFluxContest::UpdateEndTime()
{
TArray<FDTFluxStage> TempStages = Stages;
TempStages.Sort([](const FDTFluxStage& A, const FDTFluxStage& B)
{
return A.EndTime < B.EndTime;
});
EndTime = TempStages.Last().EndTime;
}
inline int FDTFluxContest::GetLastStageId()
{
if (LastStageId <= 0)
{
UpdateLastStageId();
}
return LastStageId;
}
inline void FDTFluxContest::UpdateLastStageId()
{
TArray<FDTFluxStage> 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<FDTFluxStage> 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)
{
OutStage = Stage;
return true;
}
}
return false;
}
USTRUCT()
struct DTFLUXCORE_API FDTFluxRaceData
{
GENERATED_BODY()
public:
public:
UPROPERTY()
// ReSharper disable once IdentifierTypo
TArray<FDTFluxContest> Datas;
};

View File

@ -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<FDTFluxContestRanking> 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,24 +85,25 @@ public:
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere)
FDateTime StartTime;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere)
float SpeedRunning;
FString SpeedRunning;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere)
float SpeedTotal;
FString SpeedTotal;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model", VisibleAnywhere)
float SpeedSwim;
FString SpeedSwim;
void Dump() const;
};
USTRUCT(BlueprintType)
struct FDTFluxDetailedRankings
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere)
int ContestId;
UPROPERTY(BlueprintReadWrite,Category="DTFlux|Model|Ranking", VisibleAnywhere)
int StageId;
UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere)
int ContestId = -1;
UPROPERTY(BlueprintReadWrite, Category="DTFlux|Model|Ranking", VisibleAnywhere)
int StageId = -1;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Model|Ranking", VisibleAnywhere)
TArray<FDTFluxDetailedRankingItem> Rankings;
};
@ -139,29 +143,61 @@ struct FDTFluxStageRankings : public FDTFluxDetailedRankings
{
return FDTFluxStageKey(InRankings.ContestId, InRankings.StageId);
}
inline FDTFluxStageKey GetCompositeKey() const
{
return FDTFluxStageKey(ContestId, StageId);
}
};
inline bool IsInitialized() const
{
return ContestId > 0 && StageId > 0;
}
void Initialize()
{
for (auto& Ranking : Rankings)
{
FDateTime RankingStartTime;
if (Ranking.TimeStart != "")
{
TArray<FString> Exploded;
Ranking.TimeStart.ParseIntoArray(Exploded, TEXT(":"), true);
if (Exploded.Num() == 3)
{
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;
}
};

View File

@ -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;
@ -68,14 +66,17 @@ public:
return FirstName.ToLower() + LastName.ToLower() + 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()
@ -91,11 +92,12 @@ struct DTFLUXCORE_API FDTFluxParticipant
friend class UDTFluxModelAsset;
friend class UDTFluxParticipantFactory;
public:
// Constructeur public par défaut requis par Unreal
FDTFluxParticipant()
: Bib(-1)
,ContestId(-1)
, ContestId(-1)
, Elite(false)
, Status(static_cast<EDTFluxParticipantStatusType>(0))
, bIsMassStartParticipant(false)
@ -105,14 +107,25 @@ public:
}
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
return Bib == -1
&& ContestId == -1
&& Category.IsEmpty()
&& Club.IsEmpty()
&& !Elite
&& Status == static_cast<EDTFluxParticipantStatusType>(0)
&& Team.IsEmpty()
&& !bIsMassStartParticipant
&& CurrentSplit == -1
&& Teammate.IsEmpty();
}
UPROPERTY(BlueprintReadOnly, Category="DTFlux|model", EditAnywhere)
int Bib = -1;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|model", EditAnywhere)
@ -135,11 +148,45 @@ 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<FDTFluxPerson> GetTeammate() const { return Teammate; }
private:
// --- Constructeur privé ---
explicit FDTFluxParticipant(const TSharedPtr<FJsonObject>& JsonObject);
@ -149,7 +196,7 @@ protected:
// Méthode publique pour construire à partir d'un JSON (utilisée par la factory)
static FDTFluxParticipant CreateFromJson(const TSharedPtr<FJsonObject>& JsonObject);
int GetTeammateNum() const;
bool IsTeam();
bool IsTeam() const;
};
@ -162,6 +209,7 @@ USTRUCT(BlueprintType)
struct DTFLUXCORE_API FDTFluxTeamListDefinition
{
GENERATED_BODY()
public:
UPROPERTY()
// ReSharper disable once IdentifierTypo
@ -175,9 +223,12 @@ struct FDTFluxTeamStatusUpdate
public:
FDTFluxTeamStatusUpdate() = default;
FDTFluxTeamStatusUpdate(const int InBib, const int InStatus)
:Bib(InBib)
, Status(static_cast<EDTFluxParticipantStatusType>(InStatus)){};
: Bib(InBib)
, Status(static_cast<EDTFluxParticipantStatusType>(InStatus))
{
};
UPROPERTY(BlueprintReadWrite, Category="DTFlux|Participant")

View File

@ -1,27 +0,0 @@
// 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 "FDTFluxPoursuiteStruct.generated.h"
/**
* @struct FDTFluxPoursuite
* Representing a
*/
USTRUCT(BlueprintType, Category="DTFlux|Poursuite")
struct FDTFluxPoursuite
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Poursuite")
FDTFluxParticipant Participant;
UPROPERTY(BlueprintReadOnly, Category="DTFlux|Poursuite")
FDateTime TimeStart;
FText GetParticipantFormatedName() const;
};

View File

@ -0,0 +1,38 @@
// 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)
{
};
//TODO : Set this property to BlueprintReadOnly
UPROPERTY(BlueprintReadWrite, EditAnywhere)
bool bIsMassStart = false;
//TODO : Set this property to BlueprintReadOnly
UPROPERTY(BlueprintReadWrite, EditAnywhere)
int Bib = -1;
//TODO : Set this property to BlueprintReadOnly
UPROPERTY(BlueprintReadWrite, EditAnywhere)
FDateTime StartTime;
UPROPERTY()
int ContestId = -1;
};

View File

@ -6,6 +6,7 @@
#include "DTFluxCoreSubsystemModule.h"
#include "DTFluxGeneralSettings.h"
#include "DTFluxPursuitManager.h"
#include "FileHelpers.h"
#include "Assets/DTFluxModelAsset.h"
#include "Subsystems/DTFluxNetworkSubsystem.h"
@ -15,23 +16,23 @@ void UDTFluxCoreSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("[UDTFluxCoreSubsystem] Initializing..."));
if(!DataStorage)
if (!DataStorage)
{
const UDTFluxGeneralSettings* GeneralSettings = GetDefault<UDTFluxGeneralSettings>();
TSoftObjectPtr<UDTFluxModelAsset> 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<UDTFluxNetworkSubsystem>();
if(NetworkSubsystem->WsStatus != EDTFluxConnectionStatus::Connected)
if (NetworkSubsystem->WsStatus != EDTFluxConnectionStatus::Connected)
{
RegisterDelegates();
}
PursuitManager = NewObject<UDTFluxPursuitManager>();
}
void UDTFluxCoreSubsystem::Deinitialize()
@ -41,11 +42,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 +54,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 +126,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 +160,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 +173,33 @@ void UDTFluxCoreSubsystem::ProcessTeamStatusUpdate(const FDTFluxTeamStatusUpdate
DataStorage->UpdateParticipantStatus(NewParticipantStatus);
}
void UDTFluxCoreSubsystem::ProcessTeamUpdate(const FDTFluxParticipant& Participant)
void UDTFluxCoreSubsystem::ProcessTeamUpdate(const FDTFluxTeamListDefinition& TeamListDefinitiont)
{
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);
}
@ -207,12 +250,119 @@ void UDTFluxCoreSubsystem::RequestAllSplitRankingOfContest(int InContestId, int
// TODO Implement this
}
FDTFluxStageRankings UDTFluxCoreSubsystem::GetStageRankings(FDTFluxStageKey StageKey)
{
if (DataStorage->StageRankings.Contains(StageKey))
{
return DataStorage->StageRankings[StageKey];
}
UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Cannot find StageRankings for key [%s]"), *StageKey.GetDisplayName());
return FDTFluxStageRankings();
}
void UDTFluxCoreSubsystem::RequestAllSplitRankingOfStage(int InContestId, int InStageId, int InSplitId)
{
// TODO Implement this
}
const FDTFluxParticipant UDTFluxCoreSubsystem::GetParticipant(int InBib)
{
if (DataStorage->Participants.Contains(InBib))
{
return DataStorage->Participants[InBib];
}
return FDTFluxParticipant();
}
void UDTFluxCoreSubsystem::RefreshStorage()
{
// TODO Implement this
}
TArray<int> UDTFluxCoreSubsystem::GetCurrentContestsId()
{
return GetContestsIdForTime(FDateTime::Now());
}
TArray<FDTFluxContest> UDTFluxCoreSubsystem::GetCurrentContests()
{
return GetContestsForTime(FDateTime::Now());
}
TArray<int> UDTFluxCoreSubsystem::GetContestsIdForTime(const FDateTime Time)
{
TArray<int> Contests;
for (const auto& Pair : DataStorage->Contests)
{
FDTFluxContest Contest = Pair.Value;
int ContestId = Contest.ContestId;
if (Contest.Date < Time && Contest.EndTime > Time)
{
Contests.Add(ContestId);
}
}
return Contests;
}
bool UDTFluxCoreSubsystem::GetContestForId(const int Id, FDTFluxContest& OutContest)
{
for (auto KeyPair : DataStorage->Contests)
{
if (KeyPair.Value.ContestId == Id)
{
OutContest = KeyPair.Value;
return true;
}
}
UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Cannot find Contest for Id [%i]"), Id);
return false;
}
TArray<FDTFluxContest> UDTFluxCoreSubsystem::GetContestsForTime(const FDateTime Time)
{
TArray<FDTFluxContest> Contests;
for (const auto& Pair : DataStorage->Contests)
{
FDTFluxContest Contest = Pair.Value;
int ContestId = Contest.ContestId;
if (Contest.Date < Time && Contest.EndTime > Time)
{
Contests.Add(Contest);
}
}
return Contests;
}
void UDTFluxCoreSubsystem::RequestRankingsForStages(TArray<FDTFluxStage> RequestedStages) const
{
}
TArray<FDTFluxContest> UDTFluxCoreSubsystem::GetContests()
{
if (DataStorage)
{
TArray<FDTFluxContest> OutContests;
DataStorage->Contests.GenerateValueArray(OutContests);
return OutContests;
}
return TArray<FDTFluxContest>();
}
void UDTFluxCoreSubsystem::LaunchPursuitSequenceFor(const TArray<int> ContestIds)
{
TArray<FDTFluxContest> Contests = TArray<FDTFluxContest>();
for (const auto& ContestId : ContestIds)
{
FDTFluxContest Contest;
GetContestForId(ContestId, Contest);
Contests.Add(Contest);
if (PursuitManager)
{
PursuitManager->LaunchPursuitSequenceFor(Contests);
}
else
{
UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("PursuitManager is null"));
}
}
}

View File

@ -0,0 +1,103 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "DTFluxCoreSubsystemTools.h"
void UDTFluxCoreSubsystemTools::FilterContestRankings(FDTFluxContestRankings& ContestRankings,
const EDTFluxSortingRankingType RankingType, TArray<FDTFluxContestRanking>& OutContestRankings, bool bAscendant)
{
// On fait une copie locale des Rankings
TArray<FDTFluxContestRanking> ContestArray = ContestRankings.Rankings;
// Tri par type + direction
ContestArray.Sort([RankingType, bAscendant](const FDTFluxContestRanking& A, const FDTFluxContestRanking& B)
{
switch (RankingType)
{
case EDTFluxSortingRankingType::Rank:
return bAscendant ? A.Rank < B.Rank : A.Rank > B.Rank;
case EDTFluxSortingRankingType::Bib:
return bAscendant ? A.Bib < B.Bib : A.Bib > B.Bib;
case EDTFluxSortingRankingType::Gap:
return CompareTimeString(A.Gap, B.Gap, bAscendant);
case EDTFluxSortingRankingType::SwimSpeed:
return CompareSpeed(A.SpeedSwimAverage, B.SpeedSwimAverage, bAscendant);
case EDTFluxSortingRankingType::RunningSpeed:
return CompareSpeed(A.SpeedRunningAverage, B.SpeedRunningAverage, bAscendant);
case EDTFluxSortingRankingType::TotalSpeed:
return CompareSpeed(A.SpeedTotalAverage, B.SpeedTotalAverage, bAscendant);
default:
return CompareTimeString(A.Time, B.Time, bAscendant);
}
});
// Réaffecte les données triées
ContestRankings.Rankings = ContestArray;
OutContestRankings = ContestArray;
}
void UDTFluxCoreSubsystemTools::FilterStageRankings(FDTFluxStageRankings& InStageRankings,
const EDTFluxSortingRankingType RankingType, FDTFluxStageRankings& OutStageRankings, bool bAscendant)
{
// TArray<FDTFluxDetailedRankings> StageArray = static_cast<TDF>()InStageRankings.Rankings;
}
void UDTFluxCoreSubsystemTools::FilterSplitRankings(FDTFluxSplitRankings& SplitRankings,
const EDTFluxSortingRankingType RankinType, FDTFluxSplitRankings& OutSplitRankings, bool bAscendant)
{
}
float UDTFluxCoreSubsystemTools::ConvertTimeStringToSeconds(const FString& TimeString)
{
// Format attendu : "HH:MM:SS"
TArray<FString> Parts;
TimeString.ParseIntoArray(Parts, TEXT(":"), true);
if (Parts.Num() == 3)
{
const int32 Hours = FCString::Atoi(*Parts[0]);
const int32 Minutes = FCString::Atoi(*Parts[1]);
const int32 Seconds = FCString::Atoi(*Parts[2]);
return Hours * 3600 + Minutes * 60 + Seconds;
}
if (Parts.Num() == 2)
{
const int32 Minutes = FCString::Atoi(*Parts[0]);
const int32 Seconds = FCString::Atoi(*Parts[1]);
return 3600 + Minutes * 60 + Seconds;
}
if (Parts.Num() == 1)
{
return FCString::Atoi(*Parts[0]);
}
return -1.0f;
}
bool UDTFluxCoreSubsystemTools::CompareTimeString(const FString& A_TimeStr, const FString& B_TimeStr, bool bAscendant)
{
const float A_Time = ConvertTimeStringToSeconds(A_TimeStr);
const float B_Time = ConvertTimeStringToSeconds(B_TimeStr);
return bAscendant ? A_Time < B_Time : A_Time > B_Time;
}
bool UDTFluxCoreSubsystemTools::CompareSpeed(const FString& A_SpeedStr, const FString& B_SpeedStr, bool bAscendant)
{
float A_Speed = FCString::Atof(*A_SpeedStr);
float B_Speed = FCString::Atof(*B_SpeedStr);
if (bAscendant)
{
return A_Speed < B_Speed;
}
return A_Speed > B_Speed;
}

View File

@ -0,0 +1,152 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "DTFluxPursuitManager.h"
#include "DTFluxCoreSubsystemModule.h"
UDTFluxPursuitManager::UDTFluxPursuitManager(const FObjectInitializer& ObjectInitializer):
Super(ObjectInitializer)
{
}
// TODO : Add way to pass MaxSimultaneousPursuit and MassStartDelay
// For now it's done in UPROPERTIES
void UDTFluxPursuitManager::LaunchPursuitSequenceFor(const TArray<FDTFluxContest> InContests)
{
if (InitSubSystems())
{
for (const auto Contest : InContests)
{
FRequestData RequestData;
RequestData.ContestId = Contest.ContestId;
uint8 StageId = Contest.Stages.Last().StageId;
FGuid Guid = NetworkSubsystem->SendTrackedRequestWithCallback(EDTFluxApiDataType::StageRanking,
Contest.ContestId, StageId, -1,
FOnDTFluxTrackedRequestResponse::CreateUObject(
this,
&UDTFluxPursuitManager::OnRequestResponse),
FOnDTFluxTrackedRequestTimeout::CreateUObject(
this,
&UDTFluxPursuitManager::OnRequestTimeoutResponse),
FOnDTFluxRequestResponseError::CreateUObject(
this,
&UDTFluxPursuitManager::OnRequestError));
RequestData.RequestIds.Add(Guid);
PendingRequestData.Add(RequestData);
}
}
}
void UDTFluxPursuitManager::OnRequestResponse(const FGuid& RequestId, FDTFluxServerResponse& Response)
{
UE_LOG(logDTFluxCoreSubsystem, Log,
TEXT("UDTFluxPursuitManager::OnRequestResponse() Received Ranking For Stage %i"), Response.StageID)
UE_LOG(logDTFluxCoreSubsystem, Log, TEXT("Response is %s"), *UEnum::GetValueAsString(Response.GetResponseType()))
//check if request
if (Response.GetResponseType() == EDTFluxApiDataType::StageRanking)
{
FDTFluxStageRankings Rankings;
FRequestData FoundData;
if (Response.ParseStageRankingResponse(Rankings))
{
for (auto& PendingReq : PendingRequestData)
{
// Check for a matching PendingReq
if (PendingReq.IsWaitingFor(RequestId, Rankings))
{
FoundData = PendingReq;
// A request Is Terminated
UE_LOG(logDTFluxCoreSubsystem, Log,
TEXT("UDTFluxPursuitManager::OnRequestResponse() Ranking for Stage %i is complete"),
Response.StageID)
break;
}
}
if (InitPursuit(FoundData))
{
OnPursuitSequenceReady.Broadcast(NextFocusPursuits, NextFocusPursuits, bFocusIsTruncate);
}
}
}
}
void UDTFluxPursuitManager::OnRequestTimeoutResponse(const FGuid& RequestId, const FString& TimeoutMessage)
{
UE_LOG(logDTFluxCoreSubsystem, Warning, TEXT("Request Timeout [%s]"), *TimeoutMessage);
}
void UDTFluxPursuitManager::OnRequestError(const FGuid& RequestId, const FString& ErrorMessage)
{
UE_LOG(logDTFluxCoreSubsystem, Error, TEXT("Request Error [%s]"), *ErrorMessage);
}
bool UDTFluxPursuitManager::InitSubSystems()
{
if (NetworkSubsystem)
{
return true;
}
NetworkSubsystem = GEngine->GetEngineSubsystem<UDTFluxNetworkSubsystem>();
return NetworkSubsystem != nullptr;
}
bool UDTFluxPursuitManager::InitPursuit(FRequestData Data)
{
//Clean Data
NextFocusPursuits.Empty();
NextPursuits.Empty();
PursuitGrouped.Empty();
TArray<FDTFluxDetailedRankingItem> AllRankings;
TArray<FDTFluxPursuitInfo> AllPursuits;
TMap<FDateTime, FDTFluxPursuitGroup> TempGroups;
// Full the Array Of Rankings
for (auto& KeyPair : Data.StageRankings)
{
for (auto StageRanking : KeyPair.Value.Rankings)
{
int ContestId = KeyPair.Value.ContestId;
FDTFluxPursuitInfo PursuitInfo;
PursuitInfo.StartTime = StageRanking.StartTime;
PursuitInfo.Bib = StageRanking.Bib;
PursuitInfo.ContestId = ContestId;
AllPursuits.Add(PursuitInfo);
}
}
// Sort Rankings
// AllPursuits.Sort([](const FDTFluxPursuitInfo& A, const FDTFluxPursuitInfo& B) {
// return A.StartTime < B.StartTime;
// });
for (auto& Pursuit : AllPursuits)
{
if (TempGroups.Contains(Pursuit.StartTime))
{
TempGroups[Pursuit.StartTime].PursuitGroup.Add(Pursuit);
}
else
{
FDTFluxPursuitGroup Group;
Group.StartTimeGlobal = Pursuit.StartTime;
Group.PursuitGroup.Add(Pursuit);
TempGroups.Add(Pursuit.StartTime, Group);
}
}
TempGroups.KeySort([](const FDateTime& A, const FDateTime& B)
{
return A < B;
});
PursuitGrouped.Reserve(TempGroups.Num());
for (const auto& Pair : TempGroups)
{
PursuitGrouped.Add(Pair.Value);
}
PursuitGrouped.Sort([](const FDTFluxPursuitGroup& A, const FDTFluxPursuitGroup& B)
{
return A.StartTimeGlobal < B.StartTimeGlobal;
});
return true;
}

View File

@ -1,10 +1,8 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#pragma once
#include "CoreMinimal.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"
@ -12,11 +10,10 @@
#include "DTFluxCoreSubsystem.generated.h"
class UDTFluxNetworkSubsystem;
/** Forward Decl */
class UDTFluxModelAsset;
class UDTFluxPursuitManager;
/**
*
@ -26,36 +23,40 @@ class DTFLUXCORESUBSYSTEM_API UDTFluxCoreSubsystem : public UEngineSubsystem
{
GENERATED_BODY()
public:
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSplitRankings, FDTFluxSplitRankings&, SplitRankings);
// TSharedPtr<FDTFluxParser> Parser;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnSplitRankings, FDateTime, ReceivedAt, TArray<FDTFluxStageRanking>, SplitRankings);
UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem")
FOnSplitRankings OnSplitRankings;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnStageRankings, FDateTime, ReceivedAt, TArray<FDTFluxStageRanking>, StageRankings);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnStageRankings, FDTFluxStageRankings&, StageRankings);
UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem")
FOnStageRankings OnStageRankings;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnContestRankings, FDateTime, ReceivedAt, TArray<FDTFluxContestRanking>, ContestRankings);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnContestRankings, FDTFluxContestRankings&, ContestRankings);
UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem")
FOnContestRankings OnContestRankings;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTeamList, FDateTime, ReceivedAt, TArray<FDTFluxParticipant>, TeamList);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnTeamList);
UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem")
FOnTeamList OnTeamList;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTeamUpdate, FDateTime, ReceivedAt, FDTFluxParticipant, TeamUpdatedList);
UPROPERTY(BlueprintAssignable, Category="DTFlux|Core Subsystem")
FOnTeamUpdate OnTeamUpdate;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnTeamStatusUpdate, FDTFluxParticipant, TeamUpdated);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnTeamStatusUpdate, FDateTime, ReceivedAt, 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();
@ -77,19 +78,50 @@ public:
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
void RequestAllSplitRankingOfContest(int InContestId, int InStageId);
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
FDTFluxStageRankings GetStageRankings(FDTFluxStageKey StageKey);
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
void RequestAllSplitRankingOfStage(int InContestId, int InStageId, int InSplitId);
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
const FDTFluxParticipant GetParticipant(int InBib);
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
void RefreshStorage();
UFUNCTION()
TArray<int> GetCurrentContestsId();
UFUNCTION()
TArray<FDTFluxContest> GetCurrentContests();
UFUNCTION()
TArray<int> GetContestsIdForTime(const FDateTime Time);
UFUNCTION()
bool GetContestForId(const int Id, FDTFluxContest& OutContest);
UFUNCTION()
TArray<FDTFluxContest> GetContestsForTime(const FDateTime Time);
UFUNCTION()
void RequestRankingsForStages(const TArray<FDTFluxStage> RequestedStages) const;
UFUNCTION()
TArray<FDTFluxContest> GetContests();
UFUNCTION(BlueprintCallable, Category="DTFlux|Core Subsystem")
void LaunchPursuitSequenceFor(const TArray<int> ContestIds);
protected:
// ~Subsystem Interface
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
// ~Subsystem Interface
UPROPERTY()
UDTFluxPursuitManager* PursuitManager = nullptr;
UFUNCTION()
void SaveDataStorage();
private:
UDTFluxNetworkSubsystem* NetworkSubsystem = nullptr;
@ -108,12 +140,13 @@ private:
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;
};

View File

@ -0,0 +1,37 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Types/Enum/DTFluxModelEnums.h"
#include "Types/Struct/DTFluxRankingStructs.h"
#include "DTFluxCoreSubsystemTools.generated.h"
/**
*
*/
UCLASS()
class DTFLUXCORESUBSYSTEM_API UDTFluxCoreSubsystemTools : public UBlueprintFunctionLibrary
{
GENERATED_BODY()
public:
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static void FilterContestRankings(FDTFluxContestRankings& ContestRankings,
const EDTFluxSortingRankingType RankingType, TArray<FDTFluxContestRanking>& OutContestRankings, bool bAscendant);
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static void FilterStageRankings(FDTFluxStageRankings& InStageRankings, const EDTFluxSortingRankingType RankingType, FDTFluxStageRankings& OutStageRankings, bool bAscendant = true);
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static void FilterSplitRankings(FDTFluxSplitRankings& SplitRankings, const EDTFluxSortingRankingType RankinType, FDTFluxSplitRankings& OutSplitRankings, bool bAscendant = true);
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static float ConvertTimeStringToSeconds(const FString& TimeString);
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static bool CompareTimeString(const FString& A_TimeStr, const FString& B_TimeStr, bool bAscendant = true);
UFUNCTION(Blueprintable, Category="DTFlux|Core Subsystem|Tools")
static bool CompareSpeed(const FString& A_SpeedStr, const FString& B_SpeedStr, bool bAscendant=true);
};

View File

@ -0,0 +1,133 @@
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/DTFluxNetworkSubsystem.h"
#include "Types/Struct/DTFluxRaceDataStructs.h"
#include "Types/Struct/FDTFluxPursuitInfo.h"
#include "UObject/Object.h"
#include "DTFluxPursuitManager.generated.h"
USTRUCT()
struct FRequestData
{
GENERATED_BODY()
UPROPERTY()
TArray<FGuid> RequestIds;
UPROPERTY()
TMap<FGuid, FDTFluxStageRankings> StageRankings;
UPROPERTY()
int ContestId;
UPROPERTY()
bool bIsReady = false;
FRequestData() = default;
FRequestData(const TArray<FGuid>& InRequestIds, const TMap<FGuid, FDTFluxStageRankings>& InStageRankings)
: RequestIds(InRequestIds), StageRankings(InStageRankings)
{
};
/**
*
* @param RequestId
* @param InRankings
* @return True if all needed requests have responses
*/
bool IsWaitingFor(const FGuid& RequestId, const FDTFluxStageRankings& InRankings)
{
if (!StageRankings.Contains(RequestId))
{
StageRankings.Add(RequestId, InRankings);
}
bIsReady = StageRankings.Num() <= RequestIds.Num();
return bIsReady;
}
};
USTRUCT()
struct FDTFluxPursuitGroup
{
GENERATED_BODY()
UPROPERTY()
TArray<FDTFluxPursuitInfo> PursuitGroup = TArray<FDTFluxPursuitInfo>();
UPROPERTY()
FDateTime StartTimeGlobal = FDateTime::MinValue();
UPROPERTY()
bool bHasStarted = false;
UPROPERTY()
bool bIsFocus = false;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnPursuitSequenceReady, const TArray<FDTFluxPursuitInfo>,
NextFocusPursuits,
const TArray<FDTFluxPursuitInfo>, NextPursuit, bool, bIsTrtuncate);
/**
*
*/
UCLASS(BlueprintType)
class DTFLUXCORESUBSYSTEM_API UDTFluxPursuitManager : public UObject
{
GENERATED_BODY()
public:
UDTFluxPursuitManager(const FObjectInitializer& ObjectInitializer);
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TArray<FDTFluxPursuitInfo> NextFocusPursuits;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
TArray<FDTFluxPursuitInfo> NextPursuits;
UPROPERTY(BlueprintReadOnly, VisibleAnywhere)
bool bFocusIsTruncate = false;
//
// UPROPERTY()
// TArray<FDTFluxStage> TargetStages;
UPROPERTY()
int MaxSimultaneousPursuit = 7;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category="DTFlux|Pursuit",
meta=(ClampMin="1", ClampMax="60", UIMin="0", UIMax="60"))
int MassStartDelay = 10;
UPROPERTY()
TArray<FDTFluxPursuitGroup> PursuitGrouped;
UPROPERTY()
int CurrentIndex = -1;
UPROPERTY(BlueprintCallable, Category="DTFlux|Pursuit")
FOnPursuitSequenceReady OnPursuitSequenceReady;
UFUNCTION(BlueprintCallable, Category="DTFlux|Pursuit", meta=(Keywords="pursuit, launch, poursuite"))
void LaunchPursuitSequenceFor(const TArray<FDTFluxContest> InContests);
UFUNCTION()
void OnRequestResponse(const FGuid& RequestId, FDTFluxServerResponse& Response);
UFUNCTION()
void OnRequestTimeoutResponse(const FGuid& RequestId, const FString& TimeoutMessage);
UFUNCTION()
void OnRequestError(const FGuid& RequestId, const FString& ErrorMessage);
UFUNCTION()
bool InitSubSystems();
private:
TArray<FRequestData> PendingRequestData;
public:
UFUNCTION()
bool InitPursuit(FRequestData Data);
private:
UDTFluxNetworkSubsystem* NetworkSubsystem = nullptr;
};

View File

@ -9,7 +9,7 @@ public class DTFluxNetwork : ModuleRules
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core",
"Core"
}
);

View File

@ -0,0 +1,439 @@
// 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::MarkRequestAsError(const FGuid& TargetRequestGuid)
{
// TODO: Implement a retry mechanism
// For now we simply suppress the request and log a message
bool bFoundMatch = false;
FDTFluxQueuedRequest Request;
TQueue<FDTFluxQueuedRequest, EQueueMode::Mpsc> TempQueue;
while (PendingRequestsQueue.Dequeue(Request))
{
if (Request.RequestId == TargetRequestGuid)
{
UE_LOG(logDTFluxNetwork, Error,
TEXT("Marked request %s as error: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"),
*Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId,
Request.SplitId);
}
else
{
TempQueue.Enqueue(Request);
}
}
while (TempQueue.Dequeue(Request))
{
PendingRequestsQueue.Enqueue(Request);
}
if (bFoundMatch)
{
UE_LOG(logDTFluxNetwork, Error, TEXT("No Request Found with GUID %s"), *TargetRequestGuid.ToString());
}
return true;
}
bool UDTFluxQueuedManager::MarkRequestAsResponded(const FGuid& TargetRequestGuid)
{
TQueue<FDTFluxQueuedRequest, EQueueMode::Mpsc> 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(FGuid& OutRequestId, EDTFluxApiDataType RequestType, int32 ContestId,
int32 StageId,
int32 SplitId)
{
TQueue<FDTFluxQueuedRequest, EQueueMode::Mpsc> 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;
OutRequestId = Request.RequestId;
UE_LOG(logDTFluxNetwork, Verbose,
TEXT("Found pending request %s: Type=%d, ContestId=%d, StageId=%d, SplitId=%d"),
*Request.RequestId.ToString(), (int32)Request.RequestType, Request.ContestId, Request.StageId,
Request.SplitId);
}
// 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<FDTFluxQueuedRequest, EQueueMode::Mpsc>& Queue) -> FDTFluxQueuedRequest*
{
// Copie temporaire de la queue pour la recherche
TQueue<FDTFluxQueuedRequest> 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<FDTFluxQueuedRequest, EQueueMode::Mpsc>& Queue) -> FDTFluxQueuedRequest*
{
// Copie temporaire de la queue pour la recherche
TQueue<FDTFluxQueuedRequest> 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<FDTFluxQueuedRequest, EQueueMode::Mpsc> 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<FDTFluxQueuedRequest, EQueueMode::Mpsc> 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<FDTFluxQueuedRequest, EQueueMode::Mpsc> 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);
}

View File

@ -0,0 +1,531 @@
#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<FJsonObject> JsonObject;
if (!ParseJsonObject(JsonObject))
{
ParsingStatus = EDTFluxResponseStatus::JsonParseError;
return false;
}
const TArray<TSharedPtr<FJsonValue>>* 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<FJsonValue>& Value : *DataArray)
{
if (Value->Type == EJson::Object)
{
const TSharedPtr<FJsonObject> 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<FDTFluxRaceDataResponse>(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<FDTFluxContestRankingResponse>(
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)
{
// UE_LOG(logDTFluxNetwork, Log, TEXT("Response is stage-ranking type %s"), *RawMessage);
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<FDTFluxStageRankingResponse>(RawMessage, &RankingResponse))
{
UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to parse stage-ranking JSON: %s"), *RawMessage);
ParsingStatus = EDTFluxResponseStatus::JsonParseError;
return false;
}
UE_LOG(logDTFluxNetwork, Log, TEXT("Reponse Update"));
OutStageRankings.ContestId = ContestID;
OutStageRankings.StageId = StageID;
OutStageRankings.Rankings = static_cast<TArray<FDTFluxDetailedRankingItem>>(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<TArray<FDTFluxDetailedRankingItem>>(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<FDTFluxTeamStatusUpdate>(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<FDTFluxSplitSensorInfo>& 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<FJsonObject>& OutJsonObject) const
{
TSharedRef<TJsonReader<>> 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;
}

View File

@ -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,6 +21,7 @@
#include "Types/Struct/DTFluxSplitSensor.h"
// === CONNEXION WEBSOCKET ===
void UDTFluxNetworkSubsystem::Connect()
{
WsClient->SetAddress(ConstructWsAddress(WsSettings.Address, WsSettings.Path, WsSettings.Port));
@ -35,6 +38,148 @@ void UDTFluxNetworkSubsystem::Reconnect()
ReconnectWs(FName("Ws_Client_0"));
}
// === REQUÊTES AVEC TRACKING ===
FGuid UDTFluxNetworkSubsystem::SendTrackedRequest(
EDTFluxApiDataType RequestType,
int32 ContestId,
int32 StageId,
int32 SplitId,
float TimeoutSeconds)
{
if (!QueueManager)
{
UE_LOG(logDTFluxNetwork, Error, TEXT("QueueManager is not initialized"));
return FGuid();
}
// Vérifier si une requête similaire est déjà en cours (optionnel)
if (IsRequestPending(RequestType, ContestId, StageId, SplitId))
{
UE_LOG(logDTFluxNetwork, Warning,
TEXT("Similar request already pending: Type=%d, Contest=%d, Stage=%d, Split=%d"),
(int32)RequestType, ContestId, StageId, SplitId);
}
// Créer et enqueue la requête
FGuid RequestId = QueueManager->QueueRequest(RequestType, ContestId, StageId, SplitId);
// Envoyer immédiatement si possible (le QueueManager gère la queue)
if (const FDTFluxQueuedRequest* QueuedRequest = QueueManager->GetRequest(RequestId))
{
SendQueuedRequest(*QueuedRequest);
}
UE_LOG(logDTFluxNetwork, Log, TEXT("Queued tracked request %s: Type=%d, Contest=%d, Stage=%d, Split=%d"),
*RequestId.ToString(), (int32)RequestType, ContestId, StageId, SplitId);
return RequestId;
}
FGuid UDTFluxNetworkSubsystem::SendTrackedRequestWithCallback(
EDTFluxApiDataType RequestType,
int32 ContestId,
int32 StageId,
int32 SplitId,
FOnDTFluxTrackedRequestResponse OnCompleted,
FOnDTFluxTrackedRequestTimeout OnTimeout,
TOptional<FOnDTFluxRequestResponseError> OnError,
float TimeoutSeconds)
{
FGuid RequestId = SendTrackedRequest(RequestType, ContestId, StageId, SplitId, TimeoutSeconds);
if (RequestId.IsValid())
{
// Stocker les callbacks pour cette requête
if (OnCompleted.IsBound())
{
PendingCallbacks.Add(RequestId, OnCompleted);
}
if (OnTimeout.IsBound())
{
PendingTimeoutCallbacks.Add(RequestId, OnTimeout);
}
if (OnError.IsSet() && OnError.GetValue().IsBound())
{
PendingErrorCallbacks.Add(RequestId, OnError.GetValue());
}
}
return RequestId;
}
bool UDTFluxNetworkSubsystem::GetTrackedRequest(const FGuid& RequestId, FDTFluxQueuedRequest& OutRequest) const
{
if (!QueueManager)
{
return false;
}
const FDTFluxQueuedRequest* Request = QueueManager->GetRequest(RequestId);
if (Request)
{
OutRequest = *Request;
return true;
}
return false;
}
const FDTFluxQueuedRequest* UDTFluxNetworkSubsystem::GetTrackedRequestPtr(const FGuid& RequestId) const
{
if (!QueueManager)
{
return nullptr;
}
return QueueManager->GetRequest(RequestId);
}
bool UDTFluxNetworkSubsystem::HasRequestReceivedResponse(const FGuid& RequestId) const
{
FDTFluxQueuedRequest Request;
if (GetTrackedRequest(RequestId, Request))
{
return Request.bHasReceivedResponse;
}
return false;
}
FString UDTFluxNetworkSubsystem::GetRequestResponseData(const FGuid& RequestId) const
{
FDTFluxQueuedRequest Request;
if (GetTrackedRequest(RequestId, Request))
{
return Request.RawResponse;
}
return FString();
}
bool UDTFluxNetworkSubsystem::IsRequestPending(EDTFluxRequestType RequestType, int32 ContestId, int32 StageId,
int32 SplitId) const
{
if (!QueueManager)
{
return false;
}
FGuid OutRequestId;
return QueueManager->IsRequestPending(OutRequestId, RequestType, ContestId, StageId, SplitId);
}
int32 UDTFluxNetworkSubsystem::GetPendingRequestCount() const
{
if (!QueueManager)
{
return 0;
}
return QueueManager->GetPendingRequestCount();
}
UDTFluxQueuedManager* UDTFluxNetworkSubsystem::GetQueueManager() const
{
return QueueManager;
}
void UDTFluxNetworkSubsystem::SendRequest(const EDTFluxRequestType RequestType, int InContestId, int InStageId,
int InSplitId)
{
@ -48,7 +193,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 +206,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 +216,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 +231,7 @@ void UDTFluxNetworkSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
FDTFluxCoreModule& DTFluxCore = FModuleManager::Get().LoadModuleChecked<FDTFluxCoreModule>("DTFluxCore");
FString StatusString = UEnum::GetValueAsString(WsStatus);
UE_LOG(logDTFluxNetwork, Log, TEXT("Status is %s"), *StatusString);
UDTFluxNetworkSettings* NetworkSettings = GetMutableDefault<UDTFluxNetworkSettings>();
UDTFluxNetworkSettings::GetWebSocketSettings(NetworkSettings, WsSettings);
UDTFluxNetworkSettings::GetHTTPSettings(NetworkSettings, HttpSettings);
@ -99,16 +243,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<UDTFluxQueuedManager>(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 +281,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 +301,9 @@ void UDTFluxNetworkSubsystem::ReconnectWs(const FName WsClientId)
WsClient->SetAddress(NewAddress);
WsClient->Reconnect();
}
void UDTFluxNetworkSubsystem::ReconnectHttp(const FName WsClientId)
{
}
void UDTFluxNetworkSubsystem::RegisterWebSocketEvents()
@ -166,25 +330,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 +367,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();
}
@ -216,254 +380,236 @@ void UDTFluxNetworkSubsystem::OnWebSocketClosedEvent_Subsystem(int32 StatusCode,
WsStatus = EDTFluxConnectionStatus::Closed;
}
void UDTFluxNetworkSubsystem::ParseTeamListResponse(const FDTFluxServerResponse& ServerResponse)
void UDTFluxNetworkSubsystem::ParseTeamListResponse(FDTFluxServerResponse& Response)
{
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> 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<TSharedPtr<FJsonValue>>* 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<FJsonValue>& 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<FJsonObject> 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<FDTFluxRaceDataResponse>(Response.RawMessage, &RaceData))
FDTFluxRaceData RaceData;
Response.ParseRaceData(RaceData);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
//convert
FDTFluxRaceData RaceDataDefinition;
for(auto Contest : RaceData.Datas)
{
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)
{
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());
}
UE_LOG(logDTFluxNetwork, Warning, TEXT("Contest %i [%s]\nSplits: \n"), Contest.Id, *Contest.Name);
for(auto Split: Contest.Splits)
{
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);
}
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;
}
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(const FDTFluxServerResponse& Response)
void UDTFluxNetworkSubsystem::ParseContestRanking(FDTFluxServerResponse& Response)
{
FDTFluxContestRankingResponse ContestRankingResponse;
if(FJsonObjectConverter::JsonObjectStringToUStruct<FDTFluxContestRankingResponse>(Response.RawMessage, &ContestRankingResponse))
{
FDTFluxContestRankings ContestRankings;
ContestRankings.ContestId = ContestRankingResponse.ContestID;
for(auto& RankingItem : ContestRankingResponse.Datas)
Response.ParseContestRanking(ContestRankings);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
FDTFluxContestRanking Temp = RankingItem;
ContestRankings.Rankings.Add(Temp);
}
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);
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(const FDTFluxServerResponse& Response)
void UDTFluxNetworkSubsystem::ParseStageRankingResponse(FDTFluxServerResponse& Response)
{
FDTFluxStageRankingResponse RankingResponse;
if(FJsonObjectConverter::JsonObjectStringToUStruct<FDTFluxStageRankingResponse>(Response.RawMessage, &RankingResponse))
FDTFluxStageRankings StageRankings;
Response.ParseStageRankingResponse(StageRankings);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
FDTFluxStageRankings NewRankings;
NewRankings.ContestId = Response.ContestID;
NewRankings.StageId = Response.StageID;
NewRankings.Rankings = static_cast<TArray<FDTFluxDetailedRankingItem>>(RankingResponse.Datas);
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"),
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);
StageRankings.ContestId, StageRankings.StageId,
bIsSuccessfullyBounded ? TEXT("SUCCESS_SEND") : TEXT("NOT_BOUNDED"));
}
void UDTFluxNetworkSubsystem::ParseSplitRankingResponse(const FDTFluxServerResponse& Response)
void UDTFluxNetworkSubsystem::ParseSplitRankingResponse(FDTFluxServerResponse& Response)
{
FDTFluxSplitRankingResponse SplitRankingResponse;
if(FJsonObjectConverter::JsonObjectStringToUStruct<FDTFluxSplitRankingResponse>(Response.RawMessage, &SplitRankingResponse))
FDTFluxSplitRankings SplitRankings;
Response.ParseSplitRankingResponse(SplitRankings);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
FDTFluxSplitRankings NewSplitRankings;
NewSplitRankings.ContestId = Response.ContestID;
NewSplitRankings.StageId = Response.StageID;
NewSplitRankings.SplitId = Response.SplitID;
NewSplitRankings.Rankings = static_cast<TArray<FDTFluxDetailedRankingItem>>(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);
}
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(const FDTFluxServerResponse& Response)
void UDTFluxNetworkSubsystem::ParseStatusUpdateResponse(FDTFluxServerResponse& Response)
{
FDTFluxTeamStatusUpdate StatusUpdateResponse;
if (FJsonObjectConverter::JsonObjectStringToUStruct<FDTFluxTeamStatusUpdate>(Response.RawMessage, &StatusUpdateResponse))
FDTFluxTeamStatusUpdate StatusUpdate;
Response.ParseStatusUpdateResponse(StatusUpdate);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
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);
}
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(const FDTFluxServerResponse& Response)
void UDTFluxNetworkSubsystem::ParseSplitSensorResponse(FDTFluxServerResponse& Response)
{
FDTFluxSplitSensorResponse SplitSensorResponse;
if(FJsonObjectConverter::JsonObjectStringToUStruct(Response.RawMessage, &SplitSensorResponse))
TArray<FDTFluxSplitSensorInfo> SplitSensorInfos = TArray<FDTFluxSplitSensorInfo>();
Response.ParseSplitSensorResponse(SplitSensorInfos);
if (Response.GetParsingStatus() != EDTFluxResponseStatus::Success)
{
for(const auto& SplitSensorInfoResponse : SplitSensorResponse.Datas)
UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitSensorResponse() for JSON Response : %s"),
*Response.RawMessage);
}
for (auto& SplitSensorInfo : SplitSensorInfos)
{
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"));
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())
{
case EDTFluxApiDataType::SplitSensor:
{
TArray<FDTFluxSplitSensorInfo> SplitSensorInfos;
if (Response.ParseSplitSensorResponse(SplitSensorInfos))
{
for (const auto& SplitSensorInfo : SplitSensorInfos)
{
OnSplitSensorReceived.ExecuteIfBound(SplitSensorInfo);
}
}
ResponseStatus = Response.GetParsingStatus();
break;
}
case EDTFluxApiDataType::StatusUpdate:
{
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;
}
}
}
return ResponseStatus;
}
void UDTFluxNetworkSubsystem::Parse(FDTFluxServerResponse& Response)
{
EDTFluxResponseStatus ResponseStatus = EDTFluxResponseStatus::Success;
switch (Response.GetResponseType())
{
case EDTFluxApiDataType::RaceData:
{
UE_LOG(logDTFluxNetwork, Warning, TEXT("Legacy Parsing RaceData"));
ParseRaceData(Response);
ResponseStatus = Response.GetParsingStatus();
break;
}
case EDTFluxApiDataType::TeamList:
{
UE_LOG(logDTFluxNetwork, Warning, TEXT("Legacy Parsing TeamList"));
ParseTeamListResponse(Response);
ResponseStatus = Response.GetParsingStatus();
break;
}
case EDTFluxApiDataType::ContestRanking:
{
ParseContestRanking(Response);
ResponseStatus = Response.GetParsingStatus();
break;
}
case EDTFluxApiDataType::StageRanking:
{
ParseStageRankingResponse(Response);
ResponseStatus = Response.GetParsingStatus();
break;
}
case EDTFluxApiDataType::SplitRanking:
{
ParseSplitRankingResponse(Response);
ResponseStatus = Response.GetParsingStatus();
break;
}
default:
{
UE_LOG(logDTFluxNetwork, Error, TEXT("Legacy Parsing Unknown"));
ResponseStatus = EDTFluxResponseStatus::UnknownError;
break;
}
}
if (ResponseStatus != EDTFluxResponseStatus::Success)
{
UE_LOG(logDTFluxNetwork, Warning, TEXT("UDTFluxNetworkSubsystem::Parse() Parsing failed"));
}
}
UE_LOG(logDTFluxNetwork, Error, TEXT("ParseSplitSensorResponse() failed for JSON Response : %s"), *Response.RawMessage);
}
//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);
UE_LOG(logDTFluxNetwork, Warning, TEXT("Not a push message"));
// Legacy
Parse(Response);
return;
}
}
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, Error, TEXT("Ws %s :\nMessage Received : %s Cannot be Parsed"), *WsClient->GetAddress(), *MessageString);
// return DataReceived.Broadcast(Response);
// // if we are here we have a tracked Message
// QueueManager->MarkRequestAsResponded()
}
void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FString& MessageSent)
@ -471,15 +617,176 @@ void UDTFluxNetworkSubsystem::OnWebSocketMessageSentEvent_Subsystem(const FStrin
UE_LOG(logDTFluxNetwork, Warning, TEXT("Ws %s :\nMessage Sent: %s"), *WsClient->GetAddress(), *MessageSent);
}
bool UDTFluxNetworkSubsystem::CleanRequestCallbacks(const FGuid& RequestId)
{
bool bCbSuppressSuccess = false;
bool bErrorCbSuppressSuccess = false;
bool bTimeoutCbSuppressSuccess = false;
if (PendingCallbacks.Contains(RequestId))
{
PendingCallbacks.Remove(RequestId);
bCbSuppressSuccess = true;
}
if (PendingTimeoutCallbacks.Contains(RequestId))
{
PendingTimeoutCallbacks.Remove(RequestId);
bTimeoutCbSuppressSuccess = true;
}
if (PendingTimeoutCallbacks.Contains(RequestId))
{
PendingTimeoutCallbacks.Remove(RequestId);
bErrorCbSuppressSuccess = true;
}
return bCbSuppressSuccess && bErrorCbSuppressSuccess && bTimeoutCbSuppressSuccess;
}
void UDTFluxNetworkSubsystem::OnRequestTimedOut_Internal(const FDTFluxQueuedRequest& TimedOutRequest)
{
UE_LOG(logDTFluxNetwork, Warning, TEXT("Request %s timed out: Type=%d, Contest=%d, Stage=%d, Split=%d"),
*TimedOutRequest.RequestId.ToString(),
(int32)TimedOutRequest.RequestType,
TimedOutRequest.ContestId,
TimedOutRequest.StageId,
TimedOutRequest.SplitId);
// Appeler le callback de timeout si présent
if (FOnDTFluxTrackedRequestTimeout* TimeoutCallback = PendingTimeoutCallbacks.Find(TimedOutRequest.RequestId))
{
if (TimeoutCallback->IsBound())
{
TimeoutCallback->Execute(TimedOutRequest.RequestId, TEXT("Request timeout"));
}
PendingTimeoutCallbacks.Remove(TimedOutRequest.RequestId);
}
// Nettoyer les callbacks de succès aussi
PendingCallbacks.Remove(TimedOutRequest.RequestId);
// Broadcaster l'événement Blueprint
OnTrackedRequestFailed.Broadcast(TimedOutRequest.RequestId, TimedOutRequest.RequestType, TEXT("Request timeout"));
}
bool UDTFluxNetworkSubsystem::TryMatchResponseToQueuedRequest(FDTFluxServerResponse& Response)
{
if (!QueueManager)
{
return false;
}
FGuid FoundRequestId;
if (QueueManager->IsRequestPending(FoundRequestId, Response.GetResponseType(), Response.ContestID, Response.StageID,
Response.SplitID))
{
UE_LOG(logDTFluxNetwork, Log,
TEXT("Matched response to queued request: Type=%s, Contest=%d, Stage=%d, Split=%d"),
*UEnum::GetValueAsString(Response.GetResponseType()), Response.ContestID, Response.StageID,
Response.SplitID);
if (PendingCallbacks.Contains(FoundRequestId))
{
FOnDTFluxTrackedRequestResponse* SuccessCallback = PendingCallbacks.Find(FoundRequestId);
SuccessCallback->ExecuteIfBound(FoundRequestId, Response);
//Suppress Callback;
return CleanRequestCallbacks(FoundRequestId);
}
return QueueManager->MarkRequestAsResponded(FoundRequestId);
}
return false;
}
void UDTFluxNetworkSubsystem::CompleteTrackedRequest(const FGuid& RequestId, const FString& ResponseData,
EDTFluxRequestType RequestType)
{
// Marquer la requête comme ayant reçu une réponse
if (QueueManager)
{
QueueManager->MarkRequestAsResponded(RequestId);
}
// Appeler le callback de succès si présent
if (FOnDTFluxTrackedRequestResponse* SuccessCallback = PendingCallbacks.Find(RequestId))
{
if (SuccessCallback->IsBound())
{
EDTFluxResponseStatus ResponseStatus;
FDTFluxServerResponse Response(ResponseData, ResponseStatus);
if (ResponseStatus == EDTFluxResponseStatus::Success)
{
SuccessCallback->Execute(RequestId, Response);
QueueManager->MarkRequestAsResponded(RequestId);
PendingCallbacks.Remove(RequestId);
PendingTimeoutCallbacks.Remove(RequestId);
}
else
{
QueueManager->MarkRequestAsError(RequestId);
// Fail
// FailTrackedRequest()
}
}
}
// Nettoyer le callback de timeout
PendingTimeoutCallbacks.Remove(RequestId);
// Broadcaster l'événement Blueprint
OnTrackedRequestCompleted.Broadcast(RequestId, RequestType, ResponseData);
UE_LOG(logDTFluxNetwork, Log, TEXT("Completed tracked request %s"), *RequestId.ToString());
}
void UDTFluxNetworkSubsystem::FailTrackedRequest(const FGuid& RequestId, const FString& ErrorMessage,
EDTFluxRequestType RequestType)
{
// Appeler le callback d'erreur si présent
if (FOnDTFluxTrackedRequestTimeout* ErrorCallback = PendingTimeoutCallbacks.Find(RequestId))
{
if (ErrorCallback->IsBound())
{
ErrorCallback->ExecuteIfBound(RequestId, ErrorMessage);
}
PendingTimeoutCallbacks.Remove(RequestId);
}
// Nettoyer les callbacks
PendingCallbacks.Remove(RequestId);
// Broadcaster l'événement Blueprint
OnTrackedRequestFailed.Broadcast(RequestId, RequestType, ErrorMessage);
UE_LOG(logDTFluxNetwork, Error, TEXT("Failed tracked request %s: %s"), *RequestId.ToString(), *ErrorMessage);
}
void UDTFluxNetworkSubsystem::SendQueuedRequest(const FDTFluxQueuedRequest& QueuedRequest)
{
// Générer le message JSON à partir de la requête
FString Message = QueuedRequest.Serialize();
if (Message.IsEmpty())
{
UE_LOG(logDTFluxNetwork, Error, TEXT("Failed to serialize queued request %s"),
*QueuedRequest.RequestId.ToString());
FailTrackedRequest(QueuedRequest.RequestId, TEXT("Serialization failed"), QueuedRequest.RequestType);
return;
}
// Dirty trick to fix Case (comme dans l'original)
Message = Message.Replace(TEXT("Id"), TEXT("ID"), ESearchCase::CaseSensitive);
UE_LOG(logDTFluxNetwork, Log, TEXT("Sending queued request %s: %s"), *QueuedRequest.RequestId.ToString(), *Message);
// Envoyer via WebSocket
SendMessage(Message);
}
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);
}

View File

@ -0,0 +1,66 @@
// 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"
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 MarkRequestAsError(const FGuid& TargetRequestGuid);
bool MarkRequestAsResponded(const FGuid& TargetRequestGuid);
bool MarkRequestAsResponded(const FDTFluxQueuedRequest& TargetRequest);
bool IsRequestPending(FGuid& OutRequestId, EDTFluxRequestType RequestType, int32 ContestId = -1, int32 StageId = -1,
int32 SplitId = -1);
FDTFluxQueuedRequest* GetRequestPending(EDTFluxRequestType RequestType, int32 ContestId = -1, int32 StageId = -1,
int32 SplitId = -1);
const FDTFluxQueuedRequest* GetRequest(const FGuid& SearchedGuid);
int32 GetPendingRequestCount();
int32 CleanupTimedOutRequests();
int32 CleanCashedRequests();
void ClearAllRequests();
// bool TryProcessResponse(const FDTFluxServerResponse& Response);
// 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<FDTFluxQueuedRequest, EQueueMode::Mpsc> PendingRequestsQueue;
TQueue<FDTFluxQueuedRequest, EQueueMode::Mpsc> CompletedRequestsQueue;
TQueue<FDTFluxQueuedRequest, EQueueMode::Mpsc> TimedOutRequestsQueue;
bool bIsInitialized;
float CheckInterval;
float TimeSinceLastCheck;
};

View File

@ -3,6 +3,7 @@
#pragma once
#include "CoreMinimal.h"
#include "Types/Enum/DTFluxCoreEnum.h"
#include "UObject/Object.h"
#include "DTFluxRequestStructs.generated.h"
@ -15,9 +16,13 @@ USTRUCT()
struct FDTFluxRequestBase
{
GENERATED_BODY()
public:
UPROPERTY()
FString Path = "";
FDateTime CreatedAt = FDateTime::Now();
FGuid RequestId = FGuid::NewGuid();
};
/**
@ -25,11 +30,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 +46,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,7 +61,7 @@ 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()
@ -62,6 +71,7 @@ public:
Path = "contest-ranking";
ContestID = -1;
}
FDTFluxContestRankingRequest(int InContestID)
{
Path = "contest-ranking";
@ -76,7 +86,7 @@ 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()
@ -88,6 +98,7 @@ public:
StageID = -1;
SplitID = -1;
}
FDTFluxStageRankingRequest(int InContestID, int InStageId)
{
Path = "stage-ranking";
@ -102,15 +113,13 @@ 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()
@ -122,6 +131,7 @@ public:
StageID = -1;
SplitID = -1;
}
FDTFluxSplitRankingRequest(int InContestID, int InStageId, int InSplitId)
{
Path = "stage-ranking";
@ -129,5 +139,105 @@ public:
StageID = InStageId;
SplitID = InSplitId;
}
};
/**
* @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);
}
};

View File

@ -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,99 @@ 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<FDTFluxSplitSensorInfo>& 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<FJsonObject>& OutJsonObject) const;
bool ValidateResponseType(const FString& ExpectedType) const;
EDTFluxResponseStatus InitializeFromJson(const FString& JsonMessage, bool bLogErrors);
static FString GetJsonType(const EJson Type)
{
switch (Type)
{
case EJson::None:
return TEXT("None");
case EJson::Null:
return TEXT("Null");
case EJson::String:
return TEXT("String");
case EJson::Number:
return TEXT("Number");
case EJson::Boolean:
return TEXT("Boolean");
case EJson::Array:
return TEXT("Array");
case EJson::Object:
return TEXT("Object");
default:
return TEXT("Unknown");
}
}
};

View File

@ -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,25 @@
#include "DTFluxNetworkSubsystem.generated.h"
class FDTFluxWebSocketClient;
class UDTFluxQueuedManager;
typedef TSharedPtr<FDTFluxWebSocketClient> FDTFluxWebSocketClientSP;
class FDTFluxHttpClient;
typedef TSharedPtr<FDTFluxHttpClient> FDTFluxHttpClientSP;
// Delegates pour les requêtes avec callback
DECLARE_DELEGATE_TwoParams(FOnDTFluxRequestResponseError, const FGuid&, const FString&);
DECLARE_DELEGATE_TwoParams(FOnDTFluxTrackedRequestResponse, const FGuid&, FDTFluxServerResponse&);
DECLARE_DELEGATE_TwoParams(FOnDTFluxTrackedRequestTimeout, const FGuid&, const FString& /*ErrorMessage*/);
// Delegates Blueprint pour les requêtes avec tracking
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDTFluxTrackedRequestCompleted, const FGuid&, RequestId,
EDTFluxApiDataType, RequestType, const FString&, ResponseData);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnDTFluxTrackedRequestFailed, const FGuid&, RequestId,
EDTFluxApiDataType, RequestType, const FString&, ErrorMessage);
/**
*
*/
@ -29,23 +43,26 @@ class DTFLUXNETWORK_API UDTFluxNetworkSubsystem : public UEngineSubsystem
GENERATED_BODY()
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,12 +70,15 @@ 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;
@ -66,19 +86,24 @@ public:
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 +111,7 @@ public:
DECLARE_DELEGATE_OneParam(FOnTeamStatusUpdateReceived, const FDTFluxTeamStatusUpdate& /*TeamToUpdate*/);
FOnTeamStatusUpdateReceived OnTeamStatusUpdateReceived;
FOnTeamStatusUpdateReceived& OnReceivedTeamStatusUpdate()
{
return OnTeamStatusUpdateReceived;
@ -99,10 +125,43 @@ 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,
FOnDTFluxTrackedRequestResponse OnCompleted,
FOnDTFluxTrackedRequestTimeout OnTimeout,
TOptional<FOnDTFluxRequestResponseError> OnError = TOptional<
FOnDTFluxRequestResponseError>(),
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);
// === 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);
@ -114,9 +173,23 @@ protected:
private:
// === CONFIGURATION ===
FDTFluxWsSettings WsSettings;
FDTFluxHttpSettings HttpSettings;
UPROPERTY()
UDTFluxQueuedManager* QueueManager;
// === MAPPING DES CALLBACKS C++ ===
TMap<FGuid, FOnDTFluxTrackedRequestResponse> PendingCallbacks;
TMap<FGuid, FOnDTFluxTrackedRequestTimeout> PendingTimeoutCallbacks;
TMap<FGuid, FOnDTFluxRequestResponseError> PendingErrorCallbacks;
// === CLIENTS RÉSEAU ===
FDTFluxWebSocketClientSP WsClient = nullptr;
FDTFluxHttpClientSP HttpClient = nullptr;
// === MÉTHODES DE CONFIGURATION ===
UFUNCTION()
void WsSettingsChanged(const FDTFluxWsSettings& NewWsSettings);
UFUNCTION()
@ -124,30 +197,13 @@ 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<FDTFluxWebSocketClientSP> WsClients;
// // For Future use of Multi-Connections
// TArray<FDTFluxHttpClientSP> 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;
@ -155,9 +211,33 @@ private:
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);
bool CleanRequestCallbacks(const FGuid& RequestId);
// === GESTION DES REQUÊTES TRACKÉES ===
UFUNCTION()
void OnRequestTimedOut_Internal(const FDTFluxQueuedRequest& TimedOutRequest);
bool TryMatchResponseToQueuedRequest(FDTFluxServerResponse& Response);
void CompleteTrackedRequest(const FGuid& RequestId, const FString& ResponseData, EDTFluxRequestType RequestType);
void FailTrackedRequest(const FGuid& RequestId, const FString& ErrorMessage, EDTFluxRequestType RequestType);
void SendQueuedRequest(const FDTFluxQueuedRequest& QueuedRequest);
// === UTILITAIRES ===
static FString ConstructWsAddress(const FString& Address, const FString& Path, const int& Port);
};

View File

@ -0,0 +1,34 @@
using UnrealBuildTool;
public class DTFluxRaceResult : ModuleRules
{
public DTFluxRaceResult(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"DTFluxProjectSettings",
"UMG",
"WebBrowser",
"Projects",
"ToolMenus",
"HTTP",
"JsonUtilities",
"Json"
}
);
}
}

View File

@ -0,0 +1,161 @@
#include "DTFluxRaceResultModule.h"
#include "LevelEditor.h"
#include "Widget/SDTFluxRaceResultWidget.h"
#include "Widget/Style/DTFluxRaceResultStyle.h"
#define LOCTEXT_NAMESPACE "FDTFluxRaceResultModule"
DEFINE_LOG_CATEGORY(logDTFluxRaceResult)
FName DTFLUXRACERESULT_API FDTFluxRaceResult::RaceResultTabId = "DTFluxRaceResult";
FText DTFLUXRACERESULT_API FDTFluxRaceResult::RaceResultTabDisplayName = FText::FromString(TEXT("DTFlux RaceResult"));
void DTFLUXRACERESULT_API FDTFluxRaceResult::StartupModule()
{
UE_LOG(logDTFluxRaceResult, Warning, TEXT("DTFluxRaceResult Module Started"))
FDTFluxRaceResultStyle::RegisterStyle();
InitMenuExtension();
RegisterRaceResultTab();
}
#pragma region MenuExtension
void DTFLUXRACERESULT_API FDTFluxRaceResult::InitMenuExtension()
{
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this,
&FDTFluxRaceResult::RegisterMenuExtensions));
}
void DTFLUXRACERESULT_API FDTFluxRaceResult::RegisterMenuExtensions()
{
// Étendre le menu enregistré
if (UToolMenu* DTFluxMenu = UToolMenus::Get()->ExtendMenu("DTFlux.MainMenu"))
{
FToolMenuSection& ToolsSection = DTFluxMenu->FindOrAddSection("Tools");
ToolsSection.AddMenuEntry(
"DTFluxRaceResult",
FText::FromString("RaceResult"),
FText::FromString("Launch DTFlux RaceResult Control Panel"),
FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
FUIAction(FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked))
);
}
}
void FDTFluxRaceResult::CreateSubmenu(UToolMenu* Menu)
{
// Section 2 : Tools
FToolMenuSection& ToolsSection = Menu->FindOrAddSection("Tools");
ToolsSection.Label = FText::FromString("Tools");
ToolsSection.AddMenuEntry(
"DTFluxRaceResult",
FText::FromString("RaceResult"),
FText::FromString("Launch Race Result WebAdmin Console"),
FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
// Adaptez selon votre icône
FUIAction(FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked))
);
}
void DTFLUXRACERESULT_API FDTFluxRaceResult::OnButtonClicked()
{
FGlobalTabmanager::Get()->TryInvokeTab(RaceResultTabId);
UE_LOG(LogTemp, Log, TEXT("Race Result Launched"))
}
#pragma endregion EditorTab
#pragma region
void DTFLUXRACERESULT_API FDTFluxRaceResult::RegisterRaceResultTab()
{
FTabSpawnerEntry& SpawnerEntry =
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
RaceResultTabId,
FOnSpawnTab::CreateRaw(this, &FDTFluxRaceResult::OnSpawnTab)
)
.SetDisplayName(RaceResultTabDisplayName)
.SetTooltipText(FText::FromString(TEXT("Race Result Control Panel")));
}
TSharedRef<SDockTab> DTFLUXRACERESULT_API FDTFluxRaceResult::OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs)
{
return
SNew(
SDockTab
)
.TabRole(ETabRole::NomadTab)
// .ShouldAutosize(true)
[
SNew(SDTFluxRaceResultWidget)
];
}
#pragma endregion
void DTFLUXRACERESULT_API FDTFluxRaceResult::ShutdownModule()
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(RaceResultTabId);
FDTFluxRaceResultStyle::UnregisterStyle();
}
//
// // Dans votre code de debug
// void DTFLUXRACERESULT_API FDTFluxRaceResult::DebugMenus()
// {
// UToolMenus* ToolMenus = UToolMenus::Get();
// if (ToolMenus)
// {
// TArray<FName> MenuNames;
// ToolMenus->GetAllMenuNames(MenuNames);
//
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("=== ALL AVAILABLE MENUS ==="));
// for (const FName& MenuName : MenuNames)
// {
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("Menu: %s"), *MenuName.ToString());
//
// // Obtenir les sections de chaque menu
// UToolMenu* Menu = ToolMenus->FindMenu(MenuName);
// if (Menu)
// {
// for (const FToolMenuSection& Section : Menu->Sections)
// {
// UE_LOG(logDTFluxRaceResult, Warning, TEXT(" Section: %s"), *Section.Name.ToString());
// }
// }
// }
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("=== END MENU LIST ==="));
// }
// }
// void DTFLUXRACERESULT_API FDTFluxRaceResult::AddMenu(FMenuBarBuilder& MenuBarBuilder)
// {
// MenuBarBuilder.AddPullDownMenu(
// FText::FromString("DTFlux"),
// FText::FromString("DTFlux API Tools"),
// FNewMenuDelegate::CreateRaw(this, &FDTFluxRaceResult::FillMenu)
// );
// }
// void DTFLUXRACERESULT_API FDTFluxRaceResult::FillMenu(FMenuBuilder& MenuBuilder)
// {
// MenuBuilder.AddMenuEntry(
// FText::FromString("RaceResult ControlPanel"),
// FText::FromString("Launch RaceResult Control Panel"),
// FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
// FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked)
// );
// MenuBuilder.EndSection();
// }
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FDTFluxRaceResult, DTFluxRaceResult)

View File

@ -0,0 +1,283 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Widget/SDTFluxRaceResultWidget.h"
#include "DTFluxRaceResultModule.h"
#include "HttpModule.h"
#include "IWebBrowserCookieManager.h"
#include "IWebBrowserWindow.h"
#include "SlateOptMacros.h"
#include "WebBrowserModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SDTFluxRaceResultWidget::Construct(const FArguments& InArgs)
{
FWebBrowserInitSettings browserInitSettings = FWebBrowserInitSettings();
IWebBrowserModule::Get().CustomInitialize(browserInitSettings);
WindowSettings.InitialURL = TEXT("about:blank");
WindowSettings.BrowserFrameRate = 25;
if (IWebBrowserModule::IsAvailable() && IWebBrowserModule::Get().IsWebModuleAvailable())
{
IWebBrowserSingleton* WebBrowserSingleton = IWebBrowserModule::Get().GetSingleton();
Browser = WebBrowserSingleton->CreateBrowserWindow(WindowSettings);
// Browser->OnLoadUrl().BindRaw(this, &SDTFluxRaceResultWidget::OnLoadOverride);
}
ChildSlot
[
SNew(SBox)
.Padding(5.0f)
[
SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SAssignNew(WebBrowser, SWebBrowser, Browser)
.ShowControls(true)
.SupportsTransparency(true)
.OnUrlChanged(this, &SDTFluxRaceResultWidget::OnUrlChanged)
.OnBeforeNavigation(this, &SDTFluxRaceResultWidget::OnBeforeNavigation)
.OnLoadCompleted(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadCompleted))
.OnLoadError(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadError))
.OnLoadStarted(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadStarted))
.ShowErrorMessage(true)
.ShowAddressBar(true)
]
]
]
];
}
void SDTFluxRaceResultWidget::OnCookieSet(bool bSuccess)
{
}
void SDTFluxRaceResultWidget::LoadContentViaHTTP()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading initial content via HTTP: %s"), *RaceResultUrl);
LoadSpecificURL(RaceResultUrl);
}
void SDTFluxRaceResultWidget::LoadSpecificURL(const FString& Url)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading via HTTP: %s"), *Url);
// Créer la requête HTTP
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
// Ajouter l'authentification Basic pour TOUTES les requêtes
FString Credentials = Username + TEXT(":") + Password;
FString EncodedCredentials = FBase64::Encode(Credentials);
Request->SetURL(Url);
Request->SetVerb("GET");
Request->SetHeader("Authorization", "Basic " + EncodedCredentials);
Request->SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
Request->SetHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
Request->SetHeader("Accept-Language", "en-US,en;q=0.5");
Request->SetHeader("Accept-Encoding", "gzip, deflate");
Request->SetHeader("Connection", "keep-alive");
Request->OnProcessRequestComplete().BindRaw(this, &SDTFluxRaceResultWidget::OnHTTPContentLoaded);
Request->ProcessRequest();
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTTP request sent with Basic Auth"));
}
void SDTFluxRaceResultWidget::OnHTTPContentLoaded(TSharedPtr<IHttpRequest> Request,
TSharedPtr<IHttpResponse> HttpResponse, bool bWasSuccessful)
{
if (bWasSuccessful && HttpResponse.IsValid())
{
int32 ResponseCode = HttpResponse->GetResponseCode();
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTTP Response Code: %d"), ResponseCode);
if (ResponseCode == 200)
{
FString Content = HttpResponse->GetContentAsString();
UE_LOG(logDTFluxRaceResult, Log, TEXT("Content loaded successfully, size: %d characters"), Content.Len());
// Traiter le contenu HTML
FString ProcessedContent = ProcessHTMLContent(Content, Request->GetURL());
// Charger le contenu dans le browser via LoadString
if (Browser.IsValid())
{
Browser->LoadString(ProcessedContent, Request->GetURL());
UE_LOG(logDTFluxRaceResult, Log, TEXT("Content loaded into browser"));
}
}
else if (ResponseCode == 401)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Authentication failed - 401 Unauthorized. Check your credentials."));
}
else if (ResponseCode == 403)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Access forbidden - 403 Forbidden"));
}
else if (ResponseCode == 404)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Page not found - 404 Not Found"));
}
else
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("HTTP request failed with code: %d"), ResponseCode);
}
}
else
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("HTTP request failed completely"));
}
}
void SDTFluxRaceResultWidget::OnLoadOverride()
{
}
void SDTFluxRaceResultWidget::OnUrlChanged(const FText& NewUrl)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("URL changed to: %s"), *NewUrl.ToString());
}
FString SDTFluxRaceResultWidget::ProcessHTMLContent(const FString& Content, const FString& BaseUrl)
{
FString ProcessedContent = Content;
// Extraire le domaine de base
FString BaseDomain = ExtractDomain(BaseUrl);
UE_LOG(logDTFluxRaceResult, Log, TEXT("Processing HTML content, base domain: %s"), *BaseDomain);
// Convertir tous les liens relatifs en liens absolus
ProcessedContent = ProcessedContent.Replace(TEXT("src=\"/"), *FString::Printf(TEXT("src=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href=\"/"), *FString::Printf(TEXT("href=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("src='/"), *FString::Printf(TEXT("src='%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href='/"), *FString::Printf(TEXT("href='%s/"), *BaseDomain));
// Gérer les liens relatifs avec ./
ProcessedContent = ProcessedContent.Replace(TEXT("src=\"./"), *FString::Printf(TEXT("src=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href=\"./"), *FString::Printf(TEXT("href=\"%s/"), *BaseDomain));
// Ajouter une balise base pour aider avec les liens relatifs restants
FString BaseTag = FString::Printf(TEXT("<base href=\"%s/\">"), *BaseDomain);
if (ProcessedContent.Contains(TEXT("<head>")))
{
FString HeadReplacement = TEXT("<head>") + BaseTag;
ProcessedContent = ProcessedContent.Replace(TEXT("<head>"), *HeadReplacement);
}
else if (ProcessedContent.Contains(TEXT("<HEAD>")))
{
FString HeadReplacement = TEXT("<HEAD>") + BaseTag;
ProcessedContent = ProcessedContent.Replace(TEXT("<HEAD>"), *HeadReplacement);
}
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTML content processed"));
return ProcessedContent;
}
FString SDTFluxRaceResultWidget::ExtractDomain(const FString& Url)
{
FString Domain = Url;
// Enlever le protocole
if (Url.StartsWith(TEXT("https://")))
{
Domain = Url.Mid(8);
}
else if (Url.StartsWith(TEXT("http://")))
{
Domain = Url.Mid(7);
}
// Trouver le premier slash (début du path)
int32 SlashIndex;
if (Domain.FindChar(TEXT('/'), SlashIndex))
{
Domain = Url.Left(Url.Len() - (Domain.Len() - SlashIndex));
}
// Enlever le port si présent
FString DomainPart = Domain;
if (Domain.StartsWith(TEXT("https://")))
{
DomainPart = Domain.Mid(8);
}
else if (Domain.StartsWith(TEXT("http://")))
{
DomainPart = Domain.Mid(7);
}
int32 ColonIndex;
if (DomainPart.FindChar(TEXT(':'), ColonIndex))
{
Domain = Domain.Left(Domain.Len() - (DomainPart.Len() - ColonIndex));
}
return Domain;
}
bool SDTFluxRaceResultWidget::OnBeforeNavigation(const FString& Url, const FWebNavigationRequest& Request)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading via HTTP: %s"), *Url);
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HTTPRequest = FHttpModule::Get().CreateRequest();
// Authentification pour toutes les requêtes
FString Credentials = Username + TEXT(":") + Password;
FString EncodedCredentials = FBase64::Encode(Credentials);
HTTPRequest->SetURL(RaceResultUrl);
HTTPRequest->SetVerb("GET");
HTTPRequest->SetHeader("Authorization", "Basic " + EncodedCredentials);
HTTPRequest->SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
HTTPRequest->SetHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
HTTPRequest->OnProcessRequestComplete().BindRaw(this, &SDTFluxRaceResultWidget::OnHTTPContentLoaded);
HTTPRequest->ProcessRequest();
return true;
}
void SDTFluxRaceResultWidget::OnLoadCompleted()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Completed"));
}
void SDTFluxRaceResultWidget::OnLoadStarted()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Started"));
}
void SDTFluxRaceResultWidget::OnLoadError()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Error"));
}
void SDTFluxRaceResultWidget::OnBeforeResourceLoad(FString Url, FString ResourceType, FContextRequestHeaders& AdditionalHeaders, const bool AllowUserCredentials)
{
}
void SDTFluxRaceResultWidget::SetupBasicAuth()
{
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

View File

@ -0,0 +1,47 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Widget/Style/DTFluxRaceResultStyle.h"
#include "Interfaces/IPluginManager.h"
#include "Styling/SlateStyleRegistry.h"
#include "Styling/SlateStyleMacros.h"
#define RootToContentDir Style->RootToContentDir
TSharedPtr<ISlateStyle> FDTFluxRaceResultStyle::StyleSet = nullptr;
void FDTFluxRaceResultStyle::RegisterStyle()
{
if(StyleSet.IsValid()) return;
StyleSet = Create();
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet);
}
void FDTFluxRaceResultStyle::UnregisterStyle()
{
if(StyleSet.IsValid())
{
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet);
ensure(StyleSet.IsUnique());
StyleSet.Reset();
}
}
void FDTFluxRaceResultStyle::ReloadTextures()
{
}
TSharedPtr<ISlateStyle> FDTFluxRaceResultStyle::Create()
{
TSharedPtr<FSlateStyleSet> Style = MakeShareable(new FSlateStyleSet("DTFluxRaceResultStyle"));
Style->SetContentRoot(IPluginManager::Get().FindPlugin("DTFluxAPI")->GetBaseDir()/TEXT("Resources"));
Style->Set("LevelEditor.Tab.IconRaceResult", new IMAGE_BRUSH_SVG("DTFluxRaceResult16x16", FVector2d(16)) );
return Style;
}

View File

@ -0,0 +1,33 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
DTFLUXRACERESULT_API DECLARE_LOG_CATEGORY_EXTERN(logDTFluxRaceResult, All, All);
class DTFLUXRACERESULT_API FDTFluxRaceResult : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
#pragma region MenuExtention
void RegisterMenuExtensions();
void InitMenuExtension();
void CreateSubmenu(UToolMenu* Menu);
// void AddMenu(FMenuBarBuilder& MenuBarBuilder);
// void FillMenu(FMenuBuilder& MenuBuilder);
void OnButtonClicked();
#pragma endregion
#pragma region EditorTab
void RegisterRaceResultTab();
TSharedRef<SDockTab> OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs);
private:
// static void DebugMenus();
static FName RaceResultTabId;
static FText RaceResultTabDisplayName;
TSharedPtr<class SDTFluxRaceResultWidget> RaceResultWidget;
#pragma endregion
};

View File

@ -0,0 +1,54 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SWebBrowser.h"
#include "Widgets/SCompoundWidget.h"
class IHttpResponse;
class IHttpRequest;
/**
*
*/
class DTFLUXRACERESULT_API SDTFluxRaceResultWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SDTFluxRaceResultWidget)
{
}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
void OnCookieSet(bool bSuccess);
void LoadSpecificURL(const FString& Url);
void LoadContentViaHTTP();
private:
TSharedPtr<SWebBrowser> WebBrowser;
TSharedPtr<IWebBrowserWindow> Browser;
TSharedPtr<IWebBrowserAdapter> BrowserAdapter;
FCreateBrowserWindowSettings WindowSettings;
void OnUrlChanged(const FText& NewUrl);
FString ProcessHTMLContent(const FString& Content, const FString& BaseUrl);
FString ExtractDomain(const FString& Url);
void OnHTTPContentLoaded(TSharedPtr<IHttpRequest> Request, TSharedPtr<IHttpResponse> HttpResponse, bool bWasSuccessful);
bool OnBeforeNavigation(const FString& Url, const FWebNavigationRequest& Request);
void OnLoadCompleted();
void OnLoadStarted();
void OnLoadError();
void OnLoadOverride();
void OnBeforeResourceLoad(FString Url, FString ResourceType, FContextRequestHeaders& AdditionalHeaders, const bool AllowUserCredentials);
FString RaceResultUrl = "https://raceresult.tds-france.com";
FString Username = "sporkrono";
FString Password = "Notre 3ème décennie d'action pour le climat";
void SetupBasicAuth();
};

View File

@ -0,0 +1,34 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Styling/ISlateStyle.h"
/**
*
*/
class DTFLUXRACERESULT_API FDTFluxRaceResultStyle
{
public:
static void RegisterStyle();
static void UnregisterStyle();
static void ReloadTextures();
static const ISlateStyle& Get()
{
return *StyleSet;
}
static const FName& GetStyleSetName()
{
return StyleSet->GetStyleSetName();
}
private:
static TSharedPtr<ISlateStyle> Create();
static TSharedPtr<ISlateStyle> StyleSet;
};

View File

@ -19,7 +19,9 @@ public class DTFluxUtilities : ModuleRules
"CoreUObject",
"Engine",
"Slate",
"SlateCore"
"SlateCore",
"DTFluxCore",
"DTFluxCoreSubsystem",
}
);
}

View File

@ -0,0 +1,19 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "FTDFluxUtils.h"
#include "DTFluxCoreSubsystem.h"
FText UFTDFluxUtils::GetFormatedName(const int& Bib, const int MaxChar, const FString OverFlowChar)
{
UDTFluxCoreSubsystem* CoreSubsystem = GEngine->GetEngineSubsystem<UDTFluxCoreSubsystem>();
const FDTFluxParticipant OutParticipant = CoreSubsystem->GetParticipant(Bib);
return OutParticipant.GetFormattedNameText(MaxChar, OverFlowChar);
}
FText UFTDFluxUtils::GetParticipantFormatedName(FDTFluxParticipant& Participant, const int MaxChar,
const FString OverFlowChar)
{
return Participant.GetFormattedNameText(MaxChar, OverFlowChar);
}

View File

@ -0,0 +1,25 @@
// 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(const int& Bib, const int MaxChar = 10,
const FString OverFlowChar = "...");
UFUNCTION(BlueprintCallable, Category="DTFlux|Utils", Meta=(Keywords="name, concat, participant"))
static FText GetParticipantFormatedName(FDTFluxParticipant& Participant, const int MaxChar = 10,
const FString OverFlowChar = "...");
};