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.
This commit is contained in:
2026-05-31 20:02:47 -03:00
parent ede49c1777
commit 4e14f1feb6
14 changed files with 222 additions and 186 deletions

View File

@@ -18,10 +18,10 @@ namespace ZMMOCharOp
constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason)
// 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 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_REJECT= 2022;
@@ -37,14 +37,15 @@ namespace ZMMOCharOp
constexpr int32 S_CHAR_SELECT_OK = 2041;
constexpr int32 S_CHAR_SELECT_REJECT= 2042;
// Listagem de mundos (Fase 1 do ARQUITETURA_SERVER_SELECT)
constexpr int32 C_WORLD_LIST_REQUEST = 2060; // payload vazio
constexpr int32 S_WORLD_LIST = 2061; // uint16 count + entries
// Listagem de Realms (renomeado de WORLD_LIST no refator R4 — Server
// Meshing). Server-side: `Server/ZeusCharServer/src/services/realm-list.service.ts`.
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).
* Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state
* Push do CharServer com update de UM Realm (state/pop/queueLen).
* 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 AccountSuspended = 6;
constexpr uint16 CredentialsAuthDisabled = 7;
constexpr uint16 WorldOffline = 8;
constexpr uint16 WorldMaintenance = 9;
constexpr uint16 WorldFull = 10;
constexpr uint16 WorldRegionMismatch = 11;
constexpr uint16 WorldNotFound = 12;
constexpr uint16 RealmOffline = 8;
constexpr uint16 RealmMaintenance = 9;
constexpr uint16 RealmFull = 10;
constexpr uint16 RealmRegionMismatch = 11;
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 InvalidName = 21;
constexpr uint16 SlotOccupied = 22;
@@ -81,8 +84,8 @@ namespace ZMMOCharRejectReason
constexpr uint16 DeleteNotScheduled = 25;
}
/** Estado do mundo no wire (uint8). Espelha `WireWorldState` em CharOpcodes.ts. */
namespace ZMMOWireWorldState
/** Estado do Realm no wire (uint8). Espelha `WireRealmState` em CharOpcodes.ts. */
namespace ZMMOWireRealmState
{
constexpr uint8 Offline = 0;
constexpr uint8 Online = 1;

View File

@@ -55,10 +55,10 @@ void UUICharacterCreatePage_Base::SubmitCreate()
UZeusCharServerSubsystem* Char = GI->GetSubsystem<UZeusCharServerSubsystem>();
if (!Flow || !Char) return;
const FString WorldId = Flow->GetSelectedWorldId();
if (WorldId.IsEmpty())
const FString RealmId = Flow->GetSelectedRealmId();
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;
}
@@ -73,9 +73,9 @@ void UUICharacterCreatePage_Base::SubmitCreate()
const uint8 SlotIdx = 0;
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;
}
WriteUInt8(Payload, SlotIdx);

View File

@@ -19,7 +19,7 @@ DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnCharCreateCancelled);
* S_CHAR_CREATE_OK/REJECT.
*
* 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
*/
UCLASS(Abstract, Blueprintable)

View File

@@ -238,11 +238,18 @@ void UUIFrontEndFlowSubsystem::ResolveAndPushScreen(EZMMOFrontEndState State)
// (HandlePostLoadMap/HandlePlayerSpawned podem ter
// rodado durante o RequestAsyncLoad). Sem isso o
// 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);
}
PendingLoadingSteps_.Reset();
}
}
}
@@ -381,18 +388,18 @@ void UUIFrontEndFlowSubsystem::RequestEnterServerSelect()
}
}
void UUIFrontEndFlowSubsystem::SetSelectedWorldId(const FString& WorldId)
void UUIFrontEndFlowSubsystem::SetSelectedRealmId(const FString& RealmId)
{
SelectedWorldId = WorldId;
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: mundo selecionado = %s"), *SelectedWorldId);
SelectedRealmId = RealmId;
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);
SelectedWorldId.Reset();
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando realm selecionado (%s)"), *SelectedRealmId);
SelectedRealmId.Reset();
}
}

View File

@@ -83,18 +83,18 @@ public:
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
* logout/back para ServerSelect.
*/
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds")
void SetSelectedWorldId(const FString& WorldId);
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Realms")
void SetSelectedRealmId(const FString& RealmId);
UFUNCTION(BlueprintPure, Category = "FrontEnd|Worlds")
FString GetSelectedWorldId() const { return SelectedWorldId; }
UFUNCTION(BlueprintPure, Category = "FrontEnd|Realms")
FString GetSelectedRealmId() const { return SelectedRealmId; }
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds")
void ClearSelectedWorld();
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Realms")
void ClearSelectedRealm();
/**
* Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps.
@@ -222,7 +222,7 @@ private:
EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None;
UPROPERTY(Transient)
FString SelectedWorldId;
FString SelectedRealmId;
/** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */
UPROPERTY(Transient)

View File

@@ -41,17 +41,17 @@ void UUIServerCard_Base::NativeDestruct()
Super::NativeDestruct();
}
void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
void UUIServerCard_Base::SetFromEntry(const FZMMORealmEntry& InEntry)
{
Entry = InEntry;
if (Text_Name)
{
Text_Name->SetText(FText::FromString(Entry.WorldName));
Text_Name->SetText(FText::FromString(Entry.Name));
}
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)
{
@@ -60,15 +60,15 @@ void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
if (Bar_Pop)
{
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;
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/
// deletar. O gate de "entrar no mundo" fica no proprio Lobby (botao
// "Entrar no Servidor"), que checa World.State antes do handoff.
// deletar. O gate de "entrar no realm" fica no proprio Lobby (botao
// "Entrar no Servidor"), que checa Realm.State antes do handoff.
// TODO(ui-system): reativar quando UUIButton_Base voltar a ter RefreshUIStyle.
// if (Btn_Enter)
// {
@@ -78,7 +78,7 @@ void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
void UUIServerCard_Base::HandleEnterClicked()
{
OnCardPressed.Broadcast(Entry.WorldId, Entry.State);
OnCardPressed.Broadcast(Entry.RealmId, Entry.State);
}
FText UUIServerCard_Base::FormatStatus_Implementation(uint8 State) const

View File

@@ -2,7 +2,7 @@
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZMMOWorldEntry.h"
#include "ZMMORealmEntry.h"
#include "UIServerCard_Base.generated.h"
class UCommonTextBlock;
@@ -11,10 +11,10 @@ class UUIButton_Base;
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
* em "Entrar" dispara `OnCardPressed.Broadcast(WorldId, State)`. A tela
* Recebe um `FZMMORealmEntry` e renderiza nome/pop/status/bar; ao clicar
* em "Entrar" dispara `OnCardPressed.Broadcast(RealmId, State)`. A tela
* dona (UUIServerSelectScreen_Base) escuta esse delegate.
*
* WBP filho (`WBP_ServerCard`) precisa expor (via nomes de variavel):
@@ -25,7 +25,7 @@ class UProgressBar;
* - Btn_Enter : Button
* - 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)
class ZMMO_API UUIServerCard_Base : public UUserWidget
@@ -34,14 +34,14 @@ class ZMMO_API UUIServerCard_Base : public UUserWidget
public:
/**
* Aplica os dados de um mundo no card.
* Aplica os dados de um realm no card.
*/
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerCard")
void SetFromEntry(const FZMMOWorldEntry& Entry);
void SetFromEntry(const FZMMORealmEntry& Entry);
/** Dados atuais (copia). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerCard")
FZMMOWorldEntry Entry;
FZMMORealmEntry Entry;
/**
* 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()
@@ -110,23 +110,23 @@ void UUIServerSelectScreen_Base::RefreshUIStyle_Implementation()
}
}
void UUIServerSelectScreen_Base::RequestWorldList()
void UUIServerSelectScreen_Base::RequestRealmList()
{
UZeusCharServerSubsystem* Char = GetCharServer();
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;
}
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;
}
UGameInstance* GI = GetGameInstance();
@@ -136,18 +136,18 @@ void UUIServerSelectScreen_Base::SelectWorldAndProceed(const FString& WorldId)
}
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->SetSelectedWorldId(WorldId);
Flow->SetSelectedRealmId(RealmId);
Flow->SetState(EZMMOFrontEndState::Lobby);
}
}
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);
}
@@ -155,24 +155,24 @@ void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray
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;
FString WorldId;
FString RealmId;
uint16 Pop = 0;
uint16 QueueLen = 0;
uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, WorldId)
if (!ReadStringUtf8(Payload, Pos, RealmId)
|| !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen)
|| !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;
}
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;
break;
@@ -180,76 +180,76 @@ void UUIServerSelectScreen_Base::ApplyStatusUpdate(const TArray<uint8>& Payload)
}
if (Idx == INDEX_NONE)
{
// Mundo desconhecido (ainda nao recebemos via S_WORLD_LIST). Pede lista
// completa pra trazer dados estaticos (name/host/region/capacity).
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: STATUS_UPDATE de mundo desconhecido (%s) — pedindo lista"), *WorldId);
RequestWorldList();
// Realm desconhecido (ainda nao recebemos via S_REALM_LIST). Pede lista
// completa pra trazer dados estaticos (name/gatewayEndpoint/region/capacity).
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: STATUS_UPDATE de realm desconhecido (%s) — pedindo lista"), *RealmId);
RequestRealmList();
return;
}
FZMMOWorldEntry& Entry = Worlds[Idx];
Entry.Population = static_cast<int32>(Pop);
FZMMORealmEntry& Entry = Realms[Idx];
Entry.Pop = static_cast<int32>(Pop);
Entry.QueueLen = static_cast<int32>(QueueLen);
Entry.State = State;
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: world %s atualizado (state=%d pop=%d)"),
*WorldId, State, Entry.Population);
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: realm %s atualizado (state=%d pop=%d)"),
*RealmId, State, Entry.Pop);
// Atualiza apenas o card afetado (preserva animacoes/scroll dos demais).
if (SpawnedCards.IsValidIndex(Idx) && SpawnedCards[Idx])
{
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;
uint16 Count = 0;
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;
}
TArray<FZMMOWorldEntry> NewWorlds;
NewWorlds.Reserve(Count);
TArray<FZMMORealmEntry> NewRealms;
NewRealms.Reserve(Count);
for (uint16 i = 0; i < Count; ++i)
{
FZMMOWorldEntry Entry;
FZMMORealmEntry Entry;
FString RegionStr;
uint16 Port = 0;
uint16 Cap = 0;
uint16 Pop = 0;
uint16 QueueLen = 0;
uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, Entry.WorldId)
|| !ReadStringUtf8(Payload, Pos, Entry.WorldName)
// Wire: string realmId + string name + string region + string gatewayEndpoint
// + 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, Entry.Host)
|| !ReadU16(Payload, Pos, Port)
|| !ReadStringUtf8(Payload, Pos, Entry.GatewayEndpoint)
|| !ReadU16(Payload, Pos, Cap)
|| !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen)
|| !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;
}
Entry.Region = RegionStr;
Entry.Port = static_cast<int32>(Port);
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.State = State;
NewWorlds.Add(MoveTemp(Entry));
NewRealms.Add(MoveTemp(Entry));
}
Worlds = MoveTemp(NewWorlds);
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: recebidos %d mundos"), Worlds.Num());
Realms = MoveTemp(NewRealms);
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: recebidos %d realms"), Realms.Num());
RebuildCards();
OnWorldListReceived();
OnRealmListReceived();
}
void UUIServerSelectScreen_Base::RebuildCards()
@@ -280,15 +280,15 @@ void UUIServerSelectScreen_Base::RebuildCards()
const int32 GridColumns = 2; // bate com o mock (ColumnFill[0]/[1])
int32 Index = 0;
for (const FZMMOWorldEntry& W : Worlds)
for (const FZMMORealmEntry& R : Realms)
{
UUIServerCard_Base* Card = CreateWidget<UUIServerCard_Base>(this, CardClass);
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;
}
Card->SetFromEntry(W);
Card->SetFromEntry(R);
Card->OnCardPressed.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCardPressed);
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/
// offline) — o usuario entra no Lobby pra gerenciar chars desse mundo.
// O bloqueio real (handoff -> world) acontece no botao "Entrar no
// Servidor" do Lobby, que checa World.State antes do C_CHAR_SELECT.
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: mundo %s selecionado (state=%d)"), *WorldId, State);
SelectWorldAndProceed(WorldId);
// O ServerSelect permite escolher qualquer realm (online/maintenance/
// offline) — o usuario entra no Lobby pra gerenciar chars desse realm.
// O bloqueio real (handoff -> gateway) acontece no botao "Entrar no
// Servidor" do Lobby, que checa Realm.State antes do C_CHAR_SELECT.
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: realm %s selecionado (state=%d)"), *RealmId, State);
SelectRealmAndProceed(RealmId);
}
UZeusCharServerSubsystem* UUIServerSelectScreen_Base::GetCharServer() const

View File

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

View File

@@ -66,27 +66,27 @@ void UUIUserLobbyScreen_Base::RequestCharList()
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected()) return;
FString WorldId;
FString RealmId;
if (UGameInstance* GI = GetGameInstance())
{
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;
if (WorldId.IsEmpty())
if (RealmId.IsEmpty())
{
Payload.Add(0);
}
else
{
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.Add(0);
}
@@ -110,7 +110,7 @@ void UUIUserLobbyScreen_Base::BackToServerSelect()
if (!GI) return;
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->ClearSelectedWorld();
Flow->ClearSelectedRealm();
Flow->SetState(EZMMOFrontEndState::ServerSelect);
}
}
@@ -160,7 +160,7 @@ void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
uint64 deleteAt = 0;
if (!ReadU64(Payload, Pos, charId64)
|| !ReadUuid16(Payload, Pos, E.WorldId)
|| !ReadUuid16(Payload, Pos, E.RealmId)
|| !ReadU8(Payload, Pos, slot)
|| !ReadStringUtf8(Payload, Pos, E.Name)
|| !ReadU16(Payload, Pos, classId)
@@ -328,25 +328,45 @@ void UUIUserLobbyScreen_Base::HandleCharDeleteCancelAck(const TArray<uint8>& Pay
void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
{
// 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;
uint64 CharId64 = 0; FString WorldHost, HandoffToken, Region;
uint64 CharId64 = 0; FString GatewayEndpoint, HandoffToken, Region;
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)
|| !ReadU16(Payload, Pos, MapId16)
|| !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz)
|| !ReadFloat(Payload, Pos, Yaw)
|| !ReadStringUtf8(Payload, Pos, WorldHost)
|| !ReadU16(Payload, Pos, WorldPort)
|| !ReadStringUtf8(Payload, Pos, GatewayEndpoint)
|| !ReadStringUtf8(Payload, Pos, HandoffToken)
|| !ReadStringUtf8(Payload, Pos, Region))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado"));
return;
}
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d mapId=%u token=%s..."),
*WorldHost, WorldPort, static_cast<uint32>(MapId16), *HandoffToken.Left(8));
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> realm gateway=%s mapId=%u token=%s..."),
*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();
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
// 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).
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
{
ZeusNet->ConnectToZeusServerWithTicket(WorldHost, static_cast<int32>(WorldPort), HandoffToken);
ZeusNet->ConnectToZeusServerWithTicket(GwHost, GwPort, HandoffToken);
}
else
{

View File

@@ -18,13 +18,13 @@ class UZeusCharServerSubsystem;
* Tela Lobby (host) — Fase 1.5 do ARQUITETURA_SERVER_SELECT.
*
* Fluxo:
* 1. NativeOnActivated: pega `SelectedWorldId` do FlowSubsystem, envia
* C_CHAR_LIST_REQUEST filtrado por esse mundo
* 1. NativeOnActivated: pega `SelectedRealmId` do FlowSubsystem, envia
* C_CHAR_LIST_REQUEST filtrado por esse realm
* 2. Parse S_CHAR_LIST -> Chars[]; instancia 1 WBP_CharCard por entry
* 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`
* 5. Botao "Voltar" -> ClearSelectedWorld + Flow.SetState(ServerSelect)
* 5. Botao "Voltar" -> ClearSelectedRealm + Flow.SetState(ServerSelect)
*
* Pages internas via UWidgetSwitcher:
* - PageIndex 0: Lista de chars (CardContainer)
@@ -96,6 +96,9 @@ protected:
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUICharacterCreatePage_Base> CharCreatePage;
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<UUIButton_Base> Btn_Back;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Create;

View File

@@ -43,7 +43,7 @@ struct FZMMOCharSummary
FString CharId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString WorldId;
FString RealmId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
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;
};