Lado CLIENTE ZMMO (par com o commit de mesmo Change-Set no repo ZeusServerEngine -- devem ir juntos, o wire do ENT_SPAWN mudou). - UZeusAOIComponent: migrado do transporte legacy (SendDebugAoiRequest/OnDebugAoiInfo, desativado no V1) pro canonico (EmitDebugAoiRequest 6160 / OnDebugAoiInfo 6161). Esfera de debug 32 -> 48 segments + clamp visual de 500m (resolve "zona sem limite / poucas linhas"). - ZeusWorldSubsystem::OnNetEntitySpawned: recebe vel + grounded + serverTimeMs (delegate FZeusV1OnEntitySpawned 4 -> 7 params) e semeia o proxy recem-criado via HandlePlayerStateUpdate -> ancora o relogio de interpolacao + a vel inicial -> proxy nasce ja' animando no re-spawn (sai/volta do raio AOI segurando W), sem Idle deslizando. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
599 lines
20 KiB
C++
599 lines
20 KiB
C++
#include "ZeusWorldSubsystem.h"
|
|
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "ZMMO.h"
|
|
#include "ZeusEntity.h"
|
|
#include "ZeusEntityInterface.h"
|
|
#include "ZeusGASComponent.h"
|
|
#include "ZeusPlayerProxy.h"
|
|
#include "ZeusPlayerState.h"
|
|
#include "ZeusNetworkSubsystem.h"
|
|
#include "ZeusNetworkingClientSubsystem.h"
|
|
|
|
void UZeusWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
if (RemoteEntityClasses.IsEmpty())
|
|
{
|
|
RemoteEntityClasses.Add(EZeusEntityType::Player, AZeusPlayerProxy::StaticClass());
|
|
RemoteEntityClasses.Add(EZeusEntityType::Mob, AZeusEntity::StaticClass());
|
|
RemoteEntityClasses.Add(EZeusEntityType::NPC, AZeusEntity::StaticClass());
|
|
RemoteEntityClasses.Add(EZeusEntityType::Object, AZeusEntity::StaticClass());
|
|
}
|
|
// Bind + replay vivem em OnWorldBeginPlay (mundo pronto). Initialize roda
|
|
// MUITO cedo no pipeline de LoadMap — antes do GameMode ser instanciado
|
|
// e antes do WorldPartition terminar de carregar cells. Spawns dinamicos
|
|
// feitos aqui podem ser perdidos durante a fase de setup posterior.
|
|
}
|
|
|
|
void UZeusWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
|
{
|
|
Super::OnWorldBeginPlay(InWorld);
|
|
|
|
// 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())
|
|
{
|
|
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."));
|
|
|
|
// 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;
|
|
Net->ForEachRemoteEntity(
|
|
[this, &ReplayCount](int64 EntityId, FVector PosCm, float YawDeg)
|
|
{
|
|
// 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) ja conhecidos."), ReplayCount);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: network client subsystem not found at OnWorldBeginPlay."));
|
|
}
|
|
}
|
|
|
|
void UZeusWorldSubsystem::Deinitialize()
|
|
{
|
|
if (UZeusNetworkingClientSubsystem* Net = ResolveNetClientSubsystem())
|
|
{
|
|
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();
|
|
}
|
|
|
|
void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* LocalActor)
|
|
{
|
|
if (EntityId == 0 || LocalActor == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
void UZeusWorldSubsystem::HandlePlayerSpawned(const int64 EntityId, const bool bIsLocal,
|
|
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
|
{
|
|
if (EntityId == 0)
|
|
{
|
|
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
|
|
// delegate por outro caminho. Aqui apenas memorizamos para filtrar
|
|
// snapshots locais (cliente solto, sem reconciliacao em V0).
|
|
LocalEntityId = EntityId;
|
|
return;
|
|
}
|
|
|
|
if (RemoteEntities.Contains(EntityId))
|
|
{
|
|
// Ja existe um proxy: tenta reactivar e re-snap. Se o weak ptr ficou
|
|
// stale (actor coletado pelo GC, stream-out do World Partition, etc.),
|
|
// remove a entry orfa e cai no caminho de spawn novo — evita SPAWN
|
|
// silenciosamente ignorado depois de oscilacoes na AOI.
|
|
if (AActor* Existing = RemoteEntities[EntityId].Get())
|
|
{
|
|
if (IZeusEntityInterface* AsEntity = Cast<IZeusEntityInterface>(Existing))
|
|
{
|
|
AsEntity->SetEntityRelevant(true);
|
|
}
|
|
Existing->SetActorLocationAndRotation(PosCm, FRotator(0.0f, YawDeg, 0.0f));
|
|
UE_LOG(LogZMMO, Verbose, TEXT("ZeusWorldSubsystem: re-snap EntityId=%d"), EntityId);
|
|
return;
|
|
}
|
|
// weak ptr stale — limpa entry e cai pro spawn novo abaixo.
|
|
RemoteEntities.Remove(EntityId);
|
|
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: stale entry para EntityId=%d, respawnando"), EntityId);
|
|
}
|
|
|
|
UWorld* World = GetWorld();
|
|
if (!World)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UClass* ProxyClass = ResolveActorClass(EZeusEntityType::Player);
|
|
if (!ProxyClass)
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: no class registered for Player; spawn aborted."));
|
|
return;
|
|
}
|
|
|
|
FActorSpawnParameters Params;
|
|
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
const FRotator SpawnRot(0.0f, YawDeg, 0.0f);
|
|
AActor* SpawnedActor = World->SpawnActor<AActor>(ProxyClass, PosCm, SpawnRot, Params);
|
|
if (!SpawnedActor)
|
|
{
|
|
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: SpawnActor failed for EntityId=%d"), EntityId);
|
|
return;
|
|
}
|
|
|
|
if (IZeusEntityInterface* AsEntity = Cast<IZeusEntityInterface>(SpawnedActor))
|
|
{
|
|
// Inject identity via dedicated setter when the spawned actor exposes one.
|
|
if (AZeusPlayerProxy* Proxy = Cast<AZeusPlayerProxy>(SpawnedActor))
|
|
{
|
|
Proxy->SetZeusIdentity(EntityId, EZeusEntityType::Player);
|
|
}
|
|
else if (AZeusEntity* Entity = Cast<AZeusEntity>(SpawnedActor))
|
|
{
|
|
Entity->SetZeusIdentity(EntityId, EZeusEntityType::Player);
|
|
}
|
|
AsEntity->SetEntityRelevant(true);
|
|
}
|
|
|
|
// === PlayerState pra proxies (Batch 2.5+) ===
|
|
//
|
|
// Sem PlayerState, o proxy fica fora do GameState->PlayerArray. Daqui pra
|
|
// cima, qualquer sistema que itera PlayerArray (UZeusGASNetworkHandler::
|
|
// FindGASComponentForEntity, HUD, chat, friend list) NAO consegue achar o
|
|
// proxy -> fallback ao local player -> efeito apareceria no char errado
|
|
// (bug do cue do Dash). Criar PS replicado-fake permite que o pipeline GAS
|
|
// trate proxies igual a local players.
|
|
//
|
|
// PlayerState tem o UZeusGASComponent via DefaultGame.ini Component Registry,
|
|
// entao GASComp e' auto-instanciado. So' precisamos seed o EntityId.
|
|
if (APawn* ProxyPawn = Cast<APawn>(SpawnedActor))
|
|
{
|
|
FActorSpawnParameters PSParams;
|
|
PSParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
|
|
PSParams.Owner = ProxyPawn;
|
|
AZeusPlayerState* ProxyPS = World->SpawnActor<AZeusPlayerState>(
|
|
AZeusPlayerState::StaticClass(), FVector::ZeroVector, FRotator::ZeroRotator, PSParams);
|
|
if (ProxyPS)
|
|
{
|
|
ProxyPawn->SetPlayerState(ProxyPS); // tambem chama ProxyPS->SetPawn(ProxyPawn)
|
|
|
|
// Add ao PlayerArray do GameState (replicacao UE faria isso automatico,
|
|
// mas no nosso pipeline custom o PS local-fake precisa ser inserido).
|
|
if (AGameStateBase* GS = World->GetGameState())
|
|
{
|
|
GS->AddPlayerState(ProxyPS);
|
|
}
|
|
|
|
// Seed EntityId no GASComp do PS (criado via Component Registry).
|
|
// FindGASComponentForEntity vai resolver via PS->FindComponentByClass.
|
|
if (UZeusGASComponent* GASComp = ProxyPS->FindComponentByClass<UZeusGASComponent>())
|
|
{
|
|
GASComp->SeedEntityId(EntityId);
|
|
}
|
|
|
|
UE_LOG(LogZMMO, Log,
|
|
TEXT("ZeusWorldSubsystem: PlayerState criado p/ proxy EntityId=%lld PS=%s"),
|
|
EntityId, *ProxyPS->GetName());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZMMO, Warning,
|
|
TEXT("ZeusWorldSubsystem: SpawnActor<AZeusPlayerState> falhou pra proxy EntityId=%lld"),
|
|
EntityId);
|
|
}
|
|
}
|
|
|
|
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);
|
|
if (!Entry)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Destruir o actor pra valer + remover do mapa. Antes, despawn apenas
|
|
// fazia SetActorHiddenInGame(true) via SetEntityRelevant(false), o que
|
|
// deixava o entry no mapa apontando pra um actor escondido. SPAWN
|
|
// subsequente do mesmo entityId era detectado como "duplicate" e nada
|
|
// acontecia — pawn ficava invisivel ate' o weak ptr ser GC'd. Bug
|
|
// confirmado nos logs do AOI ao oscilar entrada/saida da ZI/ZD.
|
|
if (AActor* Actor = Entry->Get())
|
|
{
|
|
if (IZeusEntityInterface* AsEntity = Cast<IZeusEntityInterface>(Actor))
|
|
{
|
|
AsEntity->SetEntityRelevant(false);
|
|
}
|
|
// Destrui PS proxy linkado (Batch 2.5+). Sem isso o GameState->PlayerArray
|
|
// fica com PS orfa apos o proxy ser destruido.
|
|
if (APawn* Pawn = Cast<APawn>(Actor))
|
|
{
|
|
if (APlayerState* PS = Pawn->GetPlayerState())
|
|
{
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (AGameStateBase* GS = World->GetGameState())
|
|
{
|
|
GS->RemovePlayerState(PS);
|
|
}
|
|
}
|
|
PS->Destroy();
|
|
}
|
|
}
|
|
Actor->Destroy();
|
|
}
|
|
RemoteEntities.Remove(EntityId);
|
|
|
|
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: despawn EntityId=%d (destroyed)"), EntityId);
|
|
}
|
|
|
|
void UZeusWorldSubsystem::HandlePlayerStateUpdate(const int64 EntityId, const int32 InputSeq,
|
|
const FVector PosCm, const FVector VelCmS, const bool bGrounded, const int64 ServerTimeMs)
|
|
{
|
|
if (EntityId == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (LocalEntityId != 0 && static_cast<int64>(EntityId) == LocalEntityId)
|
|
{
|
|
// Cliente local solto: ignoramos snapshots autoritativos para o nosso
|
|
// proprio personagem em V0 (ADR 0038). Reconciliacao opcional pode ser
|
|
// activada por flag em `AZeusCharacter` quando colisao de objetos
|
|
// chegar.
|
|
return;
|
|
}
|
|
|
|
const TWeakObjectPtr<AActor>* Entry = RemoteEntities.Find(EntityId);
|
|
if (!Entry)
|
|
{
|
|
// Snapshot antes de spawn: o spawn vira em `OnPlayerSpawned` em breve.
|
|
return;
|
|
}
|
|
|
|
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.LastProcessedInputSeq = InputSeq;
|
|
Snapshot.PositionCm = PosCm;
|
|
Snapshot.VelocityCmS = VelCmS;
|
|
|
|
// Etapa 7 (server-side networking refactor): yaw agora chega no
|
|
// snapshot quantizado v3. Buscamos via TryGetLastPlayerStateExtended
|
|
// (cache local do plugin). Se PSF_HasYaw nao estiver set ou se o cache
|
|
// nao tiver entrada, caimos no fallback antigo (derivar do XY da
|
|
// velocidade ou manter rotacao do ator).
|
|
bool bYawFromSnapshot = false;
|
|
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
|
|
{
|
|
int32 CachedInputSeq = 0;
|
|
FVector CachedPos = FVector::ZeroVector;
|
|
FVector CachedVel = FVector::ZeroVector;
|
|
float CachedYawDeg = 0.f;
|
|
int32 CachedFlags = 0;
|
|
int64 CachedServerTimeMs = 0;
|
|
if (ZeusNet->TryGetLastPlayerStateExtended(
|
|
EntityId, CachedInputSeq, CachedPos, CachedVel,
|
|
CachedYawDeg, CachedFlags, CachedServerTimeMs))
|
|
{
|
|
constexpr int32 PSF_HasYaw = 1 << 5;
|
|
if ((CachedFlags & PSF_HasYaw) != 0)
|
|
{
|
|
Snapshot.YawDeg = CachedYawDeg;
|
|
bYawFromSnapshot = true;
|
|
}
|
|
}
|
|
}
|
|
if (!bYawFromSnapshot)
|
|
{
|
|
// Fallback: deriva yaw do XY da velocidade quando significativo;
|
|
// caso contrario mantem rotacao do ator (comportamento pre-Etapa 7).
|
|
if (!FVector(VelCmS.X, VelCmS.Y, 0.0f).IsNearlyZero(1.0f))
|
|
{
|
|
Snapshot.YawDeg = FMath::RadiansToDegrees(FMath::Atan2(VelCmS.Y, VelCmS.X));
|
|
}
|
|
else
|
|
{
|
|
Snapshot.YawDeg = Actor->GetActorRotation().Yaw;
|
|
}
|
|
}
|
|
Snapshot.bGrounded = bGrounded;
|
|
Snapshot.ServerTimeMs = ServerTimeMs;
|
|
|
|
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))
|
|
{
|
|
if (Found->Get())
|
|
{
|
|
return Found->Get();
|
|
}
|
|
}
|
|
return AZeusEntity::StaticClass();
|
|
}
|
|
|
|
UZeusNetworkSubsystem* UZeusWorldSubsystem::ResolveZeusNetworkSubsystem() const
|
|
{
|
|
const UWorld* World = GetWorld();
|
|
if (!World)
|
|
{
|
|
return nullptr;
|
|
}
|
|
UGameInstance* GI = World->GetGameInstance();
|
|
if (!GI)
|
|
{
|
|
return nullptr;
|
|
}
|
|
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>();
|
|
}
|