fix(frontend): HandleServerTravelRequested idempotente quando ja no mapa certo

Cliente fazia OpenLevel 2x:
1. CharSelect resolve mapId via DT_Maps -> TravelToMapById -> OpenLevel
2. Server confirma com S_TRAVEL_TO_MAP -> HandleServerTravelRequested ->
   OpenLevel **de novo**

O segundo OpenLevel destruia o World, PlayerState, AttributeComponent e HUD
que ja tinham sido criados/bindados. AttributeComponent novo iniciava com
snapshot vazio (entity=0, level=1, hp=0/0) — HUD ficava nesse default.
S_ATTRIBUTE_SNAPSHOT_FULL com dados reais chegava DEPOIS do reload, mas
HUD nao tinha sido recriado/reativado no novo mundo, entao HandleAttributesChanged
nunca rodava.

A deduplicacao em ZeusNetworkSubsystem (`S_TRAVEL_TO_MAP duplicate ignored`)
so pegava do 3o TRAVEL em diante — o 2o ja fazia o estrago.

Fix: compara nome curto do World atual (strip prefixo UEDPIE_N_ do PIE) com
o target. Se ja' estao no mesmo mapa, skipa OpenLevel e so seta state pra
InWorld — preservando HUD/PlayerState/AttributeComponent.
This commit is contained in:
2026-05-31 23:43:13 -03:00
parent 4e14f1feb6
commit b2c1153229

View File

@@ -19,7 +19,9 @@
#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)
{
@@ -406,6 +408,46 @@ void UUIFrontEndFlowSubsystem::ClearSelectedRealm()
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 != EZMMOFrontEndState::InWorld)
{
SetState(EZMMOFrontEndState::InWorld);
}
return;
}
}
}
bTravelingToWorld = true;
SetState(EZMMOFrontEndState::EnteringWorld);
@@ -415,9 +457,6 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam
// World Settings do .umap podem ter override herdado de L_FrontEnd
// (GameModeOverride=ZMMOFrontEndGameMode) — sobrescrevemos via URL
// option pra nao depender de cada artista configurar.
// Evolucao natural: quando o WorldServer enviar GameMode no
// S_TRAVEL_TO_MAP (wire estendido), trocar este literal por
// `?game=` + valor recebido do server.
const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode");
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
}