fix(client): catch-up de presenca + wire helpers compartilhados

ZMMOWorldSubsystem:
- Bind dos delegates + replay do cache de proxies remotos movidos
  de Initialize pra OnWorldBeginPlay. Initialize roda muito cedo no
  pipeline de LoadMap (antes do GameMode ser instanciado e do
  WorldPartition terminar de carregar cells) — atores dinamicos
  spawnados ali eram perdidos durante o setup posterior do mundo.
- Replay agora acontece quando o World REALMENTE comeca (GameMode
  ativo, cells inicializadas) — proxies remotos persistem.

WireHelpers.h (novo):
- Helpers de leitura/escrita binarios LE/UTF-8 (ReadU8/U16/U32/U64/
  Float/StringUtf8/Uuid16 + Write*) como inline em namespace
  ZMMOWire. Substitui anonymous namespaces duplicados que existiam
  em 3 telas do FrontEnd. UBT Adaptive Build agrupava os .cpp em
  unity files diferentes; quando reorganizava as unities, dois
  arquivos caiam na mesma TU e os anonymous namespaces colidiam
  (ODR violation — "funcao ja tem corpo").
- inline em header tem semantica de "uma definicao por programa",
  resolvendo a colisao independente de como o UBT organize unities.

UIServerSelectScreen_Base, UIUserLobbyScreen_Base, UICharacterCreatePage_Base:
- Removidas as definicoes duplicadas, agora usam ZMMOWire::* via
  using-declarations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 22:01:33 -03:00
parent 99aee52317
commit 6968ac4656
6 changed files with 183 additions and 167 deletions

View File

@@ -19,6 +19,15 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
RemoteEntityClasses.Add(EZMMOEntityType::NPC, AZMMOEntity::StaticClass());
RemoteEntityClasses.Add(EZMMOEntityType::Object, AZMMOEntity::StaticClass());
}
// Bind + replay vivem em OnWorldBeginPlay (mundo pronto). Initialize roda
// MUITO cedo no pipeline de LoadMap — antes do GameMode ser instanciado
// e antes do WorldPartition terminar de carregar cells. Spawns dinamicos
// feitos aqui podem ser perdidos durante a fase de setup posterior.
}
void UZMMOWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
{
Super::OnWorldBeginPlay(InWorld);
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
{
@@ -26,10 +35,26 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerDespawned);
ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerStateUpdate);
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem bound to ZeusNetworkSubsystem delegates."));
// Catch-up race fix (Fase 3): broadcasts de proxies remotos chegam
// enquanto o cliente novo ainda esta em OpenLevel. UZeusNetworkSubsystem
// cacheia em PendingRemoteSpawnsByEntity. Aplicamos aqui — mundo ja
// esta pronto (GameMode ativo, WorldPartition cells inicializadas).
int32 ReplayCount = 0;
ZeusNet->ForEachPendingRemoteSpawn(
[this, &ReplayCount](const int32 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs);
++ReplayCount;
});
if (ReplayCount > 0)
{
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount);
}
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at Initialize."));
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay."));
}
}

View File

@@ -44,6 +44,7 @@ class ZMMO_API UZMMOWorldSubsystem : public UWorldSubsystem
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
virtual void Deinitialize() override;
/**

View File

@@ -3,6 +3,7 @@
#include "ZMMO.h"
#include "CharServerOpcodes.h"
#include "UIFrontEndFlowSubsystem.h"
#include "WireHelpers.h"
#include "ZeusCharServerSubsystem.h"
#include "Components/EditableTextBox.h"
#include "Components/ComboBoxString.h"
@@ -10,48 +11,11 @@
#include "Engine/GameInstance.h"
#include "UI/Widgets/UIButton_Base.h"
namespace
{
bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
{
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
if (Hex.Len() != 32) return false;
for (int32 i = 0; i < 16; ++i)
{
TCHAR hi = Hex[i * 2];
TCHAR lo = Hex[i * 2 + 1];
auto HexVal = [](TCHAR c) -> int32 {
if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0');
if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10;
if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10;
return -1;
};
int32 H = HexVal(hi);
int32 L = HexVal(lo);
if (H < 0 || L < 0) return false;
Buf.Add(static_cast<uint8>((H << 4) | L));
}
return true;
}
void WriteUInt8(TArray<uint8>& Buf, uint8 v) { Buf.Add(v); }
void WriteUInt16(TArray<uint8>& Buf, uint16 v)
{
Buf.Add(static_cast<uint8>(v & 0xFF));
Buf.Add(static_cast<uint8>((v >> 8) & 0xFF));
}
void WriteUInt32(TArray<uint8>& Buf, uint32 v)
{
for (int32 i = 0; i < 4; ++i) Buf.Add(static_cast<uint8>((v >> (8 * i)) & 0xFF));
}
void WriteStringUtf8(TArray<uint8>& Buf, const FString& S)
{
FTCHARToUTF8 Conv(*S);
const int32 Len = Conv.Length();
WriteUInt16(Buf, static_cast<uint16>(Len));
Buf.Append(reinterpret_cast<const uint8*>(Conv.Get()), Len);
}
}
using ZMMOWire::WriteUuid16;
using ZMMOWire::WriteUInt8;
using ZMMOWire::WriteUInt16;
using ZMMOWire::WriteUInt32;
using ZMMOWire::WriteStringUtf8;
void UUICharacterCreatePage_Base::NativeConstruct()
{

View File

@@ -5,6 +5,7 @@
#include "CharServerOpcodes.h"
#include "UIFrontEndFlowSubsystem.h"
#include "UIServerCard_Base.h"
#include "WireHelpers.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonTextBlock.h"
#include "Components/Border.h"
@@ -15,6 +16,10 @@
#include "Engine/DataTable.h"
#include "UI/UIStyleRow.h"
using ZMMOWire::ReadU8;
using ZMMOWire::ReadU16;
using ZMMOWire::ReadStringUtf8;
namespace
{
FUIStyle ResolveServerSelectStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
@@ -43,40 +48,6 @@ namespace
}
return bHas ? DesignStyle : Fallback;
}
// ---- Wire reader (LE) ----
bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
{
if (Pos + 1 > Buf.Num()) return false;
Out = Buf[Pos];
Pos += 1;
return true;
}
bool ReadU16(const TArray<uint8>& Buf, int32& Pos, uint16& Out)
{
if (Pos + 2 > Buf.Num()) return false;
Out = static_cast<uint16>(Buf[Pos]) | (static_cast<uint16>(Buf[Pos + 1]) << 8);
Pos += 2;
return true;
}
bool ReadStringUtf8(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
uint16 Len = 0;
if (!ReadU16(Buf, Pos, Len)) return false;
if (Pos + Len > Buf.Num()) return false;
if (Len == 0)
{
Out.Reset();
}
else
{
Out = FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buf.GetData() + Pos), Len));
}
Pos += Len;
return true;
}
}
void UUIServerSelectScreen_Base::NativePreConstruct()

View File

@@ -5,6 +5,7 @@
#include "UIFrontEndFlowSubsystem.h"
#include "UICharCard_Base.h"
#include "UICharacterCreatePage_Base.h"
#include "WireHelpers.h"
#include "ZeusCharServerSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "CommonTextBlock.h"
@@ -13,96 +14,14 @@
#include "Engine/GameInstance.h"
#include "UI/Widgets/UIButton_Base.h"
namespace
{
bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
{
if (Pos + 1 > Buf.Num()) return false;
Out = Buf[Pos]; Pos += 1; return true;
}
bool ReadU16(const TArray<uint8>& Buf, int32& Pos, uint16& Out)
{
if (Pos + 2 > Buf.Num()) return false;
Out = static_cast<uint16>(Buf[Pos]) | (static_cast<uint16>(Buf[Pos + 1]) << 8);
Pos += 2; return true;
}
bool ReadU32(const TArray<uint8>& Buf, int32& Pos, uint32& Out)
{
if (Pos + 4 > Buf.Num()) return false;
Out = static_cast<uint32>(Buf[Pos])
| (static_cast<uint32>(Buf[Pos + 1]) << 8)
| (static_cast<uint32>(Buf[Pos + 2]) << 16)
| (static_cast<uint32>(Buf[Pos + 3]) << 24);
Pos += 4; return true;
}
bool ReadU64(const TArray<uint8>& Buf, int32& Pos, uint64& Out)
{
if (Pos + 8 > Buf.Num()) return false;
Out = 0;
for (int i = 0; i < 8; ++i) Out |= (static_cast<uint64>(Buf[Pos + i]) << (8 * i));
Pos += 8; return true;
}
bool ReadFloat(const TArray<uint8>& Buf, int32& Pos, float& Out)
{
uint32 raw;
if (!ReadU32(Buf, Pos, raw)) return false;
FMemory::Memcpy(&Out, &raw, sizeof(float));
return true;
}
bool ReadStringUtf8(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
uint16 Len = 0;
if (!ReadU16(Buf, Pos, Len)) return false;
if (Pos + Len > Buf.Num()) return false;
if (Len == 0) { Out.Reset(); return true; }
Out = FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buf.GetData() + Pos), Len));
Pos += Len;
return true;
}
bool ReadUuid16(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
if (Pos + 16 > Buf.Num()) return false;
// 16 bytes raw -> 8-4-4-4-12
auto Hex = [](uint8 b, ANSICHAR* dst)
{
static const ANSICHAR* H = "0123456789abcdef";
dst[0] = H[(b >> 4) & 0xF];
dst[1] = H[b & 0xF];
};
ANSICHAR raw[37]; raw[36] = 0;
int32 j = 0;
for (int32 i = 0; i < 16; ++i)
{
if (i == 4 || i == 6 || i == 8 || i == 10) raw[j++] = '-';
Hex(Buf[Pos + i], &raw[j]); j += 2;
}
Out = ANSI_TO_TCHAR(raw);
Pos += 16;
return true;
}
bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
{
// Espera xxxxxxxx-xxxx-...
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
if (Hex.Len() != 32) return false;
for (int32 i = 0; i < 16; ++i)
{
TCHAR hi = Hex[i * 2];
TCHAR lo = Hex[i * 2 + 1];
auto HexVal = [](TCHAR c) -> int32 {
if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0');
if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10;
if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10;
return -1;
};
int32 H = HexVal(hi);
int32 L = HexVal(lo);
if (H < 0 || L < 0) return false;
Buf.Add(static_cast<uint8>((H << 4) | L));
}
return true;
}
}
using ZMMOWire::ReadU8;
using ZMMOWire::ReadU16;
using ZMMOWire::ReadU32;
using ZMMOWire::ReadU64;
using ZMMOWire::ReadFloat;
using ZMMOWire::ReadStringUtf8;
using ZMMOWire::ReadUuid16;
using ZMMOWire::WriteUuid16;
void UUIUserLobbyScreen_Base::NativeOnActivated()
{

View File

@@ -0,0 +1,136 @@
#pragma once
// Helpers de leitura/escrita binarios LE (UTF-8) compartilhados pelas telas
// do FrontEnd (Boot/Login/ServerSelect/Lobby/CharCreate). Funcoes `inline`
// pra evitar ODR violation quando dois .cpp caem no mesmo unity build do
// UBT — definicoes em anonymous namespace cada um colidiam apos a
// reorganizacao automatica de unities.
#include "CoreMinimal.h"
namespace ZMMOWire
{
inline bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
{
if (Pos + 1 > Buf.Num()) return false;
Out = Buf[Pos];
Pos += 1;
return true;
}
inline bool ReadU16(const TArray<uint8>& Buf, int32& Pos, uint16& Out)
{
if (Pos + 2 > Buf.Num()) return false;
Out = static_cast<uint16>(Buf[Pos]) | (static_cast<uint16>(Buf[Pos + 1]) << 8);
Pos += 2;
return true;
}
inline bool ReadU32(const TArray<uint8>& Buf, int32& Pos, uint32& Out)
{
if (Pos + 4 > Buf.Num()) return false;
Out = static_cast<uint32>(Buf[Pos])
| (static_cast<uint32>(Buf[Pos + 1]) << 8)
| (static_cast<uint32>(Buf[Pos + 2]) << 16)
| (static_cast<uint32>(Buf[Pos + 3]) << 24);
Pos += 4;
return true;
}
inline bool ReadU64(const TArray<uint8>& Buf, int32& Pos, uint64& Out)
{
if (Pos + 8 > Buf.Num()) return false;
Out = 0;
for (int32 i = 0; i < 8; ++i)
{
Out |= (static_cast<uint64>(Buf[Pos + i]) << (8 * i));
}
Pos += 8;
return true;
}
inline bool ReadFloat(const TArray<uint8>& Buf, int32& Pos, float& Out)
{
uint32 Raw = 0;
if (!ReadU32(Buf, Pos, Raw)) return false;
FMemory::Memcpy(&Out, &Raw, sizeof(float));
return true;
}
inline bool ReadStringUtf8(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
uint16 Len = 0;
if (!ReadU16(Buf, Pos, Len)) return false;
if (Pos + Len > Buf.Num()) return false;
if (Len == 0) { Out.Reset(); return true; }
Out = FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buf.GetData() + Pos), Len));
Pos += Len;
return true;
}
/** Le 16 bytes raw e devolve UUID canonico "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". */
inline bool ReadUuid16(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
if (Pos + 16 > Buf.Num()) return false;
auto HexNibble = [](uint8 B, ANSICHAR* Dst)
{
static const ANSICHAR* H = "0123456789abcdef";
Dst[0] = H[(B >> 4) & 0xF];
Dst[1] = H[B & 0xF];
};
ANSICHAR Raw[37]; Raw[36] = 0;
int32 J = 0;
for (int32 I = 0; I < 16; ++I)
{
if (I == 4 || I == 6 || I == 8 || I == 10) Raw[J++] = '-';
HexNibble(Buf[Pos + I], &Raw[J]);
J += 2;
}
Out = ANSI_TO_TCHAR(Raw);
Pos += 16;
return true;
}
inline void WriteUInt8(TArray<uint8>& Buf, uint8 V) { Buf.Add(V); }
inline void WriteUInt16(TArray<uint8>& Buf, uint16 V)
{
Buf.Add(static_cast<uint8>(V & 0xFF));
Buf.Add(static_cast<uint8>((V >> 8) & 0xFF));
}
inline void WriteUInt32(TArray<uint8>& Buf, uint32 V)
{
for (int32 I = 0; I < 4; ++I) Buf.Add(static_cast<uint8>((V >> (8 * I)) & 0xFF));
}
inline void WriteStringUtf8(TArray<uint8>& Buf, const FString& S)
{
FTCHARToUTF8 Conv(*S);
const int32 Len = Conv.Length();
WriteUInt16(Buf, static_cast<uint16>(Len));
Buf.Append(reinterpret_cast<const uint8*>(Conv.Get()), Len);
}
/** Aceita UUID canonico (com hifens) e escreve 16 bytes binarios. */
inline bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
{
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
if (Hex.Len() != 32) return false;
auto HexVal = [](TCHAR C) -> int32
{
if (C >= TEXT('0') && C <= TEXT('9')) return C - TEXT('0');
if (C >= TEXT('a') && C <= TEXT('f')) return C - TEXT('a') + 10;
if (C >= TEXT('A') && C <= TEXT('F')) return C - TEXT('A') + 10;
return -1;
};
for (int32 I = 0; I < 16; ++I)
{
const int32 H = HexVal(Hex[I * 2]);
const int32 L = HexVal(Hex[I * 2 + 1]);
if (H < 0 || L < 0) return false;
Buf.Add(static_cast<uint8>((H << 4) | L));
}
return true;
}
}