diff --git a/Config/DefaultGame.ini b/Config/DefaultGame.ini index 11749f3..cd2c91a 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -35,6 +35,12 @@ ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet ; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps +; Loading dinâmico (etapas + dicas). DA_LoadingProfiles tem 1 perfil por +; contexto (EZMMOLoadingContext); DT_LoadingTips rotaciona durante o load. +; Cliente local marca etapas via eventos (sem opcode novo). +LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles +LoadingTipsAsset=/Game/ZMMO/Data/UI/Loading/DT_LoadingTips.DT_LoadingTips + ; ----------------------------------------------------------------------------- ; UI in-game (PR 19+). Espelho do front-end pattern: ; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD, diff --git a/Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset b/Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset new file mode 100644 index 0000000..49a3c2a Binary files /dev/null and b/Content/ZMMO/Data/UI/Loading/DA_LoadingProfiles.uasset differ diff --git a/Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset b/Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset new file mode 100644 index 0000000..7d7ea6f Binary files /dev/null and b/Content/ZMMO/Data/UI/Loading/DT_LoadingTips.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset index 5cdc89e..99508a4 100644 Binary files a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset and b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset differ diff --git a/Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset b/Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset new file mode 100644 index 0000000..e25bc98 Binary files /dev/null and b/Content/ZMMO/UI/Shared/Loading/WBP_Loading.uasset differ diff --git a/Source/ZMMO/Data/UI/LoadingTypes.h b/Source/ZMMO/Data/UI/LoadingTypes.h new file mode 100644 index 0000000..11e90e9 --- /dev/null +++ b/Source/ZMMO/Data/UI/LoadingTypes.h @@ -0,0 +1,66 @@ +#pragma once + +#include "CoreMinimal.h" +#include "LoadingTypes.generated.h" + +/** + * Contexto do loading screen genérico. Cada contexto resolve um perfil + * (lista de etapas) no UZMMOLoadingProfilesDataAsset. + * + * Cliente local rastreia o progresso assinando eventos próprios + * (HandlePostLoadMap, OnPlayerSpawned, etc) — server não envia opcode de + * progresso, ver decisão em [[project_ui_loading_dynamic]]. + */ +UENUM(BlueprintType) +enum class EZMMOLoadingContext : uint8 +{ + None, + FrontEndEnteringWorld, ///< FrontEnd → mundo (handoff CharServer→WorldServer + spawn). + InGameEnteringInstance, ///< Mundo → dungeon/instance (futuro). + InGameRespawn, ///< Pós-morte, retorno ao mundo (futuro). +}; + +UENUM(BlueprintType) +enum class EZMMOLoadingStepStatus : uint8 +{ + Pending, ///< Ainda não começou. + Running, ///< Em andamento (o "barber pole"). + Done, ///< Concluída. + Failed, ///< Falhou (com reason em Status). +}; + +/** + * Uma etapa do loading. StepId é o identificador estável usado por código + * pra chamar MarkStepDone(StepId); Label é o texto localizado mostrado. + */ +USTRUCT(BlueprintType) +struct FZMMOLoadingStepDef +{ + GENERATED_BODY() + + /** Id estável (ex.: "Travel", "MapLoaded", "Connect", "Spawn"). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FName StepId; + + /** Texto localizado ("Carregando mapa...", "Conectando..."). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText Label; +}; + +/** + * Perfil de loading por contexto. Designer edita no DA — código só + * consulta StepId pra avançar etapa. + */ +USTRUCT(BlueprintType) +struct FZMMOLoadingProfile +{ + GENERATED_BODY() + + /** Etapas em ordem. ProgressBar = #Done / #Steps. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + TArray Steps; + + /** Título da tela (vazio = usa default da classe). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText TitleOverride; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp new file mode 100644 index 0000000..87bf61a --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.cpp @@ -0,0 +1,10 @@ +#include "UILoadingProfilesDataAsset.h" + +FZMMOLoadingProfile UZMMOLoadingProfilesDataAsset::GetProfile(EZMMOLoadingContext Context) const +{ + if (const FZMMOLoadingProfile* Found = Profiles.Find(Context)) + { + return *Found; + } + return FZMMOLoadingProfile(); +} diff --git a/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h new file mode 100644 index 0000000..f3d83a2 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingProfilesDataAsset.h @@ -0,0 +1,29 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "UI/LoadingTypes.h" +#include "UILoadingProfilesDataAsset.generated.h" + +/** + * Mapa data-driven contexto → perfil (etapas + título). Designer edita o + * asset; código consulta por EZMMOLoadingContext na hora de configurar a + * tela. Asset canônico: DA_LoadingProfiles em /Game/ZMMO/Data/UI/Loading/. + * + * Configurado em DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles + */ +UCLASS(BlueprintType) +class ZMMO_API UZMMOLoadingProfilesDataAsset : public UDataAsset +{ + GENERATED_BODY() + +public: + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Loading") + TMap Profiles; + + /** Retorna o perfil do contexto ou um perfil vazio (sem etapas). */ + UFUNCTION(BlueprintCallable, Category = "Loading") + FZMMOLoadingProfile GetProfile(EZMMOLoadingContext Context) const; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp new file mode 100644 index 0000000..cf3eb46 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.cpp @@ -0,0 +1,235 @@ +#include "UILoadingScreen_Base.h" + +#include "Components/ProgressBar.h" +#include "CommonTextBlock.h" +#include "Engine/DataTable.h" +#include "Engine/World.h" +#include "TimerManager.h" +#include "UILoadingProfilesDataAsset.h" +#include "UILoadingTipRow.h" + +void UUILoadingScreen_Base::Configure(EZMMOLoadingContext InContext, + UZMMOLoadingProfilesDataAsset* Profiles, + UDataTable* TipsTable) +{ + Context_ = InContext; + bCompleteFired_ = false; + + // Perfil (etapas + título). + Profile_ = (Profiles != nullptr) + ? Profiles->GetProfile(InContext) + : FZMMOLoadingProfile(); + + StepStatus_.Reset(); + for (const FZMMOLoadingStepDef& Step : Profile_.Steps) + { + StepStatus_.Add(Step.StepId, EZMMOLoadingStepStatus::Pending); + } + + // Pool de dicas filtrada por contexto (None = qualquer). + TipPool_.Reset(); + if (TipsTable != nullptr) + { + TipsTable->ForeachRow(TEXT("UUILoadingScreen_Base::Configure"), + [this](const FName& /*Key*/, const FZMMOLoadingTipRow& Row) + { + if (Row.Context == EZMMOLoadingContext::None || Row.Context == Context_) + { + if (!Row.Tip.IsEmpty()) + { + TipPool_.Add(Row.Tip); + } + } + }); + } + NextTipIndex_ = 0; + + RefreshUI(); + + if (IsActivated()) + { + StartTipTimer(); + } +} + +void UUILoadingScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + RefreshUI(); + StartTipTimer(); +} + +void UUILoadingScreen_Base::NativeOnDeactivated() +{ + StopTipTimer(); + Super::NativeOnDeactivated(); +} + +void UUILoadingScreen_Base::MarkStepRunning(FName StepId) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + // Já Done não regride. + if (*Status != EZMMOLoadingStepStatus::Done && *Status != EZMMOLoadingStepStatus::Failed) + { + *Status = EZMMOLoadingStepStatus::Running; + RefreshUI(); + } + } +} + +void UUILoadingScreen_Base::MarkStepDone(FName StepId) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + *Status = EZMMOLoadingStepStatus::Done; + RefreshUI(); + + if (!bCompleteFired_ && AreAllStepsDone()) + { + bCompleteFired_ = true; + OnLoadingComplete.Broadcast(); + } + } +} + +void UUILoadingScreen_Base::MarkStepFailed(FName StepId, const FText& Reason) +{ + if (EZMMOLoadingStepStatus* Status = StepStatus_.Find(StepId)) + { + *Status = EZMMOLoadingStepStatus::Failed; + if (Text_Status && !Reason.IsEmpty()) + { + Text_Status->SetText(Reason); + } + RefreshUI(); + } +} + +bool UUILoadingScreen_Base::AreAllStepsDone() const +{ + if (Profile_.Steps.Num() == 0) + { + return false; + } + for (const FZMMOLoadingStepDef& Step : Profile_.Steps) + { + const EZMMOLoadingStepStatus* Status = StepStatus_.Find(Step.StepId); + if (Status == nullptr || *Status != EZMMOLoadingStepStatus::Done) + { + return false; + } + } + return true; +} + +int32 UUILoadingScreen_Base::FindStepIndex(FName StepId) const +{ + for (int32 i = 0; i < Profile_.Steps.Num(); ++i) + { + if (Profile_.Steps[i].StepId == StepId) + { + return i; + } + } + return INDEX_NONE; +} + +EZMMOLoadingStepStatus UUILoadingScreen_Base::GetStepStatus(int32 Index) const +{ + if (!Profile_.Steps.IsValidIndex(Index)) return EZMMOLoadingStepStatus::Pending; + const EZMMOLoadingStepStatus* Status = StepStatus_.Find(Profile_.Steps[Index].StepId); + return Status ? *Status : EZMMOLoadingStepStatus::Pending; +} + +FText UUILoadingScreen_Base::ComputeCurrentStatusText() const +{ + // Prioridade: Running > último Done > primeiro Pending > Status default. + int32 LastDone = INDEX_NONE; + for (int32 i = 0; i < Profile_.Steps.Num(); ++i) + { + const EZMMOLoadingStepStatus St = GetStepStatus(i); + if (St == EZMMOLoadingStepStatus::Running) + { + return Profile_.Steps[i].Label; + } + if (St == EZMMOLoadingStepStatus::Done) + { + LastDone = i; + } + } + if (LastDone != INDEX_NONE) + { + return Profile_.Steps[LastDone].Label; + } + if (Profile_.Steps.Num() > 0) + { + return Profile_.Steps[0].Label; + } + return StatusDefault; +} + +float UUILoadingScreen_Base::ComputeProgress01() const +{ + const int32 N = Profile_.Steps.Num(); + if (N == 0) return 0.f; + float Acc = 0.f; + for (int32 i = 0; i < N; ++i) + { + const EZMMOLoadingStepStatus St = GetStepStatus(i); + if (St == EZMMOLoadingStepStatus::Done) Acc += 1.f; + else if (St == EZMMOLoadingStepStatus::Running) Acc += 0.5f; + } + return FMath::Clamp(Acc / static_cast(N), 0.f, 1.f); +} + +void UUILoadingScreen_Base::RefreshUI() +{ + if (Text_Title) + { + const FText& Title = Profile_.TitleOverride.IsEmpty() ? TitleDefault : Profile_.TitleOverride; + Text_Title->SetText(Title); + } + if (Text_Status) + { + Text_Status->SetText(ComputeCurrentStatusText()); + } + if (ProgressBar_Progress) + { + ProgressBar_Progress->SetPercent(ComputeProgress01()); + } + if (Text_Tip && TipPool_.Num() > 0 && Text_Tip->GetText().IsEmpty()) + { + // Primeira dica na inicialização (timer cuida das próximas). + Text_Tip->SetText(TipPool_[0]); + NextTipIndex_ = TipPool_.Num() > 1 ? 1 : 0; + } +} + +void UUILoadingScreen_Base::RotateTip() +{ + if (!Text_Tip || TipPool_.Num() == 0) return; + Text_Tip->SetText(TipPool_[NextTipIndex_]); + NextTipIndex_ = (NextTipIndex_ + 1) % TipPool_.Num(); +} + +void UUILoadingScreen_Base::StartTipTimer() +{ + StopTipTimer(); + if (TipRotationIntervalSeconds <= 0.f || TipPool_.Num() <= 1) return; + if (UWorld* W = GetWorld()) + { + W->GetTimerManager().SetTimer(TipTimerHandle_, this, + &UUILoadingScreen_Base::RotateTip, + TipRotationIntervalSeconds, /*bLoop=*/true); + } +} + +void UUILoadingScreen_Base::StopTipTimer() +{ + if (UWorld* W = GetWorld()) + { + W->GetTimerManager().ClearTimer(TipTimerHandle_); + } + TipTimerHandle_.Invalidate(); +} diff --git a/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h new file mode 100644 index 0000000..401afd4 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingScreen_Base.h @@ -0,0 +1,124 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UIActivatableScreen_Base.h" +#include "UI/LoadingTypes.h" +#include "UILoadingScreen_Base.generated.h" + +class UCommonTextBlock; +class UProgressBar; +class UDataTable; +class UZMMOLoadingProfilesDataAsset; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnLoadingComplete); + +/** + * Tela de loading genérica. Reusada pelo FrontEnd (handoff CharServer → + * WorldServer) e pelo InGame (entrada em instance/dungeon, respawn). Cada + * uso passa um EZMMOLoadingContext em Configure(), que resolve o perfil + * no UZMMOLoadingProfilesDataAsset (lista de etapas + título). + * + * Quem orquestra (UIFrontEndFlowSubsystem, futuro UIInGameFlowSubsystem) + * assina os próprios eventos (HandlePostLoadMap, OnPlayerSpawned, etc) e + * chama MarkStepDone(StepId) — server NÃO envia opcode de progresso (ver + * [[project_ui_loading_dynamic]]). + * + * Dismiss: a tela emite OnLoadingComplete quando todas as etapas estão + * Done. Caller decide quando popar (geralmente após pequeno fade-out). + * + * WBP concreto herda DIRETO desta classe. Subclasses C++ específicas só + * surgem quando houver necessidade real de estilo divergente. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUILoadingScreen_Base : public UUIActivatableScreen_Base +{ + GENERATED_BODY() + +public: + /** + * Configura a tela: carrega o perfil do contexto, popula a pool de + * dicas filtrada e reseta estado das etapas. Pode ser chamado depois + * de NativeOnActivated (estado é reaplicado em RefreshUI). + */ + UFUNCTION(BlueprintCallable, Category = "Loading") + void Configure(EZMMOLoadingContext InContext, + UZMMOLoadingProfilesDataAsset* Profiles, + UDataTable* TipsTable); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepRunning(FName StepId); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepDone(FName StepId); + + UFUNCTION(BlueprintCallable, Category = "Loading") + void MarkStepFailed(FName StepId, const FText& Reason); + + UFUNCTION(BlueprintPure, Category = "Loading") + bool AreAllStepsDone() const; + + /** Disparado UMA vez quando todas as etapas viram Done. */ + UPROPERTY(BlueprintAssignable, Category = "Loading") + FZMMOOnLoadingComplete OnLoadingComplete; + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + + /** Reaplica todo o estado aos widgets bindados (texto, progress, tip). */ + void RefreshUI(); + void RotateTip(); + void StartTipTimer(); + void StopTipTimer(); + + /** Acha índice da etapa por StepId; -1 se não existir. */ + int32 FindStepIndex(FName StepId) const; + + /** Status atual da etapa em [Steps_]; Pending se não rastreada. */ + EZMMOLoadingStepStatus GetStepStatus(int32 Index) const; + + /** Texto exibido em Text_Status — etapa Running atual ou última Done. */ + FText ComputeCurrentStatusText() const; + + /** Progresso 0..1: #Done / max(1,#Steps). Running conta como meio passo. */ + float ComputeProgress01() const; + + // ---- Bind widgets (BindWidgetOptional — WBP pode omitir qualquer) ---- + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Title; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Status; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ProgressBar_Progress; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Tip; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText TitleDefault = FText::FromString(TEXT("Carregando")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText StatusDefault = FText::FromString(TEXT("Entrando no mundo...")); + + /** Intervalo entre dicas. 0 = sem rotação (mostra a primeira dica fixa). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + float TipRotationIntervalSeconds = 6.f; + +private: + EZMMOLoadingContext Context_ = EZMMOLoadingContext::None; + + UPROPERTY(Transient) + FZMMOLoadingProfile Profile_; + + UPROPERTY(Transient) + TMap StepStatus_; + + UPROPERTY(Transient) + TArray TipPool_; + + int32 NextTipIndex_ = 0; + bool bCompleteFired_ = false; + FTimerHandle TipTimerHandle_; +}; diff --git a/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h b/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h new file mode 100644 index 0000000..2889894 --- /dev/null +++ b/Source/ZMMO/Game/UI/Common/UILoadingTipRow.h @@ -0,0 +1,30 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "GameplayTagContainer.h" +#include "UI/LoadingTypes.h" +#include "UILoadingTipRow.generated.h" + +/** + * Row do DT_LoadingTips. Cada row = uma dica mostrada (rotaciona durante o + * loading). FText é localizável (basta extrair pelo gather de tradução do + * UE). Filtra por contexto: Context=None significa "qualquer loading". + */ +USTRUCT(BlueprintType) +struct FZMMOLoadingTipRow : public FTableRowBase +{ + GENERATED_BODY() + + /** Texto da dica (localizável). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FText Tip; + + /** Restringe a dica a um contexto. None = todos. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + EZMMOLoadingContext Context = EZMMOLoadingContext::None; + + /** Tag opcional pra categorizar (PvE/PvP/Crafting...) — futuro filtro. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading") + FGameplayTag Category; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index cd2393e..31ba12b 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -5,6 +5,8 @@ #include "UIFrontEndScreenSet.h" #include "UIManagerSubsystem.h" #include "UIPrimaryGameLayout_Base.h" +#include "UI/Common/UILoadingProfilesDataAsset.h" +#include "UI/Common/UILoadingScreen_Base.h" #include "UI/UILayerTags.h" #include "ZMMOGameInstance.h" #include "ZMMOThemeSubsystem.h" @@ -212,13 +214,27 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State) const FSoftObjectPath Path = Soft.ToSoftObjectPath(); UAssetManager::GetStreamableManager().RequestAsyncLoad( Path, - FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer]() + FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]() { if (UUIManagerSubsystem* M = GetUIManager()) { if (UClass* Cls = Soft.Get()) { - M->PushScreenToLayer(Layer, Cls); + UCommonActivatableWidget* Pushed = M->PushScreenToLayer(Layer, Cls); + + // Loading: configura logo após o push e marca etapa "Travel" + // (chegamos aqui via HandleServerTravelRequested → SetState). + if (State == EZMMOFrontEndState::EnteringWorld) + { + if (UUILoadingScreen_Base* Loading = Cast(Pushed)) + { + ActiveLoadingScreen = Loading; + Loading->Configure(EZMMOLoadingContext::FrontEndEnteringWorld, + GetLoadingProfiles(), GetLoadingTips()); + Loading->OnLoadingComplete.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); + Loading->MarkStepDone(TEXT("Travel")); + } + } } } })); @@ -283,6 +299,7 @@ void UUIFrontEndFlowSubsystem::BindNetwork() if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); + Zeus->OnPlayerSpawned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); } bNetBound = true; } @@ -302,6 +319,7 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork() if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); + Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); } bNetBound = false; } @@ -390,11 +408,65 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/) { - if (bTravelingToWorld) + if (!bTravelingToWorld) { - bTravelingToWorld = false; - SetState(EZMMOFrontEndState::InWorld); + return; } + + // Loading dinâmico: marca a etapa "MapLoaded" e espera o spawn antes de + // liberar o InWorld (a tela limpa via OnLoadingComplete). Fallback: se + // não há screen ativa (DA não configurado), comportamento legado — + // transita já pra InWorld pra não travar. + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->MarkStepDone(TEXT("MapLoaded")); + return; + } + + bTravelingToWorld = false; + SetState(EZMMOFrontEndState::InWorld); +} + +void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int32 /*EntityId*/, bool bIsLocal, + FVector /*PosCm*/, float /*YawDeg*/, + int64 /*ServerTimeMs*/) +{ + if (!bIsLocal) return; + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->MarkStepDone(TEXT("Spawn")); + } +} + +void UUIFrontEndFlowSubsystem::HandleLoadingComplete() +{ + if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) + { + Loading->OnLoadingComplete.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete); + } + ActiveLoadingScreen.Reset(); + bTravelingToWorld = false; + SetState(EZMMOFrontEndState::InWorld); +} + +UZMMOLoadingProfilesDataAsset* UUIFrontEndFlowSubsystem::GetLoadingProfiles() +{ + if (LoadingProfiles) return LoadingProfiles; + if (!LoadingProfilesAsset.IsNull()) + { + LoadingProfiles = LoadingProfilesAsset.LoadSynchronous(); + } + return LoadingProfiles; +} + +UDataTable* UUIFrontEndFlowSubsystem::GetLoadingTips() +{ + if (LoadingTips) return LoadingTips; + if (!LoadingTipsAsset.IsNull()) + { + LoadingTips = LoadingTipsAsset.LoadSynchronous(); + } + return LoadingTips; } void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId) diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 579939b..d1168de 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -8,6 +8,8 @@ class UUIFrontEndScreenSet; class UUIManagerSubsystem; +class UUILoadingScreen_Base; +class UZMMOLoadingProfilesDataAsset; class UZeusNetworkSubsystem; class UZeusCharServerSubsystem; struct FZMMOMapDef; @@ -147,6 +149,24 @@ protected: UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps") TSoftObjectPtr MapsTableAsset; + /** + * DataAsset com os perfis de loading (etapas por contexto). Configure em + * DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingProfilesAsset=/Game/ZMMO/Data/UI/Loading/DA_LoadingProfiles.DA_LoadingProfiles + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Loading") + TSoftObjectPtr LoadingProfilesAsset; + + /** + * DataTable com dicas (rows = FZMMOLoadingTipRow). Opcional — vazio + * significa "sem dicas". Configure em DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * LoadingTipsAsset=/Game/ZMMO/Data/UI/Loading/DT_LoadingTips.DT_LoadingTips + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Loading") + TSoftObjectPtr LoadingTipsAsset; + private: void BindNetwork(); void UnbindNetwork(); @@ -170,8 +190,26 @@ private: UFUNCTION() void HandleServerTravelRequested(const FString& MapName, const FString& MapPath); + /** + * Spawn do player local — fim da última etapa do loading EnteringWorld. + * Assinatura espelha FZeusOnPlayerSpawned (FiveParams). + */ + UFUNCTION() + void HandlePlayerSpawned(int32 EntityId, bool bIsLocal, FVector PosCm, + float YawDeg, int64 ServerTimeMs); + + /** Disparado pela tela de loading quando todas as etapas viram Done. */ + UFUNCTION() + void HandleLoadingComplete(); + void HandlePostLoadMap(UWorld* LoadedWorld); + /** Resolve+carrega o DA de perfis (sync). */ + UZMMOLoadingProfilesDataAsset* GetLoadingProfiles(); + + /** Resolve+carrega o DT de dicas (sync). */ + UDataTable* GetLoadingTips(); + /** * Dívida técnica (D5): ServerHello traz ThemeId, mas o UZeusNetworkSubsystem * ainda não expõe um delegate/getter dedicado. Quando expuser, ligar aqui @@ -199,6 +237,18 @@ private: UPROPERTY(Transient) TObjectPtr ScreenSet; + UPROPERTY(Transient) + TObjectPtr LoadingProfiles; + + UPROPERTY(Transient) + TObjectPtr LoadingTips; + + /** + * Tela de loading atualmente em Modal. Weak porque o stack do CommonUI + * é dono — se popar por outro caminho não queremos dangling. + */ + TWeakObjectPtr ActiveLoadingScreen; + bool bNetBound = false; bool bTravelingToWorld = false; FDelegateHandle PostLoadMapHandle;