Files
ZMMO/Source/ZMMO/Game/Network/ZeusAOIComponent.cpp
Mateus Rodrigues 5f4c88637f ZN V1: overlay AOI canonico + ENT_SPAWN keyframe [Change-Set: AOI-VIS-2026-06-12]
Lado CLIENTE ZMMO (par com o commit de mesmo Change-Set no repo ZeusServerEngine -- devem
ir juntos, o wire do ENT_SPAWN mudou).

- UZeusAOIComponent: migrado do transporte legacy (SendDebugAoiRequest/OnDebugAoiInfo,
  desativado no V1) pro canonico (EmitDebugAoiRequest 6160 / OnDebugAoiInfo 6161). Esfera de
  debug 32 -> 48 segments + clamp visual de 500m (resolve "zona sem limite / poucas linhas").
- ZeusWorldSubsystem::OnNetEntitySpawned: recebe vel + grounded + serverTimeMs (delegate
  FZeusV1OnEntitySpawned 4 -> 7 params) e semeia o proxy recem-criado via HandlePlayerStateUpdate
  -> ancora o relogio de interpolacao + a vel inicial -> proxy nasce ja' animando no re-spawn
  (sai/volta do raio AOI segurando W), sem Idle deslizando.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 21:04:43 -03:00

244 lines
9.0 KiB
C++

// Copyright Zeus Server Engine. All rights reserved.
#include "ZeusAOIComponent.h"
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160)
#include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO)
#include "DrawDebugHelpers.h"
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
// Console vars — espelham as flags do painel (overlay liga se flag OU CVar).
static TAutoConsoleVariable<int32> CVarShowAOI(
TEXT("zeus.debug.aoi"), 0, TEXT("Desenha a Zona de Interesse (esfera do raio AOI)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowDespawn(
TEXT("zeus.debug.despawn"), 0, TEXT("Desenha a Zona de Despawn (esfera externa do AOI)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowCell(
TEXT("zeus.debug.cell"), 0, TEXT("Desenha os bounds da celula (placeholder)."), ECVF_Default);
static TAutoConsoleVariable<int32> CVarShowProxy(
TEXT("zeus.debug.proxy"), 0, TEXT("Desenha labels de proxy (TODO)."), ECVF_Default);
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);
namespace
{
const FColor kColorInterest(93, 213, 255); // cyan — Zona de Interesse
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()
{
PrimaryComponentTick.bCanEverTick = true;
PrimaryComponentTick.bStartWithTickEnabled = true;
SetIsReplicatedByDefault(false); // debug/cliente; gameplay AOI real entra depois
}
void UZeusAOIComponent::BeginPlay()
{
Super::BeginPlay();
// 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 (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig);
}
}
}
// Best-effort: ja' pede a config (no-op se ainda nao conectado — o request
// e' re-disparado ao ligar um overlay em SetOverlayEnabled).
RequestAoiConfig();
}
void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
if (AoiConfigHandle_.IsValid())
{
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_);
}
}
}
AoiConfigHandle_.Reset();
}
Super::EndPlay(EndPlayReason);
}
void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType,
FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (AnyOverlayActive())
{
DrawOverlays();
}
}
void UZeusAOIComponent::RequestAoiConfig()
{
if (UWorld* World = GetWorld())
{
if (UGameInstance* GI = World->GetGameInstance())
{
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
{
NetC->EmitDebugAoiRequest(); // V1: no-op se ainda nao Accepted
}
}
}
}
void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm)
{
InterestRadiusCm = InterestCm;
DespawnRadiusCm = DespawnCm;
// K4: log AOI cfg recebido do server (paridade com Net.AOI server-side
// "config_applied ZI=... ZD=..."). Cold path (1 por sessao).
UE_LOG(LogZeusAOI, Display,
TEXT("AOI cfg recv interestCm=%.0f (%.0fm) despawnCm=%.0f (%.0fm)"),
InterestCm, InterestCm / 100.0f,
DespawnCm, DespawnCm / 100.0f);
}
bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const
{
switch (Overlay)
{
case EZeusAOIOverlay::AOIRadius: return bShowAOIRadius || CVarShowAOI.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::DespawnZone: return bShowDespawnZone || CVarShowDespawn.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::CellBounds: return bShowCellBounds || CVarShowCell.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
default: return false;
}
}
bool UZeusAOIComponent::AnyOverlayActive() const
{
return ResolveOverlay(EZeusAOIOverlay::AOIRadius)
|| ResolveOverlay(EZeusAOIOverlay::DespawnZone)
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift);
}
void UZeusAOIComponent::DrawOverlays()
{
const AActor* Owner = GetOwner();
UWorld* World = GetWorld();
if (!Owner || !World) { return; }
const FVector Loc = Owner->GetActorLocation();
// Zona de Interesse (interna) — raio real do servidor. Sem o raio (==0),
// ainda nao chegou a config: nao desenha (loading).
if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f)
{
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)
{
const float R = FMath::Min(DespawnRadiusCm, kMaxOverlayRadiusCm);
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorDespawn, false, -1.0f, 0, LineThickness);
}
if (ResolveOverlay(EZeusAOIOverlay::CellBounds))
{
// PLACEHOLDER: box grande centrada no player. TODO bounds reais da cell.
DrawDebugBox(World, Loc, FVector(CellBoundsExtentCm, CellBoundsExtentCm, 2000.0f),
kColorCell, false, -1.0f, 0, LineThickness);
}
if (ResolveOverlay(EZeusAOIOverlay::HandoffLine))
{
// PLACEHOLDER: vetor pra frente. TODO apontar pro centro da cell vizinha.
const float Len = InterestRadiusCm > 0.0f ? InterestRadiusCm : 3000.0f;
const FVector End = Loc + Owner->GetActorForwardVector() * Len;
DrawDebugDirectionalArrow(World, Loc, End, 200.0f, kColorHandoff, false, -1.0f, 0, LineThickness);
}
// ProxyLabels / AuthDrift: TODO (precisam do registry de proxies/snapshots).
}
void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay, bool bEnabled)
{
switch (Overlay)
{
case EZeusAOIOverlay::AOIRadius: bShowAOIRadius = bEnabled; break;
case EZeusAOIOverlay::DespawnZone: bShowDespawnZone = bEnabled; break;
case EZeusAOIOverlay::CellBounds: bShowCellBounds = bEnabled; break;
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
default: break;
}
// Ao ligar uma zona de AOI sem ter os raios ainda, pede a config (loading).
const bool bAoiZone = (Overlay == EZeusAOIOverlay::AOIRadius || Overlay == EZeusAOIOverlay::DespawnZone);
if (bEnabled && bAoiZone && InterestRadiusCm <= 0.0f)
{
RequestAoiConfig();
}
}
void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled)
{
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
if (bEnabled && InterestRadiusCm <= 0.0f)
{
RequestAoiConfig();
}
}
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
{
// Reflete o estado real (flag OU CVar) p/ a UI mostrar o que esta' desenhando.
return ResolveOverlay(Overlay);
}
float UZeusAOIComponent::GetInterestRadiusCm_Implementation() const
{
return InterestRadiusCm; // 0 = config do servidor ainda nao chegou (loading)
}
float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const
{
return DespawnRadiusCm;
}
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
{
return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0)
+ (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0);
}