- ZeusWorldSubsystem consome ENT_CHAR_INFO (delegate OnCharInfo): aplica nome/guild no PlayerState do entityId certo (self ou proxy), com cache porque o CHAR_INFO chega antes do ator existir (server emite antes do ENT_SPAWN). - FIX loading travado: a etapa "Spawn" do loading dependia do legacy OnPlayerSpawned, que não dispara mais com V1 -> loading eterno. Agora a etapa é marcada pelo sinal V1 OnSelfEntityAssigned (ENT_SELF); bind legacy removido. Travel ainda passa por OnServerTravelRequested (ponte legacy) até a Fase D. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
645 lines
20 KiB
C++
645 lines
20 KiB
C++
#include "UIFrontEndFlowSubsystem.h"
|
|
|
|
#include "ZMMO.h"
|
|
#include "Data/World/MapDef.h"
|
|
#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 "ZeusGameInstance.h"
|
|
#include "ZeusThemeSubsystem.h"
|
|
#include "ZeusNetworkSubsystem.h"
|
|
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico (sinal de spawn local)
|
|
#include "ZeusCharServerSubsystem.h"
|
|
#include "CommonActivatableWidget.h"
|
|
#include "Engine/DataTable.h"
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "Engine/AssetManager.h"
|
|
#include "Engine/StreamableManager.h"
|
|
#include "Engine/World.h"
|
|
#include "Kismet/GameplayStatics.h"
|
|
#include "Misc/PackageName.h"
|
|
|
|
void UUIFrontEndFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
// Detecta chegada ao mundo após o OpenLevel disparado por travel.
|
|
PostLoadMapHandle = FCoreUObjectDelegates::PostLoadMapWithWorld.AddUObject(
|
|
this, &UUIFrontEndFlowSubsystem::HandlePostLoadMap);
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::Deinitialize()
|
|
{
|
|
if (PostLoadMapHandle.IsValid())
|
|
{
|
|
FCoreUObjectDelegates::PostLoadMapWithWorld.Remove(PostLoadMapHandle);
|
|
PostLoadMapHandle.Reset();
|
|
}
|
|
UnbindNetwork();
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::StartFrontEnd()
|
|
{
|
|
EnsureRootLayout();
|
|
BindNetwork();
|
|
|
|
SetState(EZeusFrontEndState::Boot);
|
|
|
|
// Pré-login fala com o ZeusCharServer (WebSocket), NÃO com o world server
|
|
// UDP. A tela Boot observa o CharServer e libera o botão ao conectar.
|
|
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
|
{
|
|
Char->ConnectToDefaultCharServer(); // lê CharServerUrl de Project Settings
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusCharServerSubsystem indisponível; sem conexão de Boot."));
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::EnsureRootLayout()
|
|
{
|
|
UUIManagerSubsystem* Mgr = GetUIManager();
|
|
if (!Mgr) { return; }
|
|
if (Mgr->IsRootLayoutReady())
|
|
{
|
|
return; // ja' no viewport
|
|
}
|
|
UUIFrontEndScreenSet* SS = GetScreenSet();
|
|
if (!SS || SS->RootLayoutClass.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("FrontEndFlow::EnsureRootLayout: RootLayoutClass ausente em ScreenSet"));
|
|
return;
|
|
}
|
|
TSubclassOf<UUIPrimaryGameLayout_Base> LayoutClass = SS->RootLayoutClass.LoadSynchronous();
|
|
Mgr->CreateAndAddRootLayout(LayoutClass);
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow::EnsureRootLayout: RootLayout recriado"));
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::SetState(EZeusFrontEndState NewState)
|
|
{
|
|
if (NewState == CurrentState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CurrentState = NewState;
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: estado → %s"),
|
|
*UEnum::GetValueAsString(NewState));
|
|
OnStateChanged.Broadcast(NewState);
|
|
|
|
const FGameplayTag MenuLayer = ZeusUITags::UI_Layer_Menu.GetTag();
|
|
const FGameplayTag ModalLayer = ZeusUITags::UI_Layer_Modal.GetTag();
|
|
|
|
switch (NewState)
|
|
{
|
|
case EZeusFrontEndState::Boot:
|
|
case EZeusFrontEndState::Connecting:
|
|
case EZeusFrontEndState::Login:
|
|
case EZeusFrontEndState::ServerSelect:
|
|
case EZeusFrontEndState::Lobby:
|
|
// Telas do front-end vivem na camada Menu (uma por vez).
|
|
if (UUIManagerSubsystem* Mgr = GetUIManager())
|
|
{
|
|
Mgr->ClearLayer(MenuLayer);
|
|
}
|
|
ResolveAndPushScreen(NewState);
|
|
break;
|
|
|
|
case EZeusFrontEndState::EnteringWorld:
|
|
// Loading/handoff por cima de tudo (camada Modal).
|
|
ResolveAndPushScreen(NewState);
|
|
break;
|
|
|
|
case EZeusFrontEndState::InWorld:
|
|
// HUD de gameplay assume — limpa front-end e loading.
|
|
if (UUIManagerSubsystem* Mgr = GetUIManager())
|
|
{
|
|
Mgr->ClearLayer(ModalLayer);
|
|
Mgr->ClearLayer(MenuLayer);
|
|
}
|
|
break;
|
|
|
|
case EZeusFrontEndState::None:
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::ShowLobbyPage(EZeusLobbyPage Page)
|
|
{
|
|
UUIFrontEndScreenSet* SS = GetScreenSet();
|
|
UUIManagerSubsystem* Mgr = GetUIManager();
|
|
if (!SS || !Mgr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const TSoftClassPtr<UCommonActivatableWidget> Soft = SS->GetLobbyPage(Page);
|
|
if (Soft.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: página de Lobby %s não configurada (DA vazio)."),
|
|
*UEnum::GetValueAsString(Page));
|
|
return;
|
|
}
|
|
|
|
const FGameplayTag MenuLayer = ZeusUITags::UI_Layer_Menu.GetTag();
|
|
const FSoftObjectPath Path = Soft.ToSoftObjectPath();
|
|
UAssetManager::GetStreamableManager().RequestAsyncLoad(
|
|
Path,
|
|
FStreamableDelegate::CreateWeakLambda(this, [this, Soft, MenuLayer]()
|
|
{
|
|
if (UUIManagerSubsystem* M = GetUIManager())
|
|
{
|
|
if (UClass* Cls = Soft.Get())
|
|
{
|
|
M->PushScreenToLayer(MenuLayer, Cls);
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
bool UUIFrontEndFlowSubsystem::RequestBack()
|
|
{
|
|
switch (CurrentState)
|
|
{
|
|
case EZeusFrontEndState::Login:
|
|
// Volta à Boot (CharServer segue conectado; só re-mostra a tela).
|
|
SetState(EZeusFrontEndState::Boot);
|
|
return true;
|
|
case EZeusFrontEndState::ServerSelect:
|
|
SetState(EZeusFrontEndState::Login);
|
|
return true;
|
|
case EZeusFrontEndState::Lobby:
|
|
SetState(EZeusFrontEndState::ServerSelect);
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZeusFrontEndState State)
|
|
{
|
|
UUIFrontEndScreenSet* SS = GetScreenSet();
|
|
if (!SS)
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("FrontEndFlow: ScreenSet não configurado; tela de %s ignorada."),
|
|
*UEnum::GetValueAsString(State));
|
|
return;
|
|
}
|
|
|
|
const TSoftClassPtr<UCommonActivatableWidget> Soft = SS->GetScreenForState(State);
|
|
if (Soft.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: Screen for state %s not configured."),
|
|
*UEnum::GetValueAsString(State));
|
|
return;
|
|
}
|
|
|
|
UUIManagerSubsystem* Mgr = GetUIManager();
|
|
if (!Mgr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
const FGameplayTag Layer = (State == EZeusFrontEndState::EnteringWorld)
|
|
? ZeusUITags::UI_Layer_Modal.GetTag()
|
|
: ZeusUITags::UI_Layer_Menu.GetTag();
|
|
|
|
const FSoftObjectPath Path = Soft.ToSoftObjectPath();
|
|
UAssetManager::GetStreamableManager().RequestAsyncLoad(
|
|
Path,
|
|
FStreamableDelegate::CreateWeakLambda(this, [this, Soft, Layer, State]()
|
|
{
|
|
if (UUIManagerSubsystem* M = GetUIManager())
|
|
{
|
|
if (UClass* Cls = Soft.Get())
|
|
{
|
|
UCommonActivatableWidget* Pushed = M->PushScreenToLayer(Layer, Cls);
|
|
|
|
// Loading: configura logo após o push e marca etapa "Travel"
|
|
// (chegamos aqui via HandleServerTravelRequested → SetState).
|
|
if (State == EZeusFrontEndState::EnteringWorld)
|
|
{
|
|
if (UUILoadingScreen_Base* Loading = Cast<UUILoadingScreen_Base>(Pushed))
|
|
{
|
|
ActiveLoadingScreen = Loading;
|
|
Loading->Configure(EZeusLoadingContext::FrontEndEnteringWorld,
|
|
GetLoadingProfiles(), GetLoadingTips());
|
|
Loading->OnLoadingComplete.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete);
|
|
Loading->MarkStepDone(TEXT("Travel"));
|
|
|
|
// Drena steps que dispararam ANTES do widget existir
|
|
// (HandlePostLoadMap/HandlePlayerSpawned podem ter
|
|
// rodado durante o RequestAsyncLoad). Sem isso o
|
|
// loading fica eterno se o servidor for rápido demais.
|
|
//
|
|
// Snapshot via MoveTemp pra evitar invalidacao do iterator se
|
|
// outro tick mexer no TSet durante o loop: CreateWeakLambda
|
|
// garante `this` vivo, NAO garante que membros nao sejam
|
|
// realocados entre captura e execucao (crash em
|
|
// TConstSetBitIterator::FindFirstSetBit historico).
|
|
TSet<FName> StepsSnapshot = MoveTemp(PendingLoadingSteps_);
|
|
PendingLoadingSteps_.Reset();
|
|
for (const FName StepId : StepsSnapshot)
|
|
{
|
|
Loading->MarkStepDone(StepId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
|
|
UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const
|
|
{
|
|
const UGameInstance* GI = GetGameInstance();
|
|
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
|
|
}
|
|
|
|
UZeusNetworkingClientSubsystem* UUIFrontEndFlowSubsystem::GetNetClient() const
|
|
{
|
|
const UGameInstance* GI = GetGameInstance();
|
|
return GI ? GI->GetSubsystem<UZeusNetworkingClientSubsystem>() : nullptr;
|
|
}
|
|
|
|
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
|
|
{
|
|
const UGameInstance* GI = GetGameInstance();
|
|
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
|
|
}
|
|
|
|
UUIManagerSubsystem* UUIFrontEndFlowSubsystem::GetUIManager() const
|
|
{
|
|
if (const UGameInstance* GI = GetGameInstance())
|
|
{
|
|
if (ULocalPlayer* LP = GI->GetFirstGamePlayer())
|
|
{
|
|
return LP->GetSubsystem<UUIManagerSubsystem>();
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
UUIFrontEndScreenSet* UUIFrontEndFlowSubsystem::GetScreenSet()
|
|
{
|
|
if (ScreenSet)
|
|
{
|
|
return ScreenSet;
|
|
}
|
|
if (!ScreenSetAsset.IsNull())
|
|
{
|
|
ScreenSet = ScreenSetAsset.LoadSynchronous();
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("FrontEndFlow: ScreenSetAsset não setado em DefaultGame.ini; telas farão no-op."));
|
|
}
|
|
return ScreenSet;
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::BindNetwork()
|
|
{
|
|
if (bNetBound)
|
|
{
|
|
return;
|
|
}
|
|
// CharServer (WebSocket) dirige o pré-login: Boot→Login.
|
|
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
|
{
|
|
Char->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
|
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
|
Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
|
}
|
|
// UDP (world server): o travel (TRAVEL_TO_MAP) ainda passa pelo objeto legacy
|
|
// como PONTE (HandleTravelToMap V1 reusa OnServerTravelRequested) -- isso some
|
|
// na Fase D quando o travel ganhar delegate proprio no V1.
|
|
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
|
{
|
|
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
|
}
|
|
// V1 CANONICO: o spawn do player local chega via ENT_SELF (o cliente aprende
|
|
// o proprio entityId) -> OnSelfEntityAssigned. O legacy OnPlayerSpawned NAO
|
|
// dispara mais com V1, entao a etapa "Spawn" do loading ficava eterna
|
|
// (loading travado). Liga a etapa ao sinal V1.
|
|
if (UZeusNetworkingClientSubsystem* Net = GetNetClient())
|
|
{
|
|
Net->OnSelfEntityAssigned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned);
|
|
}
|
|
bNetBound = true;
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
|
{
|
|
if (!bNetBound)
|
|
{
|
|
return;
|
|
}
|
|
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
|
{
|
|
Char->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
|
Char->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
|
Char->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
|
}
|
|
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
|
{
|
|
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
|
}
|
|
if (UZeusNetworkingClientSubsystem* Net = GetNetClient())
|
|
{
|
|
Net->OnSelfEntityAssigned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned);
|
|
}
|
|
bNetBound = false;
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleConnected()
|
|
{
|
|
// NÃO auto-avança: a tela Boot libera o botão "Iniciar"; o usuário clica
|
|
// (→ RequestEnterLogin). Mantém o estado Boot.
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer conectado (estado %s)."),
|
|
*UEnum::GetValueAsString(CurrentState));
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleConnectionFailed(FString Reason)
|
|
{
|
|
// Permanece em Boot; a tela mostra erro e o CharServer pode re-tentar.
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão ao CharServer falhou (%s)."), *Reason);
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleCharDisconnected(int32 StatusCode, FString Reason)
|
|
{
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer desconectou (code=%d, %s)."),
|
|
StatusCode, *Reason);
|
|
// Se caiu antes de entrar no mundo, volta a Boot para reconectar.
|
|
if (CurrentState != EZeusFrontEndState::InWorld &&
|
|
CurrentState != EZeusFrontEndState::EnteringWorld &&
|
|
CurrentState != EZeusFrontEndState::Boot)
|
|
{
|
|
SetState(EZeusFrontEndState::Boot);
|
|
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
|
{
|
|
Char->ConnectToDefaultCharServer();
|
|
}
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::RequestEnterLogin()
|
|
{
|
|
if (CurrentState == EZeusFrontEndState::Boot)
|
|
{
|
|
SetState(EZeusFrontEndState::Login);
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::RequestEnterServerSelect()
|
|
{
|
|
if (CurrentState == EZeusFrontEndState::Login)
|
|
{
|
|
SetState(EZeusFrontEndState::ServerSelect);
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::SetSelectedRealmId(const FString& RealmId)
|
|
{
|
|
SelectedRealmId = RealmId;
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: realm selecionado = %s"), *SelectedRealmId);
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::ClearSelectedRealm()
|
|
{
|
|
if (!SelectedRealmId.IsEmpty())
|
|
{
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando realm selecionado (%s)"), *SelectedRealmId);
|
|
SelectedRealmId.Reset();
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath)
|
|
{
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: travel pedido pelo servidor → %s (%s)."), *MapName, *MapPath);
|
|
|
|
// Idempotencia: se o cliente JA carregou esse mapa via TravelToMapById
|
|
// (ex: Lobby após CharSelect resolveu DT_Maps), e o S_TRAVEL_TO_MAP do
|
|
// servidor chega DEPOIS confirmando, NAO recarrega. Recarregar destruiria
|
|
// HUD, PlayerState e AttributeComponent — perdendo o snapshot ja aplicado.
|
|
if (!MapPath.IsEmpty())
|
|
{
|
|
if (const UWorld* CurrentWorld = GetWorld())
|
|
{
|
|
// CurrentWorld->GetOutermost()->GetName() devolve algo como
|
|
// "/Game/ZMMO/Maps/World/UEDPIE_0_L_TestWorld" no PIE. Comparamos
|
|
// pelo nome curto pra ignorar prefixo UEDPIE_N_.
|
|
const FString CurrentShort = FPackageName::GetShortName(CurrentWorld->GetOutermost()->GetName());
|
|
const FString TargetShort = FPackageName::GetShortName(MapPath);
|
|
// PIE prefixa "UEDPIE_<N>_<MapName>" — strip se presente.
|
|
FString CurrentClean = CurrentShort;
|
|
int32 UndIdx = INDEX_NONE;
|
|
if (CurrentClean.StartsWith(TEXT("UEDPIE_")) && CurrentClean.FindChar('_', UndIdx))
|
|
{
|
|
const int32 SecondUnd = CurrentClean.Find(TEXT("_"), ESearchCase::CaseSensitive, ESearchDir::FromStart, UndIdx + 1);
|
|
if (SecondUnd != INDEX_NONE)
|
|
{
|
|
CurrentClean = CurrentClean.Mid(SecondUnd + 1);
|
|
}
|
|
}
|
|
if (CurrentClean.Equals(TargetShort, ESearchCase::IgnoreCase))
|
|
{
|
|
UE_LOG(LogZMMO, Log,
|
|
TEXT("FrontEndFlow: ja estamos em %s — ignorando OpenLevel do S_TRAVEL_TO_MAP (mantem HUD/PlayerState)"),
|
|
*TargetShort);
|
|
bTravelingToWorld = false;
|
|
if (CurrentState != EZeusFrontEndState::InWorld)
|
|
{
|
|
SetState(EZeusFrontEndState::InWorld);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
bTravelingToWorld = true;
|
|
SetState(EZeusFrontEndState::EnteringWorld);
|
|
|
|
if (!MapPath.IsEmpty())
|
|
{
|
|
// Forca ZeusGameMode em qualquer mapa que venha do WorldServer.
|
|
// World Settings do .umap podem ter override herdado de L_FrontEnd
|
|
// (GameModeOverride=ZeusFrontEndGameMode) — sobrescrevemos via URL
|
|
// option pra nao depender de cada artista configurar.
|
|
const FString Options = TEXT("game=/Script/ZMMO.ZeusGameMode");
|
|
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandlePostLoadMap(UWorld* /*LoadedWorld*/)
|
|
{
|
|
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;
|
|
}
|
|
|
|
// Race fix: widget ainda em RequestAsyncLoad; memoiza pra drenar quando
|
|
// a tela for criada (ver lambda em ResolveAndPushScreen). Não fallback
|
|
// pra InWorld — esperamos a tela existir e o Spawn chegar.
|
|
PendingLoadingSteps_.Add(TEXT("MapLoaded"));
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int64 /*EntityId*/, bool bIsLocal,
|
|
FVector /*PosCm*/, float /*YawDeg*/,
|
|
int64 /*ServerTimeMs*/)
|
|
{
|
|
if (!bIsLocal) return;
|
|
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
|
|
{
|
|
Loading->MarkStepDone(TEXT("Spawn"));
|
|
return;
|
|
}
|
|
|
|
// Race fix: spawn pode chegar ANTES do widget existir se o servidor for
|
|
// rápido (TryFinalizeEnterWorld) e o RequestAsyncLoad lento. Memoiza
|
|
// pra drenar no callback do async load.
|
|
if (bTravelingToWorld)
|
|
{
|
|
PendingLoadingSteps_.Add(TEXT("Spawn"));
|
|
}
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleV1LocalSpawned(int64 EntityId)
|
|
{
|
|
// ENT_SELF e' SEMPRE o proprio char (o cliente aprendendo seu entityId), entao
|
|
// bIsLocal=true. Reusa a logica de marcar a etapa "Spawn" + memoizacao anti-race.
|
|
HandlePlayerSpawned(EntityId, /*bIsLocal=*/true, FVector::ZeroVector, 0.0f, 0);
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::HandleLoadingComplete()
|
|
{
|
|
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
|
|
{
|
|
Loading->OnLoadingComplete.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleLoadingComplete);
|
|
}
|
|
ActiveLoadingScreen.Reset();
|
|
PendingLoadingSteps_.Reset();
|
|
bTravelingToWorld = false;
|
|
SetState(EZeusFrontEndState::InWorld);
|
|
}
|
|
|
|
UZeusLoadingProfilesDataAsset* 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)
|
|
{
|
|
// Dívida técnica (D5): ainda não há gancho no ZeusNetworkSubsystem para o
|
|
// ThemeId do ServerHello. Mantido pronto para quando o plugin expuser.
|
|
if (const UGameInstance* GI = GetGameInstance())
|
|
{
|
|
if (UZeusThemeSubsystem* Theme = GI->GetSubsystem<UZeusThemeSubsystem>())
|
|
{
|
|
Theme->ApplyServerTheme(ThemeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
const FZeusMapDef* UUIFrontEndFlowSubsystem::FindMapDef(int32 MapId) const
|
|
{
|
|
if (MapId <= 0) return nullptr;
|
|
if (MapsTableAsset.IsNull())
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: MapsTableAsset nao configurado — adicione DT_Maps em DefaultGame.ini"));
|
|
return nullptr;
|
|
}
|
|
const UDataTable* Table = MapsTableAsset.LoadSynchronous();
|
|
if (!Table) return nullptr;
|
|
|
|
for (const TPair<FName, uint8*>& Row : Table->GetRowMap())
|
|
{
|
|
const FZeusMapDef* Def = reinterpret_cast<const FZeusMapDef*>(Row.Value);
|
|
if (Def && Def->MapId == MapId)
|
|
{
|
|
return Def;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
FString UUIFrontEndFlowSubsystem::ResolveLevelPathByMapId(int32 MapId) const
|
|
{
|
|
const FZeusMapDef* Def = FindMapDef(MapId);
|
|
if (!Def) return FString();
|
|
const FSoftObjectPath ObjPath = Def->ClientLevel.ToSoftObjectPath();
|
|
if (!ObjPath.IsValid()) return FString();
|
|
return ObjPath.GetLongPackageName();
|
|
}
|
|
|
|
void UUIFrontEndFlowSubsystem::SetPendingSpawnPose(FVector PosCm, float YawDeg)
|
|
{
|
|
PendingSpawnPosCm = PosCm;
|
|
PendingSpawnYawDeg = YawDeg;
|
|
bHasPendingSpawnPose = true;
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose set pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
|
|
PosCm.X, PosCm.Y, PosCm.Z, YawDeg);
|
|
}
|
|
|
|
bool UUIFrontEndFlowSubsystem::ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg)
|
|
{
|
|
if (!bHasPendingSpawnPose) return false;
|
|
OutPosCm = PendingSpawnPosCm;
|
|
OutYawDeg = PendingSpawnYawDeg;
|
|
bHasPendingSpawnPose = false;
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose consumido pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
|
|
OutPosCm.X, OutPosCm.Y, OutPosCm.Z, OutYawDeg);
|
|
return true;
|
|
}
|
|
|
|
bool UUIFrontEndFlowSubsystem::TravelToMapById(int32 MapId)
|
|
{
|
|
const FString MapPath = ResolveLevelPathByMapId(MapId);
|
|
if (MapPath.IsEmpty())
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: TravelToMapById(%d) falhou — sem entrada no DT_Maps"), MapId);
|
|
return false;
|
|
}
|
|
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: TravelToMapById(%d) -> %s"), MapId, *MapPath);
|
|
bTravelingToWorld = true;
|
|
SetState(EZeusFrontEndState::EnteringWorld);
|
|
const FString Options = TEXT("game=/Script/ZMMO.ZeusGameMode");
|
|
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
|
|
return true;
|
|
}
|