Merge pull request 'Consolidar GameplayAbilitySystem na main (baseline cliente tudo funcionando 2026-06-16)' (#6) from GameplayAbilitySystem into main
Reviewed-on: #6
This commit was merged in pull request #6.
This commit is contained in:
19
Config/DefaultZeusV1.ini
Normal file
19
Config/DefaultZeusV1.ini
Normal 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
|
||||||
83
Config/Tags/ZeusServerTags.ini
Normal file
83
Config/Tags/ZeusServerTags.ini
Normal 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="")
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,7 @@
|
|||||||
#include "GameFramework/PlayerController.h"
|
#include "GameFramework/PlayerController.h"
|
||||||
#include "ZeusWorldSubsystem.h"
|
#include "ZeusWorldSubsystem.h"
|
||||||
#include "ZeusNetworkSubsystem.h"
|
#include "ZeusNetworkSubsystem.h"
|
||||||
|
#include "ZeusNetworkingClientSubsystem.h"
|
||||||
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
|
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
|
||||||
|
|
||||||
DEFINE_LOG_CATEGORY(LogZeusPlayer);
|
DEFINE_LOG_CATEGORY(LogZeusPlayer);
|
||||||
@@ -362,7 +363,7 @@ void AZeusCharacter::SetEntityRelevant(bool /*bRelevant*/)
|
|||||||
|
|
||||||
void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
||||||
{
|
{
|
||||||
if (ZeusNetwork)
|
if (ZeusNetwork && NetClient)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -373,7 +374,14 @@ void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
|
if (!ZeusNetwork)
|
||||||
|
{
|
||||||
|
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
|
||||||
|
}
|
||||||
|
if (!NetClient)
|
||||||
|
{
|
||||||
|
NetClient = GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AZeusCharacter::BindZeusSpawnDelegate()
|
void AZeusCharacter::BindZeusSpawnDelegate()
|
||||||
@@ -382,49 +390,71 @@ void AZeusCharacter::BindZeusSpawnDelegate()
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!ZeusNetwork)
|
if (!NetClient && !ZeusNetwork)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned);
|
// Self-entity (entityId do proprio char) vem do sistema de rede novo.
|
||||||
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
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;
|
bSpawnDelegateBound = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AZeusCharacter::UnbindZeusSpawnDelegate()
|
void AZeusCharacter::UnbindZeusSpawnDelegate()
|
||||||
{
|
{
|
||||||
if (!bSpawnDelegateBound || !ZeusNetwork)
|
if (!bSpawnDelegateBound)
|
||||||
{
|
{
|
||||||
bSpawnDelegateBound = false;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned);
|
if (NetClient)
|
||||||
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
{
|
||||||
|
NetClient->OnSelfEntityAssigned.RemoveDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
|
||||||
|
}
|
||||||
|
if (ZeusNetwork)
|
||||||
|
{
|
||||||
|
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
||||||
|
}
|
||||||
bSpawnDelegateBound = false;
|
bSpawnDelegateBound = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn()
|
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;
|
// Race fix CHAR_INFO (ainda legacy): o S_CHAR_INFO pode ter chegado antes do
|
||||||
FVector CachedPosCm = FVector::ZeroVector;
|
// pawn existir; aplica o ultimo nome/guild cacheado.
|
||||||
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.
|
|
||||||
TryApplyCachedCharInfo();
|
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,
|
void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal,
|
||||||
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
||||||
{
|
{
|
||||||
@@ -548,7 +578,10 @@ void AZeusCharacter::TryApplyCachedCharInfo()
|
|||||||
void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||||
{
|
{
|
||||||
ResolveZeusNetworkSubsystem();
|
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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -557,7 +590,19 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
|||||||
SendAccumulatorSec += DeltaSeconds;
|
SendAccumulatorSec += DeltaSeconds;
|
||||||
TimeSinceLastSendSec += 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 bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
|
||||||
const bool bRateReached = SendAccumulatorSec >= SendInterval;
|
const bool bRateReached = SendAccumulatorSec >= SendInterval;
|
||||||
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
|
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
|
||||||
@@ -571,6 +616,23 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
|||||||
const bool bFallingChanged = (bIsFalling != bPreviousFalling);
|
const bool bFallingChanged = (bIsFalling != bPreviousFalling);
|
||||||
|
|
||||||
const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
|
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)
|
if (!bShouldSend)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -578,30 +640,68 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
|||||||
|
|
||||||
++InputSequence;
|
++InputSequence;
|
||||||
const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
|
const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
|
||||||
// ADR 0040: yaw da camara/controller para que o servidor consiga rotar o
|
// YAW DO CORPO (nao da mira): GetActorRotation().Yaw reflete o orient-to-movement
|
||||||
// input (referencial-camara) para velocidade mundial. Fallback para o yaw
|
// do CMC (bOrientRotationToMovement=true, RotationRate=(0,500,0)) -> o corpo vira
|
||||||
// do actor se nao houver controller (e.g. tela de loading).
|
// pra direcao do MOVIMENTO (WASD), suave a 500 deg/s, e o MOUSE nao gira o corpo
|
||||||
const float ViewYawDeg = (GetController()
|
// (bUseControllerRotationYaw=false). O servidor usa este yaw direto e o proxy nos
|
||||||
? static_cast<float>(GetController()->GetControlRotation().Yaw)
|
// outros clientes o replica -> o proxy vira IDENTICO ao corpo do dono.
|
||||||
: static_cast<float>(GetActorRotation().Yaw));
|
//
|
||||||
|
// (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
|
// ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja
|
||||||
// integrou o input local antes deste flush, portanto `GetActorLocation` e
|
// integrou o input local antes deste flush, portanto `GetActorLocation` e
|
||||||
// `GetVelocity` reflectem o estado real que o servidor deve replicar para
|
// `GetVelocity` reflectem o estado real que o servidor deve replicar para
|
||||||
// os outros clientes (sujeito ao clamp anti-cheat).
|
// os outros clientes (sujeito ao clamp anti-cheat).
|
||||||
const FVector PosCm = GetActorLocation();
|
const FVector PosCm = GetActorLocation();
|
||||||
const FVector Vel = GetVelocity();
|
const FVector Vel = GetVelocity();
|
||||||
ZeusNetwork->SendInputAxis(
|
if (bV1)
|
||||||
PendingMoveForward,
|
{
|
||||||
PendingMoveRight,
|
// Sistema novo: INPUT 6077 com a pose absoluta (ADR 0040/0041).
|
||||||
bPendingJumpPressed,
|
const bool bSent = NetClient->EmitInput(
|
||||||
bPendingJumpReleased,
|
PendingMoveForward,
|
||||||
InputSequence,
|
PendingMoveRight,
|
||||||
ClientTimeMs,
|
bPendingJumpPressed,
|
||||||
ViewYawDeg,
|
bPendingJumpReleased,
|
||||||
PosCm,
|
InputSequence,
|
||||||
FVector2D(Vel.X, Vel.Y),
|
ClientTimeMs,
|
||||||
bIsFalling,
|
BodyYawDeg,
|
||||||
static_cast<float>(Vel.Z));
|
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;
|
SendAccumulatorSec = 0.0f;
|
||||||
TimeSinceLastSendSec = 0.0f;
|
TimeSinceLastSendSec = 0.0f;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class USpringArmComponent;
|
|||||||
class UCameraComponent;
|
class UCameraComponent;
|
||||||
class UInputAction;
|
class UInputAction;
|
||||||
class UZeusNetworkSubsystem;
|
class UZeusNetworkSubsystem;
|
||||||
|
class UZeusNetworkingClientSubsystem;
|
||||||
class UZeusAOIComponent;
|
class UZeusAOIComponent;
|
||||||
class UUserWidget;
|
class UUserWidget;
|
||||||
struct FInputActionValue;
|
struct FInputActionValue;
|
||||||
@@ -164,6 +165,11 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleZeusPlayerSpawned(int64 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
|
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()
|
UFUNCTION()
|
||||||
void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName);
|
void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName);
|
||||||
|
|
||||||
@@ -172,6 +178,10 @@ private:
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork;
|
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). */
|
/** Instancia viva do painel admin (criada lazy no primeiro F8). */
|
||||||
UPROPERTY(Transient)
|
UPROPERTY(Transient)
|
||||||
TObjectPtr<UUserWidget> AdminPanelInstance;
|
TObjectPtr<UUserWidget> AdminPanelInstance;
|
||||||
|
|||||||
@@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
|||||||
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
||||||
// o proxy fugir se a rede ficar muda.
|
// o proxy fugir se a rede ficar muda.
|
||||||
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
|
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
|
||||||
const float ExtrapSec = FMath::Clamp(
|
const float RawExtrapSec =
|
||||||
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
|
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
|
||||||
0.0f, MaxExtrapolationSeconds);
|
const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
|
||||||
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
|
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;
|
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -150,12 +157,32 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
|||||||
NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
|
NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Yaw deriva da velocidade interpolada: locomotion mantem face na direcao
|
// ORIENTACAO DO CORPO = REPLICA do yaw autoritativo do servidor (Snapshot.YawDeg),
|
||||||
// do movimento; em idle preserva o yaw atual para nao "snappar".
|
// 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();
|
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.Pitch = 0.0f;
|
||||||
NewRot.Roll = 0.0f;
|
NewRot.Roll = 0.0f;
|
||||||
@@ -207,10 +234,26 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
|||||||
LastDiagLogSec = NowSec;
|
LastDiagLogSec = NowSec;
|
||||||
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
||||||
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
||||||
UE_LOG(LogZMMO, Verbose,
|
// === DIAG PERMANENTE -- jitter por FOME DE DELTAS ========================
|
||||||
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"),
|
// Bug investigado/resolvido 2026-06-13 (ver memoria
|
||||||
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
|
// project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO
|
||||||
bExtrapolating ? 1 : 0, InterpolationDelayMs);
|
// (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)
|
void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot)
|
||||||
{
|
{
|
||||||
// Estabiliza o offset de relogio na primeira amostra. Subsequente nao
|
LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick)
|
||||||
// re-sincroniza para evitar saltos visuais; um esquema mais sofisticado
|
|
||||||
// (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao.
|
// 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp.
|
||||||
|
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
|
||||||
|
// do ServerTimeMs muda (publisher antigo -> novo) e o offset fica
|
||||||
|
// permanentemente skewed, alimentando tremor pos-handoff e drift cumulativo
|
||||||
|
// de clock cliente vs server. Agora cada amostra recalcula candidato e
|
||||||
|
// suaviza (alpha=0.02, ~50 amostras pra convergir) com clamp max 2ms/snapshot
|
||||||
|
// pra evitar salto visual durante transicao.
|
||||||
|
const int64 LocalNowMs = static_cast<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)
|
if (SnapshotBuffer.Num() == 0)
|
||||||
{
|
{
|
||||||
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
// Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
|
||||||
ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs;
|
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;
|
FZeusProxySnapshot S;
|
||||||
S.PosCm = Snapshot.PositionCm;
|
S.PosCm = Snapshot.PositionCm;
|
||||||
S.VelCmS = Snapshot.VelocityCmS;
|
S.VelCmS = Snapshot.VelocityCmS;
|
||||||
|
S.YawDeg = Snapshot.YawDeg; // yaw do CORPO do dono (orient-to-movement; mouse nao gira)
|
||||||
S.bGrounded = Snapshot.bGrounded;
|
S.bGrounded = Snapshot.bGrounded;
|
||||||
S.ServerTimeMs = Snapshot.ServerTimeMs;
|
S.ServerTimeMs = Snapshot.ServerTimeMs;
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ struct FZeusProxySnapshot
|
|||||||
UPROPERTY()
|
UPROPERTY()
|
||||||
FVector VelCmS = FVector::ZeroVector;
|
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()
|
UPROPERTY()
|
||||||
bool bGrounded = true;
|
bool bGrounded = true;
|
||||||
|
|
||||||
@@ -140,6 +149,12 @@ protected:
|
|||||||
*/
|
*/
|
||||||
int64 ServerClockOffsetMs = 0;
|
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
|
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
|
||||||
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
|
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
|
||||||
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
|
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#include "ZeusAOIComponent.h"
|
#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 "DrawDebugHelpers.h"
|
||||||
#include "Engine/GameInstance.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);
|
TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default);
|
||||||
static TAutoConsoleVariable<int32> CVarShowDrift(
|
static TAutoConsoleVariable<int32> CVarShowDrift(
|
||||||
TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default);
|
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
|
namespace
|
||||||
{
|
{
|
||||||
@@ -29,6 +33,14 @@ namespace
|
|||||||
const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa)
|
const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa)
|
||||||
const FColor kColorCell(232, 192, 96);
|
const FColor kColorCell(232, 192, 96);
|
||||||
const FColor kColorHandoff(255, 155, 190);
|
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()
|
UZeusAOIComponent::UZeusAOIComponent()
|
||||||
@@ -42,14 +54,18 @@ void UZeusAOIComponent::BeginPlay()
|
|||||||
{
|
{
|
||||||
Super::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 (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (UGameInstance* GI = World->GetGameInstance())
|
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)
|
void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||||
{
|
{
|
||||||
if (AoiConfigHandle_.IsValid())
|
if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid())
|
||||||
{
|
{
|
||||||
if (UWorld* World = GetWorld())
|
if (UWorld* World = GetWorld())
|
||||||
{
|
{
|
||||||
if (UGameInstance* GI = World->GetGameInstance())
|
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();
|
AoiConfigHandle_.Reset();
|
||||||
|
FrontierConfigHandle_.Reset();
|
||||||
|
}
|
||||||
|
if (FrontierActor_)
|
||||||
|
{
|
||||||
|
FrontierActor_->Destroy();
|
||||||
|
FrontierActor_ = nullptr;
|
||||||
}
|
}
|
||||||
Super::EndPlay(EndPlayReason);
|
Super::EndPlay(EndPlayReason);
|
||||||
}
|
}
|
||||||
@@ -83,6 +106,26 @@ void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
|||||||
{
|
{
|
||||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
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())
|
if (AnyOverlayActive())
|
||||||
{
|
{
|
||||||
DrawOverlays();
|
DrawOverlays();
|
||||||
@@ -95,9 +138,9 @@ void UZeusAOIComponent::RequestAoiConfig()
|
|||||||
{
|
{
|
||||||
if (UGameInstance* GI = World->GetGameInstance())
|
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;
|
InterestRadiusCm = InterestCm;
|
||||||
DespawnRadiusCm = DespawnCm;
|
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
|
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::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0;
|
||||||
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
|
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
|
||||||
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
|
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
|
||||||
|
case EZeusAOIOverlay::FrontierCells: return bShowFrontier || CVarShowFrontier.GetValueOnGameThread() > 0;
|
||||||
default: return false;
|
default: return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,7 +238,8 @@ bool UZeusAOIComponent::AnyOverlayActive() const
|
|||||||
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|
||||||
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|
||||||
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|
||||||
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift);
|
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift)
|
||||||
|
|| ResolveOverlay(EZeusAOIOverlay::FrontierCells);
|
||||||
}
|
}
|
||||||
|
|
||||||
void UZeusAOIComponent::DrawOverlays()
|
void UZeusAOIComponent::DrawOverlays()
|
||||||
@@ -145,12 +254,14 @@ void UZeusAOIComponent::DrawOverlays()
|
|||||||
// ainda nao chegou a config: nao desenha (loading).
|
// ainda nao chegou a config: nao desenha (loading).
|
||||||
if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f)
|
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.
|
// Zona de Despawn (externa, > interesse = histerese) — raio real do servidor.
|
||||||
if (ResolveOverlay(EZeusAOIOverlay::DespawnZone) && DespawnRadiusCm > 0.0f)
|
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))
|
if (ResolveOverlay(EZeusAOIOverlay::CellBounds))
|
||||||
{
|
{
|
||||||
@@ -178,6 +289,7 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay
|
|||||||
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
|
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
|
||||||
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
|
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
|
||||||
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
|
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
|
||||||
|
case EZeusAOIOverlay::FrontierCells: bShowFrontier = bEnabled; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,17 +299,27 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay
|
|||||||
{
|
{
|
||||||
RequestAoiConfig();
|
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)
|
void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled)
|
||||||
{
|
{
|
||||||
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
|
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
|
||||||
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
|
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
|
||||||
|
bShowFrontier = bEnabled;
|
||||||
|
|
||||||
if (bEnabled && InterestRadiusCm <= 0.0f)
|
if (bEnabled && InterestRadiusCm <= 0.0f)
|
||||||
{
|
{
|
||||||
RequestAoiConfig();
|
RequestAoiConfig();
|
||||||
}
|
}
|
||||||
|
if (bEnabled) { RequestFrontierConfig(); }
|
||||||
|
UpdateFrontierOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
|
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
|
||||||
@@ -219,5 +341,6 @@ float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const
|
|||||||
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
|
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
|
||||||
{
|
{
|
||||||
return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0)
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,11 @@
|
|||||||
#include "CoreMinimal.h"
|
#include "CoreMinimal.h"
|
||||||
#include "Components/ActorComponent.h"
|
#include "Components/ActorComponent.h"
|
||||||
#include "ZeusAOIDebugTarget.h" // plugin ZeusAdminToolsRuntime: interface + EZeusAOIOverlay
|
#include "ZeusAOIDebugTarget.h" // plugin ZeusAdminToolsRuntime: interface + EZeusAOIOverlay
|
||||||
|
#include "ZeusV1Protocol.h" // ZeusV1::FDebugFrontierInfo (overlay de fronteira)
|
||||||
#include "ZeusAOIComponent.generated.h"
|
#include "ZeusAOIComponent.generated.h"
|
||||||
|
|
||||||
|
class AZeusFrontierOverlayActor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UZeusAOIComponent (cliente / jogo)
|
* 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 bShowProxyLabels = false;
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowHandoffLine = false;
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowHandoffLine = false;
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowAuthDrift = 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 ===
|
// === Parametros ===
|
||||||
// Raios reais vem do servidor (S_DEBUG_AOI_INFO). 0 = ainda nao recebido (loading).
|
// 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. */
|
/** Bind de UZeusNetworkSubsystem::OnDebugAoiInfo: armazena os raios reais. */
|
||||||
void HandleAoiConfig(float InterestCm, float DespawnCm);
|
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). */
|
/** Handle do bind do OnDebugAoiInfo (pra remover no EndPlay). */
|
||||||
FDelegateHandle AoiConfigHandle_;
|
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;
|
||||||
};
|
};
|
||||||
|
|||||||
205
Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp
Normal file
205
Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.cpp
Normal 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();
|
||||||
|
}
|
||||||
70
Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h
Normal file
70
Source/ZMMO/Game/Network/ZeusFrontierOverlayActor.h
Normal 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();
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "ZeusPlayerProxy.h"
|
#include "ZeusPlayerProxy.h"
|
||||||
#include "ZeusPlayerState.h"
|
#include "ZeusPlayerState.h"
|
||||||
#include "ZeusNetworkSubsystem.h"
|
#include "ZeusNetworkSubsystem.h"
|
||||||
|
#include "ZeusNetworkingClientSubsystem.h"
|
||||||
|
|
||||||
void UZeusWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
void UZeusWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||||
{
|
{
|
||||||
@@ -32,45 +33,59 @@ void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
|||||||
{
|
{
|
||||||
Super::OnWorldBeginPlay(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);
|
Net->OnEntitySpawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned);
|
||||||
ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned);
|
Net->OnEntityDespawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned);
|
||||||
ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate);
|
Net->OnEntityDeltaApplied.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta);
|
||||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to ZeusNetworkSubsystem delegates."));
|
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
|
// O subsystem de rede e' per-GameInstance e sobrevive ao OpenLevel, entao
|
||||||
// enquanto o cliente novo ainda esta em OpenLevel. UZeusNetworkSubsystem
|
// o self-entity e os ENT_SPAWN podem ter chegado ANTES deste bind. Puxa o
|
||||||
// cacheia em PendingRemoteSpawnsByEntity. Aplicamos aqui — mundo ja
|
// self cacheado + faz replay dos proxies ja conhecidos (anti-race).
|
||||||
// esta pronto (GameMode ativo, WorldPartition cells inicializadas).
|
if (const int64 CachedSelf = Net->GetLocalEntityId())
|
||||||
|
{
|
||||||
|
OnNetSelfEntityAssigned(CachedSelf);
|
||||||
|
}
|
||||||
int32 ReplayCount = 0;
|
int32 ReplayCount = 0;
|
||||||
ZeusNet->ForEachPendingRemoteSpawn(
|
Net->ForEachRemoteEntity(
|
||||||
[this, &ReplayCount](const int64 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
[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;
|
++ReplayCount;
|
||||||
});
|
});
|
||||||
if (ReplayCount > 0)
|
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
|
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()
|
void UZeusWorldSubsystem::Deinitialize()
|
||||||
{
|
{
|
||||||
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
|
if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem())
|
||||||
{
|
{
|
||||||
ZeusNet->OnPlayerSpawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned);
|
Net->OnEntitySpawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned);
|
||||||
ZeusNet->OnPlayerDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned);
|
Net->OnEntityDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned);
|
||||||
ZeusNet->OnPlayerStateUpdate.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate);
|
Net->OnEntityDeltaApplied.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta);
|
||||||
|
Net->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned);
|
||||||
|
Net->OnCharInfo.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteEntities.Reset();
|
RemoteEntities.Reset();
|
||||||
|
PendingCharInfo.Reset();
|
||||||
LocalEntityId = 0;
|
LocalEntityId = 0;
|
||||||
Super::Deinitialize();
|
Super::Deinitialize();
|
||||||
}
|
}
|
||||||
@@ -83,7 +98,29 @@ void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* Loca
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalEntityId = EntityId;
|
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));
|
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"),
|
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: local entity registered EntityId=%lld actor=%s"),
|
||||||
EntityId, *GetNameSafe(LocalActor));
|
EntityId, *GetNameSafe(LocalActor));
|
||||||
}
|
}
|
||||||
@@ -96,6 +133,21 @@ void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool b
|
|||||||
return;
|
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)
|
if (bIsLocal)
|
||||||
{
|
{
|
||||||
// O proprio AZeusCharacter cuida da sua identidade quando recebe o
|
// 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));
|
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"),
|
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: spawned remote EntityId=%d at (%s) yaw=%.1f t=%lld"),
|
||||||
EntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
|
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)
|
void UZeusWorldSubsystem::HandlePlayerDespawned(const int64 EntityId)
|
||||||
{
|
{
|
||||||
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
|
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
|
||||||
@@ -348,6 +453,108 @@ void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const in
|
|||||||
AsEntity->ApplyEntitySnapshot(Snapshot);
|
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
|
UClass* UZeusWorldSubsystem::ResolveActorClass(const EZeusEntityType EntityType) const
|
||||||
{
|
{
|
||||||
if (const TSubclassOf<AActor>* Found = RemoteEntityClasses.Find(EntityType))
|
if (const TSubclassOf<AActor>* Found = RemoteEntityClasses.Find(EntityType))
|
||||||
@@ -374,3 +581,18 @@ UZeusNetworkSubsystem* UZeusWorldSubsystem::ResolveZeusNetworkSubsystem() const
|
|||||||
}
|
}
|
||||||
return GI->GetSubsystem<UZeusNetworkSubsystem>();
|
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>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class AActor;
|
|||||||
class AZeusCharacter;
|
class AZeusCharacter;
|
||||||
class AZeusPlayerProxy;
|
class AZeusPlayerProxy;
|
||||||
class UZeusNetworkSubsystem;
|
class UZeusNetworkSubsystem;
|
||||||
|
class UZeusNetworkingClientSubsystem;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UZeusWorldSubsystem
|
* UZeusWorldSubsystem
|
||||||
@@ -80,8 +81,39 @@ private:
|
|||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandlePlayerStateUpdate(int64 EntityId, int32 InputSeq, FVector PosCm, FVector VelCmS, bool bGrounded, int64 ServerTimeMs);
|
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;
|
UClass* ResolveActorClass(EZeusEntityType EntityType) const;
|
||||||
UZeusNetworkSubsystem* ResolveZeusNetworkSubsystem() 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.
|
* 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). */
|
/** Cached id do jogador local, para ignorar snapshots dele (cliente solto). */
|
||||||
int64 LocalEntityId = 0;
|
int64 LocalEntityId = 0;
|
||||||
|
|
||||||
|
/** V1-CHARINFO: nome/guild recebido antes do ator existir. Chave = EntityId. */
|
||||||
|
struct FZeusPendingCharInfo
|
||||||
|
{
|
||||||
|
FString CharName;
|
||||||
|
FString GuildName;
|
||||||
|
};
|
||||||
|
TMap<int64, FZeusPendingCharInfo> PendingCharInfo;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "ZeusGameInstance.h"
|
#include "ZeusGameInstance.h"
|
||||||
#include "ZeusThemeSubsystem.h"
|
#include "ZeusThemeSubsystem.h"
|
||||||
#include "ZeusNetworkSubsystem.h"
|
#include "ZeusNetworkSubsystem.h"
|
||||||
|
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico (sinal de spawn local)
|
||||||
#include "ZeusCharServerSubsystem.h"
|
#include "ZeusCharServerSubsystem.h"
|
||||||
#include "CommonActivatableWidget.h"
|
#include "CommonActivatableWidget.h"
|
||||||
#include "Engine/DataTable.h"
|
#include "Engine/DataTable.h"
|
||||||
@@ -265,6 +266,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const
|
|||||||
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
|
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UZeusNetworkingClientSubsystem* UUIFrontEndFlowSubsystem::GetNetClient() const
|
||||||
|
{
|
||||||
|
const UGameInstance* GI = GetGameInstance();
|
||||||
|
return GI ? GI->GetSubsystem<UZeusNetworkingClientSubsystem>() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
|
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
|
||||||
{
|
{
|
||||||
const UGameInstance* GI = GetGameInstance();
|
const UGameInstance* GI = GetGameInstance();
|
||||||
@@ -314,11 +321,20 @@ void UUIFrontEndFlowSubsystem::BindNetwork()
|
|||||||
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||||
Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
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())
|
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||||
{
|
{
|
||||||
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
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;
|
bNetBound = true;
|
||||||
}
|
}
|
||||||
@@ -338,7 +354,10 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
|||||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||||
{
|
{
|
||||||
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
||||||
Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned);
|
}
|
||||||
|
if (UZeusNetworkingClientSubsystem* Net = GetNetClient())
|
||||||
|
{
|
||||||
|
Net->OnSelfEntityAssigned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned);
|
||||||
}
|
}
|
||||||
bNetBound = false;
|
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()
|
void UUIFrontEndFlowSubsystem::HandleLoadingComplete()
|
||||||
{
|
{
|
||||||
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
|
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class UUIManagerSubsystem;
|
|||||||
class UUILoadingScreen_Base;
|
class UUILoadingScreen_Base;
|
||||||
class UZeusLoadingProfilesDataAsset;
|
class UZeusLoadingProfilesDataAsset;
|
||||||
class UZeusNetworkSubsystem;
|
class UZeusNetworkSubsystem;
|
||||||
|
class UZeusNetworkingClientSubsystem;
|
||||||
class UZeusCharServerSubsystem;
|
class UZeusCharServerSubsystem;
|
||||||
struct FZeusMapDef;
|
struct FZeusMapDef;
|
||||||
|
|
||||||
@@ -171,6 +172,7 @@ private:
|
|||||||
void BindNetwork();
|
void BindNetwork();
|
||||||
void UnbindNetwork();
|
void UnbindNetwork();
|
||||||
UZeusNetworkSubsystem* GetZeusNetwork() const;
|
UZeusNetworkSubsystem* GetZeusNetwork() const;
|
||||||
|
UZeusNetworkingClientSubsystem* GetNetClient() const; // V1 canonico
|
||||||
UZeusCharServerSubsystem* GetCharServer() const;
|
UZeusCharServerSubsystem* GetCharServer() const;
|
||||||
UUIManagerSubsystem* GetUIManager() const;
|
UUIManagerSubsystem* GetUIManager() const;
|
||||||
UUIFrontEndScreenSet* GetScreenSet();
|
UUIFrontEndScreenSet* GetScreenSet();
|
||||||
@@ -198,6 +200,15 @@ private:
|
|||||||
void HandlePlayerSpawned(int64 EntityId, bool bIsLocal, FVector PosCm,
|
void HandlePlayerSpawned(int64 EntityId, bool bIsLocal, FVector PosCm,
|
||||||
float YawDeg, int64 ServerTimeMs);
|
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. */
|
/** Disparado pela tela de loading quando todas as etapas viram Done. */
|
||||||
UFUNCTION()
|
UFUNCTION()
|
||||||
void HandleLoadingComplete();
|
void HandleLoadingComplete();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "WireHelpers.h"
|
#include "WireHelpers.h"
|
||||||
#include "ZeusCharServerSubsystem.h"
|
#include "ZeusCharServerSubsystem.h"
|
||||||
#include "ZeusNetworkSubsystem.h"
|
#include "ZeusNetworkSubsystem.h"
|
||||||
|
#include "ZeusNetworkingClientSubsystem.h" // B4: V1 ConnectWithTicket
|
||||||
#include "CommonTextBlock.h"
|
#include "CommonTextBlock.h"
|
||||||
#include "Components/PanelWidget.h"
|
#include "Components/PanelWidget.h"
|
||||||
#include "Components/WidgetSwitcher.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).
|
// `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado).
|
||||||
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
|
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
|
||||||
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
|
// 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);
|
ZeusNet->ConnectToZeusServerWithTicket(GwHost, GwPort, HandoffToken);
|
||||||
}
|
}
|
||||||
else
|
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.
|
// Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo.
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ public class ZMMO : ModuleRules
|
|||||||
"CommonInput",
|
"CommonInput",
|
||||||
"GameplayTags",
|
"GameplayTags",
|
||||||
"GameplayAbilities", // GAS: UAttributeSet, FGameplayAttributeData
|
"GameplayAbilities", // GAS: UAttributeSet, FGameplayAttributeData
|
||||||
|
"ProceduralMeshComponent", // paredes do overlay de fronteira (AZeusFrontierOverlayActor)
|
||||||
"ZeusNetwork",
|
"ZeusNetwork",
|
||||||
"ZeusGAS", // UZeusGASComponent, UZeusAttributeSet, FZeusAttributesSnapshot
|
"ZeusGAS", // UZeusGASComponent, UZeusAttributeSet, FZeusAttributesSnapshot
|
||||||
"ZeusJobs",
|
"ZeusJobs",
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||||
|
|
||||||
#include "ZMMO.h"
|
#include "ZMMO.h"
|
||||||
|
#include "ZMMONetLog.h"
|
||||||
#include "Modules/ModuleManager.h"
|
#include "Modules/ModuleManager.h"
|
||||||
|
|
||||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ZMMO, "ZMMO" );
|
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
24
Source/ZMMO/ZMMONetLog.h
Normal 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);
|
||||||
Reference in New Issue
Block a user