From 9f5ccd3a05504547823b370eda3bc3f30b205eba Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Fri, 12 Jun 2026 02:51:37 -0300 Subject: [PATCH] =?UTF-8?q?ZMMO:=20migra=C3=A7=C3=A3o=20para=20ZeusNetwork?= =?UTF-8?q?ing=20V1=20can=C3=B4nico=20+=20fix=20overshoot=20do=20proxy?= 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.