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

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

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

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