2 Commits

Author SHA1 Message Date
079d05c117 feat(ui/loading): tela de loading dinamica data-driven (etapas + dicas)
- UUILoadingScreen_Base (Game/UI/Common/) generica: Configure(Context),
  MarkStepRunning/Done/Failed, OnLoadingComplete delegate, progress bar
  reativa e rotacao automatica de dicas via timer.
- Etapas configuradas por contexto em UZMMOLoadingProfilesDataAsset
  (DA_LoadingProfiles). Perfil inicial FrontEndEnteringWorld:
  Travel -> MapLoaded -> Spawn.
- Dicas em DT_LoadingTips (FText localizavel + filtro por contexto +
  GameplayTag de categoria). 4 dicas seed.
- UIFrontEndFlowSubsystem agora bloqueia a transicao InWorld ate o
  HandlePlayerSpawned (bIsLocal=true) — usuario nao ve mundo vazio.
- WBP_Loading em /UI/Shared/Loading/ (neutro, reusavel pelo InGame futuro);
  DA_FrontEndScreenSet[EnteringWorld] aponta pra ele.
- DefaultGame.ini wire LoadingProfilesAsset + LoadingTipsAsset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:24:48 -03:00
90484876f0 feat(player): CharName do DB no PlayerState + header do Status Window
- PlayerState ganha CharName (Replicated) + SetCharInfo(CharName, GuildName)
- ZMMOPlayerCharacter assina FZeusOnCharInfoReceived; TryApplyCachedCharInfo
  resolve race se S_CHAR_INFO chega antes do pawn possuir
- UIPlayerStatus_Window header le PlayerState->CharName (fallback p/ GetPlayerName)
  em vez do "Mateuus-61E9B19A4FB2" generico

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:24:29 -03:00
19 changed files with 753 additions and 19 deletions

View File

@@ -35,6 +35,12 @@ ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet
; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. ; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows.
MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps 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: ; UI in-game (PR 19+). Espelho do front-end pattern:
; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD, ; - ScreenSetAsset mapeia EZMMOInGameUIState -> WBP (Playing -> WBP_HUD,

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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<FZMMOLoadingStepDef> Steps;
/** Título da tela (vazio = usa default da classe). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Loading")
FText TitleOverride;
};

View File

@@ -18,6 +18,7 @@
#include "UObject/ConstructorHelpers.h" #include "UObject/ConstructorHelpers.h"
#include "ZMMO.h" #include "ZMMO.h"
#include "ZMMOAttributeComponent.h" #include "ZMMOAttributeComponent.h"
#include "ZMMOPlayerState.h"
#include "ZMMOWorldSubsystem.h" #include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h" #include "ZeusNetworkSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" #include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
@@ -278,6 +279,7 @@ void AZMMOPlayerCharacter::BindZeusSpawnDelegate()
} }
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned); ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusCharInfo);
bSpawnDelegateBound = true; bSpawnDelegateBound = true;
} }
@@ -289,6 +291,7 @@ void AZMMOPlayerCharacter::UnbindZeusSpawnDelegate()
return; return;
} }
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned); ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusCharInfo);
bSpawnDelegateBound = false; bSpawnDelegateBound = false;
} }
@@ -307,6 +310,10 @@ void AZMMOPlayerCharacter::TryRegisterLocalEntityFromCachedSpawn()
{ {
HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs); HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs);
} }
// Race fix: o S_CHAR_INFO chega ANTES de S_SPAWN_PLAYER mas o pawn pode
// nao existir ainda (OpenLevel em andamento). Aplica o ultimo cacheado.
TryApplyCachedCharInfo();
} }
void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int32 InEntityId, const bool bIsLocal, void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int32 InEntityId, const bool bIsLocal,
@@ -349,25 +356,74 @@ void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const F
} }
} }
// Seed do EntityId no AttributeComponent do PlayerState. O componente // Identidade publica (EntityId) + seed no AttributeComponent. CharName/Guild
// vive no AZMMOPlayerState via Component Registry — busca via PlayerState // vem separado via S_CHAR_INFO -> HandleZeusCharInfo. AttributeComponent vive
// (sobrevive ao despawn do Pawn). S_ATTRIBUTE_SNAPSHOT_FULL chega via // como subobject e recebe seed pra que o ZMMOAttributeNetworkHandler consiga
// UDP e e' rouado pelo ZMMOAttributeNetworkHandler via lookup por // rotear o snapshot por EntityId desde o primeiro pacote.
// EntityId no PlayerArray do GameState.
if (APlayerState* PS = GetPlayerState()) if (APlayerState* PS = GetPlayerState())
{ {
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetPublicIdentity(EntityIdAsInt64, /*CharId*/ FString(),
/*BaseLevel*/ 1, /*ClassId*/ 0);
}
if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass<UZMMOAttributeComponent>()) if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass<UZMMOAttributeComponent>())
{ {
AttrComp->SeedEntityId(InEntityId); AttrComp->SeedEntityId(InEntityId);
} }
} }
// Aplica char info cacheada (caso S_CHAR_INFO tenha chegado antes do PS existir).
TryApplyCachedCharInfo();
// UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do // UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do
// Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que // Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que
// pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda // pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda
// no UZMMOAttributeComponent via PlayerState. // no UZMMOAttributeComponent via PlayerState.
} }
void AZMMOPlayerCharacter::HandleZeusCharInfo(const int32 InEntityId, const FString& CharName, const FString& GuildName)
{
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: S_CHAR_INFO recebido EntityId=%d name=%s guild=%s"),
InEntityId, *CharName, *GuildName);
APlayerState* PS = GetPlayerState();
if (!PS)
{
// Race fix: pawn pode nao ter PlayerState ainda. Subsystem ja cacheou
// (LastLocalCharInfoCache); TryApplyCachedCharInfo aplica em
// HandleLocalSpawnReady ou em outra oportunidade.
return;
}
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetCharInfo(CharName, GuildName);
}
}
void AZMMOPlayerCharacter::TryApplyCachedCharInfo()
{
if (!ZeusNetwork)
{
return;
}
int32 CachedEntityId = 0;
FString CachedCharName;
FString CachedGuildName;
if (!ZeusNetwork->TryGetLastLocalCharInfo(CachedEntityId, CachedCharName, CachedGuildName))
{
return;
}
if (APlayerState* PS = GetPlayerState())
{
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetCharInfo(CachedCharName, CachedGuildName);
}
}
}
void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds) void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
{ {
ResolveZeusNetworkSubsystem(); ResolveZeusNetworkSubsystem();

View File

@@ -133,12 +133,19 @@ private:
/** /**
* Memoriza `EntityId` autoritativo e regista o pawn no `UZMMOWorldSubsystem` * Memoriza `EntityId` autoritativo e regista o pawn no `UZMMOWorldSubsystem`
* para o registry partilhado por proxies remotos (filtra snapshots locais). * para o registry partilhado por proxies remotos (filtra snapshots locais).
* Metadados publicos do char (Name/Guild) chegam via S_CHAR_INFO em
* HandleZeusCharInfo (servidor envia ANTES de S_SPAWN_PLAYER).
*/ */
void HandleLocalSpawnReady(int32 InEntityId, FVector PosCm, float YawDeg, int64 ServerTimeMs); void HandleLocalSpawnReady(int32 InEntityId, FVector PosCm, float YawDeg, int64 ServerTimeMs);
UFUNCTION() UFUNCTION()
void HandleZeusPlayerSpawned(int32 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs); void HandleZeusPlayerSpawned(int32 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
UFUNCTION()
void HandleZeusCharInfo(int32 InEntityId, const FString& CharName, const FString& GuildName);
void TryApplyCachedCharInfo();
UPROPERTY() UPROPERTY()
TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork; TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork;

View File

@@ -2,6 +2,7 @@
#include "Components/ActorComponent.h" #include "Components/ActorComponent.h"
#include "Misc/ConfigCacheIni.h" #include "Misc/ConfigCacheIni.h"
#include "Net/UnrealNetwork.h"
#include "ZMMO.h" #include "ZMMO.h"
AZMMOPlayerState::AZMMOPlayerState() AZMMOPlayerState::AZMMOPlayerState()
@@ -72,6 +73,17 @@ AZMMOPlayerState::AZMMOPlayerState()
} }
} }
void AZMMOPlayerState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AZMMOPlayerState, ZMMOEntityId);
DOREPLIFETIME(AZMMOPlayerState, CharId);
DOREPLIFETIME(AZMMOPlayerState, CharName);
DOREPLIFETIME(AZMMOPlayerState, BaseLevel);
DOREPLIFETIME(AZMMOPlayerState, ClassId);
DOREPLIFETIME(AZMMOPlayerState, GuildName);
}
void AZMMOPlayerState::SetPublicIdentity(int64 InEntityId, const FString& InCharId, void AZMMOPlayerState::SetPublicIdentity(int64 InEntityId, const FString& InCharId,
int32 InBaseLevel, int32 InClassId) int32 InBaseLevel, int32 InClassId)
{ {
@@ -80,3 +92,9 @@ void AZMMOPlayerState::SetPublicIdentity(int64 InEntityId, const FString& InChar
BaseLevel = InBaseLevel; BaseLevel = InBaseLevel;
ClassId = InClassId; ClassId = InClassId;
} }
void AZMMOPlayerState::SetCharInfo(const FString& InCharName, const FString& InGuildName)
{
CharName = InCharName;
GuildName = InGuildName;
}

View File

@@ -5,6 +5,7 @@
#include "Templates/SubclassOf.h" #include "Templates/SubclassOf.h"
#include "ZMMOPlayerState.generated.h" #include "ZMMOPlayerState.generated.h"
class FLifetimeProperty;
class UActorComponent; class UActorComponent;
/** /**
@@ -52,36 +53,55 @@ public:
AZMMOPlayerState(); AZMMOPlayerState();
// === Identidade publica === // === Identidade publica ===
//
// Todos os campos publicos sao Replicated — hoje o cliente UE5 roda em
// Standalone (servidor C++ standalone, sem replicacao UE5 ativa), entao
// a marca nao tem efeito em runtime; deixa a classe pronta caso o projeto
// passe a usar dedicated UE5 no futuro.
/** EntityId autoritativo do server. 0 ate o spawn local confirmar. */ /** EntityId autoritativo do server. 0 ate o spawn local confirmar. */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity")
int64 ZMMOEntityId = 0; int64 ZMMOEntityId = 0;
/** CharId (BIGINT no DB; FString pra preservar 64 bits em Blueprint). */ /** CharId (BIGINT no DB; FString pra preservar 64 bits em Blueprint). */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity") UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity")
FString CharId; FString CharId;
/** Nome do char vindo do DB (`characters.name`). Setado por
* AZMMOPlayerCharacter::HandleLocalSpawnReady com o payload do
* S_SPAWN_PLAYER. UI prefere isto a APlayerState::GetPlayerName(). */
UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Identity")
FString CharName;
// === Progressao publica === // === Progressao publica ===
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress")
int32 BaseLevel = 1; int32 BaseLevel = 1;
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress") UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Progress")
int32 ClassId = 0; int32 ClassId = 0;
// === Guild (futuro) === // === Guild (futuro) ===
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Guild") UPROPERTY(Replicated, VisibleInstanceOnly, BlueprintReadOnly, Category = "ZMMO|Guild")
FString GuildName; FString GuildName;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
// === Helpers === // === Helpers ===
/// Atualizado quando S_ATTRIBUTE_SNAPSHOT_FULL chega — UZMMOHudWidget chama /// Setado por AZMMOPlayerCharacter::HandleLocalSpawnReady (pose/EntityId)
/// isso pra refletir os dados publicos do snapshot no PlayerState. /// e atualizado por handlers de snapshot/level up para refletir dados
/// publicos do char no PlayerState.
UFUNCTION(BlueprintCallable, Category = "ZMMO|Identity") UFUNCTION(BlueprintCallable, Category = "ZMMO|Identity")
void SetPublicIdentity(int64 InEntityId, const FString& InCharId, void SetPublicIdentity(int64 InEntityId, const FString& InCharId,
int32 InBaseLevel, int32 InClassId); int32 InBaseLevel, int32 InClassId);
/// Setado pelo handler do S_CHAR_INFO (chega antes de S_SPAWN_PLAYER) —
/// metadados publicos do char vindos do servidor.
UFUNCTION(BlueprintCallable, Category = "ZMMO|Identity")
void SetCharInfo(const FString& InCharName, const FString& InGuildName);
/// Template helper: PS->GetZMMOComponent<UZMMOAttributeComponent>() /// Template helper: PS->GetZMMOComponent<UZMMOAttributeComponent>()
template <typename T> template <typename T>
T* GetZMMOComponent() const T* GetZMMOComponent() const

View File

@@ -0,0 +1,10 @@
#include "UILoadingProfilesDataAsset.h"
FZMMOLoadingProfile UZMMOLoadingProfilesDataAsset::GetProfile(EZMMOLoadingContext Context) const
{
if (const FZMMOLoadingProfile* Found = Profiles.Find(Context))
{
return *Found;
}
return FZMMOLoadingProfile();
}

View File

@@ -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<EZMMOLoadingContext, FZMMOLoadingProfile> Profiles;
/** Retorna o perfil do contexto ou um perfil vazio (sem etapas). */
UFUNCTION(BlueprintCallable, Category = "Loading")
FZMMOLoadingProfile GetProfile(EZMMOLoadingContext Context) const;
};

View File

@@ -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<FZMMOLoadingTipRow>(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<float>(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();
}

View File

@@ -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<UCommonTextBlock> Text_Title;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> Text_Status;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> ProgressBar_Progress;
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> 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<FName, EZMMOLoadingStepStatus> StepStatus_;
UPROPERTY(Transient)
TArray<FText> TipPool_;
int32 NextTipIndex_ = 0;
bool bCompleteFired_ = false;
FTimerHandle TipTimerHandle_;
};

View File

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

View File

@@ -5,6 +5,8 @@
#include "UIFrontEndScreenSet.h" #include "UIFrontEndScreenSet.h"
#include "UIManagerSubsystem.h" #include "UIManagerSubsystem.h"
#include "UIPrimaryGameLayout_Base.h" #include "UIPrimaryGameLayout_Base.h"
#include "UI/Common/UILoadingProfilesDataAsset.h"
#include "UI/Common/UILoadingScreen_Base.h"
#include "UI/UILayerTags.h" #include "UI/UILayerTags.h"
#include "ZMMOGameInstance.h" #include "ZMMOGameInstance.h"
#include "ZMMOThemeSubsystem.h" #include "ZMMOThemeSubsystem.h"
@@ -212,13 +214,27 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State)
const FSoftObjectPath Path = Soft.ToSoftObjectPath(); const FSoftObjectPath Path = Soft.ToSoftObjectPath();
UAssetManager::GetStreamableManager().RequestAsyncLoad( UAssetManager::GetStreamableManager().RequestAsyncLoad(
Path, Path,
FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer]() FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]()
{ {
if (UUIManagerSubsystem* M = GetUIManager()) if (UUIManagerSubsystem* M = GetUIManager())
{ {
if (UClass* Cls = Soft.Get()) 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<UUILoadingScreen_Base>(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()) if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
{ {
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
Zeus->OnPlayerSpawned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned);
} }
bNetBound = true; bNetBound = true;
} }
@@ -302,6 +319,7 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
{ {
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned);
} }
bNetBound = false; bNetBound = false;
} }
@@ -390,13 +408,67 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam
void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/) void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/)
{ {
if (bTravelingToWorld) if (!bTravelingToWorld)
{ {
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; bTravelingToWorld = false;
SetState(EZMMOFrontEndState::InWorld); 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) void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId)
{ {
// Dívida técnica (D5): ainda não há gancho no ZeusNetworkSubsystem para o // Dívida técnica (D5): ainda não há gancho no ZeusNetworkSubsystem para o

View File

@@ -8,6 +8,8 @@
class UUIFrontEndScreenSet; class UUIFrontEndScreenSet;
class UUIManagerSubsystem; class UUIManagerSubsystem;
class UUILoadingScreen_Base;
class UZMMOLoadingProfilesDataAsset;
class UZeusNetworkSubsystem; class UZeusNetworkSubsystem;
class UZeusCharServerSubsystem; class UZeusCharServerSubsystem;
struct FZMMOMapDef; struct FZMMOMapDef;
@@ -147,6 +149,24 @@ protected:
UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps") UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps")
TSoftObjectPtr<UDataTable> MapsTableAsset; TSoftObjectPtr<UDataTable> 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<UZMMOLoadingProfilesDataAsset> 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<UDataTable> LoadingTipsAsset;
private: private:
void BindNetwork(); void BindNetwork();
void UnbindNetwork(); void UnbindNetwork();
@@ -170,8 +190,26 @@ private:
UFUNCTION() UFUNCTION()
void HandleServerTravelRequested(const FString& MapName, const FString& MapPath); 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); 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 * Dívida técnica (D5): ServerHello traz ThemeId, mas o UZeusNetworkSubsystem
* ainda não expõe um delegate/getter dedicado. Quando expuser, ligar aqui * ainda não expõe um delegate/getter dedicado. Quando expuser, ligar aqui
@@ -199,6 +237,18 @@ private:
UPROPERTY(Transient) UPROPERTY(Transient)
TObjectPtr<UUIFrontEndScreenSet> ScreenSet; TObjectPtr<UUIFrontEndScreenSet> ScreenSet;
UPROPERTY(Transient)
TObjectPtr<UZMMOLoadingProfilesDataAsset> LoadingProfiles;
UPROPERTY(Transient)
TObjectPtr<UDataTable> LoadingTips;
/**
* Tela de loading atualmente em Modal. Weak porque o stack do CommonUI
* é dono — se popar por outro caminho não queremos dangling.
*/
TWeakObjectPtr<UUILoadingScreen_Base> ActiveLoadingScreen;
bool bNetBound = false; bool bNetBound = false;
bool bTravelingToWorld = false; bool bTravelingToWorld = false;
FDelegateHandle PostLoadMapHandle; FDelegateHandle PostLoadMapHandle;

View File

@@ -11,6 +11,7 @@
#include "ZMMOAttributeComponent.h" #include "ZMMOAttributeComponent.h"
#include "ZMMOJobDataAsset.h" #include "ZMMOJobDataAsset.h"
#include "ZMMOJobsSubsystem.h" #include "ZMMOJobsSubsystem.h"
#include "ZMMOPlayerState.h"
#include "UI/InGame/UIInGameFlowSubsystem.h" #include "UI/InGame/UIInGameFlowSubsystem.h"
#include "UI/InGameTypes.h" #include "UI/InGameTypes.h"
#include "UI/Widgets/UIPlayerStatus_BonusRow.h" #include "UI/Widgets/UIPlayerStatus_BonusRow.h"
@@ -87,10 +88,20 @@ void UUIPlayerStatus_Window::BindToLocalPlayer()
APlayerController* PC = World->GetFirstPlayerController(); APlayerController* PC = World->GetFirstPlayerController();
if (!PC || !PC->PlayerState) return; if (!PC || !PC->PlayerState) return;
// CharName vem do PlayerState (estável por sessão) — popula uma vez. // CharName vem do AZMMOPlayerState (setado pelo S_SPAWN_PLAYER no spawn
// local). Fallback pra APlayerState::GetPlayerName() se o cast falhar ou
// se CharName ainda nao chegou (race com handler que dispara o widget).
if (Text_CharName) if (Text_CharName)
{ {
Text_CharName->SetText(FText::FromString(PC->PlayerState->GetPlayerName())); FString DisplayName = PC->PlayerState->GetPlayerName();
if (const AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PC->PlayerState))
{
if (!ZMMOPs->CharName.IsEmpty())
{
DisplayName = ZMMOPs->CharName;
}
}
Text_CharName->SetText(FText::FromString(DisplayName));
} }
UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>(); UZMMOAttributeComponent* Comp = PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();