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 "ZeusWorldSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "ZeusNetworkingClientSubsystem.h"
|
||||
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogZeusPlayer);
|
||||
@@ -362,7 +363,7 @@ void AZeusCharacter::SetEntityRelevant(bool /*bRelevant*/)
|
||||
|
||||
void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
||||
{
|
||||
if (ZeusNetwork)
|
||||
if (ZeusNetwork && NetClient)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -373,7 +374,14 @@ void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
||||
return;
|
||||
}
|
||||
|
||||
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
|
||||
if (!ZeusNetwork)
|
||||
{
|
||||
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
|
||||
}
|
||||
if (!NetClient)
|
||||
{
|
||||
NetClient = GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
|
||||
}
|
||||
}
|
||||
|
||||
void AZeusCharacter::BindZeusSpawnDelegate()
|
||||
@@ -382,49 +390,71 @@ void AZeusCharacter::BindZeusSpawnDelegate()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (!ZeusNetwork)
|
||||
if (!NetClient && !ZeusNetwork)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned);
|
||||
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
||||
// Self-entity (entityId do proprio char) vem do sistema de rede novo.
|
||||
if (NetClient)
|
||||
{
|
||||
NetClient->OnSelfEntityAssigned.AddDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
|
||||
}
|
||||
// CHAR_INFO (nome/guild) ainda nao tem equivalente no sistema novo -> legacy.
|
||||
if (ZeusNetwork)
|
||||
{
|
||||
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
||||
}
|
||||
bSpawnDelegateBound = true;
|
||||
}
|
||||
|
||||
void AZeusCharacter::UnbindZeusSpawnDelegate()
|
||||
{
|
||||
if (!bSpawnDelegateBound || !ZeusNetwork)
|
||||
if (!bSpawnDelegateBound)
|
||||
{
|
||||
bSpawnDelegateBound = false;
|
||||
return;
|
||||
}
|
||||
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZeusCharacter::HandleZeusPlayerSpawned);
|
||||
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
||||
if (NetClient)
|
||||
{
|
||||
NetClient->OnSelfEntityAssigned.RemoveDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
|
||||
}
|
||||
if (ZeusNetwork)
|
||||
{
|
||||
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
||||
}
|
||||
bSpawnDelegateBound = false;
|
||||
}
|
||||
|
||||
void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn()
|
||||
{
|
||||
if (!ZeusNetwork)
|
||||
// Self-entity vem do sistema de rede novo (ENT_SELF). O subsystem e'
|
||||
// per-GameInstance e sobrevive ao OpenLevel, entao o entityId pode ja' ter
|
||||
// chegado antes deste pawn existir -- puxa o cache agora. Caso ainda nao
|
||||
// tenha chegado, o bind em OnSelfEntityAssigned cobre quando chegar.
|
||||
// A pos/yaw vem do proprio pawn (ja posicionado via PendingSpawnPose do DB);
|
||||
// o sistema novo so' carrega o entityId.
|
||||
if (NetClient)
|
||||
{
|
||||
return;
|
||||
const int64 SelfId = NetClient->GetLocalEntityId();
|
||||
if (SelfId != 0)
|
||||
{
|
||||
HandleLocalSpawnReady(SelfId, GetActorLocation(), GetActorRotation().Yaw, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int64 CachedEntityId = 0;
|
||||
FVector CachedPosCm = FVector::ZeroVector;
|
||||
float CachedYawDeg = 0.0f;
|
||||
int64 CachedServerTimeMs = 0;
|
||||
if (ZeusNetwork->TryGetLastLocalSpawn(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs))
|
||||
{
|
||||
HandleLocalSpawnReady(CachedEntityId, CachedPosCm, CachedYawDeg, CachedServerTimeMs);
|
||||
}
|
||||
|
||||
// Race fix: o S_CHAR_INFO chega ANTES de S_SPAWN_PLAYER mas o pawn pode
|
||||
// nao existir ainda (OpenLevel em andamento). Aplica o ultimo cacheado.
|
||||
// Race fix CHAR_INFO (ainda legacy): o S_CHAR_INFO pode ter chegado antes do
|
||||
// pawn existir; aplica o ultimo nome/guild cacheado.
|
||||
TryApplyCachedCharInfo();
|
||||
}
|
||||
|
||||
void AZeusCharacter::HandleSelfEntityAssigned(const int64 InEntityId)
|
||||
{
|
||||
// Sistema novo informou o entityId do proprio char (ENT_SELF). Reusa o mesmo
|
||||
// caminho do spawn local legacy (seta ZeusEntityId + RegisterLocalEntity).
|
||||
// pos/yaw vem do pawn (ja posicionado); o sistema novo so' traz o entityId.
|
||||
HandleLocalSpawnReady(InEntityId, GetActorLocation(), GetActorRotation().Yaw, 0);
|
||||
}
|
||||
|
||||
void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal,
|
||||
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
||||
{
|
||||
@@ -548,7 +578,10 @@ void AZeusCharacter::TryApplyCachedCharInfo()
|
||||
void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||
{
|
||||
ResolveZeusNetworkSubsystem();
|
||||
if (!ZeusNetwork || !ZeusNetwork->IsConnected())
|
||||
// Sistema de rede novo (canonico) tem prioridade; legacy e' fallback.
|
||||
const bool bV1 = (NetClient
|
||||
&& NetClient->GetConnectionState() == EZeusV1ConnectionState::Accepted);
|
||||
if (!bV1 && (!ZeusNetwork || !ZeusNetwork->IsConnected()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -557,7 +590,19 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||
SendAccumulatorSec += DeltaSeconds;
|
||||
TimeSinceLastSendSec += DeltaSeconds;
|
||||
|
||||
const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
|
||||
// Movimento p/ efeitos de rate NAO e' so' o input (W/A/S/D): o CMC do dono
|
||||
// continua desacelerando (braking) por ~0.3s depois de soltar a tecla. Se
|
||||
// pararmos de enviar no rate rapido assim que o input zera, o servidor fica
|
||||
// congelado na ultima `simpleVelCmS` alta e o proxy remoto extrapola ~1m alem
|
||||
// (NewPos = Last.Pos + Last.Vel*ExtrapSec) ate o heartbeat corrigir -> overshoot
|
||||
// + snap-back ("velocidade grande que demora a zerar" + jitter). Tratar a
|
||||
// velocidade residual como movimento mantem o envio a InputSendRateHz ate o
|
||||
// dono realmente parar, alimentando o proxy com a curva de desaceleracao.
|
||||
const bool bHasInputAxis = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
|
||||
const FVector OwnerVelNow = GetVelocity();
|
||||
constexpr float kResidualSpeedSqCmS = 25.0f * 25.0f; // abaixo de 25 cm/s tratamos como parado
|
||||
const bool bHasResidualVel = OwnerVelNow.SizeSquared() > kResidualSpeedSqCmS;
|
||||
const bool bMovingInput = bHasInputAxis || bHasResidualVel;
|
||||
const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
|
||||
const bool bRateReached = SendAccumulatorSec >= SendInterval;
|
||||
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
|
||||
@@ -571,6 +616,23 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||
const bool bFallingChanged = (bIsFalling != bPreviousFalling);
|
||||
|
||||
const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
|
||||
// DEBUG-INPUT (2026-06-12): log ~1x/s COM o PIE. Remover apos achar o gap.
|
||||
{
|
||||
static float DbgInputAcc = 0.0f;
|
||||
DbgInputAcc += DeltaSeconds;
|
||||
if (DbgInputAcc >= 1.0f)
|
||||
{
|
||||
DbgInputAcc = 0.0f;
|
||||
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
|
||||
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
|
||||
UE_LOG(LogZeusPlayer, Warning,
|
||||
TEXT("[PIE%d][InputDbg] bV1=%d state=%d moving=%d rate=%d should=%d hbReached=%d fwd=%.2f right=%.2f"),
|
||||
PieId, bV1 ? 1 : 0,
|
||||
NetClient ? static_cast<int32>(NetClient->GetConnectionState()) : -99,
|
||||
bMovingInput ? 1 : 0, bRateReached ? 1 : 0, bShouldSend ? 1 : 0,
|
||||
bHeartbeatReached ? 1 : 0, PendingMoveForward, PendingMoveRight);
|
||||
}
|
||||
}
|
||||
if (!bShouldSend)
|
||||
{
|
||||
return;
|
||||
@@ -578,30 +640,68 @@ void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
||||
|
||||
++InputSequence;
|
||||
const int32 ClientTimeMs = static_cast<int32>(FPlatformTime::Seconds() * 1000.0);
|
||||
// ADR 0040: yaw da camara/controller para que o servidor consiga rotar o
|
||||
// input (referencial-camara) para velocidade mundial. Fallback para o yaw
|
||||
// do actor se nao houver controller (e.g. tela de loading).
|
||||
const float ViewYawDeg = (GetController()
|
||||
? static_cast<float>(GetController()->GetControlRotation().Yaw)
|
||||
: static_cast<float>(GetActorRotation().Yaw));
|
||||
// YAW DO CORPO (nao da mira): GetActorRotation().Yaw reflete o orient-to-movement
|
||||
// do CMC (bOrientRotationToMovement=true, RotationRate=(0,500,0)) -> o corpo vira
|
||||
// pra direcao do MOVIMENTO (WASD), suave a 500 deg/s, e o MOUSE nao gira o corpo
|
||||
// (bUseControllerRotationYaw=false). O servidor usa este yaw direto e o proxy nos
|
||||
// outros clientes o replica -> o proxy vira IDENTICO ao corpo do dono.
|
||||
//
|
||||
// (Historico bug 2026-06-13: antes mandava GetControlRotation().Yaw -- a MIRA --
|
||||
// e o servidor fazia atan2(vel) INSTANTANEO quando movia (proxy "virava demais"
|
||||
// 45 graus) e usava a mira quando parado (proxy seguia o mouse). ADR 0040: o
|
||||
// servidor NAO rotaciona mais o input por este yaw (ADR 0041 manda pos+vel
|
||||
// mundiais), entao trocar a mira pelo corpo e' seguro. Quando precisar da MIRA
|
||||
// real (aim/combate), adicionar um campo aimYawDeg dedicado -- nao reusar este.)
|
||||
const float BodyYawDeg = static_cast<float>(GetActorRotation().Yaw);
|
||||
// ADR 0041: cliente autoritativo da posicao XYZ + velocidade XY. O CMC ja
|
||||
// integrou o input local antes deste flush, portanto `GetActorLocation` e
|
||||
// `GetVelocity` reflectem o estado real que o servidor deve replicar para
|
||||
// os outros clientes (sujeito ao clamp anti-cheat).
|
||||
const FVector PosCm = GetActorLocation();
|
||||
const FVector Vel = GetVelocity();
|
||||
ZeusNetwork->SendInputAxis(
|
||||
PendingMoveForward,
|
||||
PendingMoveRight,
|
||||
bPendingJumpPressed,
|
||||
bPendingJumpReleased,
|
||||
InputSequence,
|
||||
ClientTimeMs,
|
||||
ViewYawDeg,
|
||||
PosCm,
|
||||
FVector2D(Vel.X, Vel.Y),
|
||||
bIsFalling,
|
||||
static_cast<float>(Vel.Z));
|
||||
if (bV1)
|
||||
{
|
||||
// Sistema novo: INPUT 6077 com a pose absoluta (ADR 0040/0041).
|
||||
const bool bSent = NetClient->EmitInput(
|
||||
PendingMoveForward,
|
||||
PendingMoveRight,
|
||||
bPendingJumpPressed,
|
||||
bPendingJumpReleased,
|
||||
InputSequence,
|
||||
ClientTimeMs,
|
||||
BodyYawDeg,
|
||||
PosCm,
|
||||
FVector2D(Vel.X, Vel.Y),
|
||||
bIsFalling,
|
||||
static_cast<float>(Vel.Z));
|
||||
// DEBUG-INPUT (2026-06-12): confirma chamada + retorno do SendPacket.
|
||||
static float DbgEmitAcc = 0.0f;
|
||||
DbgEmitAcc += DeltaSeconds;
|
||||
if (DbgEmitAcc >= 1.0f)
|
||||
{
|
||||
DbgEmitAcc = 0.0f;
|
||||
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
|
||||
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
|
||||
UE_LOG(LogZeusPlayer, Warning,
|
||||
TEXT("[PIE%d][InputDbg] EmitInput seq=%d sent=%d pos=%s"),
|
||||
PieId, InputSequence, bSent ? 1 : 0, *PosCm.ToString());
|
||||
}
|
||||
}
|
||||
else if (ZeusNetwork)
|
||||
{
|
||||
ZeusNetwork->SendInputAxis(
|
||||
PendingMoveForward,
|
||||
PendingMoveRight,
|
||||
bPendingJumpPressed,
|
||||
bPendingJumpReleased,
|
||||
InputSequence,
|
||||
ClientTimeMs,
|
||||
BodyYawDeg,
|
||||
PosCm,
|
||||
FVector2D(Vel.X, Vel.Y),
|
||||
bIsFalling,
|
||||
static_cast<float>(Vel.Z));
|
||||
}
|
||||
|
||||
SendAccumulatorSec = 0.0f;
|
||||
TimeSinceLastSendSec = 0.0f;
|
||||
|
||||
@@ -11,6 +11,7 @@ class USpringArmComponent;
|
||||
class UCameraComponent;
|
||||
class UInputAction;
|
||||
class UZeusNetworkSubsystem;
|
||||
class UZeusNetworkingClientSubsystem;
|
||||
class UZeusAOIComponent;
|
||||
class UUserWidget;
|
||||
struct FInputActionValue;
|
||||
@@ -164,6 +165,11 @@ private:
|
||||
UFUNCTION()
|
||||
void HandleZeusPlayerSpawned(int64 InEntityId, bool bIsLocal, FVector PosCm, float YawDeg, int64 ServerTimeMs);
|
||||
|
||||
// Self-entity do sistema de rede novo (ENT_SELF): o cliente aprende o
|
||||
// proprio entityId. Substitui o caminho legacy OnPlayerSpawned(bIsLocal=true).
|
||||
UFUNCTION()
|
||||
void HandleSelfEntityAssigned(int64 InEntityId);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleZeusCharInfo(int64 InEntityId, const FString& CharName, const FString& GuildName);
|
||||
|
||||
@@ -172,6 +178,10 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UZeusNetworkSubsystem> ZeusNetwork;
|
||||
|
||||
// Sistema de rede novo (canonico): fonte do self-entity (ENT_SELF).
|
||||
UPROPERTY()
|
||||
TObjectPtr<UZeusNetworkingClientSubsystem> NetClient;
|
||||
|
||||
/** Instancia viva do painel admin (criada lazy no primeiro F8). */
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UUserWidget> AdminPanelInstance;
|
||||
|
||||
@@ -132,11 +132,18 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
||||
// conhecida, clampada por `MaxExtrapolationSeconds` para nao deixar
|
||||
// o proxy fugir se a rede ficar muda.
|
||||
const FZeusProxySnapshot& Last = SnapshotBuffer.Last();
|
||||
const float ExtrapSec = FMath::Clamp(
|
||||
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f,
|
||||
0.0f, MaxExtrapolationSeconds);
|
||||
const float RawExtrapSec =
|
||||
static_cast<float>(RenderMs - Last.ServerTimeMs) / 1000.0f;
|
||||
const float ExtrapSec = FMath::Clamp(RawExtrapSec, 0.0f, MaxExtrapolationSeconds);
|
||||
NewPos = Last.PosCm + Last.VelCmS * ExtrapSec;
|
||||
NewVel = Last.VelCmS;
|
||||
// STARVATION (buffer=1 por muito tempo -- ex: 1o login sem DELTA_BATCH por
|
||||
// segundos): a extrapolacao satura no clamp e a POSE congela, mas manter a
|
||||
// velocidade faz o AnimBP rodar locomocao PARADO ("andar no lugar"/moonwalk
|
||||
// = o "jitter" reportado). Se ja passamos bem do clamp, zera a vel visual
|
||||
// pra o proxy ir pra Idle ate' os deltas voltarem (em vez de animar
|
||||
// andando travado). Quando os deltas chegam, o ramo de interpolacao retoma.
|
||||
const bool bStarved = RawExtrapSec > (MaxExtrapolationSeconds + 0.15f);
|
||||
NewVel = bStarved ? FVector::ZeroVector : Last.VelCmS;
|
||||
bExtrapolating = ExtrapSec > KINDA_SMALL_NUMBER;
|
||||
}
|
||||
else
|
||||
@@ -150,12 +157,32 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
||||
NewVel = FMath::Lerp(A.VelCmS, B.VelCmS, InterpAlpha);
|
||||
}
|
||||
|
||||
// Yaw deriva da velocidade interpolada: locomotion mantem face na direcao
|
||||
// do movimento; em idle preserva o yaw atual para nao "snappar".
|
||||
// ORIENTACAO DO CORPO = REPLICA do yaw autoritativo do servidor (Snapshot.YawDeg),
|
||||
// interpolado shortest-path entre os MESMOS A/B da posicao. Esse yaw agora carrega
|
||||
// o yaw do CORPO do dono (orient-to-movement do CMC: vira pra direcao do MOVIMENTO
|
||||
// /WASD, suave a 500 deg/s; parado mantem; o MOUSE nao gira o corpo) -- o cliente
|
||||
// dono manda GetActorRotation().Yaw e o servidor repassa direto. Por isso o proxy
|
||||
// so' REPLICA: vira identico ao corpo do dono, sem re-derivar nada.
|
||||
//
|
||||
// NAO derivamos de Atan2(velocidade): isso era a fonte do bug 2026-06-13 ("virava
|
||||
// demais 45 graus") -- a direcao da velocidade salta, o yaw do corpo nao. A
|
||||
// suavizacao correta ja' aconteceu no CMC do dono; aqui so' interpolamos amostras.
|
||||
//
|
||||
// FUTURO -- Motion Matching/aim: quando "a mira dita a locomocao" (strafe/aim
|
||||
// offset), o servidor passara a mandar um aimYawDeg separado e o AnimBP usara a
|
||||
// velocidade relativa a este yaw pra escolher fwd/back/left/right. Por ORA, 1 yaw.
|
||||
FRotator NewRot = GetActorRotation();
|
||||
if (FVector(NewVel.X, NewVel.Y, 0.0f).SizeSquared() > 1.0f)
|
||||
if (IdxB == INDEX_NONE)
|
||||
{
|
||||
NewRot.Yaw = FMath::RadiansToDegrees(FMath::Atan2(NewVel.Y, NewVel.X));
|
||||
NewRot.Yaw = SnapshotBuffer.Last().YawDeg; // extrapolando: segura o ultimo yaw
|
||||
}
|
||||
else
|
||||
{
|
||||
const float YawA = SnapshotBuffer[IdxB - 1].YawDeg;
|
||||
const float YawB = SnapshotBuffer[IdxB].YawDeg;
|
||||
// FindDeltaAngleDegrees -> menor arco (-180..180); evita girar "o lado longo".
|
||||
const float DeltaYaw = FMath::FindDeltaAngleDegrees(YawA, YawB);
|
||||
NewRot.Yaw = YawA + DeltaYaw * InterpAlpha;
|
||||
}
|
||||
NewRot.Pitch = 0.0f;
|
||||
NewRot.Roll = 0.0f;
|
||||
@@ -207,10 +234,26 @@ void AZeusPlayerProxy::Tick(const float DeltaSeconds)
|
||||
LastDiagLogSec = NowSec;
|
||||
const int64 NewestMs = SnapshotBuffer.Last().ServerTimeMs;
|
||||
const int64 RenderLagMs = ServerNowMs - NewestMs;
|
||||
UE_LOG(LogZMMO, Verbose,
|
||||
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld interpAlpha=%.2f extrap=%d delayMs=%.0f"),
|
||||
EntityId, SnapshotBuffer.Num(), RenderLagMs, InterpAlpha,
|
||||
bExtrapolating ? 1 : 0, InterpolationDelayMs);
|
||||
// === DIAG PERMANENTE -- jitter por FOME DE DELTAS ========================
|
||||
// Bug investigado/resolvido 2026-06-13 (ver memoria
|
||||
// project_proxy_delta_starvation_jitter). Este Warning FICA DE PROPOSITO
|
||||
// (decisao do dono: NAO rebaixar p/ Verbose nem remover) -- e' a sonda pra
|
||||
// cacar regressao do jitter de proxy. Como ler:
|
||||
// secsSinceSnap = ha quanto tempo o proxy NAO recebe snapshot. Sobe = FOME
|
||||
// (player remoto parado -> ComputeDelta suprime no server -> sem DELTA_BATCH).
|
||||
// newestMs = serverTimeMs do ultimo snapshot; CONGELA na fome (deveria avancar).
|
||||
// renderLagMs = ServerNowMs - newestMs; cresce 1:1 com o wall-clock na fome.
|
||||
// speed=0 na fome = a mitigacao (zera a vel visual -> Idle, sem "moonwalk") agindo.
|
||||
// Ao o player voltar a se mover sai "buffer STALE ... reset+reseed" (anti-stale)
|
||||
// e renderLagMs/secsSinceSnap voltam a ~0 (snap limpo, sem salto/jitter).
|
||||
// =========================================================================
|
||||
const double SecsSinceSnap =
|
||||
(LastSnapshotRecvSec > 0.0) ? (NowSec - LastSnapshotRecvSec) : -1.0;
|
||||
UE_LOG(LogZMMO, Warning,
|
||||
TEXT("ZeusPlayerProxy[%lld] buffer=%d renderLagMs=%lld newestMs=%lld secsSinceSnap=%.1f interpAlpha=%.2f extrap=%d speed=%.0f"),
|
||||
EntityId, SnapshotBuffer.Num(), RenderLagMs, NewestMs, SecsSinceSnap, InterpAlpha,
|
||||
bExtrapolating ? 1 : 0,
|
||||
FVector(VisualVelocity.X, VisualVelocity.Y, 0.0f).Size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,18 +265,65 @@ void AZeusPlayerProxy::SetZeusIdentity(const int64 InEntityId, const EZeusEntity
|
||||
|
||||
void AZeusPlayerProxy::ApplyEntitySnapshot(const FZeusEntitySnapshot& Snapshot)
|
||||
{
|
||||
// Estabiliza o offset de relogio na primeira amostra. Subsequente nao
|
||||
// re-sincroniza para evitar saltos visuais; um esquema mais sofisticado
|
||||
// (ema do offset) cabe quando tivermos ping/RTT estabilizado por sessao.
|
||||
LastSnapshotRecvSec = FPlatformTime::Seconds(); // DIAG: marca chegada (mede a fome no Tick)
|
||||
|
||||
// 2026-06-06 H3 fix — disciplina o ServerClockOffsetMs via EMA com clamp.
|
||||
// Antes: offset congelado na 1a amostra. Em cross-server handoff a fonte
|
||||
// do ServerTimeMs muda (publisher antigo -> novo) e o offset fica
|
||||
// permanentemente skewed, alimentando tremor pos-handoff e drift cumulativo
|
||||
// de clock cliente vs server. Agora cada amostra recalcula candidato e
|
||||
// suaviza (alpha=0.02, ~50 amostras pra convergir) com clamp max 2ms/snapshot
|
||||
// pra evitar salto visual durante transicao.
|
||||
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
||||
const int64 OffsetCandidate = LocalNowMs - Snapshot.ServerTimeMs;
|
||||
|
||||
// Anti-stale (2026-06-13): se este snapshot esta MUITO a frente do mais novo
|
||||
// do buffer, o proxy ficou "mudo" por segundos -- player remoto parado ->
|
||||
// ComputeDelta suprime -> sem DELTA_BATCH novo -> renderLagMs disparou pra
|
||||
// dezenas de segundos. Manter os snapshots velhos faz a interpolacao usar um
|
||||
// span gigante (pose de ~100s atras -> agora) quando o player volta a se mover,
|
||||
// causando rasteio/salto (o jitter reportado). Descarta o buffer obsoleto +
|
||||
// re-bootstrapa o relogio pra a interpolacao recomecar limpa neste keyframe.
|
||||
if (SnapshotBuffer.Num() > 0
|
||||
&& (Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs) > 1000)
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning,
|
||||
TEXT("ZeusPlayerProxy[%lld] buffer STALE gap=%lldms -> reset+reseed"),
|
||||
EntityId, Snapshot.ServerTimeMs - SnapshotBuffer.Last().ServerTimeMs);
|
||||
SnapshotBuffer.Reset();
|
||||
ServerClockOffsetMs = OffsetCandidate;
|
||||
}
|
||||
|
||||
if (SnapshotBuffer.Num() == 0)
|
||||
{
|
||||
const int64 LocalNowMs = static_cast<int64>(FPlatformTime::Seconds() * 1000.0);
|
||||
ServerClockOffsetMs = LocalNowMs - Snapshot.ServerTimeMs;
|
||||
// Primeira amostra: bootstrap direto (nao tem baseline pra suavizar).
|
||||
ServerClockOffsetMs = OffsetCandidate;
|
||||
// DIAG (jitter 1o login): confirma o seed do relogio. serverTimeMs deve ser
|
||||
// != 0 e o offset razoavel; se vier 0 aqui, o keyframe/catch-up corrompeu o
|
||||
// bootstrap (hipotese descartada, mas o log fecha a questao em campo).
|
||||
UE_LOG(LogZMMO, Warning,
|
||||
TEXT("ZeusPlayerProxy[%lld] SEED clock serverTimeMs=%lld localNowMs=%lld offset=%lld"),
|
||||
EntityId, Snapshot.ServerTimeMs, LocalNowMs, ServerClockOffsetMs);
|
||||
}
|
||||
else
|
||||
{
|
||||
const double Alpha = 0.02;
|
||||
const int64 RawTarget = static_cast<int64>(
|
||||
static_cast<double>(ServerClockOffsetMs) * (1.0 - Alpha)
|
||||
+ static_cast<double>(OffsetCandidate) * Alpha);
|
||||
// Clamp max 2ms por amostra — em 30Hz isso converge a 60ms/s, mais que
|
||||
// o suficiente pra absorver drift normal sem causar tremor visual.
|
||||
const int64 Delta = RawTarget - ServerClockOffsetMs;
|
||||
const int64 ClampedDelta = (Delta > 2) ? 2
|
||||
: (Delta < -2) ? -2
|
||||
: Delta;
|
||||
ServerClockOffsetMs += ClampedDelta;
|
||||
}
|
||||
|
||||
FZeusProxySnapshot S;
|
||||
S.PosCm = Snapshot.PositionCm;
|
||||
S.VelCmS = Snapshot.VelocityCmS;
|
||||
S.YawDeg = Snapshot.YawDeg; // yaw do CORPO do dono (orient-to-movement; mouse nao gira)
|
||||
S.bGrounded = Snapshot.bGrounded;
|
||||
S.ServerTimeMs = Snapshot.ServerTimeMs;
|
||||
|
||||
|
||||
@@ -42,6 +42,15 @@ struct FZeusProxySnapshot
|
||||
UPROPERTY()
|
||||
FVector VelCmS = FVector::ZeroVector;
|
||||
|
||||
/** Yaw autoritativo do servidor (graus) = o MESMO yaw que o player LOCAL aplicou
|
||||
* ao virar. O proxy replica EXATAMENTE a viradinha do local (apertou D = mesma
|
||||
* virada nos dois). Sem armazenar isto, o proxy derivava o yaw da velocidade
|
||||
* (Atan2) e "virava totalmente pra direcao do movimento" (bug 2026-06-13).
|
||||
* NAO confundir com "mira dita a locomocao" (strafe / Motion Matching) -- isso
|
||||
* esta' DESABILITADO por ora (ver AVISO em ZeusPlayerProxy.cpp::Tick). */
|
||||
UPROPERTY()
|
||||
float YawDeg = 0.0f;
|
||||
|
||||
UPROPERTY()
|
||||
bool bGrounded = true;
|
||||
|
||||
@@ -140,6 +149,12 @@ protected:
|
||||
*/
|
||||
int64 ServerClockOffsetMs = 0;
|
||||
|
||||
/** DIAG (2026-06-13): instante (s, monotonic) do ultimo snapshot recebido em
|
||||
* ApplyEntitySnapshot. Usado no log do Tick pra medir "secsSinceSnap" = quanto
|
||||
* tempo o proxy ficou MUDO (sem delta). secsSinceSnap grande == fome (player
|
||||
* remoto parado -> ComputeDelta suprime). 0 = nenhum snapshot ainda. */
|
||||
double LastSnapshotRecvSec = 0.0;
|
||||
|
||||
/** Aceleracao derivada (com smoothing) escrita no CMC custom para alimentar
|
||||
* o AnimBP (`ShouldMove`/`Jump_Start` exigem Acceleration!=0). */
|
||||
FVector LastDerivedAccelerationCmS2 = FVector::ZeroVector;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
#include "ZeusAOIComponent.h"
|
||||
|
||||
#include "ZeusNetworkSubsystem.h" // plugin ZeusNetwork: OnDebugAoiInfo + SendDebugAoiRequest
|
||||
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico: OnDebugAoiInfo (6161) + EmitDebugAoiRequest (6160)
|
||||
#include "ZeusFrontierOverlayActor.h" // ator que desenha as paredes de fronteira
|
||||
#include "ZMMONetLog.h" // Batch 2.5: LogZeusAOI (categoria do modulo ZMMO)
|
||||
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
@@ -22,6 +24,8 @@ static TAutoConsoleVariable<int32> CVarShowHandoff(
|
||||
TEXT("zeus.debug.handoff"), 0, TEXT("Desenha a linha de handoff (placeholder)."), ECVF_Default);
|
||||
static TAutoConsoleVariable<int32> CVarShowDrift(
|
||||
TEXT("zeus.debug.drift"), 0, TEXT("Desenha o drift autoritativo (TODO)."), ECVF_Default);
|
||||
static TAutoConsoleVariable<int32> CVarShowFrontier(
|
||||
TEXT("zeus.debug.frontier"), 0, TEXT("Desenha as paredes das fronteiras do mesh (own<->neighbor)."), ECVF_Default);
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -29,6 +33,14 @@ namespace
|
||||
const FColor kColorDespawn(255, 140, 90); // laranja — Zona de Despawn (externa)
|
||||
const FColor kColorCell(232, 192, 96);
|
||||
const FColor kColorHandoff(255, 155, 190);
|
||||
|
||||
// Resolucao das esferas de overlay. 32 segments deixavam a "bolha" facetada
|
||||
// (poucas linhas) num raio de 60-80m; 48 deixa o circulo bem mais denso/legivel.
|
||||
constexpr int32 kSphereSegments = 48;
|
||||
// Clamp visual defensivo: se o raio vier absurdo (config corrompida), nao desenha
|
||||
// uma esfera gigante que engole o mapa. 500m cobre qualquer AOI real (max=800m,
|
||||
// mas overlay > 500m ja' nao ajuda a leitura). So' afeta o desenho, nao o gameplay.
|
||||
constexpr float kMaxOverlayRadiusCm = 50000.0f;
|
||||
}
|
||||
|
||||
UZeusAOIComponent::UZeusAOIComponent()
|
||||
@@ -42,14 +54,18 @@ void UZeusAOIComponent::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Bind do feed de config de AOI (servidor -> cliente). Multicast plain C++.
|
||||
// Bind do feed de config de AOI (servidor -> cliente) pelo transporte V1
|
||||
// canonico. O legacy (UZeusNetworkSubsystem) esta desativado no V1 -> usava
|
||||
// SendDebugAoiRequest que logava "ignorado: nao conectado" e o overlay nunca
|
||||
// recebia a config. Agora via UZeusNetworkingClientSubsystem (OnDebugAoiInfo 6161).
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (UGameInstance* GI = World->GetGameInstance())
|
||||
{
|
||||
if (UZeusNetworkSubsystem* Net = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
||||
{
|
||||
AoiConfigHandle_ = Net->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig);
|
||||
AoiConfigHandle_ = NetC->OnDebugAoiInfo.AddUObject(this, &UZeusAOIComponent::HandleAoiConfig);
|
||||
FrontierConfigHandle_ = NetC->OnDebugFrontierInfo.AddUObject(this, &UZeusAOIComponent::HandleFrontierConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,19 +77,26 @@ void UZeusAOIComponent::BeginPlay()
|
||||
|
||||
void UZeusAOIComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
if (AoiConfigHandle_.IsValid())
|
||||
if (AoiConfigHandle_.IsValid() || FrontierConfigHandle_.IsValid())
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (UGameInstance* GI = World->GetGameInstance())
|
||||
{
|
||||
if (UZeusNetworkSubsystem* Net = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
||||
{
|
||||
Net->OnDebugAoiInfo.Remove(AoiConfigHandle_);
|
||||
if (AoiConfigHandle_.IsValid()) { NetC->OnDebugAoiInfo.Remove(AoiConfigHandle_); }
|
||||
if (FrontierConfigHandle_.IsValid()) { NetC->OnDebugFrontierInfo.Remove(FrontierConfigHandle_); }
|
||||
}
|
||||
}
|
||||
}
|
||||
AoiConfigHandle_.Reset();
|
||||
FrontierConfigHandle_.Reset();
|
||||
}
|
||||
if (FrontierActor_)
|
||||
{
|
||||
FrontierActor_->Destroy();
|
||||
FrontierActor_ = nullptr;
|
||||
}
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
@@ -83,6 +106,26 @@ void UZeusAOIComponent::TickComponent(float DeltaTime, ELevelTick TickType,
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// Frontier: sincroniza o overlay com o estado atual. Pega o liga/desliga via
|
||||
// CVar zeus.debug.frontier (que nao passa por SetOverlayEnabled) e garante que
|
||||
// ao desligar as paredes sejam REMOVIDAS (UpdateFrontierOverlay -> ClearOverlay).
|
||||
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells) != bFrontierApplied_)
|
||||
{
|
||||
UpdateFrontierOverlay();
|
||||
}
|
||||
|
||||
// Re-pede o frontier periodicamente enquanto ligado -> o overlay reflete
|
||||
// split/merge da topologia dinamica (debug client-side, baixa freq, so' ON).
|
||||
if (ResolveOverlay(EZeusAOIOverlay::FrontierCells))
|
||||
{
|
||||
FrontierRepollAccumSec_ += DeltaTime;
|
||||
if (FrontierRepollAccumSec_ >= FrontierRepollIntervalSec)
|
||||
{
|
||||
FrontierRepollAccumSec_ = 0.0f;
|
||||
RequestFrontierConfig();
|
||||
}
|
||||
}
|
||||
|
||||
if (AnyOverlayActive())
|
||||
{
|
||||
DrawOverlays();
|
||||
@@ -95,9 +138,9 @@ void UZeusAOIComponent::RequestAoiConfig()
|
||||
{
|
||||
if (UGameInstance* GI = World->GetGameInstance())
|
||||
{
|
||||
if (UZeusNetworkSubsystem* Net = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
||||
{
|
||||
Net->SendDebugAoiRequest(); // no-op se nao conectado
|
||||
NetC->EmitDebugAoiRequest(); // V1: no-op se ainda nao Accepted
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,6 +150,70 @@ void UZeusAOIComponent::HandleAoiConfig(float InterestCm, float DespawnCm)
|
||||
{
|
||||
InterestRadiusCm = InterestCm;
|
||||
DespawnRadiusCm = DespawnCm;
|
||||
// K4: log AOI cfg recebido do server (paridade com Net.AOI server-side
|
||||
// "config_applied ZI=... ZD=..."). Cold path (1 por sessao).
|
||||
UE_LOG(LogZeusAOI, Display,
|
||||
TEXT("AOI cfg recv interestCm=%.0f (%.0fm) despawnCm=%.0f (%.0fm)"),
|
||||
InterestCm, InterestCm / 100.0f,
|
||||
DespawnCm, DespawnCm / 100.0f);
|
||||
}
|
||||
|
||||
void UZeusAOIComponent::RequestFrontierConfig()
|
||||
{
|
||||
if (UWorld* World = GetWorld())
|
||||
{
|
||||
if (UGameInstance* GI = World->GetGameInstance())
|
||||
{
|
||||
if (UZeusNetworkingClientSubsystem* NetC = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
||||
{
|
||||
NetC->EmitDebugFrontierRequest(); // V1: no-op se ainda nao Accepted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UZeusAOIComponent::HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info)
|
||||
{
|
||||
FrontierInfo_ = Info;
|
||||
bFrontierReceived_ = true;
|
||||
UE_LOG(LogZeusAOI, Display,
|
||||
TEXT("Frontier cfg recv own=%d neighbors=%d"),
|
||||
Info.OwnedCells.Num(), Info.NeighborCells.Num());
|
||||
UpdateFrontierOverlay();
|
||||
}
|
||||
|
||||
AZeusFrontierOverlayActor* UZeusAOIComponent::EnsureFrontierActor()
|
||||
{
|
||||
if (FrontierActor_) { return FrontierActor_; }
|
||||
UWorld* World = GetWorld();
|
||||
if (!World) { return nullptr; }
|
||||
FActorSpawnParameters Params;
|
||||
Params.Owner = GetOwner();
|
||||
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
||||
FrontierActor_ = World->SpawnActor<AZeusFrontierOverlayActor>(
|
||||
AZeusFrontierOverlayActor::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, Params);
|
||||
return FrontierActor_;
|
||||
}
|
||||
|
||||
void UZeusAOIComponent::UpdateFrontierOverlay()
|
||||
{
|
||||
const bool bOn = ResolveOverlay(EZeusAOIOverlay::FrontierCells);
|
||||
bFrontierApplied_ = bOn;
|
||||
if (!bOn)
|
||||
{
|
||||
if (FrontierActor_) { FrontierActor_->ClearOverlay(); }
|
||||
return;
|
||||
}
|
||||
if (!bFrontierReceived_)
|
||||
{
|
||||
// Ainda nao temos as cells; pede e espera o 6163 (HandleFrontierConfig re-chama).
|
||||
RequestFrontierConfig();
|
||||
return;
|
||||
}
|
||||
AZeusFrontierOverlayActor* Actor = EnsureFrontierActor();
|
||||
if (!Actor) { return; }
|
||||
const float AnchorZ = GetOwner() ? GetOwner()->GetActorLocation().Z : 0.0f;
|
||||
Actor->RebuildFromFrontier(FrontierInfo_, AnchorZ);
|
||||
}
|
||||
|
||||
bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const
|
||||
@@ -119,6 +226,7 @@ bool UZeusAOIComponent::ResolveOverlay(EZeusAOIOverlay Overlay) const
|
||||
case EZeusAOIOverlay::ProxyLabels: return bShowProxyLabels || CVarShowProxy.GetValueOnGameThread() > 0;
|
||||
case EZeusAOIOverlay::HandoffLine: return bShowHandoffLine || CVarShowHandoff.GetValueOnGameThread() > 0;
|
||||
case EZeusAOIOverlay::AuthDrift: return bShowAuthDrift || CVarShowDrift.GetValueOnGameThread() > 0;
|
||||
case EZeusAOIOverlay::FrontierCells: return bShowFrontier || CVarShowFrontier.GetValueOnGameThread() > 0;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +238,8 @@ bool UZeusAOIComponent::AnyOverlayActive() const
|
||||
|| ResolveOverlay(EZeusAOIOverlay::CellBounds)
|
||||
|| ResolveOverlay(EZeusAOIOverlay::ProxyLabels)
|
||||
|| ResolveOverlay(EZeusAOIOverlay::HandoffLine)
|
||||
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift);
|
||||
|| ResolveOverlay(EZeusAOIOverlay::AuthDrift)
|
||||
|| ResolveOverlay(EZeusAOIOverlay::FrontierCells);
|
||||
}
|
||||
|
||||
void UZeusAOIComponent::DrawOverlays()
|
||||
@@ -145,12 +254,14 @@ void UZeusAOIComponent::DrawOverlays()
|
||||
// ainda nao chegou a config: nao desenha (loading).
|
||||
if (ResolveOverlay(EZeusAOIOverlay::AOIRadius) && InterestRadiusCm > 0.0f)
|
||||
{
|
||||
DrawDebugSphere(World, Loc, InterestRadiusCm, 32, kColorInterest, false, -1.0f, 0, LineThickness);
|
||||
const float R = FMath::Min(InterestRadiusCm, kMaxOverlayRadiusCm);
|
||||
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorInterest, false, -1.0f, 0, LineThickness);
|
||||
}
|
||||
// Zona de Despawn (externa, > interesse = histerese) — raio real do servidor.
|
||||
if (ResolveOverlay(EZeusAOIOverlay::DespawnZone) && DespawnRadiusCm > 0.0f)
|
||||
{
|
||||
DrawDebugSphere(World, Loc, DespawnRadiusCm, 32, kColorDespawn, false, -1.0f, 0, LineThickness);
|
||||
const float R = FMath::Min(DespawnRadiusCm, kMaxOverlayRadiusCm);
|
||||
DrawDebugSphere(World, Loc, R, kSphereSegments, kColorDespawn, false, -1.0f, 0, LineThickness);
|
||||
}
|
||||
if (ResolveOverlay(EZeusAOIOverlay::CellBounds))
|
||||
{
|
||||
@@ -178,6 +289,7 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay
|
||||
case EZeusAOIOverlay::ProxyLabels: bShowProxyLabels = bEnabled; break;
|
||||
case EZeusAOIOverlay::HandoffLine: bShowHandoffLine = bEnabled; break;
|
||||
case EZeusAOIOverlay::AuthDrift: bShowAuthDrift = bEnabled; break;
|
||||
case EZeusAOIOverlay::FrontierCells: bShowFrontier = bEnabled; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -187,17 +299,27 @@ void UZeusAOIComponent::SetOverlayEnabled_Implementation(EZeusAOIOverlay Overlay
|
||||
{
|
||||
RequestAoiConfig();
|
||||
}
|
||||
|
||||
// Frontier: ao ligar pede as cells (6162) e reconstroi; ao desligar limpa as paredes.
|
||||
if (Overlay == EZeusAOIOverlay::FrontierCells)
|
||||
{
|
||||
if (bEnabled) { RequestFrontierConfig(); }
|
||||
UpdateFrontierOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void UZeusAOIComponent::SetAllOverlays_Implementation(bool bEnabled)
|
||||
{
|
||||
bShowAOIRadius = bShowDespawnZone = bShowCellBounds = bEnabled;
|
||||
bShowProxyLabels = bShowHandoffLine = bShowAuthDrift = bEnabled;
|
||||
bShowFrontier = bEnabled;
|
||||
|
||||
if (bEnabled && InterestRadiusCm <= 0.0f)
|
||||
{
|
||||
RequestAoiConfig();
|
||||
}
|
||||
if (bEnabled) { RequestFrontierConfig(); }
|
||||
UpdateFrontierOverlay();
|
||||
}
|
||||
|
||||
bool UZeusAOIComponent::IsOverlayEnabled_Implementation(EZeusAOIOverlay Overlay) const
|
||||
@@ -219,5 +341,6 @@ float UZeusAOIComponent::GetDespawnRadiusCm_Implementation() const
|
||||
int32 UZeusAOIComponent::GetEnabledOverlayCount() const
|
||||
{
|
||||
return (bShowAOIRadius ? 1 : 0) + (bShowDespawnZone ? 1 : 0) + (bShowCellBounds ? 1 : 0)
|
||||
+ (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0);
|
||||
+ (bShowProxyLabels ? 1 : 0) + (bShowHandoffLine ? 1 : 0) + (bShowAuthDrift ? 1 : 0)
|
||||
+ (bShowFrontier ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
#include "CoreMinimal.h"
|
||||
#include "Components/ActorComponent.h"
|
||||
#include "ZeusAOIDebugTarget.h" // plugin ZeusAdminToolsRuntime: interface + EZeusAOIOverlay
|
||||
#include "ZeusV1Protocol.h" // ZeusV1::FDebugFrontierInfo (overlay de fronteira)
|
||||
#include "ZeusAOIComponent.generated.h"
|
||||
|
||||
class AZeusFrontierOverlayActor;
|
||||
|
||||
/**
|
||||
* UZeusAOIComponent (cliente / jogo)
|
||||
*
|
||||
@@ -53,6 +56,9 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowProxyLabels = false;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowHandoffLine = false;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowAuthDrift = false;
|
||||
// Fronteiras do mesh: paredes own<->neighbor com label "Cell <id>", renderizadas
|
||||
// por AZeusFrontierOverlayActor a partir do DEBUG_FRONTIER_INFO (6163).
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Zeus|AOI Debug|Overlays") bool bShowFrontier = false;
|
||||
|
||||
// === Parametros ===
|
||||
// Raios reais vem do servidor (S_DEBUG_AOI_INFO). 0 = ainda nao recebido (loading).
|
||||
@@ -76,6 +82,30 @@ private:
|
||||
/** Bind de UZeusNetworkSubsystem::OnDebugAoiInfo: armazena os raios reais. */
|
||||
void HandleAoiConfig(float InterestCm, float DespawnCm);
|
||||
|
||||
/** Pede o frontier do mesh ao servidor (DEBUG_FRONTIER_REQUEST 6162). */
|
||||
void RequestFrontierConfig();
|
||||
/** Bind de OnDebugFrontierInfo: guarda as cells e (se overlay on) reconstroi as paredes. */
|
||||
void HandleFrontierConfig(const ZeusV1::FDebugFrontierInfo& Info);
|
||||
/** Spawna (lazy) o ator que desenha as paredes + labels. */
|
||||
AZeusFrontierOverlayActor* EnsureFrontierActor();
|
||||
/** Liga / reconstroi / limpa o overlay de fronteira conforme bShowFrontier + dados. */
|
||||
void UpdateFrontierOverlay();
|
||||
|
||||
/** Handle do bind do OnDebugAoiInfo (pra remover no EndPlay). */
|
||||
FDelegateHandle AoiConfigHandle_;
|
||||
|
||||
/** Handle do bind do OnDebugFrontierInfo (frontier overlay). */
|
||||
FDelegateHandle FrontierConfigHandle_;
|
||||
/** Ultimo frontier recebido do server (own cells + neighbors). */
|
||||
ZeusV1::FDebugFrontierInfo FrontierInfo_;
|
||||
bool bFrontierReceived_ = false;
|
||||
/** Estado do overlay JA aplicado (paredes desenhadas). O TickComponent compara
|
||||
* com ResolveOverlay p/ pegar liga/desliga via CVar tambem (nao so' o toggle). */
|
||||
bool bFrontierApplied_ = false;
|
||||
/** Re-poll periodico do frontier enquanto ligado -> overlay reflete split/merge
|
||||
* da topologia dinamica (o 6163 traz o estado atual do topology:current). */
|
||||
float FrontierRepollIntervalSec = 2.0f;
|
||||
float FrontierRepollAccumSec_ = 0.0f;
|
||||
/** Ator que desenha as paredes + labels (spawn lazy ao ligar o overlay). */
|
||||
UPROPERTY() TObjectPtr<AZeusFrontierOverlayActor> FrontierActor_ = nullptr;
|
||||
};
|
||||
|
||||
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 "ZeusPlayerState.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "ZeusNetworkingClientSubsystem.h"
|
||||
|
||||
void UZeusWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
{
|
||||
@@ -32,45 +33,59 @@ void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
|
||||
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
|
||||
// Sistema de rede novo (canonico) -- substitui o ZeusNetworkSubsystem legacy
|
||||
// para spawn/despawn/movimento de proxies. O legacy fica so' com o que ainda
|
||||
// nao tem equivalente (CHAR_INFO/nome, tratado em AZeusCharacter).
|
||||
if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem())
|
||||
{
|
||||
ZeusNet->OnPlayerSpawned.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned);
|
||||
ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned);
|
||||
ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to ZeusNetworkSubsystem delegates."));
|
||||
Net->OnEntitySpawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned);
|
||||
Net->OnEntityDespawned.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned);
|
||||
Net->OnEntityDeltaApplied.AddDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta);
|
||||
Net->OnSelfEntityAssigned.AddDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned);
|
||||
Net->OnCharInfo.AddDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem bound to network client subsystem delegates."));
|
||||
|
||||
// Catch-up race fix (Fase 3): broadcasts de proxies remotos chegam
|
||||
// enquanto o cliente novo ainda esta em OpenLevel. UZeusNetworkSubsystem
|
||||
// cacheia em PendingRemoteSpawnsByEntity. Aplicamos aqui — mundo ja
|
||||
// esta pronto (GameMode ativo, WorldPartition cells inicializadas).
|
||||
// O subsystem de rede e' per-GameInstance e sobrevive ao OpenLevel, entao
|
||||
// o self-entity e os ENT_SPAWN podem ter chegado ANTES deste bind. Puxa o
|
||||
// self cacheado + faz replay dos proxies ja conhecidos (anti-race).
|
||||
if (const int64 CachedSelf = Net->GetLocalEntityId())
|
||||
{
|
||||
OnNetSelfEntityAssigned(CachedSelf);
|
||||
}
|
||||
int32 ReplayCount = 0;
|
||||
ZeusNet->ForEachPendingRemoteSpawn(
|
||||
[this, &ReplayCount](const int64 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
||||
Net->ForEachRemoteEntity(
|
||||
[this, &ReplayCount](int64 EntityId, FVector PosCm, float YawDeg)
|
||||
{
|
||||
HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs);
|
||||
// Catch-up local: ForEachRemoteEntity nao expoe vel/serverTimeMs ->
|
||||
// ZeroVector + grounded + ts=0 (seed nao roda; o 1o delta ancora o relogio).
|
||||
OnNetEntitySpawned(EntityId, /*Kind=*/1 /*Player*/, PosCm, YawDeg,
|
||||
FVector::ZeroVector, /*bGrounded=*/true, /*ServerTimeMs=*/0);
|
||||
++ReplayCount;
|
||||
});
|
||||
if (ReplayCount > 0)
|
||||
{
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: replay de %d proxy(ies) ja conhecidos."), ReplayCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay."));
|
||||
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: network client subsystem not found at OnWorldBeginPlay."));
|
||||
}
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::Deinitialize()
|
||||
{
|
||||
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
|
||||
if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem())
|
||||
{
|
||||
ZeusNet->OnPlayerSpawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned);
|
||||
ZeusNet->OnPlayerDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned);
|
||||
ZeusNet->OnPlayerStateUpdate.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate);
|
||||
Net->OnEntitySpawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntitySpawned);
|
||||
Net->OnEntityDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDespawned);
|
||||
Net->OnEntityDeltaApplied.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetEntityDelta);
|
||||
Net->OnSelfEntityAssigned.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetSelfEntityAssigned);
|
||||
Net->OnCharInfo.RemoveDynamic(this, &UZeusWorldSubsystem::OnNetCharInfo);
|
||||
}
|
||||
|
||||
RemoteEntities.Reset();
|
||||
PendingCharInfo.Reset();
|
||||
LocalEntityId = 0;
|
||||
Super::Deinitialize();
|
||||
}
|
||||
@@ -83,7 +98,29 @@ void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* Loca
|
||||
}
|
||||
|
||||
LocalEntityId = EntityId;
|
||||
|
||||
// Caso de borda: se o ENT_SPAWN do proprio chegou antes do self-entity, ja
|
||||
// existe um proxy-fantasma com esta chave. Destroi antes de registrar o pawn
|
||||
// real, senao o RemoteEntities.Add sobrescreve a entry e o fantasma fica
|
||||
// orfao no mundo (sem ninguem pra despawna-lo).
|
||||
if (TWeakObjectPtr<AActor>* Existing = RemoteEntities.Find(EntityId))
|
||||
{
|
||||
if (AActor* Ghost = Existing->Get())
|
||||
{
|
||||
if (Ghost != LocalActor && Ghost->IsA(AZeusPlayerProxy::StaticClass()))
|
||||
{
|
||||
HandlePlayerDespawned(EntityId);
|
||||
UE_LOG(LogZMMO, Log,
|
||||
TEXT("ZeusWorldSubsystem: ghost proxy do proprio limpo no RegisterLocalEntity EntityId=%lld"),
|
||||
EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RemoteEntities.Add(EntityId, TWeakObjectPtr<AActor>(LocalActor));
|
||||
// V1-CHARINFO: o nome do proprio char pode ter chegado antes do pawn local
|
||||
// existir (CHAR_INFO vem logo apos ENT_SELF); aplica agora.
|
||||
FlushPendingCharInfo(EntityId);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: local entity registered EntityId=%lld actor=%s"),
|
||||
EntityId, *GetNameSafe(LocalActor));
|
||||
}
|
||||
@@ -96,6 +133,21 @@ void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool b
|
||||
return;
|
||||
}
|
||||
|
||||
// P10-FIX-LEGACY (workflow wcyeqgrad finding): server pode reenviar
|
||||
// S_SPAWN_PLAYER com bIsLocal=false do PROPRIO entityId via AOI re-emit
|
||||
// (catch-up cross-server, SetOnEnter callback recomputando InterestSet,
|
||||
// handoff em flight). Sem guard, o segundo spawn cria proxy fantasma do
|
||||
// proprio player; GAS/HUD passa a rotear via proxy fantasma e o pawn real
|
||||
// perde vinculo. Sintoma observado: player ve 2 pawns sobrepostos, ou
|
||||
// outros players nao veem o autoritativo.
|
||||
if (LocalEntityId != 0 && EntityId == LocalEntityId)
|
||||
{
|
||||
UE_LOG(LogZMMO, Verbose,
|
||||
TEXT("ZeusWorldSubsystem: ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"),
|
||||
LocalEntityId, bIsLocal ? 1 : 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bIsLocal)
|
||||
{
|
||||
// O proprio AZeusCharacter cuida da sua identidade quando recebe o
|
||||
@@ -212,10 +264,63 @@ void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool b
|
||||
}
|
||||
|
||||
RemoteEntities.Add(EntityId, TWeakObjectPtr<AActor>(SpawnedActor));
|
||||
// V1-CHARINFO: o nome chega ANTES do ator (server emite ENT_CHAR_INFO antes
|
||||
// do ENT_SPAWN); aplica agora que o proxy + PlayerState ja existem.
|
||||
FlushPendingCharInfo(EntityId);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: spawned remote EntityId=%d at (%s) yaw=%.1f t=%lld"),
|
||||
EntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::OnNetCharInfo(const int64 EntityId, const FString& CharName, const FString& GuildName)
|
||||
{
|
||||
if (EntityId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Aplica direto se o ator/PlayerState ja existe; senao guarda pra aplicar no spawn.
|
||||
if (!ApplyCharInfoToEntity(EntityId, CharName, GuildName))
|
||||
{
|
||||
PendingCharInfo.Add(EntityId, FZeusPendingCharInfo{ CharName, GuildName });
|
||||
}
|
||||
}
|
||||
|
||||
bool UZeusWorldSubsystem::ApplyCharInfoToEntity(const int64 EntityId, const FString& CharName, const FString& GuildName)
|
||||
{
|
||||
const TWeakObjectPtr<AActor>* Found = RemoteEntities.Find(EntityId);
|
||||
if (!Found || !Found->IsValid())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
APawn* Pawn = Cast<APawn>(Found->Get());
|
||||
if (!Pawn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
AZeusPlayerState* PS = Pawn->GetPlayerState<AZeusPlayerState>();
|
||||
if (!PS)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
PS->SetCharInfo(CharName, GuildName);
|
||||
UE_LOG(LogZMMO, Log,
|
||||
TEXT("ZeusWorldSubsystem: CHAR_INFO aplicado EntityId=%lld name='%s' guild='%s'"),
|
||||
EntityId, *CharName, *GuildName);
|
||||
return true;
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::FlushPendingCharInfo(const int64 EntityId)
|
||||
{
|
||||
const FZeusPendingCharInfo* Pending = PendingCharInfo.Find(EntityId);
|
||||
if (!Pending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (ApplyCharInfoToEntity(EntityId, Pending->CharName, Pending->GuildName))
|
||||
{
|
||||
PendingCharInfo.Remove(EntityId);
|
||||
}
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::HandlePlayerDespawned(const int64 EntityId)
|
||||
{
|
||||
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
|
||||
@@ -348,6 +453,108 @@ void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const in
|
||||
AsEntity->ApplyEntitySnapshot(Snapshot);
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::OnNetEntitySpawned(int64 EntityId, int32 /*Kind*/, FVector PosCm, float YawDeg,
|
||||
FVector VelCmS, bool bGrounded, int64 ServerTimeMs)
|
||||
{
|
||||
// O sistema novo nao manda bIsLocal: derivamos do LocalEntityId (setado por
|
||||
// ENT_SELF). Se ainda for 0 e este ENT_SPAWN for do proprio char, o
|
||||
// OnNetSelfEntityAssigned destruira o fantasma quando o ENT_SELF chegar.
|
||||
const bool bIsLocal = (LocalEntityId != 0 && EntityId == LocalEntityId);
|
||||
HandlePlayerSpawned(EntityId, bIsLocal, PosCm, YawDeg, ServerTimeMs);
|
||||
|
||||
// V1-SPAWN-KEYFRAME(-TS): o ENT_SPAWN carrega vel + grounded + serverTimeMs. Aplica
|
||||
// esse keyframe INICIAL no proxy recem-criado pelo mesmo caminho do delta (semeia o
|
||||
// SnapshotBuffer + ANCORA o ServerClockOffsetMs com o tempo CERTO). Antes o seed usava
|
||||
// ServerTimeMs=0 -> bootstrap do relogio de interpolacao errado -> proxy preso/flutuando
|
||||
// ate' o EMA convergir. Agora com o serverTimeMs real o proxy interpola desde o frame 1.
|
||||
if (!bIsLocal && ServerTimeMs > 0)
|
||||
{
|
||||
HandlePlayerStateUpdate(EntityId, /*InputSeq=*/0, PosCm, VelCmS, bGrounded, ServerTimeMs);
|
||||
}
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::OnNetEntityDespawned(int64 EntityId, int32 /*Reason*/)
|
||||
{
|
||||
// NUNCA despawnar o proprio char local via rede (ex: ENT_DESPAWN do proprio
|
||||
// disparado por timeout do server). Destruir o pawn local quebraria o jogo
|
||||
// do dono. O proprio so' sai quando o cliente realmente desconecta.
|
||||
if (LocalEntityId != 0 && EntityId == LocalEntityId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
HandlePlayerDespawned(EntityId);
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::OnNetEntityDelta(int64 EntityId, FVector PosCm, FVector VelCmS,
|
||||
float YawDeg, bool bGrounded, int64 ServerTimeMs)
|
||||
{
|
||||
if (EntityId == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (LocalEntityId != 0 && EntityId == LocalEntityId)
|
||||
{
|
||||
return; // movimento do proprio char e' local; ignora snapshot autoritativo
|
||||
}
|
||||
|
||||
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
|
||||
if (!Entry)
|
||||
{
|
||||
return; // spawn chega em breve via ENT_SPAWN
|
||||
}
|
||||
AActor* Actor = Entry->Get();
|
||||
if (!Actor)
|
||||
{
|
||||
RemoteEntities.Remove(EntityId);
|
||||
return;
|
||||
}
|
||||
IZeusEntityInterface* AsEntity = Cast<IZeusEntityInterface>(Actor);
|
||||
if (!AsEntity)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FZeusEntitySnapshot Snapshot;
|
||||
Snapshot.EntityId = EntityId;
|
||||
Snapshot.EntityType = AsEntity->GetZeusEntityType();
|
||||
Snapshot.PositionCm = PosCm;
|
||||
Snapshot.YawDeg = YawDeg;
|
||||
// V1-REPL-FULL (2026-06-12): velocidade + grounded + serverTimeMs -> o proxy
|
||||
// anima (AnimBP ShouldMove via Acceleration!=0 derivada da vel), cola no chao
|
||||
// (MOVE_Walking quando grounded) e interpola com a timeline autoritativa.
|
||||
Snapshot.VelocityCmS = VelCmS;
|
||||
Snapshot.bGrounded = bGrounded;
|
||||
Snapshot.ServerTimeMs = ServerTimeMs;
|
||||
AsEntity->ApplyEntitySnapshot(Snapshot);
|
||||
}
|
||||
|
||||
void UZeusWorldSubsystem::OnNetSelfEntityAssigned(int64 EntityId)
|
||||
{
|
||||
if (EntityId == 0 || LocalEntityId == EntityId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
LocalEntityId = EntityId;
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: self entity assigned EntityId=%lld"), EntityId);
|
||||
|
||||
// Re-filtragem tardia: se o ENT_SPAWN do proprio chegou ANTES do ENT_SELF,
|
||||
// ja existe um proxy-fantasma do proprio char. Destroi agora -- mas NUNCA o
|
||||
// pawn local real (o RegisterLocalEntity do AZeusCharacter registra o pawn
|
||||
// com a mesma chave; so destruimos se a entry for um AZeusPlayerProxy).
|
||||
if (TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId))
|
||||
{
|
||||
if (AActor* Ghost = Entry->Get())
|
||||
{
|
||||
if (Ghost->IsA(AZeusPlayerProxy::StaticClass()))
|
||||
{
|
||||
HandlePlayerDespawned(EntityId);
|
||||
UE_LOG(LogZMMO, Log,
|
||||
TEXT("ZeusWorldSubsystem: ghost proxy do proprio destruido EntityId=%lld"), EntityId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UClass* UZeusWorldSubsystem::ResolveActorClass(const EZeusEntityType EntityType) const
|
||||
{
|
||||
if (const TSubclassOf<AActor>* Found = RemoteEntityClasses.Find(EntityType))
|
||||
@@ -374,3 +581,18 @@ UZeusNetworkSubsystem* UZeusWorldSubsystem::ResolveZeusNetworkSubsystem() const
|
||||
}
|
||||
return GI->GetSubsystem<UZeusNetworkSubsystem>();
|
||||
}
|
||||
|
||||
UZeusNetworkingClientSubsystem* UZeusWorldSubsystem::ResolveNetClientSubsystem() const
|
||||
{
|
||||
const UWorld* World = GetWorld();
|
||||
if (!World)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
UGameInstance* GI = World->GetGameInstance();
|
||||
if (!GI)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ class AActor;
|
||||
class AZeusCharacter;
|
||||
class AZeusPlayerProxy;
|
||||
class UZeusNetworkSubsystem;
|
||||
class UZeusNetworkingClientSubsystem;
|
||||
|
||||
/**
|
||||
* UZeusWorldSubsystem
|
||||
@@ -80,8 +81,39 @@ private:
|
||||
UFUNCTION()
|
||||
void HandlePlayerStateUpdate(int64 EntityId, int32 InputSeq, FVector PosCm, FVector VelCmS, bool bGrounded, int64 ServerTimeMs);
|
||||
|
||||
// Sistema de rede novo (canonico) -- recebe spawn/despawn/delta + self-entity.
|
||||
// Adaptam a assinatura nova para os handlers de spawn/despawn/delta acima.
|
||||
UFUNCTION()
|
||||
void OnNetEntitySpawned(int64 EntityId, int32 Kind, FVector PosCm, float YawDeg,
|
||||
FVector VelCmS, bool bGrounded, int64 ServerTimeMs);
|
||||
|
||||
UFUNCTION()
|
||||
void OnNetEntityDespawned(int64 EntityId, int32 Reason);
|
||||
|
||||
UFUNCTION()
|
||||
void OnNetEntityDelta(int64 EntityId, FVector PosCm, FVector VelCmS, float YawDeg,
|
||||
bool bGrounded, int64 ServerTimeMs);
|
||||
|
||||
UFUNCTION()
|
||||
void OnNetSelfEntityAssigned(int64 EntityId);
|
||||
|
||||
// V1-CHARINFO: nome/guild do char (opcode ENT_CHAR_INFO 6045). Chega ANTES
|
||||
// do ENT_SPAWN do proxy / do RegisterLocalEntity do self, entao se o ator
|
||||
// ainda nao existe o dado fica em PendingCharInfo e e' aplicado no spawn.
|
||||
UFUNCTION()
|
||||
void OnNetCharInfo(int64 EntityId, const FString& CharName, const FString& GuildName);
|
||||
|
||||
UClass* ResolveActorClass(EZeusEntityType EntityType) const;
|
||||
UZeusNetworkSubsystem* ResolveZeusNetworkSubsystem() const;
|
||||
UZeusNetworkingClientSubsystem* ResolveNetClientSubsystem() const;
|
||||
|
||||
// Aplica nome/guild no AZeusPlayerState do EntityId (self ou proxy). Retorna
|
||||
// false se o ator/PlayerState ainda nao existe (caller deve cachear).
|
||||
bool ApplyCharInfoToEntity(int64 EntityId, const FString& CharName, const FString& GuildName);
|
||||
|
||||
// Aplica (e consome) o char info pendente de EntityId, se houver. Chamado
|
||||
// quando o ator passa a existir (spawn de proxy / RegisterLocalEntity).
|
||||
void FlushPendingCharInfo(int64 EntityId);
|
||||
|
||||
/**
|
||||
* Registry de proxies remotos. Chave = `EntityId` autoritativo do servidor.
|
||||
@@ -93,4 +125,12 @@ private:
|
||||
|
||||
/** Cached id do jogador local, para ignorar snapshots dele (cliente solto). */
|
||||
int64 LocalEntityId = 0;
|
||||
|
||||
/** V1-CHARINFO: nome/guild recebido antes do ator existir. Chave = EntityId. */
|
||||
struct FZeusPendingCharInfo
|
||||
{
|
||||
FString CharName;
|
||||
FString GuildName;
|
||||
};
|
||||
TMap<int64, FZeusPendingCharInfo> PendingCharInfo;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "ZeusGameInstance.h"
|
||||
#include "ZeusThemeSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "ZeusNetworkingClientSubsystem.h" // V1 canonico (sinal de spawn local)
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "CommonActivatableWidget.h"
|
||||
#include "Engine/DataTable.h"
|
||||
@@ -265,6 +266,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const
|
||||
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
UZeusNetworkingClientSubsystem* UUIFrontEndFlowSubsystem::GetNetClient() const
|
||||
{
|
||||
const UGameInstance* GI = GetGameInstance();
|
||||
return GI ? GI->GetSubsystem<UZeusNetworkingClientSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
|
||||
{
|
||||
const UGameInstance* GI = GetGameInstance();
|
||||
@@ -314,11 +321,20 @@ void UUIFrontEndFlowSubsystem::BindNetwork()
|
||||
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||
Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
||||
}
|
||||
// UDP (world server) só interessa para o handoff de travel.
|
||||
// UDP (world server): o travel (TRAVEL_TO_MAP) ainda passa pelo objeto legacy
|
||||
// como PONTE (HandleTravelToMap V1 reusa OnServerTravelRequested) -- isso some
|
||||
// na Fase D quando o travel ganhar delegate proprio no V1.
|
||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||
{
|
||||
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
||||
Zeus->OnPlayerSpawned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned);
|
||||
}
|
||||
// V1 CANONICO: o spawn do player local chega via ENT_SELF (o cliente aprende
|
||||
// o proprio entityId) -> OnSelfEntityAssigned. O legacy OnPlayerSpawned NAO
|
||||
// dispara mais com V1, entao a etapa "Spawn" do loading ficava eterna
|
||||
// (loading travado). Liga a etapa ao sinal V1.
|
||||
if (UZeusNetworkingClientSubsystem* Net = GetNetClient())
|
||||
{
|
||||
Net->OnSelfEntityAssigned.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned);
|
||||
}
|
||||
bNetBound = true;
|
||||
}
|
||||
@@ -338,7 +354,10 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||
{
|
||||
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
||||
Zeus->OnPlayerSpawned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandlePlayerSpawned);
|
||||
}
|
||||
if (UZeusNetworkingClientSubsystem* Net = GetNetClient())
|
||||
{
|
||||
Net->OnSelfEntityAssigned.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleV1LocalSpawned);
|
||||
}
|
||||
bNetBound = false;
|
||||
}
|
||||
@@ -505,6 +524,13 @@ void UUIFrontEndFlowSubsystem::HandlePlayerSpawned(int64 /*EntityId*/, bool bIsL
|
||||
}
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::HandleV1LocalSpawned(int64 EntityId)
|
||||
{
|
||||
// ENT_SELF e' SEMPRE o proprio char (o cliente aprendendo seu entityId), entao
|
||||
// bIsLocal=true. Reusa a logica de marcar a etapa "Spawn" + memoizacao anti-race.
|
||||
HandlePlayerSpawned(EntityId, /*bIsLocal=*/true, FVector::ZeroVector, 0.0f, 0);
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::HandleLoadingComplete()
|
||||
{
|
||||
if (UUILoadingScreen_Base* Loading = ActiveLoadingScreen.Get())
|
||||
|
||||
@@ -11,6 +11,7 @@ class UUIManagerSubsystem;
|
||||
class UUILoadingScreen_Base;
|
||||
class UZeusLoadingProfilesDataAsset;
|
||||
class UZeusNetworkSubsystem;
|
||||
class UZeusNetworkingClientSubsystem;
|
||||
class UZeusCharServerSubsystem;
|
||||
struct FZeusMapDef;
|
||||
|
||||
@@ -171,6 +172,7 @@ private:
|
||||
void BindNetwork();
|
||||
void UnbindNetwork();
|
||||
UZeusNetworkSubsystem* GetZeusNetwork() const;
|
||||
UZeusNetworkingClientSubsystem* GetNetClient() const; // V1 canonico
|
||||
UZeusCharServerSubsystem* GetCharServer() const;
|
||||
UUIManagerSubsystem* GetUIManager() const;
|
||||
UUIFrontEndScreenSet* GetScreenSet();
|
||||
@@ -198,6 +200,15 @@ private:
|
||||
void HandlePlayerSpawned(int64 EntityId, bool bIsLocal, FVector PosCm,
|
||||
float YawDeg, int64 ServerTimeMs);
|
||||
|
||||
/**
|
||||
* V1 canonico: o spawn do player local chega via ENT_SELF (o cliente
|
||||
* aprende o proprio entityId) -> OnSelfEntityAssigned. Marca a etapa
|
||||
* "Spawn" do loading no fluxo V1 (o legacy OnPlayerSpawned nao dispara
|
||||
* mais). Adapta a assinatura OneParam pra logica de HandlePlayerSpawned.
|
||||
*/
|
||||
UFUNCTION()
|
||||
void HandleV1LocalSpawned(int64 EntityId);
|
||||
|
||||
/** Disparado pela tela de loading quando todas as etapas viram Done. */
|
||||
UFUNCTION()
|
||||
void HandleLoadingComplete();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "WireHelpers.h"
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "ZeusNetworkingClientSubsystem.h" // B4: V1 ConnectWithTicket
|
||||
#include "CommonTextBlock.h"
|
||||
#include "Components/PanelWidget.h"
|
||||
#include "Components/WidgetSwitcher.h"
|
||||
@@ -384,13 +385,29 @@ void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
|
||||
// `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado).
|
||||
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
|
||||
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
|
||||
if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
// B4 (2026-06-11): se V1 canonico, roteia ConnectWithTicket pro V1 subsystem.
|
||||
// V1 envia CONN_HELLO_CLIENT (6000) com handoffTicket embutido — server
|
||||
// valida via Valkey GETDEL antes do CHALLENGE (HandshakeProcessor::OnHelloClient).
|
||||
const UZeusNetworkingClientSubsystem* V1Cdo = GetDefault<UZeusNetworkingClientSubsystem>();
|
||||
const bool bUseV1 = V1Cdo && V1Cdo->bUseZeusNetworkingV1;
|
||||
if (bUseV1)
|
||||
{
|
||||
if (UZeusNetworkingClientSubsystem* V1 = GI->GetSubsystem<UZeusNetworkingClientSubsystem>())
|
||||
{
|
||||
V1->ConnectWithTicket(GwHost, GwPort, HandoffToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Error, TEXT("Lobby: V1 subsystem ausente — handoff abortado"));
|
||||
}
|
||||
}
|
||||
else if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
{
|
||||
ZeusNet->ConnectToZeusServerWithTicket(GwHost, GwPort, HandoffToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Error, TEXT("Lobby: ZeusNetworkSubsystem ausente — handoff abortado"));
|
||||
UE_LOG(LogZMMO, Error, TEXT("Lobby: nenhum ZeusNetworkSubsystem disponivel — handoff abortado"));
|
||||
}
|
||||
|
||||
// Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo.
|
||||
|
||||
@@ -19,6 +19,7 @@ public class ZMMO : ModuleRules
|
||||
"CommonInput",
|
||||
"GameplayTags",
|
||||
"GameplayAbilities", // GAS: UAttributeSet, FGameplayAttributeData
|
||||
"ProceduralMeshComponent", // paredes do overlay de fronteira (AZeusFrontierOverlayActor)
|
||||
"ZeusNetwork",
|
||||
"ZeusGAS", // UZeusGASComponent, UZeusAttributeSet, FZeusAttributesSnapshot
|
||||
"ZeusJobs",
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
// Copyright Epic Games, Inc. All Rights Reserved.
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "ZMMONetLog.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ZMMO, "ZMMO" );
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogZMMO)
|
||||
DEFINE_LOG_CATEGORY(LogZMMO)
|
||||
|
||||
// Batch 2.5 — categorias vivem no modulo ZMMO (nao no plugin) por causa
|
||||
// do adaptive non-unity build do UBT (ver ZMMONetLog.h).
|
||||
DEFINE_LOG_CATEGORY(LogZeusHandoff)
|
||||
DEFINE_LOG_CATEGORY(LogZeusAOI)
|
||||
DEFINE_LOG_CATEGORY(LogZeusProxy)
|
||||
DEFINE_LOG_CATEGORY(LogZeusHaloTransition)
|
||||
24
Source/ZMMO/ZMMONetLog.h
Normal file
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