From 6968ac4656f1b75ce411d25df6a7f0b328ce5ed0 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 22 May 2026 22:01:33 -0300 Subject: [PATCH] fix(client): catch-up de presenca + wire helpers compartilhados MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ZMMO/Game/Network/ZMMOWorldSubsystem.cpp | 27 +++- Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h | 1 + .../FrontEnd/UICharacterCreatePage_Base.cpp | 48 +------ .../UI/FrontEnd/UIServerSelectScreen_Base.cpp | 39 +---- .../UI/FrontEnd/UIUserLobbyScreen_Base.cpp | 99 ++----------- Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h | 136 ++++++++++++++++++ 6 files changed, 183 insertions(+), 167 deletions(-) create mode 100644 Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h diff --git a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp index f5ce148..cf72d08 100644 --- a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp @@ -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.")); } } diff --git a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h index 3359784..44edfcf 100644 --- a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h +++ b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h @@ -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; /** diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp index 0b578f3..e318910 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp @@ -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& 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((H << 4) | L)); - } - return true; - } - - void WriteUInt8(TArray& Buf, uint8 v) { Buf.Add(v); } - void WriteUInt16(TArray& Buf, uint16 v) - { - Buf.Add(static_cast(v & 0xFF)); - Buf.Add(static_cast((v >> 8) & 0xFF)); - } - void WriteUInt32(TArray& Buf, uint32 v) - { - for (int32 i = 0; i < 4; ++i) Buf.Add(static_cast((v >> (8 * i)) & 0xFF)); - } - void WriteStringUtf8(TArray& Buf, const FString& S) - { - FTCHARToUTF8 Conv(*S); - const int32 Len = Conv.Length(); - WriteUInt16(Buf, static_cast(Len)); - Buf.Append(reinterpret_cast(Conv.Get()), Len); - } -} +using ZMMOWire::WriteUuid16; +using ZMMOWire::WriteUInt8; +using ZMMOWire::WriteUInt16; +using ZMMOWire::WriteUInt32; +using ZMMOWire::WriteStringUtf8; void UUICharacterCreatePage_Base::NativeConstruct() { diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp index c624a48..671717b 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp @@ -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& Buf, int32& Pos, uint8& Out) - { - if (Pos + 1 > Buf.Num()) return false; - Out = Buf[Pos]; - Pos += 1; - return true; - } - - bool ReadU16(const TArray& Buf, int32& Pos, uint16& Out) - { - if (Pos + 2 > Buf.Num()) return false; - Out = static_cast(Buf[Pos]) | (static_cast(Buf[Pos + 1]) << 8); - Pos += 2; - return true; - } - - bool ReadStringUtf8(const TArray& 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(Buf.GetData() + Pos), Len)); - } - Pos += Len; - return true; - } } void UUIServerSelectScreen_Base::NativePreConstruct() diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp index 56655d4..c436de2 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp @@ -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& Buf, int32& Pos, uint8& Out) - { - if (Pos + 1 > Buf.Num()) return false; - Out = Buf[Pos]; Pos += 1; return true; - } - bool ReadU16(const TArray& Buf, int32& Pos, uint16& Out) - { - if (Pos + 2 > Buf.Num()) return false; - Out = static_cast(Buf[Pos]) | (static_cast(Buf[Pos + 1]) << 8); - Pos += 2; return true; - } - bool ReadU32(const TArray& Buf, int32& Pos, uint32& Out) - { - if (Pos + 4 > Buf.Num()) return false; - Out = static_cast(Buf[Pos]) - | (static_cast(Buf[Pos + 1]) << 8) - | (static_cast(Buf[Pos + 2]) << 16) - | (static_cast(Buf[Pos + 3]) << 24); - Pos += 4; return true; - } - bool ReadU64(const TArray& Buf, int32& Pos, uint64& Out) - { - if (Pos + 8 > Buf.Num()) return false; - Out = 0; - for (int i = 0; i < 8; ++i) Out |= (static_cast(Buf[Pos + i]) << (8 * i)); - Pos += 8; return true; - } - bool ReadFloat(const TArray& 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& 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(Buf.GetData() + Pos), Len)); - Pos += Len; - return true; - } - bool ReadUuid16(const TArray& 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& 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((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() { diff --git a/Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h b/Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h new file mode 100644 index 0000000..c383bcc --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h @@ -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& Buf, int32& Pos, uint8& Out) + { + if (Pos + 1 > Buf.Num()) return false; + Out = Buf[Pos]; + Pos += 1; + return true; + } + + inline bool ReadU16(const TArray& Buf, int32& Pos, uint16& Out) + { + if (Pos + 2 > Buf.Num()) return false; + Out = static_cast(Buf[Pos]) | (static_cast(Buf[Pos + 1]) << 8); + Pos += 2; + return true; + } + + inline bool ReadU32(const TArray& Buf, int32& Pos, uint32& Out) + { + if (Pos + 4 > Buf.Num()) return false; + Out = static_cast(Buf[Pos]) + | (static_cast(Buf[Pos + 1]) << 8) + | (static_cast(Buf[Pos + 2]) << 16) + | (static_cast(Buf[Pos + 3]) << 24); + Pos += 4; + return true; + } + + inline bool ReadU64(const TArray& Buf, int32& Pos, uint64& Out) + { + if (Pos + 8 > Buf.Num()) return false; + Out = 0; + for (int32 i = 0; i < 8; ++i) + { + Out |= (static_cast(Buf[Pos + i]) << (8 * i)); + } + Pos += 8; + return true; + } + + inline bool ReadFloat(const TArray& 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& 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(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& 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& Buf, uint8 V) { Buf.Add(V); } + + inline void WriteUInt16(TArray& Buf, uint16 V) + { + Buf.Add(static_cast(V & 0xFF)); + Buf.Add(static_cast((V >> 8) & 0xFF)); + } + + inline void WriteUInt32(TArray& Buf, uint32 V) + { + for (int32 I = 0; I < 4; ++I) Buf.Add(static_cast((V >> (8 * I)) & 0xFF)); + } + + inline void WriteStringUtf8(TArray& Buf, const FString& S) + { + FTCHARToUTF8 Conv(*S); + const int32 Len = Conv.Length(); + WriteUInt16(Buf, static_cast(Len)); + Buf.Append(reinterpret_cast(Conv.Get()), Len); + } + + /** Aceita UUID canonico (com hifens) e escreve 16 bytes binarios. */ + inline bool WriteUuid16(TArray& 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((H << 4) | L)); + } + return true; + } +}