Consolidar GameplayAbilitySystem na main (baseline cliente tudo funcionando 2026-06-16) #6

Merged
Mateuus merged 10 commits from GameplayAbilitySystem into main 2026-06-16 00:26:54 -03:00
22 changed files with 1190 additions and 96 deletions

19
Config/DefaultZeusV1.ini Normal file
View File

@@ -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

View File

@@ -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 <json> <ini>`.
[/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="")

View File

@@ -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<UZeusNetworkSubsystem>();
if (!ZeusNetwork)
{
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
}
if (!NetClient)
{
NetClient = GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
}
}
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<int32>(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<int32>(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<float>(GetController()->GetControlRotation().Yaw)
: static_cast<float>(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<float>(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<float>(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<float>(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<float>(Vel.Z));
}
SendAccumulatorSec = 0.0f;
TimeSinceLastSendSec = 0.0f;

View File

@@ -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<UZeusNetworkSubsystem> ZeusNetwork;
// Sistema de rede novo (canonico): fonte do self-entity (ENT_SELF).
UPROPERTY()
TObjectPtr<UZeusNetworkingClientSubsystem> NetClient;
/** Instancia viva do painel admin (criada lazy no primeiro F8). */
UPROPERTY(Transient)
TObjectPtr<UUserWidget> AdminPanelInstance;

View File

@@ -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<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
0.0f, MaxExtrapolationSeconds);
const float RawExtrapSec =
static_cast<float>(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<int64>(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<int64>(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<int64>(
static_cast<double>(ServerClockOffsetMs) * (1.0 - Alpha)
+ static_cast<double>(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;

View File

@@ -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;

View File

@@ -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<int32> CVarShowHandoff(
TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowDrift(
TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default);
static TAutoConsoleVariable<int32> 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<UZeusNetworkSubsystem>())
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
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<UZeusNetworkSubsystem>())
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
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<UZeusNetworkSubsystem>())
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
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<UZeusNetworkingClientSubsystem>())
{
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>(
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);
}

View File

@@ -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 <id>", 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<AZeusFrontierOverlayActor> FrontierActor_ = nullptr;
};

View File

@@ -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 ->
// "<pai>,<quadrante>" (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<UProceduralMeshComponent>(TEXT("WallMesh"));
SetRootComponent(WallMesh);
WallMesh->bUseAsyncCooking = false;
WallMesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
WallMesh->SetCastShadow(false);
static ConstructorHelpers::FObjectFinder<UMaterialInterface> 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<float>(MinX); V.MinY = static_cast<float>(MinY);
V.MaxX = static_cast<float>(MaxX); V.MaxY = static_cast<float>(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<j -> dedup).
const float BaseZ = AnchorZCm - WallHeightCm * 0.5f;
const float TopZ = AnchorZCm + WallHeightCm * 0.5f;
TArray<FVector> Verts;
TArray<int32> Tris;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FProcMeshTangent> Tangents;
TArray<FLinearColor> 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();
}

View File

@@ -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<FCellView> Cells_;
float AnchorZ_ = 0.0f;
bool bHasOverlay_ = false;
/// Desenha "Cell X,Y\n<server>" no centro de cada cell (chamado no Tick).
void DrawCellLabels();
};

View File

@@ -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<AActor>* 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<AActor>(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<AActor>(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<AActor>* Found = RemoteEntities.Find(EntityId);
if (!Found || !Found->IsValid())
{
return false;
}
APawn* Pawn = Cast<APawn>(Found->Get());
if (!Pawn)
{
return false;
}
AZeusPlayerState* PS = Pawn->GetPlayerState<AZeusPlayerState>();
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<AActor>* 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<AActor>* 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<IZeusEntityInterface>(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<AActor>* 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<AActor>* Found = RemoteEntityClasses.Find(EntityType))
@@ -374,3 +581,18 @@ UZeusNetworkSubsystem* UZeusWorldSubsystem::ResolveZeusNetworkSubsystem() const
}
return GI->GetSubsystem<UZeusNetworkSubsystem>();
}
UZeusNetworkingClientSubsystem* UZeusWorldSubsystem::ResolveNetClientSubsystem() const
{
const UWorld* World = GetWorld();
if (!World)
{
return nullptr;
}
UGameInstance* GI = World->GetGameInstance();
if (!GI)
{
return nullptr;
}
return GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
}

View File

@@ -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<int64, FZeusPendingCharInfo> PendingCharInfo;
};

View File

@@ -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<UZeusNetworkSubsystem>() : nullptr;
}
UZeusNetworkingClientSubsystem* UUIFrontEndFlowSubsystem::GetNetClient() const
{
const UGameInstance* GI = GetGameInstance();
return GI ? GI->GetSubsystem<UZeusNetworkingClientSubsystem>() : 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())

View File

@@ -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();

View File

@@ -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<uint8>& 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<UZeusNetworkSubsystem>())
// 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<UZeusNetworkingClientSubsystem>();
const bool bUseV1 = V1Cdo && V1Cdo->bUseZeusNetworkingV1;
if (bUseV1)
{
if (UZeusNetworkingClientSubsystem* V1 = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
V1->ConnectWithTicket(GwHost, GwPort, HandoffToken);
}
else
{
UE_LOG(LogZMMO, Error, TEXT("Lobby: V1 subsystem ausente — handoff abortado"));
}
}
else if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
{
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.

View File

@@ -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",

View File

@@ -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)
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)

24
Source/ZMMO/ZMMONetLog.h Normal file
View File

@@ -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);