Files
ZMMO/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp
Mateus Rodrigues 77d52a703b ZMMO: CHAR_INFO via V1 (Fase A) + fix loading travado
- 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>
2026-06-12 13:06:42 -03:00

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