From e87b2cec6ced9d8c8c7c8520e21f2f595e79046c Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Mon, 8 Jun 2026 20:33:41 -0300 Subject: [PATCH 01/10] fix(zmmo-client): guard HandlePlayerSpawned ignore proprio entityId + ZeusV1 config Acompanha commit server 54f2f9a (P10-FIX-LEGACY). Resolve bug "vejo proxy de UM mas o outro NAO ve" + "proxy fantasma do proprio player" -- causado por server emitir S_SPAWN_PLAYER local=0 do proprio entityId via AOI re-emit (workflow wcyeqgrad finding). Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp ================================================ HandlePlayerSpawned guard novo logo apos EntityId==0 check: if (LocalEntityId != 0 && EntityId == LocalEntityId) { UE_LOG(LogZMMO, Verbose, TEXT("ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"), LocalEntityId, bIsLocal ? 1 : 0); return; } Sem o guard, server re-emit S_SPAWN_PLAYER local=0 do proprio entityId (via AOI SetOnEnter callback em catch-up cross-server ou recompute pos- handoff) criava proxy fantasma do proprio player. GAS/HUD/PlayerArray roteava via fantasma e o pawn real perdia vinculo de identidade visivel pros outros players. Defesa em profundidade -- complementa fix server-side (AoiReplicationHelpers.cpp:28 SendSpawnPlayerToConn agora tem skip-self guard antes de construir payload) + SpawnKey dedupe sem IsLocal no plugin (ZeusNetworkSubsystem.cpp:949 removeu IsLocal do hash). Config/DefaultZeusV1.ini ========================= Config inicial para UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin V1). Aponta para Gateway 127.0.0.1:7777 (fluxo padrao producao). Documenta alternativa de direct connect 9001 (debug only -- bug Windows multi-NIC loopback). bUseZeusNetworkingV1=True ativa V1 plugin em paralelo ao legacy (transport V1 hoje quebrado via Gateway -- channelId nao eh routing key, Phase 12 territory; gameplay continua via legacy ate la). Validado smoke 2 PIE 2026-06-08 20:32 ====================================== * SP_01: players=2 conns=2 + AOI players=2 maxSet=1 enters=5 hyst=92 stable * UE: [PIE0] e [PIE1] prefix funcionando (commit server 54f2f9a ZNET_PIE_PREFIX) * "S_SPAWN_PLAYER duplicate ignored" dedupe pegando re-emit (Batch 2) * S_CHAR_INFO trocado correto: PIE0 ve Olatudook, PIE1 ve Mateuus * Saida limpa: UnregisterViewer + Broadcast S_DESPAWN_PLAYER targets=1 Co-Authored-By: Claude Opus 4.7 --- Config/DefaultZeusV1.ini | 19 +++++++++++++++++++ .../ZMMO/Game/Network/ZeusWorldSubsystem.cpp | 15 +++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 Config/DefaultZeusV1.ini diff --git a/Config/DefaultZeusV1.ini b/Config/DefaultZeusV1.ini new file mode 100644 index 0000000..c163a79 --- /dev/null +++ b/Config/DefaultZeusV1.ini @@ -0,0 +1,19 @@ +; DefaultZeusV1.ini +; Config inicial pra UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin). +; Editar via UE Project Settings -> Plugins -> Zeus V1 OU editar este arquivo +; e relancar o editor. +; +; PORT 7777: ZeusGateway. NodeService popula routes via charId lookup quando +; cliente conecta (validado: handshake routed charId=7 -> SP_01 cell 0). Fluxo +; padrao de producao -- UE conecta no Gateway, Gateway resolve cell ownership +; via lease em Valkey + encaminha pro ZS correto. +; +; ALTERNATIVO (debug only -- bug aberto pendente investigacao): +; ServerPort=9001 conecta DIRETO em SP_01 mas hoje pacote UE nao chega no +; server (FUdpSocketBuilder + Windows multi-NIC loopback issue). Use 7777. + +[/Script/ZeusNetwork.ZeusNetworkingClientSubsystem] +ServerHost=127.0.0.1 +ServerPort=7777 +bUseZeusNetworkingV1=True +bAutoConnectOnStart=True diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp index e1897ca..4abef2d 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp @@ -96,6 +96,21 @@ void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool b return; } + // P10-FIX-LEGACY (workflow wcyeqgrad finding): server pode reenviar + // S_SPAWN_PLAYER com bIsLocal=false do PROPRIO entityId via AOI re-emit + // (catch-up cross-server, SetOnEnter callback recomputando InterestSet, + // handoff em flight). Sem guard, o segundo spawn cria proxy fantasma do + // proprio player; GAS/HUD passa a rotear via proxy fantasma e o pawn real + // perde vinculo. Sintoma observado: player ve 2 pawns sobrepostos, ou + // outros players nao veem o autoritativo. + if (LocalEntityId != 0 && EntityId == LocalEntityId) + { + UE_LOG(LogZMMO, Verbose, + TEXT("ZeusWorldSubsystem: ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"), + LocalEntityId, bIsLocal ? 1 : 0); + return; + } + if (bIsLocal) { // O proprio AZeusCharacter cuida da sua identidade quando recebe o From f21d059b67473ca5b55f68b8ca00ec12702ecb2e Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Mon, 8 Jun 2026 20:36:28 -0300 Subject: [PATCH 02/10] fix(zmmo-client): ZeusPlayerProxy clock offset EMA + asset cleanup L_TestWorld ZeusPlayerProxy.cpp -- H3 fix (2026-06-06): EMA com clamp para ServerClockOffsetMs ================================================================ Antes: offset congelado na 1a amostra de ApplyEntitySnapshot. Em cross-server handoff, a fonte do ServerTimeMs muda (publisher antigo emitia 12340000, novo emite 12351000) e o offset fica permanentemente skewed. Causa tremor pos-handoff + drift cumulativo de clock cliente vs server. Depois: cada amostra recalcula candidato (LocalNowMs - Snapshot.ServerTimeMs) e suaviza via EMA com alpha=0.02 (~50 amostras para convergir) + clamp max 2ms por amostra (a 30Hz isso converge a 60ms/s, suficiente para absorver drift normal sem causar tremor visual). 1a amostra continua bootstrap direto. Trecho: const double Alpha = 0.02; const int64 RawTarget = ServerClockOffsetMs*(1-Alpha) + OffsetCandidate*Alpha; const int64 Delta = RawTarget - ServerClockOffsetMs; const int64 ClampedDelta = clamp(Delta, -2, 2); ServerClockOffsetMs += ClampedDelta; Asset cleanup L_TestWorld ========================== DT_Maps.uasset modificado + 3 ExternalActors deletados (provavelmente limpeza no UE editor ao reorganizar mapa). Empacotado junto pra branch ficar limpa. * Content/ZMMO/Data/World/DT_Maps.uasset (M) * Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset (D) * Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset (D) * Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset (D) Co-Authored-By: Claude Opus 4.7 --- Content/ZMMO/Data/World/DT_Maps.uasset | Bin 3062 -> 3062 bytes .../2/BR/38W34OMKW0W3L1PGS3Y98G.uasset | Bin 6229 -> 0 bytes .../4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset | Bin 10597 -> 0 bytes .../8/L1/C6M65Q91PG5E7ML91VQZX3.uasset | Bin 10911 -> 0 bytes Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp | 30 +++++++++++++++--- 5 files changed, 25 insertions(+), 5 deletions(-) delete mode 100644 Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset delete mode 100644 Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset delete mode 100644 Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset diff --git a/Content/ZMMO/Data/World/DT_Maps.uasset b/Content/ZMMO/Data/World/DT_Maps.uasset index fd76096e26156d7b7f1640739312026d2335a999..be9dffcb34bfd67041cab4174057685561e27a0f 100644 GIT binary patch delta 91 zcmew+{!M&>gvf`6Xx(i>x}N#(9DmlITJE7QxNQAKo!KnPsinofi3KjHX$-HucJ1e7 sU|@)X2n81;mgg~iWLn!Wu|R1v59?}XW}w98T^vzNObiT@#kd~;0J6~{e*gdg delta 91 zcmew+{!M&>gh*d;ed*QEr)%GR`EkEFadXM!Yld?+>dai7C@zvKaRR0D$!+m;e9( diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset deleted file mode 100644 index 3e4faf32d44955b74ab304bb78f07380f066bf4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6229 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 diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset deleted file mode 100644 index 2552681ede7cd4fe98b38bf630e551f287bc0288..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10597 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 diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset deleted file mode 100644 index 059ff5ab3a4fa01683ff1c6b4bd062109cc2d208..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10911 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 L^7h%HE+GFulJ_zh diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp index 574cb68..eb1ed6d 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp @@ -222,13 +222,33 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) { - // Estabiliza o offset de relogio na primeira amostra. Subsequente nao - // re-sincroniza para evitar saltos visuais; um esquema mais sofisticado - // (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao. + // 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp. + // Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte + // do ServerTimeMs muda (publisher antigo -> novo) e o offset fica + // permanentemente skewed, alimentando tremor pos-handoff e drift cumulativo + // de clock cliente vs server. Agora cada amostra recalcula candidato e + // suaviza (alpha=0.02, ~50 amostras pra convergir) com clamp max 2ms/snapshot + // pra evitar salto visual durante transicao. + const int64 LocalNowMs = static_cast(FPlatformTime::Seconds() * 1000.0); + const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs; if (SnapshotBuffer.Num() == 0) { - const int64 LocalNowMs = static_cast(FPlatformTime::Seconds() * 1000.0); - ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs; + // Primeira amostra: bootstrap direto (nao tem baseline pra suavizar). + ServerClockOffsetMs = OffsetCandidate; + } + else + { + const double Alpha = 0.02; + const int64 RawTarget = static_cast( + static_cast(ServerClockOffsetMs) * (1.0 - Alpha) + + static_cast(OffsetCandidate) * Alpha); + // Clamp max 2ms por amostra — em 30Hz isso converge a 60ms/s, mais que + // o suficiente pra absorver drift normal sem causar tremor visual. + const int64 Delta = RawTarget - ServerClockOffsetMs; + const int64 ClampedDelta = (Delta > 2) ? 2 + : (Delta < -2) ? -2 + : Delta; + ServerClockOffsetMs += ClampedDelta; } FZeusProxySnapshot S; From c0f6d32d95ef2676a8bd8dd7231c8ad230bdf26a Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Thu, 11 Jun 2026 18:01:32 -0300 Subject: [PATCH 03/10] feat(zmmo-client): logs AOI categorias + K4 cfg recv Rodando sem Jitter visivel. - DECLARE/DEFINE LogZeusHandoff/AOI/Proxy/HaloTransition no modulo ZMMO (nao no plugin) por causa do adaptive non-unity build do UBT que usa git status do repo do cliente -- plugin em outro repo nao dispara rebuild quando edit so o header dele. - ZeusAOIComponent::HandleAoiConfig loga raios recebidos do server pra cruzar com Net.AOI server-side ("AOI cfg recv interestCm=6000 (60m) despawnCm=8000 (80m)"). - Parte da milestone 2026-06-11: proxy/PIE estaveis apos fix de double-emit AOI no server (AOIRegistry::ForceTickNow). --- Source/ZMMO/Game/Network/ZeusAOIComponent.cpp | 7 ++++++ Source/ZMMO/ZMMO.cpp | 10 +++++++- Source/ZMMO/ZMMONetLog.h | 24 +++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 Source/ZMMO/ZMMONetLog.h diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp index 4ef4005..abcb5b6 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp @@ -3,6 +3,7 @@ #include "ZeusAOIComponent.h" #include "ZeusNetworkSubsystem.h" // plugin ZeusNetwork: OnDebugAoiInfo + SendDebugAoiRequest +#include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO) #include "DrawDebugHelpers.h" #include "Engine/GameInstance.h" @@ -107,6 +108,12 @@ void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm) { InterestRadiusCm = InterestCm; DespawnRadiusCm = DespawnCm; + // K4: log AOI cfg recebido do server (paridade com Net.AOI server-side + // "config_applied ZI=... ZD=..."). Cold path (1 por sessao). + UE_LOG(LogZeusAOI, Display, + TEXT("AOI cfg recv interestCm=%.0f (%.0fm) despawnCm=%.0f (%.0fm)"), + InterestCm, InterestCm / 100.0f, + DespawnCm, DespawnCm / 100.0f); } bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const diff --git a/Source/ZMMO/ZMMO.cpp b/Source/ZMMO/ZMMO.cpp index a6afa61..a680f89 100644 --- a/Source/ZMMO/ZMMO.cpp +++ b/Source/ZMMO/ZMMO.cpp @@ -1,8 +1,16 @@ // Copyright Epic Games, Inc. All Rights Reserved. #include "ZMMO.h" +#include "ZMMONetLog.h" #include "Modules/ModuleManager.h" IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ZMMO, "ZMMO" ); -DEFINE_LOG_CATEGORY(LogZMMO) \ No newline at end of file +DEFINE_LOG_CATEGORY(LogZMMO) + +// Batch 2.5 — categorias vivem no modulo ZMMO (nao no plugin) por causa +// do adaptive non-unity build do UBT (ver ZMMONetLog.h). +DEFINE_LOG_CATEGORY(LogZeusHandoff) +DEFINE_LOG_CATEGORY(LogZeusAOI) +DEFINE_LOG_CATEGORY(LogZeusProxy) +DEFINE_LOG_CATEGORY(LogZeusHaloTransition) \ No newline at end of file diff --git a/Source/ZMMO/ZMMONetLog.h b/Source/ZMMO/ZMMONetLog.h new file mode 100644 index 0000000..6cccc12 --- /dev/null +++ b/Source/ZMMO/ZMMONetLog.h @@ -0,0 +1,24 @@ +// ============================================================================= +// ZMMONetLog.h — categorias UE_LOG do CLIENTE ZMMO (modulo Game), nao do plugin. +// +// Por que aqui e nao no plugin ZeusNetwork? +// UBT usa `git status` em Clients/ZMMO/ pra adaptive non-unity build. O plugin +// ZeusNetwork mora em Server/Plugins/Unreal/ZeusNetwork/ (OUTRO repo git), +// entao UBT nao enxerga mudancas la e pula o rebuild do plugin. Resultado: +// adicionar DECLARE_LOG_CATEGORY_EXTERN no header do plugin nao funciona +// sem rebuild manual do plugin (Build.bat). Solucao: categorias que vivem +// no proprio CLIENTE moram aqui — UBT enxerga e linka direto. +// +// Setup -logcmds="LogZeusHandoff Verbose, LogZeusAOI Verbose, LogZeusProxy Verbose" +// pra filtrar Output Log seletivamente. +// +// DEFINE_LOG_CATEGORY correspondente em ZMMO.cpp. +// ============================================================================= +#pragma once + +#include "CoreMinimal.h" + +DECLARE_LOG_CATEGORY_EXTERN(LogZeusHandoff, Log, All); +DECLARE_LOG_CATEGORY_EXTERN(LogZeusAOI, Log, All); +DECLARE_LOG_CATEGORY_EXTERN(LogZeusProxy, Verbose, All); +DECLARE_LOG_CATEGORY_EXTERN(LogZeusHaloTransition, Log, All); From 9f5ccd3a05504547823b370eda3bc3f30b205eba Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 12 Jun 2026 02:51:37 -0300 Subject: [PATCH 04/10] =?UTF-8?q?ZMMO:=20migra=C3=A7=C3=A3o=20para=20ZeusN?= =?UTF-8?q?etworking=20V1=20can=C3=B4nico=20+=20fix=20overshoot=20do=20pro?= =?UTF-8?q?xy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZeusCharacter/ZeusWorldSubsystem passam a usar UZeusNetworkingClientSubsystem: ENT_SELF (entidade própria), INPUT 6077 via EmitInput, spawn/despawn/delta rebindados pro subsystem novo (legacy vira fallback). - ZeusPlayerProxy consome velocidade/grounded/serverTimeMs (delta 0x02): anima, vira na direção do movimento e cola no chão (MOVE_Walking). - Fix overshoot/inércia: FlushInputAxisToServer considera velocidade residual (braking) e envia a 30Hz até o dono parar de fato; o proxy desacelera suave, sem extrapolar ~1m além nem snap-back. - Char-select -> ConnectWithTicket (handoff ticket) quando bUseZeusNetworkingV1. Inclui logs [InputDbg] (ZeusCharacter) e DEBUG-PROXY (ZeusPlayerProxy) mantidos para testes posteriores de input/jitter. Co-Authored-By: Claude Opus 4.8 (1M context) --- Source/ZMMO/Game/Entity/ZeusCharacter.cpp | 165 +++++++++++++---- Source/ZMMO/Game/Entity/ZeusCharacter.h | 10 ++ Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp | 10 +- .../ZMMO/Game/Network/ZeusWorldSubsystem.cpp | 170 ++++++++++++++++-- Source/ZMMO/Game/Network/ZeusWorldSubsystem.h | 17 ++ .../UI/FrontEnd/UIUserLobbyScreen_Base.cpp | 21 ++- 6 files changed, 334 insertions(+), 59 deletions(-) diff --git a/Source/ZMMO/Game/Entity/ZeusCharacter.cpp b/Source/ZMMO/Game/Entity/ZeusCharacter.cpp index eff9f5f..d4eb22a 100644 --- a/Source/ZMMO/Game/Entity/ZeusCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZeusCharacter.cpp @@ -26,6 +26,7 @@ #include "GameFramework/PlayerController.h" #include "ZeusWorldSubsystem.h" #include "ZeusNetworkSubsystem.h" +#include "ZeusNetworkingClientSubsystem.h" #include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" DEFINE_LOG_CATEGORY(LogZeusPlayer); @@ -362,7 +363,7 @@ void AZeusCharacter::SetEntityRelevant(bool /*bRelevant*/) void AZeusCharacter::ResolveZeusNetworkSubsystem() { - if (ZeusNetwork) + if (ZeusNetwork && NetClient) { return; } @@ -373,7 +374,14 @@ void AZeusCharacter::ResolveZeusNetworkSubsystem() return; } - ZeusNetwork = GI->GetSubsystem(); + if (!ZeusNetwork) + { + ZeusNetwork = GI->GetSubsystem(); + } + if (!NetClient) + { + NetClient = GI->GetSubsystem(); + } } void AZeusCharacter::BindZeusSpawnDelegate() @@ -382,49 +390,71 @@ void AZeusCharacter::BindZeusSpawnDelegate() { return; } - if (!ZeusNetwork) + if (!NetClient && !ZeusNetwork) { return; } - ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned); - ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo); + // Self-entity (entityId do proprio char) vem do sistema de rede novo. + if (NetClient) + { + NetClient->OnSelfEntityAssigned.AddDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned); + } + // CHAR_INFO (nome/guild) ainda nao tem equivalente no sistema novo -> legacy. + if (ZeusNetwork) + { + ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo); + } bSpawnDelegateBound = true; } void AZeusCharacter::UnbindZeusSpawnDelegate() { - if (!bSpawnDelegateBound || !ZeusNetwork) + if (!bSpawnDelegateBound) { - bSpawnDelegateBound = false; return; } - ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned); - ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo); + if (NetClient) + { + NetClient->OnSelfEntityAssigned.RemoveDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned); + } + if (ZeusNetwork) + { + ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo); + } bSpawnDelegateBound = false; } void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn() { - if (!ZeusNetwork) + // Self-entity vem do sistema de rede novo (ENT_SELF). O subsystem e' + // per-GameInstance e sobrevive ao OpenLevel, entao o entityId pode ja' ter + // chegado antes deste pawn existir -- puxa o cache agora. Caso ainda nao + // tenha chegado, o bind em OnSelfEntityAssigned cobre quando chegar. + // A pos/yaw vem do proprio pawn (ja posicionado via PendingSpawnPose do DB); + // o sistema novo so' carrega o entityId. + if (NetClient) { - return; + const int64 SelfId = NetClient->GetLocalEntityId(); + if (SelfId != 0) + { + HandleLocalSpawnReady(SelfId, GetActorLocation(), GetActorRotation().Yaw, 0); + } } - int64 CachedEntityId = 0; - FVector CachedPosCm = FVector::ZeroVector; - float CachedYawDeg = 0.0f; - int64 CachedServerTimeMs = 0; - if (ZeusNetwork->TryGetLastLocalSpawn(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs)) - { - HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs); - } - - // Race fix: o S_CHAR_INFO chega ANTES de S_SPAWN_PLAYER mas o pawn pode - // nao existir ainda (OpenLevel em andamento). Aplica o ultimo cacheado. + // Race fix CHAR_INFO (ainda legacy): o S_CHAR_INFO pode ter chegado antes do + // pawn existir; aplica o ultimo nome/guild cacheado. TryApplyCachedCharInfo(); } +void AZeusCharacter::HandleSelfEntityAssigned(const int64 InEntityId) +{ + // Sistema novo informou o entityId do proprio char (ENT_SELF). Reusa o mesmo + // caminho do spawn local legacy (seta ZeusEntityId + RegisterLocalEntity). + // pos/yaw vem do pawn (ja posicionado); o sistema novo so' traz o entityId. + HandleLocalSpawnReady(InEntityId, GetActorLocation(), GetActorRotation().Yaw, 0); +} + void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs) { @@ -548,7 +578,10 @@ void AZeusCharacter::TryApplyCachedCharInfo() void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) { ResolveZeusNetworkSubsystem(); - if (!ZeusNetwork || !ZeusNetwork->IsConnected()) + // Sistema de rede novo (canonico) tem prioridade; legacy e' fallback. + const bool bV1 = (NetClient + && NetClient->GetConnectionState() == EZeusV1ConnectionState::Accepted); + if (!bV1 && (!ZeusNetwork || !ZeusNetwork->IsConnected())) { return; } @@ -557,7 +590,19 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) SendAccumulatorSec += DeltaSeconds; TimeSinceLastSendSec += DeltaSeconds; - const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight); + // Movimento p/ efeitos de rate NAO e' so' o input (W/A/S/D): o CMC do dono + // continua desacelerando (braking) por ~0.3s depois de soltar a tecla. Se + // pararmos de enviar no rate rapido assim que o input zera, o servidor fica + // congelado na ultima `simpleVelCmS` alta e o proxy remoto extrapola ~1m alem + // (NewPos = Last.Pos + Last.Vel*ExtrapSec) ate o heartbeat corrigir -> overshoot + // + snap-back ("velocidade grande que demora a zerar" + jitter). Tratar a + // velocidade residual como movimento mantem o envio a InputSendRateHz ate o + // dono realmente parar, alimentando o proxy com a curva de desaceleracao. + const bool bHasInputAxis = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight); + const FVector OwnerVelNow = GetVelocity(); + constexpr float kResidualSpeedSqCmS = 25.0f * 25.0f; // abaixo de 25 cm/s tratamos como parado + const bool bHasResidualVel = OwnerVelNow.SizeSquared() > kResidualSpeedSqCmS; + const bool bMovingInput = bHasInputAxis || bHasResidualVel; const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased; const bool bRateReached = SendAccumulatorSec >= SendInterval; const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec; @@ -571,6 +616,23 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) const bool bFallingChanged = (bIsFalling != bPreviousFalling); const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached); + // DEBUG-INPUT (2026-06-12): log ~1x/s COM o PIE. Remover apos achar o gap. + { + static float DbgInputAcc = 0.0f; + DbgInputAcc += DeltaSeconds; + if (DbgInputAcc >= 1.0f) + { + DbgInputAcc = 0.0f; + const int32 PieId = (GetWorld() && GetWorld()->GetOutermost()) + ? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1; + UE_LOG(LogZeusPlayer, Warning, + TEXT("[PIE%d][InputDbg] bV1=%d state=%d moving=%d rate=%d should=%d hbReached=%d fwd=%.2f right=%.2f"), + PieId, bV1 ? 1 : 0, + NetClient ? static_cast(NetClient->GetConnectionState()) : -99, + bMovingInput ? 1 : 0, bRateReached ? 1 : 0, bShouldSend ? 1 : 0, + bHeartbeatReached ? 1 : 0, PendingMoveForward, PendingMoveRight); + } + } if (!bShouldSend) { return; @@ -590,18 +652,49 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) // os outros clientes (sujeito ao clamp anti-cheat). const FVector PosCm = GetActorLocation(); const FVector Vel = GetVelocity(); - ZeusNetwork->SendInputAxis( - PendingMoveForward, - PendingMoveRight, - bPendingJumpPressed, - bPendingJumpReleased, - InputSequence, - ClientTimeMs, - ViewYawDeg, - PosCm, - FVector2D(Vel.X, Vel.Y), - bIsFalling, - static_cast(Vel.Z)); + if (bV1) + { + // Sistema novo: INPUT 6077 com a pose absoluta (ADR 0040/0041). + const bool bSent = NetClient->EmitInput( + PendingMoveForward, + PendingMoveRight, + bPendingJumpPressed, + bPendingJumpReleased, + InputSequence, + ClientTimeMs, + ViewYawDeg, + PosCm, + FVector2D(Vel.X, Vel.Y), + bIsFalling, + static_cast(Vel.Z)); + // DEBUG-INPUT (2026-06-12): confirma chamada + retorno do SendPacket. + static float DbgEmitAcc = 0.0f; + DbgEmitAcc += DeltaSeconds; + if (DbgEmitAcc >= 1.0f) + { + DbgEmitAcc = 0.0f; + const int32 PieId = (GetWorld() && GetWorld()->GetOutermost()) + ? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1; + UE_LOG(LogZeusPlayer, Warning, + TEXT("[PIE%d][InputDbg] EmitInput seq=%d sent=%d pos=%s"), + PieId, InputSequence, bSent ? 1 : 0, *PosCm.ToString()); + } + } + else if (ZeusNetwork) + { + ZeusNetwork->SendInputAxis( + PendingMoveForward, + PendingMoveRight, + bPendingJumpPressed, + bPendingJumpReleased, + InputSequence, + ClientTimeMs, + ViewYawDeg, + PosCm, + FVector2D(Vel.X, Vel.Y), + bIsFalling, + static_cast(Vel.Z)); + } SendAccumulatorSec = 0.0f; TimeSinceLastSendSec = 0.0f; diff --git a/Source/ZMMO/Game/Entity/ZeusCharacter.h b/Source/ZMMO/Game/Entity/ZeusCharacter.h index 03f957e..6b2ae8f 100644 --- a/Source/ZMMO/Game/Entity/ZeusCharacter.h +++ b/Source/ZMMO/Game/Entity/ZeusCharacter.h @@ -11,6 +11,7 @@ class USpringArmComponent; class UCameraComponent; class UInputAction; class UZeusNetworkSubsystem; +class UZeusNetworkingClientSubsystem; class UZeusAOIComponent; class UUserWidget; struct FInputActionValue; @@ -164,6 +165,11 @@ private: UFUNCTION() void HandleZeusPlayerSpawned(int64 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs); + // Self-entity do sistema de rede novo (ENT_SELF): o cliente aprende o + // proprio entityId. Substitui o caminho legacy OnPlayerSpawned(bIsLocal=true). + UFUNCTION() + void HandleSelfEntityAssigned(int64 InEntityId); + UFUNCTION() void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName); @@ -172,6 +178,10 @@ private: UPROPERTY() TObjectPtr ZeusNetwork; + // Sistema de rede novo (canonico): fonte do self-entity (ENT_SELF). + UPROPERTY() + TObjectPtr NetClient; + /** Instancia viva do painel admin (criada lazy no primeiro F8). */ UPROPERTY(Transient) TObjectPtr AdminPanelInstance; diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp index eb1ed6d..521bfcc 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp @@ -207,10 +207,14 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) LastDiagLogSec = NowSec; const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs; const int64 RenderLagMs = ServerNowMs - NewestMs; - UE_LOG(LogZMMO, Verbose, - TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"), + // DEBUG-PROXY (2026-06-12): Warning temporario p/ diagnosticar jitter-andando. + // extrap=1 frequente => buffer faminto (subir InterpolationDelayMs/rate). + // Rebaixar p/ Verbose depois de validar. speed em cm/s da velocidade visual. + UE_LOG(LogZMMO, Warning, + TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f speed=%.0f"), EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha, - bExtrapolating ? 1 : 0, InterpolationDelayMs); + bExtrapolating ? 1 : 0, InterpolationDelayMs, + FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size()); } } diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp index 4abef2d..205b8ec 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp @@ -10,6 +10,7 @@ #include "ZeusPlayerProxy.h" #include "ZeusPlayerState.h" #include "ZeusNetworkSubsystem.h" +#include "ZeusNetworkingClientSubsystem.h" void UZeusWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection) { @@ -32,42 +33,50 @@ void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld) { Super::OnWorldBeginPlay(InWorld); - if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem()) + // Sistema de rede novo (canonico) -- substitui o ZeusNetworkSubsystem legacy + // para spawn/despawn/movimento de proxies. O legacy fica so' com o que ainda + // nao tem equivalente (CHAR_INFO/nome, tratado em AZeusCharacter). + if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem()) { - ZeusNet->OnPlayerSpawned.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned); - ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned); - ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate); - UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to ZeusNetworkSubsystem delegates.")); + Net->OnEntitySpawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned); + Net->OnEntityDespawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned); + Net->OnEntityDeltaApplied.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta); + Net->OnSelfEntityAssigned.AddDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned); + UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to network client subsystem 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). + // O subsystem de rede e' per-GameInstance e sobrevive ao OpenLevel, entao + // o self-entity e os ENT_SPAWN podem ter chegado ANTES deste bind. Puxa o + // self cacheado + faz replay dos proxies ja conhecidos (anti-race). + if (const int64 CachedSelf = Net->GetLocalEntityId()) + { + OnNetSelfEntityAssigned(CachedSelf); + } int32 ReplayCount = 0; - ZeusNet->ForEachPendingRemoteSpawn( - [this, &ReplayCount](const int64 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs) + Net->ForEachRemoteEntity( + [this, &ReplayCount](int64 EntityId, FVector PosCm, float YawDeg) { - HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs); + OnNetEntitySpawned(EntityId, /*Kind=*/1 /*Player*/, PosCm, YawDeg); ++ReplayCount; }); if (ReplayCount > 0) { - UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount); + UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: replay de %d proxy(ies) ja conhecidos."), ReplayCount); } } else { - UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay.")); + UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: network client subsystem not found at OnWorldBeginPlay.")); } } void UZeusWorldSubsystem::Deinitialize() { - if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem()) + if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem()) { - ZeusNet->OnPlayerSpawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned); - ZeusNet->OnPlayerDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned); - ZeusNet->OnPlayerStateUpdate.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate); + Net->OnEntitySpawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned); + Net->OnEntityDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned); + Net->OnEntityDeltaApplied.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta); + Net->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned); } RemoteEntities.Reset(); @@ -83,6 +92,25 @@ void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* Loca } LocalEntityId = EntityId; + + // Caso de borda: se o ENT_SPAWN do proprio chegou antes do self-entity, ja + // existe um proxy-fantasma com esta chave. Destroi antes de registrar o pawn + // real, senao o RemoteEntities.Add sobrescreve a entry e o fantasma fica + // orfao no mundo (sem ninguem pra despawna-lo). + if (TWeakObjectPtr* Existing = RemoteEntities.Find(EntityId)) + { + if (AActor* Ghost = Existing->Get()) + { + if (Ghost != LocalActor && Ghost->IsA(AZeusPlayerProxy::StaticClass())) + { + HandlePlayerDespawned(EntityId); + UE_LOG(LogZMMO, Log, + TEXT("ZeusWorldSubsystem: ghost proxy do proprio limpo no RegisterLocalEntity EntityId=%lld"), + EntityId); + } + } + } + RemoteEntities.Add(EntityId, TWeakObjectPtr(LocalActor)); UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: local entity registered EntityId=%lld actor=%s"), EntityId, *GetNameSafe(LocalActor)); @@ -363,6 +391,97 @@ void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const in AsEntity->ApplyEntitySnapshot(Snapshot); } +void UZeusWorldSubsystem::OnNetEntitySpawned(int64 EntityId, int32 /*Kind*/, FVector PosCm, float YawDeg) +{ + // O sistema novo nao manda bIsLocal: derivamos do LocalEntityId (setado por + // ENT_SELF). Se ainda for 0 e este ENT_SPAWN for do proprio char, o + // OnNetSelfEntityAssigned destruira o fantasma quando o ENT_SELF chegar. + const bool bIsLocal = (LocalEntityId != 0 && EntityId == LocalEntityId); + HandlePlayerSpawned(EntityId, bIsLocal, PosCm, YawDeg, /*ServerTimeMs=*/0); +} + +void UZeusWorldSubsystem::OnNetEntityDespawned(int64 EntityId, int32 /*Reason*/) +{ + // NUNCA despawnar o proprio char local via rede (ex: ENT_DESPAWN do proprio + // disparado por timeout do server). Destruir o pawn local quebraria o jogo + // do dono. O proprio so' sai quando o cliente realmente desconecta. + if (LocalEntityId != 0 && EntityId == LocalEntityId) + { + return; + } + HandlePlayerDespawned(EntityId); +} + +void UZeusWorldSubsystem::OnNetEntityDelta(int64 EntityId, FVector PosCm, FVector VelCmS, + float YawDeg, bool bGrounded, int64 ServerTimeMs) +{ + if (EntityId == 0) + { + return; + } + if (LocalEntityId != 0 && EntityId == LocalEntityId) + { + return; // movimento do proprio char e' local; ignora snapshot autoritativo + } + + const TWeakObjectPtr* Entry = RemoteEntities.Find(EntityId); + if (!Entry) + { + return; // spawn chega em breve via ENT_SPAWN + } + AActor* Actor = Entry->Get(); + if (!Actor) + { + RemoteEntities.Remove(EntityId); + return; + } + IZeusEntityInterface* AsEntity = Cast(Actor); + if (!AsEntity) + { + return; + } + + FZeusEntitySnapshot Snapshot; + Snapshot.EntityId = EntityId; + Snapshot.EntityType = AsEntity->GetZeusEntityType(); + Snapshot.PositionCm = PosCm; + Snapshot.YawDeg = YawDeg; + // V1-REPL-FULL (2026-06-12): velocidade + grounded + serverTimeMs -> o proxy + // anima (AnimBP ShouldMove via Acceleration!=0 derivada da vel), cola no chao + // (MOVE_Walking quando grounded) e interpola com a timeline autoritativa. + Snapshot.VelocityCmS = VelCmS; + Snapshot.bGrounded = bGrounded; + Snapshot.ServerTimeMs = ServerTimeMs; + AsEntity->ApplyEntitySnapshot(Snapshot); +} + +void UZeusWorldSubsystem::OnNetSelfEntityAssigned(int64 EntityId) +{ + if (EntityId == 0 || LocalEntityId == EntityId) + { + return; + } + LocalEntityId = EntityId; + UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: self entity assigned EntityId=%lld"), EntityId); + + // Re-filtragem tardia: se o ENT_SPAWN do proprio chegou ANTES do ENT_SELF, + // ja existe um proxy-fantasma do proprio char. Destroi agora -- mas NUNCA o + // pawn local real (o RegisterLocalEntity do AZeusCharacter registra o pawn + // com a mesma chave; so destruimos se a entry for um AZeusPlayerProxy). + if (TWeakObjectPtr* Entry = RemoteEntities.Find(EntityId)) + { + if (AActor* Ghost = Entry->Get()) + { + if (Ghost->IsA(AZeusPlayerProxy::StaticClass())) + { + HandlePlayerDespawned(EntityId); + UE_LOG(LogZMMO, Log, + TEXT("ZeusWorldSubsystem: ghost proxy do proprio destruido EntityId=%lld"), EntityId); + } + } + } +} + UClass* UZeusWorldSubsystem::ResolveActorClass(const EZeusEntityType EntityType) const { if (const TSubclassOf* Found = RemoteEntityClasses.Find(EntityType)) @@ -389,3 +508,18 @@ UZeusNetworkSubsystem* UZeusWorldSubsystem::ResolveZeusNetworkSubsystem() const } return GI->GetSubsystem(); } + +UZeusNetworkingClientSubsystem* UZeusWorldSubsystem::ResolveNetClientSubsystem() const +{ + const UWorld* World = GetWorld(); + if (!World) + { + return nullptr; + } + UGameInstance* GI = World->GetGameInstance(); + if (!GI) + { + return nullptr; + } + return GI->GetSubsystem(); +} diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h index 3ab425e..5050f9a 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h @@ -9,6 +9,7 @@ class AActor; class AZeusCharacter; class AZeusPlayerProxy; class UZeusNetworkSubsystem; +class UZeusNetworkingClientSubsystem; /** * UZeusWorldSubsystem @@ -80,8 +81,24 @@ private: UFUNCTION() void HandlePlayerStateUpdate(int64 EntityId, int32 InputSeq, FVector PosCm, FVector VelCmS, bool bGrounded, int64 ServerTimeMs); + // Sistema de rede novo (canonico) -- recebe spawn/despawn/delta + self-entity. + // Adaptam a assinatura nova para os handlers de spawn/despawn/delta acima. + UFUNCTION() + void OnNetEntitySpawned(int64 EntityId, int32 Kind, FVector PosCm, float YawDeg); + + UFUNCTION() + void OnNetEntityDespawned(int64 EntityId, int32 Reason); + + UFUNCTION() + void OnNetEntityDelta(int64 EntityId, FVector PosCm, FVector VelCmS, float YawDeg, + bool bGrounded, int64 ServerTimeMs); + + UFUNCTION() + void OnNetSelfEntityAssigned(int64 EntityId); + UClass* ResolveActorClass(EZeusEntityType EntityType) const; UZeusNetworkSubsystem* ResolveZeusNetworkSubsystem() const; + UZeusNetworkingClientSubsystem* ResolveNetClientSubsystem() const; /** * Registry de proxies remotos. Chave = `EntityId` autoritativo do servidor. diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp index d0a4770..c201631 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp @@ -8,6 +8,7 @@ #include "WireHelpers.h" #include "ZeusCharServerSubsystem.h" #include "ZeusNetworkSubsystem.h" +#include "ZeusNetworkingClientSubsystem.h" // B4: V1 ConnectWithTicket #include "CommonTextBlock.h" #include "Components/PanelWidget.h" #include "Components/WidgetSwitcher.h" @@ -384,13 +385,29 @@ void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray& Payload) // `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()) + // B4 (2026-06-11): se V1 canonico, roteia ConnectWithTicket pro V1 subsystem. + // V1 envia CONN_HELLO_CLIENT (6000) com handoffTicket embutido — server + // valida via Valkey GETDEL antes do CHALLENGE (HandshakeProcessor::OnHelloClient). + const UZeusNetworkingClientSubsystem* V1Cdo = GetDefault(); + const bool bUseV1 = V1Cdo && V1Cdo->bUseZeusNetworkingV1; + if (bUseV1) + { + if (UZeusNetworkingClientSubsystem* V1 = GI->GetSubsystem()) + { + V1->ConnectWithTicket(GwHost, GwPort, HandoffToken); + } + else + { + UE_LOG(LogZMMO, Error, TEXT("Lobby: V1 subsystem ausente — handoff abortado")); + } + } + else if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem()) { ZeusNet->ConnectToZeusServerWithTicket(GwHost, GwPort, HandoffToken); } else { - UE_LOG(LogZMMO, Error, TEXT("Lobby: ZeusNetworkSubsystem ausente — handoff abortado")); + UE_LOG(LogZMMO, Error, TEXT("Lobby: nenhum ZeusNetworkSubsystem disponivel — handoff abortado")); } // Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo. From 77d52a703bdecf3dcaa151f012f1b186bd604437 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 12 Jun 2026 13:06:42 -0300 Subject: [PATCH 05/10] ZMMO: CHAR_INFO via V1 (Fase A) + fix loading travado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZeusWorldSubsystem consome ENT_CHAR_INFO (delegate OnCharInfo): aplica nome/guild no PlayerState do entityId certo (self ou proxy), com cache porque o CHAR_INFO chega antes do ator existir (server emite antes do ENT_SPAWN). - FIX loading travado: a etapa "Spawn" do loading dependia do legacy OnPlayerSpawned, que não dispara mais com V1 -> loading eterno. Agora a etapa é marcada pelo sinal V1 OnSelfEntityAssigned (ENT_SELF); bind legacy removido. Travel ainda passa por OnServerTravelRequested (ponte legacy) até a Fase D. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../ZMMO/Game/Network/ZeusWorldSubsystem.cpp | 59 +++++++++++++++++++ Source/ZMMO/Game/Network/ZeusWorldSubsystem.h | 22 +++++++ .../UI/FrontEnd/UIFrontEndFlowSubsystem.cpp | 32 +++++++++- .../UI/FrontEnd/UIFrontEndFlowSubsystem.h | 11 ++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp index 205b8ec..2ca28c1 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp @@ -42,6 +42,7 @@ void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld) Net->OnEntityDespawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned); Net->OnEntityDeltaApplied.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta); Net->OnSelfEntityAssigned.AddDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned); + Net->OnCharInfo.AddDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo); UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to network client subsystem delegates.")); // O subsystem de rede e' per-GameInstance e sobrevive ao OpenLevel, entao @@ -77,9 +78,11 @@ void UZeusWorldSubsystem::Deinitialize() Net->OnEntityDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned); Net->OnEntityDeltaApplied.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta); Net->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned); + Net->OnCharInfo.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo); } RemoteEntities.Reset(); + PendingCharInfo.Reset(); LocalEntityId = 0; Super::Deinitialize(); } @@ -112,6 +115,9 @@ void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* Loca } RemoteEntities.Add(EntityId, TWeakObjectPtr(LocalActor)); + // V1-CHARINFO: o nome do proprio char pode ter chegado antes do pawn local + // existir (CHAR_INFO vem logo apos ENT_SELF); aplica agora. + FlushPendingCharInfo(EntityId); UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: local entity registered EntityId=%lld actor=%s"), EntityId, *GetNameSafe(LocalActor)); } @@ -255,10 +261,63 @@ void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool b } RemoteEntities.Add(EntityId, TWeakObjectPtr(SpawnedActor)); + // V1-CHARINFO: o nome chega ANTES do ator (server emite ENT_CHAR_INFO antes + // do ENT_SPAWN); aplica agora que o proxy + PlayerState ja existem. + FlushPendingCharInfo(EntityId); UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: spawned remote EntityId=%d at (%s) yaw=%.1f t=%lld"), EntityId, *PosCm.ToString(), YawDeg, ServerTimeMs); } +void UZeusWorldSubsystem::OnNetCharInfo(const int64 EntityId, const FString& CharName, const FString& GuildName) +{ + if (EntityId == 0) + { + return; + } + // Aplica direto se o ator/PlayerState ja existe; senao guarda pra aplicar no spawn. + if (!ApplyCharInfoToEntity(EntityId, CharName, GuildName)) + { + PendingCharInfo.Add(EntityId, FZeusPendingCharInfo{ CharName, GuildName }); + } +} + +bool UZeusWorldSubsystem::ApplyCharInfoToEntity(const int64 EntityId, const FString& CharName, const FString& GuildName) +{ + const TWeakObjectPtr* Found = RemoteEntities.Find(EntityId); + if (!Found || !Found->IsValid()) + { + return false; + } + APawn* Pawn = Cast(Found->Get()); + if (!Pawn) + { + return false; + } + AZeusPlayerState* PS = Pawn->GetPlayerState(); + if (!PS) + { + return false; + } + PS->SetCharInfo(CharName, GuildName); + UE_LOG(LogZMMO, Log, + TEXT("ZeusWorldSubsystem: CHAR_INFO aplicado EntityId=%lld name='%s' guild='%s'"), + EntityId, *CharName, *GuildName); + return true; +} + +void UZeusWorldSubsystem::FlushPendingCharInfo(const int64 EntityId) +{ + const FZeusPendingCharInfo* Pending = PendingCharInfo.Find(EntityId); + if (!Pending) + { + return; + } + if (ApplyCharInfoToEntity(EntityId, Pending->CharName, Pending->GuildName)) + { + PendingCharInfo.Remove(EntityId); + } +} + void UZeusWorldSubsystem::HandlePlayerDespawned(const int64 EntityId) { const TWeakObjectPtr* Entry = RemoteEntities.Find(EntityId); diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h index 5050f9a..3bf7811 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h @@ -96,10 +96,24 @@ private: UFUNCTION() void OnNetSelfEntityAssigned(int64 EntityId); + // V1-CHARINFO: nome/guild do char (opcode ENT_CHAR_INFO 6045). Chega ANTES + // do ENT_SPAWN do proxy / do RegisterLocalEntity do self, entao se o ator + // ainda nao existe o dado fica em PendingCharInfo e e' aplicado no spawn. + UFUNCTION() + void OnNetCharInfo(int64 EntityId, const FString& CharName, const FString& GuildName); + UClass* ResolveActorClass(EZeusEntityType EntityType) const; UZeusNetworkSubsystem* ResolveZeusNetworkSubsystem() const; UZeusNetworkingClientSubsystem* ResolveNetClientSubsystem() const; + // Aplica nome/guild no AZeusPlayerState do EntityId (self ou proxy). Retorna + // false se o ator/PlayerState ainda nao existe (caller deve cachear). + bool ApplyCharInfoToEntity(int64 EntityId, const FString& CharName, const FString& GuildName); + + // Aplica (e consome) o char info pendente de EntityId, se houver. Chamado + // quando o ator passa a existir (spawn de proxy / RegisterLocalEntity). + void FlushPendingCharInfo(int64 EntityId); + /** * Registry de proxies remotos. Chave = `EntityId` autoritativo do servidor. * Usamos `TWeakObjectPtr` para nao impedir GC se algo der errado e o ator @@ -110,4 +124,12 @@ private: /** Cached id do jogador local, para ignorar snapshots dele (cliente solto). */ int64 LocalEntityId = 0; + + /** V1-CHARINFO: nome/guild recebido antes do ator existir. Chave = EntityId. */ + struct FZeusPendingCharInfo + { + FString CharName; + FString GuildName; + }; + TMap PendingCharInfo; }; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index 5d7074c..c9379a1 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -11,6 +11,7 @@ #include "ZeusGameInstance.h" #include "ZeusThemeSubsystem.h" #include "ZeusNetworkSubsystem.h" +#include "ZeusNetworkingClientSubsystem.h" // V1 canonico (sinal de spawn local) #include "ZeusCharServerSubsystem.h" #include "CommonActivatableWidget.h" #include "Engine/DataTable.h" @@ -265,6 +266,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const return GI ? GI->GetSubsystem() : nullptr; } +UZeusNetworkingClientSubsystem* UUIFrontEndFlowSubsystem::GetNetClient() const +{ + const UGameInstance* GI = GetGameInstance(); + return GI ? GI->GetSubsystem() : nullptr; +} + UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const { const UGameInstance* GI = GetGameInstance(); @@ -314,11 +321,20 @@ void UUIFrontEndFlowSubsystem::BindNetwork() Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed); Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected); } - // UDP (world server) só interessa para o handoff de travel. + // UDP (world server): o travel (TRAVEL_TO_MAP) ainda passa pelo objeto legacy + // como PONTE (HandleTravelToMap V1 reusa OnServerTravelRequested) -- isso some + // na Fase D quando o travel ganhar delegate proprio no V1. if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); - Zeus->OnPlayerSpawned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); + } + // V1 CANONICO: o spawn do player local chega via ENT_SELF (o cliente aprende + // o proprio entityId) -> OnSelfEntityAssigned. O legacy OnPlayerSpawned NAO + // dispara mais com V1, entao a etapa "Spawn" do loading ficava eterna + // (loading travado). Liga a etapa ao sinal V1. + if (UZeusNetworkingClientSubsystem* Net = GetNetClient()) + { + Net->OnSelfEntityAssigned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned); } bNetBound = true; } @@ -338,7 +354,10 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork() if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); - Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned); + } + if (UZeusNetworkingClientSubsystem* Net = GetNetClient()) + { + Net->OnSelfEntityAssigned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned); } bNetBound = false; } @@ -505,6 +524,13 @@ void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int64 /*EntityId*/, bool bIsL } } +void UUIFrontEndFlowSubsystem::HandleV1LocalSpawned(int64 EntityId) +{ + // ENT_SELF e' SEMPRE o proprio char (o cliente aprendendo seu entityId), entao + // bIsLocal=true. Reusa a logica de marcar a etapa "Spawn" + memoizacao anti-race. + HandlePlayerSpawned(EntityId, /*bIsLocal=*/true, FVector::ZeroVector, 0.0f, 0); +} + void UUIFrontEndFlowSubsystem::HandleLoadingComplete() { if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get()) diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index c24a181..89ec05f 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -11,6 +11,7 @@ class UUIManagerSubsystem; class UUILoadingScreen_Base; class UZeusLoadingProfilesDataAsset; class UZeusNetworkSubsystem; +class UZeusNetworkingClientSubsystem; class UZeusCharServerSubsystem; struct FZeusMapDef; @@ -171,6 +172,7 @@ private: void BindNetwork(); void UnbindNetwork(); UZeusNetworkSubsystem* GetZeusNetwork() const; + UZeusNetworkingClientSubsystem* GetNetClient() const; // V1 canonico UZeusCharServerSubsystem* GetCharServer() const; UUIManagerSubsystem* GetUIManager() const; UUIFrontEndScreenSet* GetScreenSet(); @@ -198,6 +200,15 @@ private: void HandlePlayerSpawned(int64 EntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs); + /** + * V1 canonico: o spawn do player local chega via ENT_SELF (o cliente + * aprende o proprio entityId) -> OnSelfEntityAssigned. Marca a etapa + * "Spawn" do loading no fluxo V1 (o legacy OnPlayerSpawned nao dispara + * mais). Adapta a assinatura OneParam pra logica de HandlePlayerSpawned. + */ + UFUNCTION() + void HandleV1LocalSpawned(int64 EntityId); + /** Disparado pela tela de loading quando todas as etapas viram Done. */ UFUNCTION() void HandleLoadingComplete(); From 5f4c88637fbbfedcc0b04a248ce6f5f5755bc894 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 12 Jun 2026 21:04:43 -0300 Subject: [PATCH 06/10] ZN V1: overlay AOI canonico + ENT_SPAWN keyframe [Change-Set: AOI-VIS-2026-06-12] Lado CLIENTE ZMMO (par com o commit de mesmo Change-Set no repo ZeusServerEngine -- devem ir juntos, o wire do ENT_SPAWN mudou). - UZeusAOIComponent: migrado do transporte legacy (SendDebugAoiRequest/OnDebugAoiInfo, desativado no V1) pro canonico (EmitDebugAoiRequest 6160 / OnDebugAoiInfo 6161). Esfera de debug 32 -> 48 segments + clamp visual de 500m (resolve "zona sem limite / poucas linhas"). - ZeusWorldSubsystem::OnNetEntitySpawned: recebe vel + grounded + serverTimeMs (delegate FZeusV1OnEntitySpawned 4 -> 7 params) e semeia o proxy recem-criado via HandlePlayerStateUpdate -> ancora o relogio de interpolacao + a vel inicial -> proxy nasce ja' animando no re-spawn (sai/volta do raio AOI segurando W), sem Idle deslizando. Co-Authored-By: Claude Opus 4.8 (1M context) --- Source/ZMMO/Game/Network/ZeusAOIComponent.cpp | 33 +++++++++++++------ .../ZMMO/Game/Network/ZeusWorldSubsystem.cpp | 20 +++++++++-- Source/ZMMO/Game/Network/ZeusWorldSubsystem.h | 3 +- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp index abcb5b6..3886ea0 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp @@ -2,7 +2,7 @@ #include "ZeusAOIComponent.h" -#include "ZeusNetworkSubsystem.h" // plugin ZeusNetwork: OnDebugAoiInfo + SendDebugAoiRequest +#include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160) #include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO) #include "DrawDebugHelpers.h" @@ -30,6 +30,14 @@ namespace const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa) const FColor kColorCell(232, 192, 96); const FColor kColorHandoff(255, 155, 190); + + // Resolucao das esferas de overlay. 32 segments deixavam a "bolha" facetada + // (poucas linhas) num raio de 60-80m; 48 deixa o circulo bem mais denso/legivel. + constexpr int32 kSphereSegments = 48; + // Clamp visual defensivo: se o raio vier absurdo (config corrompida), nao desenha + // uma esfera gigante que engole o mapa. 500m cobre qualquer AOI real (max=800m, + // mas overlay > 500m ja' nao ajuda a leitura). So' afeta o desenho, nao o gameplay. + constexpr float kMaxOverlayRadiusCm = 50000.0f; } UZeusAOIComponent::UZeusAOIComponent() @@ -43,14 +51,17 @@ void UZeusAOIComponent::BeginPlay() { Super::BeginPlay(); - // Bind do feed de config de AOI (servidor -> cliente). Multicast plain C++. + // Bind do feed de config de AOI (servidor -> cliente) pelo transporte V1 + // canonico. O legacy (UZeusNetworkSubsystem) esta desativado no V1 -> usava + // SendDebugAoiRequest que logava "ignorado: nao conectado" e o overlay nunca + // recebia a config. Agora via UZeusNetworkingClientSubsystem (OnDebugAoiInfo 6161). if (UWorld* World = GetWorld()) { if (UGameInstance* GI = World->GetGameInstance()) { - if (UZeusNetworkSubsystem* Net = GI->GetSubsystem()) + if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { - AoiConfigHandle_ = Net->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig); + AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig); } } } @@ -68,9 +79,9 @@ void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (UGameInstance* GI = World->GetGameInstance()) { - if (UZeusNetworkSubsystem* Net = GI->GetSubsystem()) + if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { - Net->OnDebugAoiInfo.Remove(AoiConfigHandle_); + NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); } } } @@ -96,9 +107,9 @@ void UZeusAOIComponent::RequestAoiConfig() { if (UGameInstance* GI = World->GetGameInstance()) { - if (UZeusNetworkSubsystem* Net = GI->GetSubsystem()) + if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { - Net->SendDebugAoiRequest(); // no-op se nao conectado + NetC->EmitDebugAoiRequest(); // V1: no-op se ainda nao Accepted } } } @@ -152,12 +163,14 @@ void UZeusAOIComponent::DrawOverlays() // ainda nao chegou a config: nao desenha (loading). if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f) { - DrawDebugSphere(World, Loc, InterestRadiusCm, 32, kColorInterest, false, -1.0f, 0, LineThickness); + const float R = FMath::Min(InterestRadiusCm, kMaxOverlayRadiusCm); + DrawDebugSphere(World, Loc, R, kSphereSegments, kColorInterest, false, -1.0f, 0, LineThickness); } // Zona de Despawn (externa, > interesse = histerese) — raio real do servidor. if (ResolveOverlay(EZeusAOIOverlay::DespawnZone) && DespawnRadiusCm > 0.0f) { - DrawDebugSphere(World, Loc, DespawnRadiusCm, 32, kColorDespawn, false, -1.0f, 0, LineThickness); + const float R = FMath::Min(DespawnRadiusCm, kMaxOverlayRadiusCm); + DrawDebugSphere(World, Loc, R, kSphereSegments, kColorDespawn, false, -1.0f, 0, LineThickness); } if (ResolveOverlay(EZeusAOIOverlay::CellBounds)) { diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp index 2ca28c1..ecc584e 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp @@ -56,7 +56,10 @@ void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld) Net->ForEachRemoteEntity( [this, &ReplayCount](int64 EntityId, FVector PosCm, float YawDeg) { - OnNetEntitySpawned(EntityId, /*Kind=*/1 /*Player*/, PosCm, YawDeg); + // Catch-up local: ForEachRemoteEntity nao expoe vel/serverTimeMs -> + // ZeroVector + grounded + ts=0 (seed nao roda; o 1o delta ancora o relogio). + OnNetEntitySpawned(EntityId, /*Kind=*/1 /*Player*/, PosCm, YawDeg, + FVector::ZeroVector, /*bGrounded=*/true, /*ServerTimeMs=*/0); ++ReplayCount; }); if (ReplayCount > 0) @@ -450,13 +453,24 @@ void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const in AsEntity->ApplyEntitySnapshot(Snapshot); } -void UZeusWorldSubsystem::OnNetEntitySpawned(int64 EntityId, int32 /*Kind*/, FVector PosCm, float YawDeg) +void UZeusWorldSubsystem::OnNetEntitySpawned(int64 EntityId, int32 /*Kind*/, FVector PosCm, float YawDeg, + FVector VelCmS, bool bGrounded, int64 ServerTimeMs) { // O sistema novo nao manda bIsLocal: derivamos do LocalEntityId (setado por // ENT_SELF). Se ainda for 0 e este ENT_SPAWN for do proprio char, o // OnNetSelfEntityAssigned destruira o fantasma quando o ENT_SELF chegar. const bool bIsLocal = (LocalEntityId != 0 && EntityId == LocalEntityId); - HandlePlayerSpawned(EntityId, bIsLocal, PosCm, YawDeg, /*ServerTimeMs=*/0); + HandlePlayerSpawned(EntityId, bIsLocal, PosCm, YawDeg, ServerTimeMs); + + // V1-SPAWN-KEYFRAME(-TS): o ENT_SPAWN carrega vel + grounded + serverTimeMs. Aplica + // esse keyframe INICIAL no proxy recem-criado pelo mesmo caminho do delta (semeia o + // SnapshotBuffer + ANCORA o ServerClockOffsetMs com o tempo CERTO). Antes o seed usava + // ServerTimeMs=0 -> bootstrap do relogio de interpolacao errado -> proxy preso/flutuando + // ate' o EMA convergir. Agora com o serverTimeMs real o proxy interpola desde o frame 1. + if (!bIsLocal && ServerTimeMs > 0) + { + HandlePlayerStateUpdate(EntityId, /*InputSeq=*/0, PosCm, VelCmS, bGrounded, ServerTimeMs); + } } void UZeusWorldSubsystem::OnNetEntityDespawned(int64 EntityId, int32 /*Reason*/) diff --git a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h index 3bf7811..94d184d 100644 --- a/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h +++ b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.h @@ -84,7 +84,8 @@ private: // Sistema de rede novo (canonico) -- recebe spawn/despawn/delta + self-entity. // Adaptam a assinatura nova para os handlers de spawn/despawn/delta acima. UFUNCTION() - void OnNetEntitySpawned(int64 EntityId, int32 Kind, FVector PosCm, float YawDeg); + void OnNetEntitySpawned(int64 EntityId, int32 Kind, FVector PosCm, float YawDeg, + FVector VelCmS, bool bGrounded, int64 ServerTimeMs); UFUNCTION() void OnNetEntityDespawned(int64 EntityId, int32 Reason); From 341e6895c90b9241d5bc225215eccc4141db32f9 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 12 Jun 2026 23:44:05 -0300 Subject: [PATCH 07/10] Config/Tags: ZeusServerTags.ini gerado (tags do server p/ o GAS) [Change-Set: TAGS-SYNC-2026-06-12] 80 GameplayTags do ZeusServer (Zeus.Status/Ability/Effect/Cue/Attribute/Cooldown) geradas pelo pipeline build-time (par com o commit de mesmo Change-Set no repo ZeusServerEngine). O UE le Config/Tags/*.ini no boot e registra as tags, pro cliente resolver os hashes de GE/Tag/Cue (FNV-1a64) -- fim do "tag nao resolvida". Regeravel com sync_tags.ps1; nao editar a mao. Co-Authored-By: Claude Opus 4.8 (1M context) --- Config/Tags/ZeusServerTags.ini | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 Config/Tags/ZeusServerTags.ini diff --git a/Config/Tags/ZeusServerTags.ini b/Config/Tags/ZeusServerTags.ini new file mode 100644 index 0000000..40f9bab --- /dev/null +++ b/Config/Tags/ZeusServerTags.ini @@ -0,0 +1,83 @@ +; Gerado por ZeusTool tags sync -- tags do ZeusServer p/ o cliente UE. +; NAO editar a mao: regenere com `ZeusTool tags sync `. +[/Script/GameplayTags.GameplayTagsList] +GameplayTagList=(Tag="Zeus",DevComment="") +GameplayTagList=(Tag="Zeus.Ability",DevComment="") +GameplayTagList=(Tag="Zeus.Ability.Casting",DevComment="") +GameplayTagList=(Tag="Zeus.Ability.Channeling",DevComment="") +GameplayTagList=(Tag="Zeus.Ability.Movement",DevComment="") +GameplayTagList=(Tag="Zeus.Ability.Movement.Dash",DevComment="") +GameplayTagList=(Tag="Zeus.Ability.Recovery",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Agi",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Aspd",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Atk",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.BaseExp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.BaseLevel",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Crit",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Def",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Dex",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Flee",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Hit",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Hp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Int",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.JobExp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.JobLevel",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Luk",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Matk",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.MatkMax",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.MatkMin",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.MaxHp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.MaxSp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Mdef",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.SkillPoint",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Sp",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.StatusPoint",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Str",DevComment="") +GameplayTagList=(Tag="Zeus.Attribute.Vit",DevComment="") +GameplayTagList=(Tag="Zeus.Combat",DevComment="") +GameplayTagList=(Tag="Zeus.Combat.Blocking",DevComment="") +GameplayTagList=(Tag="Zeus.Combat.Dodging",DevComment="") +GameplayTagList=(Tag="Zeus.Combat.InCombat",DevComment="") +GameplayTagList=(Tag="Zeus.Combat.LockOn",DevComment="") +GameplayTagList=(Tag="Zeus.Combo",DevComment="") +GameplayTagList=(Tag="Zeus.Combo.Slot1",DevComment="") +GameplayTagList=(Tag="Zeus.Combo.Slot2",DevComment="") +GameplayTagList=(Tag="Zeus.Combo.Slot3",DevComment="") +GameplayTagList=(Tag="Zeus.Combo.Window",DevComment="") +GameplayTagList=(Tag="Zeus.Cooldown",DevComment="") +GameplayTagList=(Tag="Zeus.Cue",DevComment="") +GameplayTagList=(Tag="Zeus.Cue.Movement",DevComment="") +GameplayTagList=(Tag="Zeus.Cue.Movement.Dash",DevComment="") +GameplayTagList=(Tag="Zeus.Damage",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Earth",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Fire",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Holy",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Magical",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Physical",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Poison",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Shadow",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Water",DevComment="") +GameplayTagList=(Tag="Zeus.Damage.Wind",DevComment="") +GameplayTagList=(Tag="Zeus.Effect",DevComment="") +GameplayTagList=(Tag="Zeus.Effect.Cooldown",DevComment="") +GameplayTagList=(Tag="Zeus.Effect.Cooldown.Dash",DevComment="") +GameplayTagList=(Tag="Zeus.Effect.Cost",DevComment="") +GameplayTagList=(Tag="Zeus.Effect.Cost.Sp_10",DevComment="") +GameplayTagList=(Tag="Zeus.Status",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Buff",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Disarmed",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Feared",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Frozen",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.KnockedDown",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Rooted",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Silenced",DevComment="") +GameplayTagList=(Tag="Zeus.Status.CC.Stunned",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Dead",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Debuff",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Invulnerable",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Regen",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Regen.Blocked",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Regen.Boosted",DevComment="") +GameplayTagList=(Tag="Zeus.Status.Regen.Poisoned",DevComment="") From 4d737829f5a2cdbbdfcb1e95e3d2ec525a04dcf1 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 13 Jun 2026 01:26:33 -0300 Subject: [PATCH 08/10] ZeusPlayerProxy: fix jitter por fome de deltas (mitigacao + anti-stale) [Change-Set: JITTER-STARVATION-2026-06-13] Player remoto parado nao gera DELTA_BATCH (ComputeDelta suprime no server) -> o proxy fica "mudo": NewestMs congela, renderLagMs cresce 1:1 com o wall-clock -> extrapola sempre -> "anda no lugar" (moonwalk) e salta quando o player volta. Provado em log (secsSinceSnap subindo enquanto o remoto esta moving=0). NAO e' clock errado nem self-proxy (red-herrings descartados); o seed do relogio esta' OK. - Tick: quando faminto (extrap alem de MaxExtrapolationSeconds+0.15s), zera a vel visual -> proxy vai pra Idle em vez de moonwalk. - ApplyEntitySnapshot: anti-stale -- snapshot > 1000ms a frente do topo do buffer descarta o buffer + re-bootstrapa o ServerClockOffsetMs -> snap limpo (sem span gigante na interpolacao). Loga "buffer STALE gap=Xms -> reset+reseed". - Logs DIAG PERMANENTES (Warning, mantidos de proposito -- ver memoria project_proxy_delta_starvation_jitter): SEED clock + renderLagMs/newestMs/ secsSinceSnap/speed no Tick. Sonda anti-regressao. Cobre tambem o interserver: shadow proxies parados sofrem a mesma fome. Validado em jogo (user: "sem jitter nenhum"). Par server: PubV2-DIAG (mesmo Change-Set). Co-Authored-By: Claude Opus 4.8 (1M context) --- Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp | 65 +++++++++++++++++---- Source/ZMMO/Game/Entity/ZeusPlayerProxy.h | 6 ++ 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp index 521bfcc..f955685 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp @@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) // conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar // o proxy fugir se a rede ficar muda. const FZeusProxySnapshot& Last = SnapshotBuffer.Last(); - const float ExtrapSec = FMath::Clamp( - static_cast(RenderMs - Last.ServerTimeMs) / 1000.0f, - 0.0f, MaxExtrapolationSeconds); + const float RawExtrapSec = + static_cast(RenderMs - Last.ServerTimeMs) / 1000.0f; + const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds); NewPos = Last.PosCm + Last.VelCmS * ExtrapSec; - NewVel = Last.VelCmS; + // STARVATION (buffer=1 por muito tempo -- ex: 1o login sem DELTA_BATCH por + // segundos): a extrapolacao satura no clamp e a POSE congela, mas manter a + // velocidade faz o AnimBP rodar locomocao PARADO ("andar no lugar"/moonwalk + // = o "jitter" reportado). Se ja passamos bem do clamp, zera a vel visual + // pra o proxy ir pra Idle ate' os deltas voltarem (em vez de animar + // andando travado). Quando os deltas chegam, o ramo de interpolacao retoma. + const bool bStarved = RawExtrapSec > (MaxExtrapolationSeconds + 0.15f); + NewVel = bStarved ? FVector::ZeroVector : Last.VelCmS; bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER; } else @@ -207,13 +214,25 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) LastDiagLogSec = NowSec; const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs; const int64 RenderLagMs = ServerNowMs - NewestMs; - // DEBUG-PROXY (2026-06-12): Warning temporario p/ diagnosticar jitter-andando. - // extrap=1 frequente => buffer faminto (subir InterpolationDelayMs/rate). - // Rebaixar p/ Verbose depois de validar. speed em cm/s da velocidade visual. + // === DIAG PERMANENTE -- jitter por FOME DE DELTAS ======================== + // Bug investigado/resolvido 2026-06-13 (ver memoria + // project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO + // (decisao do dono: NAO rebaixar p/ Verbose nem remover) -- e' a sonda pra + // cacar regressao do jitter de proxy. Como ler: + // secsSinceSnap = ha quanto tempo o proxy NAO recebe snapshot. Sobe = FOME + // (player remoto parado -> ComputeDelta suprime no server -> sem DELTA_BATCH). + // newestMs = serverTimeMs do ultimo snapshot; CONGELA na fome (deveria avancar). + // renderLagMs = ServerNowMs - newestMs; cresce 1:1 com o wall-clock na fome. + // speed=0 na fome = a mitigacao (zera a vel visual -> Idle, sem "moonwalk") agindo. + // Ao o player voltar a se mover sai "buffer STALE ... reset+reseed" (anti-stale) + // e renderLagMs/secsSinceSnap voltam a ~0 (snap limpo, sem salto/jitter). + // ========================================================================= + const double SecsSinceSnap = + (LastSnapshotRecvSec > 0.0) ? (NowSec - LastSnapshotRecvSec) : -1.0; UE_LOG(LogZMMO, Warning, - TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f speed=%.0f"), - EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha, - bExtrapolating ? 1 : 0, InterpolationDelayMs, + TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"), + EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha, + bExtrapolating ? 1 : 0, FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size()); } } @@ -226,6 +245,8 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) { + LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick) + // 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp. // Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte // do ServerTimeMs muda (publisher antigo -> novo) e o offset fica @@ -235,10 +256,34 @@ void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) // pra evitar salto visual durante transicao. const int64 LocalNowMs = static_cast(FPlatformTime::Seconds() * 1000.0); const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs; + + // Anti-stale (2026-06-13): se este snapshot esta MUITO a frente do mais novo + // do buffer, o proxy ficou "mudo" por segundos -- player remoto parado -> + // ComputeDelta suprime -> sem DELTA_BATCH novo -> renderLagMs disparou pra + // dezenas de segundos. Manter os snapshots velhos faz a interpolacao usar um + // span gigante (pose de ~100s atras -> agora) quando o player volta a se mover, + // causando rasteio/salto (o jitter reportado). Descarta o buffer obsoleto + + // re-bootstrapa o relogio pra a interpolacao recomecar limpa neste keyframe. + if (SnapshotBuffer.Num() > 0 + && (Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs) > 1000) + { + UE_LOG(LogZMMO, Warning, + TEXT("ZeusPlayerProxy[%lld] buffer STALE gap=%lldms -> reset+reseed"), + EntityId, Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs); + SnapshotBuffer.Reset(); + ServerClockOffsetMs = OffsetCandidate; + } + if (SnapshotBuffer.Num() == 0) { // Primeira amostra: bootstrap direto (nao tem baseline pra suavizar). ServerClockOffsetMs = OffsetCandidate; + // DIAG (jitter 1o login): confirma o seed do relogio. serverTimeMs deve ser + // != 0 e o offset razoavel; se vier 0 aqui, o keyframe/catch-up corrompeu o + // bootstrap (hipotese descartada, mas o log fecha a questao em campo). + UE_LOG(LogZMMO, Warning, + TEXT("ZeusPlayerProxy[%lld] SEED clock serverTimeMs=%lld localNowMs=%lld offset=%lld"), + EntityId, Snapshot.ServerTimeMs, LocalNowMs, ServerClockOffsetMs); } else { diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h index 5a21096..c8f91d2 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h @@ -140,6 +140,12 @@ protected: */ int64 ServerClockOffsetMs = 0; + /** DIAG (2026-06-13): instante (s, monotonic) do ultimo snapshot recebido em + * ApplyEntitySnapshot. Usado no log do Tick pra medir "secsSinceSnap" = quanto + * tempo o proxy ficou MUDO (sem delta). secsSinceSnap grande == fome (player + * remoto parado -> ComputeDelta suprime). 0 = nenhum snapshot ainda. */ + double LastSnapshotRecvSec = 0.0; + /** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar * o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */ FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector; From de294f4075a446050cf3105e97d7d10ea455d7a0 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Sat, 13 Jun 2026 02:22:07 -0300 Subject: [PATCH 09/10] fix(yaw): proxy replica o yaw do CORPO do dono (orient-to-movement), nao a mira [YAW-BODY-ORIENT] commit conjunto cliente + servidor. Sintoma: ao apertar D o proxy virava ~45 graus de uma vez, em vez de virar igual ao player local (suave, so um pouco), e parado seguia o mouse. Raiz: a fonte de verdade do yaw estava errada. O servidor derivava simpleYawDeg de atan2(velocidade) INSTANTANEO ao mover (snap pra direcao do movimento) e da mira (ControlRotation) ao parar -- nenhum dos dois e o yaw do corpo do dono. Correcao ponta-a-ponta: - Cliente: envia GetActorRotation().Yaw (yaw do CORPO; o CMC ja orienta pra direcao do movimento a 500 deg/s e bUseControllerRotationYaw=false faz o MOUSE nao girar o corpo) no lugar de GetControlRotation().Yaw. - Servidor: usa o yaw recebido DIRETO em simpleYawDeg (remove atan2 instantaneo + mira-no-idle). - Proxy: replica Snapshot.YawDeg interpolado shortest-path (remove a derivacao por Atan2 da velocidade). Resultado: o proxy vira identico ao corpo do dono e o mouse nao gira o corpo. Futuro (aim/combate/Motion Matching): adicionar um campo aimYawDeg dedicado em vez de reusar este. Co-Authored-By: Claude Opus 4.8 --- Source/ZMMO/Game/Entity/ZeusCharacter.cpp | 23 ++++++++++------ Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp | 29 ++++++++++++++++++--- Source/ZMMO/Game/Entity/ZeusPlayerProxy.h | 9 +++++++ 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/Source/ZMMO/Game/Entity/ZeusCharacter.cpp b/Source/ZMMO/Game/Entity/ZeusCharacter.cpp index d4eb22a..1a31eb0 100644 --- a/Source/ZMMO/Game/Entity/ZeusCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZeusCharacter.cpp @@ -640,12 +640,19 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) ++InputSequence; const int32 ClientTimeMs = static_cast(FPlatformTime::Seconds() * 1000.0); - // ADR 0040: yaw da camara/controller para que o servidor consiga rotar o - // input (referencial-camara) para velocidade mundial. Fallback para o yaw - // do actor se nao houver controller (e.g. tela de loading). - const float ViewYawDeg = (GetController() - ? static_cast(GetController()->GetControlRotation().Yaw) - : static_cast(GetActorRotation().Yaw)); + // YAW DO CORPO (nao da mira): GetActorRotation().Yaw reflete o orient-to-movement + // do CMC (bOrientRotationToMovement=true, RotationRate=(0,500,0)) -> o corpo vira + // pra direcao do MOVIMENTO (WASD), suave a 500 deg/s, e o MOUSE nao gira o corpo + // (bUseControllerRotationYaw=false). O servidor usa este yaw direto e o proxy nos + // outros clientes o replica -> o proxy vira IDENTICO ao corpo do dono. + // + // (Historico bug 2026-06-13: antes mandava GetControlRotation().Yaw -- a MIRA -- + // e o servidor fazia atan2(vel) INSTANTANEO quando movia (proxy "virava demais" + // 45 graus) e usava a mira quando parado (proxy seguia o mouse). ADR 0040: o + // servidor NAO rotaciona mais o input por este yaw (ADR 0041 manda pos+vel + // mundiais), entao trocar a mira pelo corpo e' seguro. Quando precisar da MIRA + // real (aim/combate), adicionar um campo aimYawDeg dedicado -- nao reusar este.) + const float BodyYawDeg = static_cast(GetActorRotation().Yaw); // ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja // integrou o input local antes deste flush, portanto `GetActorLocation` e // `GetVelocity` reflectem o estado real que o servidor deve replicar para @@ -662,7 +669,7 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) bPendingJumpReleased, InputSequence, ClientTimeMs, - ViewYawDeg, + BodyYawDeg, PosCm, FVector2D(Vel.X, Vel.Y), bIsFalling, @@ -689,7 +696,7 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds) bPendingJumpReleased, InputSequence, ClientTimeMs, - ViewYawDeg, + BodyYawDeg, PosCm, FVector2D(Vel.X, Vel.Y), bIsFalling, diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp index f955685..5d8dcab 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.cpp @@ -157,12 +157,32 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds) NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha); } - // Yaw deriva da velocidade interpolada: locomotion mantem face na direcao - // do movimento; em idle preserva o yaw atual para nao "snappar". + // ORIENTACAO DO CORPO = REPLICA do yaw autoritativo do servidor (Snapshot.YawDeg), + // interpolado shortest-path entre os MESMOS A/B da posicao. Esse yaw agora carrega + // o yaw do CORPO do dono (orient-to-movement do CMC: vira pra direcao do MOVIMENTO + // /WASD, suave a 500 deg/s; parado mantem; o MOUSE nao gira o corpo) -- o cliente + // dono manda GetActorRotation().Yaw e o servidor repassa direto. Por isso o proxy + // so' REPLICA: vira identico ao corpo do dono, sem re-derivar nada. + // + // NAO derivamos de Atan2(velocidade): isso era a fonte do bug 2026-06-13 ("virava + // demais 45 graus") -- a direcao da velocidade salta, o yaw do corpo nao. A + // suavizacao correta ja' aconteceu no CMC do dono; aqui so' interpolamos amostras. + // + // FUTURO -- Motion Matching/aim: quando "a mira dita a locomocao" (strafe/aim + // offset), o servidor passara a mandar um aimYawDeg separado e o AnimBP usara a + // velocidade relativa a este yaw pra escolher fwd/back/left/right. Por ORA, 1 yaw. FRotator NewRot = GetActorRotation(); - if (FVector(NewVel.X, NewVel.Y, 0.0f).SizeSquared() > 1.0f) + if (IdxB == INDEX_NONE) { - NewRot.Yaw = FMath::RadiansToDegrees(FMath::Atan2(NewVel.Y, NewVel.X)); + NewRot.Yaw = SnapshotBuffer.Last().YawDeg; // extrapolando: segura o ultimo yaw + } + else + { + const float YawA = SnapshotBuffer[IdxB - 1].YawDeg; + const float YawB = SnapshotBuffer[IdxB].YawDeg; + // FindDeltaAngleDegrees -> menor arco (-180..180); evita girar "o lado longo". + const float DeltaYaw = FMath::FindDeltaAngleDegrees(YawA, YawB); + NewRot.Yaw = YawA + DeltaYaw * InterpAlpha; } NewRot.Pitch = 0.0f; NewRot.Roll = 0.0f; @@ -303,6 +323,7 @@ void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot) FZeusProxySnapshot S; S.PosCm = Snapshot.PositionCm; S.VelCmS = Snapshot.VelocityCmS; + S.YawDeg = Snapshot.YawDeg; // yaw do CORPO do dono (orient-to-movement; mouse nao gira) S.bGrounded = Snapshot.bGrounded; S.ServerTimeMs = Snapshot.ServerTimeMs; diff --git a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h index c8f91d2..ec72ead 100644 --- a/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h +++ b/Source/ZMMO/Game/Entity/ZeusPlayerProxy.h @@ -42,6 +42,15 @@ struct FZeusProxySnapshot UPROPERTY() FVector VelCmS = FVector::ZeroVector; + /** Yaw autoritativo do servidor (graus) = o MESMO yaw que o player LOCAL aplicou + * ao virar. O proxy replica EXATAMENTE a viradinha do local (apertou D = mesma + * virada nos dois). Sem armazenar isto, o proxy derivava o yaw da velocidade + * (Atan2) e "virava totalmente pra direcao do movimento" (bug 2026-06-13). + * NAO confundir com "mira dita a locomocao" (strafe / Motion Matching) -- isso + * esta' DESABILITADO por ora (ver AVISO em ZeusPlayerProxy.cpp::Tick). */ + UPROPERTY() + float YawDeg = 0.0f; + UPROPERTY() bool bGrounded = true; From 216e580548cd12e81ccc8deea32d5670825c8646 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Mon, 15 Jun 2026 00:51:42 -0300 Subject: [PATCH 10/10] Overlay de fronteira (cliente): paredes do grid + labels Cell X,Y - AZeusFrontierOverlayActor: paredes (ProceduralMesh, material de portal) em todas as fronteiras do grid; label DrawDebugString no centro de cada cell com notacao de quadtree ("Cell 0" / "Cell 0,2" pos-split) + nome do servidor. - UZeusAOIComponent: liga via toggle "Fronteiras (Mesh)" / CVar zeus.debug.frontier, spawna/atualiza o ator, re-pede o 6163 a cada 2s (reflete split/merge). - ZMMO.Build.cs: + ProceduralMeshComponent. Co-Authored-By: Claude Opus 4.8 --- Source/ZMMO/Game/Network/ZeusAOIComponent.cpp | 111 +++++++++- Source/ZMMO/Game/Network/ZeusAOIComponent.h | 30 +++ .../Game/Network/ZeusFrontierOverlayActor.cpp | 205 ++++++++++++++++++ .../Game/Network/ZeusFrontierOverlayActor.h | 70 ++++++ Source/ZMMO/ZMMO.Build.cs | 1 + 5 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp create mode 100644 Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp index 3886ea0..0b4c95a 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp @@ -3,6 +3,7 @@ #include "ZeusAOIComponent.h" #include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160) +#include "ZeusFrontierOverlayActor.h" // ator que desenha as paredes de fronteira #include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO) #include "DrawDebugHelpers.h" @@ -23,6 +24,8 @@ static TAutoConsoleVariable CVarShowHandoff( TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default); static TAutoConsoleVariable CVarShowDrift( TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default); +static TAutoConsoleVariable CVarShowFrontier( + TEXT("zeus.debug.frontier"), 0, TEXT("Desenha as paredes das fronteiras do mesh (own<->neighbor)."), ECVF_Default); namespace { @@ -62,6 +65,7 @@ void UZeusAOIComponent::BeginPlay() if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig); + FrontierConfigHandle_ = NetC->OnDebugFrontierInfo.AddUObject(this, &UZeusAOIComponent::HandleFrontierConfig); } } } @@ -73,7 +77,7 @@ void UZeusAOIComponent::BeginPlay() void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { - if (AoiConfigHandle_.IsValid()) + if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid()) { if (UWorld* World = GetWorld()) { @@ -81,11 +85,18 @@ void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { - NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); + if (AoiConfigHandle_.IsValid()) { NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); } + if (FrontierConfigHandle_.IsValid()) { NetC->OnDebugFrontierInfo.Remove(FrontierConfigHandle_); } } } } AoiConfigHandle_.Reset(); + FrontierConfigHandle_.Reset(); + } + if (FrontierActor_) + { + FrontierActor_->Destroy(); + FrontierActor_ = nullptr; } Super::EndPlay(EndPlayReason); } @@ -95,6 +106,26 @@ void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType, { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); + // Frontier: sincroniza o overlay com o estado atual. Pega o liga/desliga via + // CVar zeus.debug.frontier (que nao passa por SetOverlayEnabled) e garante que + // ao desligar as paredes sejam REMOVIDAS (UpdateFrontierOverlay -> ClearOverlay). + if (ResolveOverlay(EZeusAOIOverlay::FrontierCells) != bFrontierApplied_) + { + UpdateFrontierOverlay(); + } + + // Re-pede o frontier periodicamente enquanto ligado -> o overlay reflete + // split/merge da topologia dinamica (debug client-side, baixa freq, so' ON). + if (ResolveOverlay(EZeusAOIOverlay::FrontierCells)) + { + FrontierRepollAccumSec_ += DeltaTime; + if (FrontierRepollAccumSec_ >= FrontierRepollIntervalSec) + { + FrontierRepollAccumSec_ = 0.0f; + RequestFrontierConfig(); + } + } + if (AnyOverlayActive()) { DrawOverlays(); @@ -127,6 +158,64 @@ void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm) DespawnCm, DespawnCm / 100.0f); } +void UZeusAOIComponent::RequestFrontierConfig() +{ + if (UWorld* World = GetWorld()) + { + if (UGameInstance* GI = World->GetGameInstance()) + { + if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) + { + NetC->EmitDebugFrontierRequest(); // V1: no-op se ainda nao Accepted + } + } + } +} + +void UZeusAOIComponent::HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info) +{ + FrontierInfo_ = Info; + bFrontierReceived_ = true; + UE_LOG(LogZeusAOI, Display, + TEXT("Frontier cfg recv own=%d neighbors=%d"), + Info.OwnedCells.Num(), Info.NeighborCells.Num()); + UpdateFrontierOverlay(); +} + +AZeusFrontierOverlayActor* UZeusAOIComponent::EnsureFrontierActor() +{ + if (FrontierActor_) { return FrontierActor_; } + UWorld* World = GetWorld(); + if (!World) { return nullptr; } + FActorSpawnParameters Params; + Params.Owner = GetOwner(); + Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + FrontierActor_ = World->SpawnActor( + AZeusFrontierOverlayActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, Params); + return FrontierActor_; +} + +void UZeusAOIComponent::UpdateFrontierOverlay() +{ + const bool bOn = ResolveOverlay(EZeusAOIOverlay::FrontierCells); + bFrontierApplied_ = bOn; + if (!bOn) + { + if (FrontierActor_) { FrontierActor_->ClearOverlay(); } + return; + } + if (!bFrontierReceived_) + { + // Ainda nao temos as cells; pede e espera o 6163 (HandleFrontierConfig re-chama). + RequestFrontierConfig(); + return; + } + AZeusFrontierOverlayActor* Actor = EnsureFrontierActor(); + if (!Actor) { return; } + const float AnchorZ = GetOwner() ? GetOwner()->GetActorLocation().Z : 0.0f; + Actor->RebuildFromFrontier(FrontierInfo_, AnchorZ); +} + bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const { switch (Overlay) @@ -137,6 +226,7 @@ bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const case EZeusAOIOverlay::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0; case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0; case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0; + case EZeusAOIOverlay::FrontierCells: return bShowFrontier || CVarShowFrontier.GetValueOnGameThread() > 0; default: return false; } } @@ -148,7 +238,8 @@ bool UZeusAOIComponent::AnyOverlayActive() const || ResolveOverlay(EZeusAOIOverlay::CellBounds) || ResolveOverlay(EZeusAOIOverlay::ProxyLabels) || ResolveOverlay(EZeusAOIOverlay::HandoffLine) - || ResolveOverlay(EZeusAOIOverlay::AuthDrift); + || ResolveOverlay(EZeusAOIOverlay::AuthDrift) + || ResolveOverlay(EZeusAOIOverlay::FrontierCells); } void UZeusAOIComponent::DrawOverlays() @@ -198,6 +289,7 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break; case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break; case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break; + case EZeusAOIOverlay::FrontierCells: bShowFrontier = bEnabled; break; default: break; } @@ -207,17 +299,27 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay { RequestAoiConfig(); } + + // Frontier: ao ligar pede as cells (6162) e reconstroi; ao desligar limpa as paredes. + if (Overlay == EZeusAOIOverlay::FrontierCells) + { + if (bEnabled) { RequestFrontierConfig(); } + UpdateFrontierOverlay(); + } } void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled) { bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled; bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled; + bShowFrontier = bEnabled; if (bEnabled && InterestRadiusCm <= 0.0f) { RequestAoiConfig(); } + if (bEnabled) { RequestFrontierConfig(); } + UpdateFrontierOverlay(); } bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const @@ -239,5 +341,6 @@ float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const int32 UZeusAOIComponent::GetEnabledOverlayCount() const { return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0) - + (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0); + + (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0) + + (bShowFrontier ? 1 : 0); } diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.h b/Source/ZMMO/Game/Network/ZeusAOIComponent.h index e91bab6..a24a5a9 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.h +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.h @@ -5,8 +5,11 @@ #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "ZeusAOIDebugTarget.h" // plugin ZeusAdminToolsRuntime: interface + EZeusAOIOverlay +#include "ZeusV1Protocol.h" // ZeusV1::FDebugFrontierInfo (overlay de fronteira) #include "ZeusAOIComponent.generated.h" +class AZeusFrontierOverlayActor; + /** * UZeusAOIComponent (cliente / jogo) * @@ -53,6 +56,9 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowProxyLabels = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowHandoffLine = false; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowAuthDrift = false; + // Fronteiras do mesh: paredes own<->neighbor com label "Cell ", renderizadas + // por AZeusFrontierOverlayActor a partir do DEBUG_FRONTIER_INFO (6163). + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowFrontier = false; // === Parametros === // Raios reais vem do servidor (S_DEBUG_AOI_INFO). 0 = ainda nao recebido (loading). @@ -76,6 +82,30 @@ private: /** Bind de UZeusNetworkSubsystem::OnDebugAoiInfo: armazena os raios reais. */ void HandleAoiConfig(float InterestCm, float DespawnCm); + /** Pede o frontier do mesh ao servidor (DEBUG_FRONTIER_REQUEST 6162). */ + void RequestFrontierConfig(); + /** Bind de OnDebugFrontierInfo: guarda as cells e (se overlay on) reconstroi as paredes. */ + void HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info); + /** Spawna (lazy) o ator que desenha as paredes + labels. */ + AZeusFrontierOverlayActor* EnsureFrontierActor(); + /** Liga / reconstroi / limpa o overlay de fronteira conforme bShowFrontier + dados. */ + void UpdateFrontierOverlay(); + /** Handle do bind do OnDebugAoiInfo (pra remover no EndPlay). */ FDelegateHandle AoiConfigHandle_; + + /** Handle do bind do OnDebugFrontierInfo (frontier overlay). */ + FDelegateHandle FrontierConfigHandle_; + /** Ultimo frontier recebido do server (own cells + neighbors). */ + ZeusV1::FDebugFrontierInfo FrontierInfo_; + bool bFrontierReceived_ = false; + /** Estado do overlay JA aplicado (paredes desenhadas). O TickComponent compara + * com ResolveOverlay p/ pegar liga/desliga via CVar tambem (nao so' o toggle). */ + bool bFrontierApplied_ = false; + /** Re-poll periodico do frontier enquanto ligado -> overlay reflete split/merge + * da topologia dinamica (o 6163 traz o estado atual do topology:current). */ + float FrontierRepollIntervalSec = 2.0f; + float FrontierRepollAccumSec_ = 0.0f; + /** Ator que desenha as paredes + labels (spawn lazy ao ligar o overlay). */ + UPROPERTY() TObjectPtr FrontierActor_ = nullptr; }; diff --git a/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp b/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp new file mode 100644 index 0000000..5027431 --- /dev/null +++ b/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp @@ -0,0 +1,205 @@ +// Copyright Zeus Server Engine. All rights reserved. + +#include "ZeusFrontierOverlayActor.h" + +#include "ZMMONetLog.h" // LogZeusAOI (categoria do modulo ZMMO) + +#include "ProceduralMeshComponent.h" +#include "Materials/MaterialInterface.h" +#include "Engine/World.h" +#include "DrawDebugHelpers.h" +#include "UObject/ConstructorHelpers.h" + +namespace +{ + const TCHAR* kWallMaterialPath = + TEXT("/Game/ZMMO/Materials/VFX/Portal/GridBoundary/Instancia/MI_VFX_Portal_GridBoundary_01.MI_VFX_Portal_GridBoundary_01"); + + const FColor kLabelAlive(93, 213, 255); // ciano -- server online + const FColor kLabelDead(255, 90, 90); // vermelho -- server offline / cell vazia + + // Offset do cellId de filha de split (espelha CHILD_CELLID_OFFSET do server). + constexpr uint32 kChildCellIdOffset = 1000000u; + + // Decodifica o cellId int na notacao de quadtree: raiz -> "0".."3"; filha -> + // "," (ex "0,2"); neto -> "0,2,1" (recursivo). Espelha + // childCellId = offset + pai*4 + quadrante do SplitCoordinator. + FString DecodeCellPath(uint32 CellId) + { + if (CellId < kChildCellIdOffset) + { + return FString::Printf(TEXT("%u"), CellId); + } + const uint32 Rel = CellId - kChildCellIdOffset; + return DecodeCellPath(Rel / 4u) + FString::Printf(TEXT(",%u"), Rel % 4u); + } + + // Aresta compartilhada entre dois retangulos axis-aligned (no plano XY). + bool SharedEdge(float aMinX, float aMinY, float aMaxX, float aMaxY, + float bMinX, float bMinY, float bMaxX, float bMaxY, + float Eps, FVector2D& OutA, FVector2D& OutB) + { + const float yLo = FMath::Max(aMinY, bMinY), yHi = FMath::Min(aMaxY, bMaxY); + const float xLo = FMath::Max(aMinX, bMinX), xHi = FMath::Min(aMaxX, bMaxX); + + if (FMath::Abs(aMaxX - bMinX) <= Eps && yHi > yLo) { OutA = {aMaxX, yLo}; OutB = {aMaxX, yHi}; return true; } + if (FMath::Abs(aMinX - bMaxX) <= Eps && yHi > yLo) { OutA = {aMinX, yLo}; OutB = {aMinX, yHi}; return true; } + if (FMath::Abs(aMaxY - bMinY) <= Eps && xHi > xLo) { OutA = {xLo, aMaxY}; OutB = {xHi, aMaxY}; return true; } + if (FMath::Abs(aMinY - bMaxY) <= Eps && xHi > xLo) { OutA = {xLo, aMinY}; OutB = {xHi, aMinY}; return true; } + return false; + } +} + +AZeusFrontierOverlayActor::AZeusFrontierOverlayActor() +{ + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = true; + + WallMesh = CreateDefaultSubobject(TEXT("WallMesh")); + SetRootComponent(WallMesh); + WallMesh->bUseAsyncCooking = false; + WallMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision); + WallMesh->SetCastShadow(false); + + static ConstructorHelpers::FObjectFinder MatFinder(kWallMaterialPath); + if (MatFinder.Succeeded()) + { + WallMaterial = MatFinder.Object; + } +} + +void AZeusFrontierOverlayActor::RebuildFromFrontier(const ZeusV1::FDebugFrontierInfo& Info, float AnchorZCm) +{ + if (!WallMesh) { return; } + AnchorZ_ = AnchorZCm; + + // 1) Junta own + neighbors numa lista unica de cells (grid inteiro). Dedup por cellId. + Cells_.Reset(); + auto AddCell = [this](uint32 CellId, int32 MinX, int32 MinY, int32 MaxX, int32 MaxY, + const FString& InOwner, bool bIsDead) + { + for (const FCellView& Ex : Cells_) { if (Ex.CellId == CellId) { return; } } + FCellView V; + V.CellId = CellId; + V.MinX = static_cast(MinX); V.MinY = static_cast(MinY); + V.MaxX = static_cast(MaxX); V.MaxY = static_cast(MaxY); + V.Owner = InOwner; V.bDead = bIsDead; + Cells_.Add(MoveTemp(V)); + }; + for (const ZeusV1::FFrontierOwnCell& C : Info.OwnedCells) + { + AddCell(C.CellId, C.MinXCm, C.MinYCm, C.MaxXCm, C.MaxYCm, C.OwnerName, false); + } + for (const ZeusV1::FFrontierNeighborCell& Nb : Info.NeighborCells) + { + AddCell(Nb.CellId, Nb.MinXCm, Nb.MinYCm, Nb.MaxXCm, Nb.MaxYCm, Nb.OwnerName, Nb.bDead); + } + + // 2) Paredes: 1 quad por aresta compartilhada entre QUALQUER par (i dedup). + const float BaseZ = AnchorZCm - WallHeightCm * 0.5f; + const float TopZ = AnchorZCm + WallHeightCm * 0.5f; + + TArray Verts; + TArray Tris; + TArray Normals; + TArray UVs; + TArray Tangents; + TArray Colors; + + for (int32 i = 0; i < Cells_.Num(); ++i) + { + for (int32 j = i + 1; j < Cells_.Num(); ++j) + { + const FCellView& A = Cells_[i]; + const FCellView& B = Cells_[j]; + FVector2D P0, P1; + if (!SharedEdge(A.MinX, A.MinY, A.MaxX, A.MaxY, + B.MinX, B.MinY, B.MaxX, B.MaxY, EdgeEpsilonCm, P0, P1)) + { + continue; + } + + const int32 Base = Verts.Num(); + Verts.Add(FVector(P0.X, P0.Y, BaseZ)); + Verts.Add(FVector(P1.X, P1.Y, BaseZ)); + Verts.Add(FVector(P1.X, P1.Y, TopZ)); + Verts.Add(FVector(P0.X, P0.Y, TopZ)); + + const FVector2D Dir2D = (P1 - P0).GetSafeNormal(); + const FVector N(-Dir2D.Y, Dir2D.X, 0.f); + Normals.Add(N); Normals.Add(N); Normals.Add(N); Normals.Add(N); + + UVs.Add(FVector2D(0.f, 1.f)); + UVs.Add(FVector2D(1.f, 1.f)); + UVs.Add(FVector2D(1.f, 0.f)); + UVs.Add(FVector2D(0.f, 0.f)); + + const FProcMeshTangent Tan(FVector(Dir2D.X, Dir2D.Y, 0.f), false); + Tangents.Add(Tan); Tangents.Add(Tan); Tangents.Add(Tan); Tangents.Add(Tan); + + const FLinearColor C = (A.bDead || B.bDead) ? FLinearColor(kLabelDead) : FLinearColor(kLabelAlive); + Colors.Add(C); Colors.Add(C); Colors.Add(C); Colors.Add(C); + + Tris.Add(Base + 0); Tris.Add(Base + 1); Tris.Add(Base + 2); + Tris.Add(Base + 0); Tris.Add(Base + 2); Tris.Add(Base + 3); + Tris.Add(Base + 0); Tris.Add(Base + 2); Tris.Add(Base + 1); + Tris.Add(Base + 0); Tris.Add(Base + 3); Tris.Add(Base + 2); + } + } + + WallMesh->ClearAllMeshSections(); + WallMesh->SetVisibility(true); + if (Verts.Num() > 0) + { + WallMesh->CreateMeshSection_LinearColor( + /*SectionIndex=*/0, Verts, Tris, Normals, UVs, Colors, Tangents, /*bCreateCollision=*/false); + if (WallMaterial) { WallMesh->SetMaterial(0, WallMaterial); } + } + bHasOverlay_ = (Cells_.Num() > 0); + + UE_LOG(LogZeusAOI, Display, + TEXT("Frontier overlay rebuilt: cells=%d walls=%d anchorZ=%.0f%s"), + Cells_.Num(), Verts.Num() / 4, AnchorZCm, + WallMaterial ? TEXT("") : TEXT(" [WARN: material nula]")); +} + +void AZeusFrontierOverlayActor::ClearOverlay() +{ + if (WallMesh) + { + WallMesh->ClearAllMeshSections(); + WallMesh->SetVisibility(false); + } + Cells_.Reset(); + bHasOverlay_ = false; +} + +void AZeusFrontierOverlayActor::DrawCellLabels() +{ + if (!bHasOverlay_ || Cells_.Num() == 0) { return; } + UWorld* World = GetWorld(); + if (!World) { return; } + + for (const FCellView& C : Cells_) + { + const float Cx = (C.MinX + C.MaxX) * 0.5f; + const float Cy = (C.MinY + C.MaxY) * 0.5f; + const FVector Pos(Cx, Cy, AnchorZ_ + LabelHeightCm); + + // "Cell 0" / "Cell 0,2" (notacao da quadtree) + nome do server dono. + FString Txt = FString::Printf(TEXT("Cell %s"), *DecodeCellPath(C.CellId)); + if (!C.Owner.IsEmpty()) { Txt += TEXT("\n") + C.Owner; } + const FColor Col = C.bDead ? kLabelDead : kLabelAlive; + + // DrawDebugString: billboard de tela (visivel de qualquer lado, nao gira, + // tamanho de tela constante). Re-desenhado a cada frame (Duration 0). + DrawDebugString(World, Pos, Txt, /*TestBaseActor=*/nullptr, Col, + /*Duration=*/0.0f, /*bDrawShadow=*/true, LabelFontScale); + } +} + +void AZeusFrontierOverlayActor::Tick(float DeltaSeconds) +{ + Super::Tick(DeltaSeconds); + DrawCellLabels(); +} diff --git a/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h b/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h new file mode 100644 index 0000000..77fcaef --- /dev/null +++ b/Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h @@ -0,0 +1,70 @@ +// Copyright Zeus Server Engine. All rights reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "ZeusV1Protocol.h" // ZeusV1::FDebugFrontierInfo (plugin ZeusNetwork) +#include "ZeusFrontierOverlayActor.generated.h" + +class UProceduralMeshComponent; +class UMaterialInterface; + +/** + * AZeusFrontierOverlayActor (cliente / debug) + * + * Desenha no MUNDO as "paredes" de TODAS as fronteiras do grid de server-meshing + * (todas as arestas compartilhadas entre quaisquer duas cells, com dedup), usando + * a material de portal MI_VFX_Portal_GridBoundary_01. As cells vem do + * DEBUG_FRONTIER_INFO (6163), que reflete a topologia DINAMICA (split/merge). + * + * Label: no CENTRO de cada cell, um texto via DrawDebugString (billboard de tela, + * visivel de qualquer lado, nao gira). Mostra a notacao da cell: + * raiz -> "Cell 0", "Cell 1", ... + * split -> "Cell 0,0", "Cell 0,1", ... (pai,quadrante) -- reflete a quadtree. + * + o nome do servidor dono. NAO e' replicado: visual no cliente local. + */ +UCLASS() +class ZMMO_API AZeusFrontierOverlayActor : public AActor +{ + GENERATED_BODY() + +public: + AZeusFrontierOverlayActor(); + + virtual void Tick(float DeltaSeconds) override; + + /** (Re)constroi as paredes do grid + guarda as cells pros labels. + * AnchorZCm ancora a altura das paredes/labels (tipicamente o Z do player). */ + void RebuildFromFrontier(const ZeusV1::FDebugFrontierInfo& Info, float AnchorZCm); + + /** Limpa a geometria (overlay desligado). */ + void ClearOverlay(); + + // === Parametros das paredes === + UPROPERTY(EditAnywhere, Category = "Zeus|Frontier") float WallHeightCm = 12000.0f; // 120m + UPROPERTY(EditAnywhere, Category = "Zeus|Frontier") float EdgeEpsilonCm = 4.0f; // tolerancia "compartilha aresta" + + // === Label "Cell X,Y" no centro de cada cell (DrawDebugString) === + UPROPERTY(EditAnywhere, Category = "Zeus|Frontier|Label") float LabelFontScale = 1.4f; // tamanho na tela (screen-space) + UPROPERTY(EditAnywhere, Category = "Zeus|Frontier|Label") float LabelHeightCm = 250.0f; // altura do label acima do anchor + +private: + UPROPERTY() UProceduralMeshComponent* WallMesh = nullptr; + UPROPERTY() UMaterialInterface* WallMaterial = nullptr; + + /// Snapshot leve das cells (own + neighbors) pros labels. + struct FCellView + { + uint32 CellId = 0; + float MinX = 0.f, MinY = 0.f, MaxX = 0.f, MaxY = 0.f; + FString Owner; // "Server1" (ou vazio) + bool bDead = false; + }; + TArray Cells_; + float AnchorZ_ = 0.0f; + bool bHasOverlay_ = false; + + /// Desenha "Cell X,Y\n" no centro de cada cell (chamado no Tick). + void DrawCellLabels(); +}; diff --git a/Source/ZMMO/ZMMO.Build.cs b/Source/ZMMO/ZMMO.Build.cs index 322e700..5095e10 100644 --- a/Source/ZMMO/ZMMO.Build.cs +++ b/Source/ZMMO/ZMMO.Build.cs @@ -19,6 +19,7 @@ public class ZMMO : ModuleRules "CommonInput", "GameplayTags", "GameplayAbilities", // GAS: UAttributeSet, FGameplayAttributeData + "ProceduralMeshComponent", // paredes do overlay de fronteira (AZeusFrontierOverlayActor) "ZeusNetwork", "ZeusGAS", // UZeusGASComponent, UZeusAttributeSet, FZeusAttributesSnapshot "ZeusJobs",