3 Commits

Author SHA1 Message Date
e3aab64c8c fix(world): destroy proxy on DESPAWN + handle stale weak ptr on SPAWN
ZMMOWorldSubsystem mantinha proxies no mapa RemoteEntities apos DESPAWN
apenas marcando SetActorHiddenInGame(true) via SetEntityRelevant(false).
Entry ficava la apontando pra actor escondido — SPAWN subsequente do
mesmo entityId caia no fast-path "reactivar" e tudo bem... ate' o
TWeakObjectPtr ficar stale (GC, World Partition stream-out, etc): a
branch Contains(EntityId) saia com return incondicional sem spawnar
nada. Resultado: SPAWN silenciosamente ignorado, pawn permanente
invisivel ate' algo limpar a key.

Fixes:
1. HandlePlayerDespawned: Actor->Destroy() + RemoteEntities.Remove
   (destroi de verdade em vez de so esconder).
2. HandlePlayerSpawned: quando Contains mas weak ptr stale, remove a
   entry orfa e cai pro caminho de spawn novo — em vez de return.

Confirmado pelos logs de cliente durante teste de oscilacao do AOI
(servidor enviando DESPAWN+SPAWN ao oscilar ZI/ZD).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:26:55 -03:00
b2c1153229 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.
2026-05-31 23:43:13 -03:00
4e14f1feb6 refactor(charserver): World -> Realm + parse gatewayEndpoint + fix PendingLoadingSteps race
- ZMMOWorldEntry -> ZMMORealmEntry (RealmId + GatewayEndpoint single string)
- CharServerOpcodes: S_WORLD_* -> S_REALM_*
- UIServerSelectScreen: parse S_REALM_LIST com gatewayEndpoint
- UIUserLobbyScreen: parse S_CHAR_SELECT_OK, split gatewayEndpoint via FString::Split(":", FromEnd) — IPv6 tolerante
- UICharacterCreatePage: SubmitCreate envia realmId
- ZMMOCharSummary: WorldId -> RealmId
- UIFrontEndFlowSubsystem: GetSelectedRealmId; fix de race em PendingLoadingSteps_ (TSet snapshot via MoveTemp antes de iterar; evita access violation em TConstSetBitIterator durante async lambda)

Conferido E2E: 2 standalone instances entram simultaneamente em Provinces diferentes via Gateway.
2026-05-31 20:02:47 -03:00
15 changed files with 282 additions and 192 deletions

View File

@@ -104,7 +104,10 @@ void UZMMOWorldSubsystem::HandlePlayerSpawned(const int32 EntityId, const bool b
if (RemoteEntities.Contains(EntityId)) if (RemoteEntities.Contains(EntityId))
{ {
// Ja existe um proxy: reactivar e re-snap. // Ja existe um proxy: tenta reactivar e re-snap. Se o weak ptr ficou
// stale (actor coletado pelo GC, stream-out do World Partition, etc.),
// remove a entry orfa e cai no caminho de spawn novo — evita SPAWN
// silenciosamente ignorado depois de oscilacoes na AOI.
if (AActor* Existing = RemoteEntities[EntityId].Get()) if (AActor* Existing = RemoteEntities[EntityId].Get())
{ {
if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Existing)) if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Existing))
@@ -112,8 +115,12 @@ void UZMMOWorldSubsystem::HandlePlayerSpawned(const int32 EntityId, const bool b
AsEntity->SetEntityRelevant(true); AsEntity->SetEntityRelevant(true);
} }
Existing->SetActorLocationAndRotation(PosCm, FRotator(0.0f, YawDeg, 0.0f)); Existing->SetActorLocationAndRotation(PosCm, FRotator(0.0f, YawDeg, 0.0f));
UE_LOG(LogZMMO, Verbose, TEXT("ZMMOWorldSubsystem: re-snap EntityId=%d"), EntityId);
return;
} }
return; // weak ptr stale — limpa entry e cai pro spawn novo abaixo.
RemoteEntities.Remove(EntityId);
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: stale entry para EntityId=%d, respawnando"), EntityId);
} }
UWorld* World = GetWorld(); UWorld* World = GetWorld();
@@ -166,15 +173,23 @@ void UZMMOWorldSubsystem::HandlePlayerDespawned(const int32 EntityId)
return; return;
} }
// Destruir o actor pra valer + remover do mapa. Antes, despawn apenas
// fazia SetActorHiddenInGame(true) via SetEntityRelevant(false), o que
// deixava o entry no mapa apontando pra um actor escondido. SPAWN
// subsequente do mesmo entityId era detectado como "duplicate" e nada
// acontecia — pawn ficava invisivel ate' o weak ptr ser GC'd. Bug
// confirmado nos logs do AOI ao oscilar entrada/saida da ZI/ZD.
if (AActor* Actor = Entry->Get()) if (AActor* Actor = Entry->Get())
{ {
if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Actor)) if (IZMMOEntityInterface* AsEntity = Cast<IZMMOEntityInterface>(Actor))
{ {
AsEntity->SetEntityRelevant(false); AsEntity->SetEntityRelevant(false);
} }
Actor->Destroy();
} }
RemoteEntities.Remove(EntityId);
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: despawn EntityId=%d"), EntityId); UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: despawn EntityId=%d (destroyed)"), EntityId);
} }
void UZMMOWorldSubsystem::HandlePlayerStateUpdate(const int32 EntityId, const int32 InputSeq, void UZMMOWorldSubsystem::HandlePlayerStateUpdate(const int32 EntityId, const int32 InputSeq,

View File

@@ -18,10 +18,10 @@ namespace ZMMOCharOp
constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason) constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason)
// Listagem/criação/seleção de personagem // Listagem/criação/seleção de personagem
constexpr int32 C_CHAR_LIST_REQUEST = 2010; // uint8 hasWorldFilter [+ uint8[16] worldId] constexpr int32 C_CHAR_LIST_REQUEST = 2010; // uint8 hasRealmFilter [+ uint8[16] realmId]
constexpr int32 S_CHAR_LIST = 2011; constexpr int32 S_CHAR_LIST = 2011;
constexpr int32 C_CHAR_CREATE = 2020; // uint8[16] worldId + slot + ... constexpr int32 C_CHAR_CREATE = 2020; // uint8[16] realmId + slot + ...
constexpr int32 S_CHAR_CREATE_OK = 2021; constexpr int32 S_CHAR_CREATE_OK = 2021;
constexpr int32 S_CHAR_CREATE_REJECT= 2022; constexpr int32 S_CHAR_CREATE_REJECT= 2022;
@@ -37,14 +37,15 @@ namespace ZMMOCharOp
constexpr int32 S_CHAR_SELECT_OK = 2041; constexpr int32 S_CHAR_SELECT_OK = 2041;
constexpr int32 S_CHAR_SELECT_REJECT= 2042; constexpr int32 S_CHAR_SELECT_REJECT= 2042;
// Listagem de mundos (Fase 1 do ARQUITETURA_SERVER_SELECT) // Listagem de Realms (renomeado de WORLD_LIST no refator R4 — Server
constexpr int32 C_WORLD_LIST_REQUEST = 2060; // payload vazio // Meshing). Server-side: `Server/ZeusCharServer/src/services/realm-list.service.ts`.
constexpr int32 S_WORLD_LIST = 2061; // uint16 count + entries constexpr int32 C_REALM_LIST_REQUEST = 2060; // payload vazio
constexpr int32 S_REALM_LIST = 2061; // uint16 count + entries
/** /**
* Push do CharServer com update de UM mundo (state/pop/queueLen). * Push do CharServer com update de UM Realm (state/pop/queueLen).
* Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state * Wire: string realmId + uint16 pop + uint16 queueLen + uint8 state
*/ */
constexpr int32 S_WORLD_STATUS_UPDATE = 2062; constexpr int32 S_REALM_STATUS_UPDATE = 2062;
} }
/** /**
@@ -68,11 +69,13 @@ namespace ZMMOCharRejectReason
constexpr uint16 AccountLocked = 5; constexpr uint16 AccountLocked = 5;
constexpr uint16 AccountSuspended = 6; constexpr uint16 AccountSuspended = 6;
constexpr uint16 CredentialsAuthDisabled = 7; constexpr uint16 CredentialsAuthDisabled = 7;
constexpr uint16 WorldOffline = 8; constexpr uint16 RealmOffline = 8;
constexpr uint16 WorldMaintenance = 9; constexpr uint16 RealmMaintenance = 9;
constexpr uint16 WorldFull = 10; constexpr uint16 RealmFull = 10;
constexpr uint16 WorldRegionMismatch = 11; constexpr uint16 RealmRegionMismatch = 11;
constexpr uint16 WorldNotFound = 12; constexpr uint16 RealmNotFound = 12;
/** Posicao do char nao esta dentro de nenhuma cell do grid_layout do Realm (dead zone). */
constexpr uint16 CharOutsideAnyCell = 13;
constexpr uint16 NameInUse = 20; constexpr uint16 NameInUse = 20;
constexpr uint16 InvalidName = 21; constexpr uint16 InvalidName = 21;
constexpr uint16 SlotOccupied = 22; constexpr uint16 SlotOccupied = 22;
@@ -81,8 +84,8 @@ namespace ZMMOCharRejectReason
constexpr uint16 DeleteNotScheduled = 25; constexpr uint16 DeleteNotScheduled = 25;
} }
/** Estado do mundo no wire (uint8). Espelha `WireWorldState` em CharOpcodes.ts. */ /** Estado do Realm no wire (uint8). Espelha `WireRealmState` em CharOpcodes.ts. */
namespace ZMMOWireWorldState namespace ZMMOWireRealmState
{ {
constexpr uint8 Offline = 0; constexpr uint8 Offline = 0;
constexpr uint8 Online = 1; constexpr uint8 Online = 1;

View File

@@ -55,10 +55,10 @@ void UUICharacterCreatePage_Base::SubmitCreate()
UZeusCharServerSubsystem* Char = GI->GetSubsystem<UZeusCharServerSubsystem>(); UZeusCharServerSubsystem* Char = GI->GetSubsystem<UZeusCharServerSubsystem>();
if (!Flow || !Char) return; if (!Flow || !Char) return;
const FString WorldId = Flow->GetSelectedWorldId(); const FString RealmId = Flow->GetSelectedRealmId();
if (WorldId.IsEmpty()) if (RealmId.IsEmpty())
{ {
if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Mundo nao selecionado."))); if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Realm nao selecionado.")));
return; return;
} }
@@ -73,9 +73,9 @@ void UUICharacterCreatePage_Base::SubmitCreate()
const uint8 SlotIdx = 0; const uint8 SlotIdx = 0;
TArray<uint8> Payload; TArray<uint8> Payload;
if (!WriteUuid16(Payload, WorldId)) if (!WriteUuid16(Payload, RealmId))
{ {
if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("WorldId invalido."))); if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("RealmId invalido.")));
return; return;
} }
WriteUInt8(Payload, SlotIdx); WriteUInt8(Payload, SlotIdx);

View File

@@ -19,7 +19,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnCharCreateCancelled);
* S_CHAR_CREATE_OK/REJECT. * S_CHAR_CREATE_OK/REJECT.
* *
* Wire C_CHAR_CREATE (espelha char-create.service.ts): * Wire C_CHAR_CREATE (espelha char-create.service.ts):
* uint8[16] worldId + uint8 slot + string name + uint16 classId * uint8[16] realmId + uint8 slot + string name + uint16 classId
* + uint32 hair + uint32 hairColor + uint32 skinColor + uint8 bodyType * + uint32 hair + uint32 hairColor + uint32 skinColor + uint8 bodyType
*/ */
UCLASS(Abstract, Blueprintable) UCLASS(Abstract, Blueprintable)

View File

@@ -19,7 +19,9 @@
#include "UObject/UObjectGlobals.h" #include "UObject/UObjectGlobals.h"
#include "Engine/AssetManager.h" #include "Engine/AssetManager.h"
#include "Engine/StreamableManager.h" #include "Engine/StreamableManager.h"
#include "Engine/World.h"
#include "Kismet/GameplayStatics.h" #include "Kismet/GameplayStatics.h"
#include "Misc/PackageName.h"
void UUIFrontEndFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection) void UUIFrontEndFlowSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{ {
@@ -238,11 +240,18 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State)
// (HandlePostLoadMap/HandlePlayerSpawned podem ter // (HandlePostLoadMap/HandlePlayerSpawned podem ter
// rodado durante o RequestAsyncLoad). Sem isso o // rodado durante o RequestAsyncLoad). Sem isso o
// loading fica eterno se o servidor for rápido demais. // loading fica eterno se o servidor for rápido demais.
for (const FName StepId : PendingLoadingSteps_) //
// 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); Loading->MarkStepDone(StepId);
} }
PendingLoadingSteps_.Reset();
} }
} }
} }
@@ -381,24 +390,64 @@ void UUIFrontEndFlowSubsystem::RequestEnterServerSelect()
} }
} }
void UUIFrontEndFlowSubsystem::SetSelectedWorldId(const FString& WorldId) void UUIFrontEndFlowSubsystem::SetSelectedRealmId(const FString& RealmId)
{ {
SelectedWorldId = WorldId; SelectedRealmId = RealmId;
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: mundo selecionado = %s"), *SelectedWorldId); UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: realm selecionado = %s"), *SelectedRealmId);
} }
void UUIFrontEndFlowSubsystem::ClearSelectedWorld() void UUIFrontEndFlowSubsystem::ClearSelectedRealm()
{ {
if (!SelectedWorldId.IsEmpty()) if (!SelectedRealmId.IsEmpty())
{ {
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando mundo selecionado (%s)"), *SelectedWorldId); UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando realm selecionado (%s)"), *SelectedRealmId);
SelectedWorldId.Reset(); SelectedRealmId.Reset();
} }
} }
void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath) void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath)
{ {
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: travel pedido pelo servidor → %s (%s)."), *MapName, *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; bTravelingToWorld = true;
SetState(EZMMOFrontEndState::EnteringWorld); SetState(EZMMOFrontEndState::EnteringWorld);
@@ -408,9 +457,6 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam
// World Settings do .umap podem ter override herdado de L_FrontEnd // World Settings do .umap podem ter override herdado de L_FrontEnd
// (GameModeOverride=ZMMOFrontEndGameMode) — sobrescrevemos via URL // (GameModeOverride=ZMMOFrontEndGameMode) — sobrescrevemos via URL
// option pra nao depender de cada artista configurar. // 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"); const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode");
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options); UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
} }

View File

@@ -83,18 +83,18 @@ public:
void RequestEnterServerSelect(); void RequestEnterServerSelect();
/** /**
* Memoriza o mundo escolhido na ServerSelect (UUID v4 string). Lido pelo * Memoriza o Realm escolhido na ServerSelect (UUID v4 string). Lido pelo
* CharSelect/CharCreate na hora de filtrar/criar personagens. Limpo no * CharSelect/CharCreate na hora de filtrar/criar personagens. Limpo no
* logout/back para ServerSelect. * logout/back para ServerSelect.
*/ */
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") UFUNCTION(BlueprintCallable, Category = "FrontEnd|Realms")
void SetSelectedWorldId(const FString& WorldId); void SetSelectedRealmId(const FString& RealmId);
UFUNCTION(BlueprintPure, Category = "FrontEnd|Worlds") UFUNCTION(BlueprintPure, Category = "FrontEnd|Realms")
FString GetSelectedWorldId() const { return SelectedWorldId; } FString GetSelectedRealmId() const { return SelectedRealmId; }
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") UFUNCTION(BlueprintCallable, Category = "FrontEnd|Realms")
void ClearSelectedWorld(); void ClearSelectedRealm();
/** /**
* Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps. * Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps.
@@ -222,7 +222,7 @@ private:
EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None; EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None;
UPROPERTY(Transient) UPROPERTY(Transient)
FString SelectedWorldId; FString SelectedRealmId;
/** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */ /** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */
UPROPERTY(Transient) UPROPERTY(Transient)

View File

@@ -41,17 +41,17 @@ void UUIServerCard_Base::NativeDestruct()
Super::NativeDestruct(); Super::NativeDestruct();
} }
void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry) void UUIServerCard_Base::SetFromEntry(const FZMMORealmEntry& InEntry)
{ {
Entry = InEntry; Entry = InEntry;
if (Text_Name) if (Text_Name)
{ {
Text_Name->SetText(FText::FromString(Entry.WorldName)); Text_Name->SetText(FText::FromString(Entry.Name));
} }
if (Text_Pop) if (Text_Pop)
{ {
Text_Pop->SetText(FText::FromString(FString::Printf(TEXT("%d/%d"), Entry.Population, Entry.Capacity))); Text_Pop->SetText(FText::FromString(FString::Printf(TEXT("%d/%d"), Entry.Pop, Entry.Capacity)));
} }
if (Text_Status) if (Text_Status)
{ {
@@ -60,15 +60,15 @@ void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
if (Bar_Pop) if (Bar_Pop)
{ {
const float Ratio = (Entry.Capacity > 0) const float Ratio = (Entry.Capacity > 0)
? FMath::Clamp(static_cast<float>(Entry.Population) / static_cast<float>(Entry.Capacity), 0.f, 1.f) ? FMath::Clamp(static_cast<float>(Entry.Pop) / static_cast<float>(Entry.Capacity), 0.f, 1.f)
: 0.f; : 0.f;
Bar_Pop->SetPercent(Ratio); Bar_Pop->SetPercent(Ratio);
} }
// Botao "Selecionar" sempre habilitado — mesmo com world offline, // Botao "Selecionar" sempre habilitado — mesmo com realm offline,
// o usuario pode entrar no Lobby pra ver a lista de personagens, criar/ // o usuario pode entrar no Lobby pra ver a lista de personagens, criar/
// deletar. O gate de "entrar no mundo" fica no proprio Lobby (botao // deletar. O gate de "entrar no realm" fica no proprio Lobby (botao
// "Entrar no Servidor"), que checa World.State antes do handoff. // "Entrar no Servidor"), que checa Realm.State antes do handoff.
// TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle. // TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle.
// if (Btn_Enter) // if (Btn_Enter)
// { // {
@@ -78,7 +78,7 @@ void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
void UUIServerCard_Base::HandleEnterClicked() void UUIServerCard_Base::HandleEnterClicked()
{ {
OnCardPressed.Broadcast(Entry.WorldId, Entry.State); OnCardPressed.Broadcast(Entry.RealmId, Entry.State);
} }
FText UUIServerCard_Base::FormatStatus_Implementation(uint8 State) const FText UUIServerCard_Base::FormatStatus_Implementation(uint8 State) const

View File

@@ -2,7 +2,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "Blueprint/UserWidget.h" #include "Blueprint/UserWidget.h"
#include "ZMMOWorldEntry.h" #include "ZMMORealmEntry.h"
#include "UIServerCard_Base.generated.h" #include "UIServerCard_Base.generated.h"
class UCommonTextBlock; class UCommonTextBlock;
@@ -11,10 +11,10 @@ class UUIButton_Base;
class UProgressBar; class UProgressBar;
/** /**
* Card de servidor (mundo) instanciado dinamicamente pela ServerSelect. * Card de servidor (realm) instanciado dinamicamente pela ServerSelect.
* *
* Recebe um `FZMMOWorldEntry` e renderiza nome/pop/status/bar; ao clicar * Recebe um `FZMMORealmEntry` e renderiza nome/pop/status/bar; ao clicar
* em "Entrar" dispara `OnCardPressed.Broadcast(WorldId, State)`. A tela * em "Entrar" dispara `OnCardPressed.Broadcast(RealmId, State)`. A tela
* dona (UUIServerSelectScreen_Base) escuta esse delegate. * dona (UUIServerSelectScreen_Base) escuta esse delegate.
* *
* WBP filho (`WBP_ServerCard`) precisa expor (via nomes de variavel): * WBP filho (`WBP_ServerCard`) precisa expor (via nomes de variavel):
@@ -25,7 +25,7 @@ class UProgressBar;
* - Btn_Enter : Button * - Btn_Enter : Button
* - Bar_Pop : ProgressBar (opcional) * - Bar_Pop : ProgressBar (opcional)
*/ */
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnServerCardPressed, FString, WorldId, uint8, State); DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnServerCardPressed, FString, RealmId, uint8, State);
UCLASS(Abstract, Blueprintable) UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIServerCard_Base : public UUserWidget class ZMMO_API UUIServerCard_Base : public UUserWidget
@@ -34,14 +34,14 @@ class ZMMO_API UUIServerCard_Base : public UUserWidget
public: public:
/** /**
* Aplica os dados de um mundo no card. * Aplica os dados de um realm no card.
*/ */
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerCard") UFUNCTION(BlueprintCallable, Category = "Zeus|ServerCard")
void SetFromEntry(const FZMMOWorldEntry& Entry); void SetFromEntry(const FZMMORealmEntry& Entry);
/** Dados atuais (copia). */ /** Dados atuais (copia). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerCard") UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerCard")
FZMMOWorldEntry Entry; FZMMORealmEntry Entry;
/** /**
* Disparado quando o botao "Entrar" do card e clicado. * Disparado quando o botao "Entrar" do card e clicado.

View File

@@ -70,7 +70,7 @@ void UUIServerSelectScreen_Base::NativeOnActivated()
} }
} }
RequestWorldList(); RequestRealmList();
} }
void UUIServerSelectScreen_Base::NativeOnDeactivated() void UUIServerSelectScreen_Base::NativeOnDeactivated()
@@ -110,23 +110,23 @@ void UUIServerSelectScreen_Base::RefreshUIStyle_Implementation()
} }
} }
void UUIServerSelectScreen_Base::RequestWorldList() void UUIServerSelectScreen_Base::RequestRealmList()
{ {
UZeusCharServerSubsystem* Char = GetCharServer(); UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected()) if (!Char || !Char->IsConnected())
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CharServer indisponivel — pulando RequestWorldList")); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CharServer indisponivel — pulando RequestRealmList"));
return; return;
} }
TArray<uint8> EmptyPayload; TArray<uint8> EmptyPayload;
Char->SendCharRequest(ZMMOCharOp::C_WORLD_LIST_REQUEST, EmptyPayload); Char->SendCharRequest(ZMMOCharOp::C_REALM_LIST_REQUEST, EmptyPayload);
} }
void UUIServerSelectScreen_Base::SelectWorldAndProceed(const FString& WorldId) void UUIServerSelectScreen_Base::SelectRealmAndProceed(const FString& RealmId)
{ {
if (WorldId.IsEmpty()) if (RealmId.IsEmpty())
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: SelectWorldAndProceed com WorldId vazio.")); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: SelectRealmAndProceed com RealmId vazio."));
return; return;
} }
UGameInstance* GI = GetGameInstance(); UGameInstance* GI = GetGameInstance();
@@ -136,18 +136,18 @@ void UUIServerSelectScreen_Base::SelectWorldAndProceed(const FString& WorldId)
} }
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>()) if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{ {
Flow->SetSelectedWorldId(WorldId); Flow->SetSelectedRealmId(RealmId);
Flow->SetState(EZMMOFrontEndState::Lobby); Flow->SetState(EZMMOFrontEndState::Lobby);
} }
} }
void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload) void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload)
{ {
if (Opcode == ZMMOCharOp::S_WORLD_LIST) if (Opcode == ZMMOCharOp::S_REALM_LIST)
{ {
ParseWorldList(Payload); ParseRealmList(Payload);
} }
else if (Opcode == ZMMOCharOp::S_WORLD_STATUS_UPDATE) else if (Opcode == ZMMOCharOp::S_REALM_STATUS_UPDATE)
{ {
ApplyStatusUpdate(Payload); ApplyStatusUpdate(Payload);
} }
@@ -155,24 +155,24 @@ void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray
void UUIServerSelectScreen_Base::ApplyStatusUpdate(const TArray<uint8>& Payload) void UUIServerSelectScreen_Base::ApplyStatusUpdate(const TArray<uint8>& Payload)
{ {
// Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state // Wire: string realmId + uint16 pop + uint16 queueLen + uint8 state
int32 Pos = 0; int32 Pos = 0;
FString WorldId; FString RealmId;
uint16 Pop = 0; uint16 Pop = 0;
uint16 QueueLen = 0; uint16 QueueLen = 0;
uint8 State = 0; uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, WorldId) if (!ReadStringUtf8(Payload, Pos, RealmId)
|| !ReadU16(Payload, Pos, Pop) || !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen) || !ReadU16(Payload, Pos, QueueLen)
|| !ReadU8(Payload, Pos, State)) || !ReadU8(Payload, Pos, State))
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_STATUS_UPDATE malformado")); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_REALM_STATUS_UPDATE malformado"));
return; return;
} }
int32 Idx = INDEX_NONE; int32 Idx = INDEX_NONE;
for (int32 i = 0; i < Worlds.Num(); ++i) for (int32 i = 0; i < Realms.Num(); ++i)
{ {
if (Worlds[i].WorldId == WorldId) if (Realms[i].RealmId == RealmId)
{ {
Idx = i; Idx = i;
break; break;
@@ -180,76 +180,76 @@ void UUIServerSelectScreen_Base::ApplyStatusUpdate(const TArray<uint8>& Payload)
} }
if (Idx == INDEX_NONE) if (Idx == INDEX_NONE)
{ {
// Mundo desconhecido (ainda nao recebemos via S_WORLD_LIST). Pede lista // Realm desconhecido (ainda nao recebemos via S_REALM_LIST). Pede lista
// completa pra trazer dados estaticos (name/host/region/capacity). // completa pra trazer dados estaticos (name/gatewayEndpoint/region/capacity).
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: STATUS_UPDATE de mundo desconhecido (%s) — pedindo lista"), *WorldId); UE_LOG(LogZMMO, Log, TEXT("ServerSelect: STATUS_UPDATE de realm desconhecido (%s) — pedindo lista"), *RealmId);
RequestWorldList(); RequestRealmList();
return; return;
} }
FZMMOWorldEntry& Entry = Worlds[Idx]; FZMMORealmEntry& Entry = Realms[Idx];
Entry.Population = static_cast<int32>(Pop); Entry.Pop = static_cast<int32>(Pop);
Entry.QueueLen = static_cast<int32>(QueueLen); Entry.QueueLen = static_cast<int32>(QueueLen);
Entry.State = State; Entry.State = State;
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: world %s atualizado (state=%d pop=%d)"), UE_LOG(LogZMMO, Log, TEXT("ServerSelect: realm %s atualizado (state=%d pop=%d)"),
*WorldId, State, Entry.Population); *RealmId, State, Entry.Pop);
// Atualiza apenas o card afetado (preserva animacoes/scroll dos demais). // Atualiza apenas o card afetado (preserva animacoes/scroll dos demais).
if (SpawnedCards.IsValidIndex(Idx) && SpawnedCards[Idx]) if (SpawnedCards.IsValidIndex(Idx) && SpawnedCards[Idx])
{ {
SpawnedCards[Idx]->SetFromEntry(Entry); SpawnedCards[Idx]->SetFromEntry(Entry);
} }
OnWorldListReceived(); OnRealmListReceived();
} }
void UUIServerSelectScreen_Base::ParseWorldList(const TArray<uint8>& Payload) void UUIServerSelectScreen_Base::ParseRealmList(const TArray<uint8>& Payload)
{ {
int32 Pos = 0; int32 Pos = 0;
uint16 Count = 0; uint16 Count = 0;
if (!ReadU16(Payload, Pos, Count)) if (!ReadU16(Payload, Pos, Count))
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_LIST malformado (sem count)")); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_REALM_LIST malformado (sem count)"));
return; return;
} }
TArray<FZMMOWorldEntry> NewWorlds; TArray<FZMMORealmEntry> NewRealms;
NewWorlds.Reserve(Count); NewRealms.Reserve(Count);
for (uint16 i = 0; i < Count; ++i) for (uint16 i = 0; i < Count; ++i)
{ {
FZMMOWorldEntry Entry; FZMMORealmEntry Entry;
FString RegionStr; FString RegionStr;
uint16 Port = 0;
uint16 Cap = 0; uint16 Cap = 0;
uint16 Pop = 0; uint16 Pop = 0;
uint16 QueueLen = 0; uint16 QueueLen = 0;
uint8 State = 0; uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, Entry.WorldId) // Wire: string realmId + string name + string region + string gatewayEndpoint
|| !ReadStringUtf8(Payload, Pos, Entry.WorldName) // + uint16 capacity + uint16 pop + uint16 queueLen + uint8 state
// Server-side: Server/ZeusCharServer/src/services/realm-list.service.ts
if (!ReadStringUtf8(Payload, Pos, Entry.RealmId)
|| !ReadStringUtf8(Payload, Pos, Entry.Name)
|| !ReadStringUtf8(Payload, Pos, RegionStr) || !ReadStringUtf8(Payload, Pos, RegionStr)
|| !ReadStringUtf8(Payload, Pos, Entry.Host) || !ReadStringUtf8(Payload, Pos, Entry.GatewayEndpoint)
|| !ReadU16(Payload, Pos, Port)
|| !ReadU16(Payload, Pos, Cap) || !ReadU16(Payload, Pos, Cap)
|| !ReadU16(Payload, Pos, Pop) || !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen) || !ReadU16(Payload, Pos, QueueLen)
|| !ReadU8(Payload, Pos, State)) || !ReadU8(Payload, Pos, State))
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_LIST malformado (entry %d)"), i); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_REALM_LIST malformado (entry %d)"), i);
return; return;
} }
Entry.Region = RegionStr; Entry.Region = RegionStr;
Entry.Port = static_cast<int32>(Port);
Entry.Capacity = static_cast<int32>(Cap); Entry.Capacity = static_cast<int32>(Cap);
Entry.Population = static_cast<int32>(Pop); Entry.Pop = static_cast<int32>(Pop);
Entry.QueueLen = static_cast<int32>(QueueLen); Entry.QueueLen = static_cast<int32>(QueueLen);
Entry.State = State; Entry.State = State;
NewWorlds.Add(MoveTemp(Entry)); NewRealms.Add(MoveTemp(Entry));
} }
Worlds = MoveTemp(NewWorlds); Realms = MoveTemp(NewRealms);
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: recebidos %d mundos"), Worlds.Num()); UE_LOG(LogZMMO, Log, TEXT("ServerSelect: recebidos %d realms"), Realms.Num());
RebuildCards(); RebuildCards();
OnWorldListReceived(); OnRealmListReceived();
} }
void UUIServerSelectScreen_Base::RebuildCards() void UUIServerSelectScreen_Base::RebuildCards()
@@ -280,15 +280,15 @@ void UUIServerSelectScreen_Base::RebuildCards()
const int32 GridColumns = 2; // bate com o mock (ColumnFill[0]/[1]) const int32 GridColumns = 2; // bate com o mock (ColumnFill[0]/[1])
int32 Index = 0; int32 Index = 0;
for (const FZMMOWorldEntry& W : Worlds) for (const FZMMORealmEntry& R : Realms)
{ {
UUIServerCard_Base* Card = CreateWidget<UUIServerCard_Base>(this, CardClass); UUIServerCard_Base* Card = CreateWidget<UUIServerCard_Base>(this, CardClass);
if (!Card) if (!Card)
{ {
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: falha ao criar card para %s"), *W.WorldName); UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: falha ao criar card para %s"), *R.Name);
continue; continue;
} }
Card->SetFromEntry(W); Card->SetFromEntry(R);
Card->OnCardPressed.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCardPressed); Card->OnCardPressed.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCardPressed);
CardContainer->AddChild(Card); CardContainer->AddChild(Card);
@@ -311,14 +311,14 @@ void UUIServerSelectScreen_Base::RebuildCards()
} }
} }
void UUIServerSelectScreen_Base::HandleCardPressed(FString WorldId, uint8 State) void UUIServerSelectScreen_Base::HandleCardPressed(FString RealmId, uint8 State)
{ {
// O ServerSelect permite escolher qualquer mundo (online/maintenance/ // O ServerSelect permite escolher qualquer realm (online/maintenance/
// offline) — o usuario entra no Lobby pra gerenciar chars desse mundo. // offline) — o usuario entra no Lobby pra gerenciar chars desse realm.
// O bloqueio real (handoff -> world) acontece no botao "Entrar no // O bloqueio real (handoff -> gateway) acontece no botao "Entrar no
// Servidor" do Lobby, que checa World.State antes do C_CHAR_SELECT. // Servidor" do Lobby, que checa Realm.State antes do C_CHAR_SELECT.
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: mundo %s selecionado (state=%d)"), *WorldId, State); UE_LOG(LogZMMO, Log, TEXT("ServerSelect: realm %s selecionado (state=%d)"), *RealmId, State);
SelectWorldAndProceed(WorldId); SelectRealmAndProceed(RealmId);
} }
UZeusCharServerSubsystem* UUIServerSelectScreen_Base::GetCharServer() const UZeusCharServerSubsystem* UUIServerSelectScreen_Base::GetCharServer() const

View File

@@ -2,7 +2,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "UIActivatableScreen_Base.h" #include "UIActivatableScreen_Base.h"
#include "ZMMOWorldEntry.h" #include "ZMMORealmEntry.h"
#include "UIServerSelectScreen_Base.generated.h" #include "UIServerSelectScreen_Base.generated.h"
class UCommonTextBlock; class UCommonTextBlock;
@@ -12,15 +12,15 @@ class UUIServerCard_Base;
class UZeusCharServerSubsystem; class UZeusCharServerSubsystem;
/** /**
* Tela de Server Select — Fase 1 do ARQUITETURA_SERVER_SELECT. * Tela de Realm Select — Fase 1 do ARQUITETURA_SERVER_SELECT.
* *
* Fluxo: * Fluxo:
* 1. NativeOnActivated subscreve OnRawMessage do UZeusCharServerSubsystem * 1. NativeOnActivated subscreve OnRawMessage do UZeusCharServerSubsystem
* 2. Envia C_WORLD_LIST_REQUEST (payload vazio) * 2. Envia C_REALM_LIST_REQUEST (payload vazio)
* 3. ParseWorldList monta `Worlds` * 3. ParseRealmList monta `Realms`
* 4. RebuildCards apaga filhos de `CardContainer` e instancia 1 WBP_ServerCard * 4. RebuildCards apaga filhos de `CardContainer` e instancia 1 WBP_ServerCard
* por mundo, bindando `OnCardPressed -> SelectWorldAndProceed` * por realm, bindando `OnCardPressed -> SelectRealmAndProceed`
* 5. Dispara `OnWorldListReceived` (BIE) pra extensoes em BP * 5. Dispara `OnRealmListReceived` (BIE) pra extensoes em BP
*/ */
UCLASS(Abstract, Blueprintable) UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIServerSelectScreen_Base : public UUIActivatableScreen_Base class ZMMO_API UUIServerSelectScreen_Base : public UUIActivatableScreen_Base
@@ -29,16 +29,16 @@ class ZMMO_API UUIServerSelectScreen_Base : public UUIActivatableScreen_Base
public: public:
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect") UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect")
void RequestWorldList(); void RequestRealmList();
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect") UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect")
void SelectWorldAndProceed(const FString& WorldId); void SelectRealmAndProceed(const FString& RealmId);
UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerSelect") UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerSelect")
TArray<FZMMOWorldEntry> Worlds; TArray<FZMMORealmEntry> Realms;
UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|ServerSelect") UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|ServerSelect")
void OnWorldListReceived(); void OnRealmListReceived();
protected: protected:
virtual void NativePreConstruct() override; virtual void NativePreConstruct() override;
@@ -50,10 +50,10 @@ protected:
void HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload); void HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload);
UFUNCTION() UFUNCTION()
void HandleCardPressed(FString WorldId, uint8 State); void HandleCardPressed(FString RealmId, uint8 State);
UZeusCharServerSubsystem* GetCharServer() const; UZeusCharServerSubsystem* GetCharServer() const;
void ParseWorldList(const TArray<uint8>& Payload); void ParseRealmList(const TArray<uint8>& Payload);
void ApplyStatusUpdate(const TArray<uint8>& Payload); void ApplyStatusUpdate(const TArray<uint8>& Payload);
void RebuildCards(); void RebuildCards();

View File

@@ -66,27 +66,27 @@ void UUIUserLobbyScreen_Base::RequestCharList()
UZeusCharServerSubsystem* Char = GetCharServer(); UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected()) return; if (!Char || !Char->IsConnected()) return;
FString WorldId; FString RealmId;
if (UGameInstance* GI = GetGameInstance()) if (UGameInstance* GI = GetGameInstance())
{ {
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>()) if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{ {
WorldId = Flow->GetSelectedWorldId(); RealmId = Flow->GetSelectedRealmId();
} }
} }
// Wire: uint8 hasFilter + (uint8[16] worldId se hasFilter==1) // Wire: uint8 hasFilter + (uint8[16] realmId se hasFilter==1)
TArray<uint8> Payload; TArray<uint8> Payload;
if (WorldId.IsEmpty()) if (RealmId.IsEmpty())
{ {
Payload.Add(0); Payload.Add(0);
} }
else else
{ {
Payload.Add(1); Payload.Add(1);
if (!WriteUuid16(Payload, WorldId)) if (!WriteUuid16(Payload, RealmId))
{ {
UE_LOG(LogZMMO, Warning, TEXT("Lobby: SelectedWorldId invalido (%s) — pedindo sem filtro"), *WorldId); UE_LOG(LogZMMO, Warning, TEXT("Lobby: SelectedRealmId invalido (%s) — pedindo sem filtro"), *RealmId);
Payload.Reset(); Payload.Reset();
Payload.Add(0); Payload.Add(0);
} }
@@ -110,7 +110,7 @@ void UUIUserLobbyScreen_Base::BackToServerSelect()
if (!GI) return; if (!GI) return;
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>()) if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{ {
Flow->ClearSelectedWorld(); Flow->ClearSelectedRealm();
Flow->SetState(EZMMOFrontEndState::ServerSelect); Flow->SetState(EZMMOFrontEndState::ServerSelect);
} }
} }
@@ -160,7 +160,7 @@ void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
uint64 deleteAt = 0; uint64 deleteAt = 0;
if (!ReadU64(Payload, Pos, charId64) if (!ReadU64(Payload, Pos, charId64)
|| !ReadUuid16(Payload, Pos, E.WorldId) || !ReadUuid16(Payload, Pos, E.RealmId)
|| !ReadU8(Payload, Pos, slot) || !ReadU8(Payload, Pos, slot)
|| !ReadStringUtf8(Payload, Pos, E.Name) || !ReadStringUtf8(Payload, Pos, E.Name)
|| !ReadU16(Payload, Pos, classId) || !ReadU16(Payload, Pos, classId)
@@ -328,25 +328,45 @@ void UUIUserLobbyScreen_Base::HandleCharDeleteCancelAck(const TArray<uint8>& Pay
void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload) void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
{ {
// Wire: uint64 charId + uint16 mapId + float x/y/z + float yaw + // Wire: uint64 charId + uint16 mapId + float x/y/z + float yaw +
// string worldHost + uint16 worldPort + string handoffToken + string region // string gatewayEndpoint + string handoffToken + string region
// gatewayEndpoint = "host:port" (UMA single string — refator R4 unificou
// host+port). Server-side: char-select.service.ts.
int32 Pos = 0; int32 Pos = 0;
uint64 CharId64 = 0; FString WorldHost, HandoffToken, Region; uint64 CharId64 = 0; FString GatewayEndpoint, HandoffToken, Region;
uint16 MapId16 = 0; uint16 MapId16 = 0;
float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0; float Px=0, Py=0, Pz=0, Yaw=0;
if (!ReadU64(Payload, Pos, CharId64) if (!ReadU64(Payload, Pos, CharId64)
|| !ReadU16(Payload, Pos, MapId16) || !ReadU16(Payload, Pos, MapId16)
|| !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz) || !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz)
|| !ReadFloat(Payload, Pos, Yaw) || !ReadFloat(Payload, Pos, Yaw)
|| !ReadStringUtf8(Payload, Pos, WorldHost) || !ReadStringUtf8(Payload, Pos, GatewayEndpoint)
|| !ReadU16(Payload, Pos, WorldPort)
|| !ReadStringUtf8(Payload, Pos, HandoffToken) || !ReadStringUtf8(Payload, Pos, HandoffToken)
|| !ReadStringUtf8(Payload, Pos, Region)) || !ReadStringUtf8(Payload, Pos, Region))
{ {
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado")); UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado"));
return; return;
} }
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d mapId=%u token=%s..."), UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> realm gateway=%s mapId=%u token=%s..."),
*WorldHost, WorldPort, static_cast<uint32>(MapId16), *HandoffToken.Left(8)); *GatewayEndpoint, static_cast<uint32>(MapId16), *HandoffToken.Left(8));
// Split "host:port" para a API legada do ZeusNetworkSubsystem que ainda
// recebe os dois separados. Defensivo: se o server mandar string vazia
// ou sem ':' a gente loga e aborta o handoff em vez de tentar conectar
// em endereco invalido.
FString GwHost;
FString GwPortStr;
if (!GatewayEndpoint.Split(TEXT(":"), &GwHost, &GwPortStr, ESearchCase::CaseSensitive, ESearchDir::FromEnd))
{
UE_LOG(LogZMMO, Error, TEXT("Lobby: gatewayEndpoint malformado (sem ':') = %s"), *GatewayEndpoint);
return;
}
int32 GwPort = 0;
LexFromString(GwPort, *GwPortStr);
if (GwHost.IsEmpty() || GwPort <= 0)
{
UE_LOG(LogZMMO, Error, TEXT("Lobby: gatewayEndpoint invalido host=%s port=%d"), *GwHost, GwPort);
return;
}
UGameInstance* GI = GetGameInstance(); UGameInstance* GI = GetGameInstance();
if (!GI) return; if (!GI) return;
@@ -360,13 +380,13 @@ void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
} }
// Fase 3: handoff UDP — apresenta o ticket no `C_CONNECT_REQUEST`. O // Fase 3: handoff UDP — apresenta o ticket no `C_CONNECT_REQUEST`. O
// WorldServer valida via GETDEL no Valkey regional. WorldServer envia // Gateway/ZeusServer valida via GETDEL no Valkey regional. Server envia
// `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado). // `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado).
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel — // Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel. // ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>()) if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
{ {
ZeusNet->ConnectToZeusServerWithTicket(WorldHost, static_cast<int32>(WorldPort), HandoffToken); ZeusNet->ConnectToZeusServerWithTicket(GwHost, GwPort, HandoffToken);
} }
else else
{ {

View File

@@ -18,13 +18,13 @@ class UZeusCharServerSubsystem;
* Tela Lobby (host) — Fase 1.5 do ARQUITETURA_SERVER_SELECT. * Tela Lobby (host) — Fase 1.5 do ARQUITETURA_SERVER_SELECT.
* *
* Fluxo: * Fluxo:
* 1. NativeOnActivated: pega `SelectedWorldId` do FlowSubsystem, envia * 1. NativeOnActivated: pega `SelectedRealmId` do FlowSubsystem, envia
* C_CHAR_LIST_REQUEST filtrado por esse mundo * C_CHAR_LIST_REQUEST filtrado por esse realm
* 2. Parse S_CHAR_LIST -> Chars[]; instancia 1 WBP_CharCard por entry * 2. Parse S_CHAR_LIST -> Chars[]; instancia 1 WBP_CharCard por entry
* 3. Card "Selecionar" -> C_CHAR_SELECT(charId) -> S_CHAR_SELECT_OK -> * 3. Card "Selecionar" -> C_CHAR_SELECT(charId) -> S_CHAR_SELECT_OK ->
* handoff UDP pro WorldServer * handoff UDP pro Gateway do Realm
* 4. Botao "Criar Personagem" -> mostra page interna `CharacterCreate` * 4. Botao "Criar Personagem" -> mostra page interna `CharacterCreate`
* 5. Botao "Voltar" -> ClearSelectedWorld + Flow.SetState(ServerSelect) * 5. Botao "Voltar" -> ClearSelectedRealm + Flow.SetState(ServerSelect)
* *
* Pages internas via UWidgetSwitcher: * Pages internas via UWidgetSwitcher:
* - PageIndex 0: Lista de chars (CardContainer) * - PageIndex 0: Lista de chars (CardContainer)
@@ -96,6 +96,9 @@ protected:
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUICharacterCreatePage_Base> CharCreatePage; UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUICharacterCreatePage_Base> CharCreatePage;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Title; UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Title;
// TODO(R4): renomear o widget Text_WorldName -> Text_RealmName no WBP_UserLobby.uasset
// tambem; aqui mantivemos o nome antigo pra nao quebrar o BindWidgetOptional ate
// alguem reabrir o WBP no editor.
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_WorldName; UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_WorldName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Back; UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Back;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Create; UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Create;

View File

@@ -43,7 +43,7 @@ struct FZMMOCharSummary
FString CharId; FString CharId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString WorldId; FString RealmId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Slot = 0; int32 Slot = 0;

View File

@@ -0,0 +1,48 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMORealmEntry.generated.h"
/**
* Entrada de Realm (cluster Gateway+Provinces) recebida do CharServer via
* S_REALM_LIST. Espelha `RealmRecord` em
* `Server/ZeusCharServer/src/types/realm.types.ts`.
*
* `GatewayEndpoint` e' o endereco "host:port" do Gateway do Realm (UMA
* single string vinda do server — substitui o par host+port da versao
* anterior). Cliente faz split por `:` quando precisa abrir UDP.
*
* State usa o wire raw (0=Offline, 1=Online, 2=Maintenance — ver
* ZMMOWireRealmState em CharServerOpcodes.h).
*/
USTRUCT(BlueprintType)
struct FZMMORealmEntry
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
FString RealmId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
FString Name;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
FString Region;
/** Endpoint do Gateway no formato "host:port" (ex: "127.0.0.1:7777"). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
FString GatewayEndpoint;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
int32 Capacity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
int32 Pop = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
int32 QueueLen = 0;
/** 0=Offline, 1=Online, 2=Maintenance (vide ZMMOWireRealmState). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Realm")
uint8 State = 0;
};

View File

@@ -1,45 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOWorldEntry.generated.h"
/**
* Entrada de mundo (server) recebida do CharServer via S_WORLD_LIST.
* Espelha `WorldRecord` em `Server/ZeusCharServer/src/types/world.types.ts`.
*
* State usa o wire raw (0=Offline, 1=Online, 2=Maintenance — ver
* ZMMOWireWorldState em CharServerOpcodes.h).
*/
USTRUCT(BlueprintType)
struct FZMMOWorldEntry
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString WorldId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString WorldName;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString Region;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString Host;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Port = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Capacity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Population = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 QueueLen = 0;
/** 0=Offline, 1=Online, 2=Maintenance (vide ZMMOWireWorldState). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
uint8 State = 0;
};