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/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="") diff --git a/Content/ZMMO/Data/World/DT_Maps.uasset b/Content/ZMMO/Data/World/DT_Maps.uasset index fd76096..be9dffc 100644 Binary files a/Content/ZMMO/Data/World/DT_Maps.uasset and b/Content/ZMMO/Data/World/DT_Maps.uasset differ 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 3e4faf3..0000000 Binary files a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/BR/38W34OMKW0W3L1PGS3Y98G.uasset and /dev/null differ 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 2552681..0000000 Binary files a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/8L/X3PMRJRGAACIU1WG1P1KJ0.uasset and /dev/null differ 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 059ff5a..0000000 Binary files a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/L1/C6M65Q91PG5E7ML91VQZX3.uasset and /dev/null differ diff --git a/Source/ZMMO/Game/Entity/ZeusCharacter.cpp b/Source/ZMMO/Game/Entity/ZeusCharacter.cpp index eff9f5f..1a31eb0 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; @@ -578,30 +640,68 @@ 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 // 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, + BodyYawDeg, + 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, + BodyYawDeg, + 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 574cb68..5d8dcab 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 @@ -150,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; @@ -207,10 +234,26 @@ 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"), - EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha, - bExtrapolating ? 1 : 0, InterpolationDelayMs); + // === 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 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()); } } @@ -222,18 +265,65 @@ 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. + 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 + // 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; + + // 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) { - const int64 LocalNowMs = static_cast(FPlatformTime::Seconds() * 1000.0); - ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs; + // 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 + { + 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; 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 5a21096..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; @@ -140,6 +149,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; diff --git a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp index 4ef4005..0b4c95a 100644 --- a/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp +++ b/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp @@ -2,7 +2,9 @@ #include "ZeusAOIComponent.h" -#include "ZeusNetworkSubsystem.h" // plugin ZeusNetwork: OnDebugAoiInfo + SendDebugAoiRequest +#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" #include "Engine/GameInstance.h" @@ -22,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 { @@ -29,6 +33,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() @@ -42,14 +54,18 @@ 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); + FrontierConfigHandle_ = NetC->OnDebugFrontierInfo.AddUObject(this, &UZeusAOIComponent::HandleFrontierConfig); } } } @@ -61,19 +77,26 @@ void UZeusAOIComponent::BeginPlay() void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) { - if (AoiConfigHandle_.IsValid()) + if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid()) { if (UWorld* World = GetWorld()) { if (UGameInstance* GI = World->GetGameInstance()) { - if (UZeusNetworkSubsystem* Net = GI->GetSubsystem()) + if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem()) { - Net->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); } @@ -83,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(); @@ -95,9 +138,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 } } } @@ -107,6 +150,70 @@ 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); +} + +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 @@ -119,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; } } @@ -130,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() @@ -145,12 +254,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)) { @@ -178,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; } @@ -187,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 @@ -219,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/Game/Network/ZeusWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp index e1897ca..ecc584e 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,45 +33,59 @@ 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); + Net->OnCharInfo.AddDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo); + 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); + // 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) { - 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); + Net->OnCharInfo.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo); } RemoteEntities.Reset(); + PendingCharInfo.Reset(); LocalEntityId = 0; Super::Deinitialize(); } @@ -83,7 +98,29 @@ 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)); + // 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)); } @@ -96,6 +133,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 @@ -212,10 +264,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); @@ -348,6 +453,108 @@ void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const in AsEntity->ApplyEntitySnapshot(Snapshot); } +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); + + // 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*/) +{ + // 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)) @@ -374,3 +581,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..94d184d 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,39 @@ 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, + FVector VelCmS, bool bGrounded, int64 ServerTimeMs); + + 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); + + // 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. @@ -93,4 +125,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(); 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. 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", 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);