Files
ZMMO/Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp
Mateus Rodrigues e87b2cec6c fix(zmmo-client): guard HandlePlayerSpawned ignore proprio entityId + ZeusV1 config
Acompanha commit server 54f2f9a (P10-FIX-LEGACY). Resolve bug "vejo proxy
de UM mas o outro NAO ve" + "proxy fantasma do proprio player" -- causado
por server emitir S_SPAWN_PLAYER local=0 do proprio entityId via AOI re-emit
(workflow wcyeqgrad finding).

Source/ZMMO/Game/Network/ZeusWorldSubsystem.cpp
================================================
HandlePlayerSpawned guard novo logo apos EntityId==0 check:

  if (LocalEntityId != 0 && EntityId == LocalEntityId)
  {
      UE_LOG(LogZMMO, Verbose,
          TEXT("ignoring S_SPAWN_PLAYER for own LocalEntityId=%lld (bIsLocal=%d)"),
          LocalEntityId, bIsLocal ? 1 : 0);
      return;
  }

Sem o guard, server re-emit S_SPAWN_PLAYER local=0 do proprio entityId
(via AOI SetOnEnter callback em catch-up cross-server ou recompute pos-
handoff) criava proxy fantasma do proprio player. GAS/HUD/PlayerArray
roteava via fantasma e o pawn real perdia vinculo de identidade visivel
pros outros players.

Defesa em profundidade -- complementa fix server-side
(AoiReplicationHelpers.cpp:28 SendSpawnPlayerToConn agora tem skip-self
guard antes de construir payload) + SpawnKey dedupe sem IsLocal no plugin
(ZeusNetworkSubsystem.cpp:949 removeu IsLocal do hash).

Config/DefaultZeusV1.ini
=========================
Config inicial para UZeusNetworkingClientSubsystem (P9-6/P9-7 plugin V1).
Aponta para Gateway 127.0.0.1:7777 (fluxo padrao producao). Documenta
alternativa de direct connect 9001 (debug only -- bug Windows multi-NIC
loopback). bUseZeusNetworkingV1=True ativa V1 plugin em paralelo ao
legacy (transport V1 hoje quebrado via Gateway -- channelId nao eh
routing key, Phase 12 territory; gameplay continua via legacy ate la).

Validado smoke 2 PIE 2026-06-08 20:32
======================================
* SP_01: players=2 conns=2 + AOI players=2 maxSet=1 enters=5 hyst=92 stable
* UE: [PIE0] e [PIE1] prefix funcionando (commit server 54f2f9a ZNET_PIE_PREFIX)
* "S_SPAWN_PLAYER duplicate ignored" dedupe pegando re-emit (Batch 2)
* S_CHAR_INFO trocado correto: PIE0 ve Olatudook, PIE1 ve Mateuus
* Saida limpa: UnregisterViewer + Broadcast S_DESPAWN_PLAYER targets=1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 20:33:41 -03:00

392 lines
13 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"
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);
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
{
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."));
// 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).
int32 ReplayCount = 0;
ZeusNet->ForEachPendingRemoteSpawn(
[this, &ReplayCount](const int64 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs);
++ReplayCount;
});
if (ReplayCount > 0)
{
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount);
}
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("ZeusWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay."));
}
}
void UZeusWorldSubsystem::Deinitialize()
{
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
{
ZeusNet->OnPlayerSpawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerSpawned);
ZeusNet->OnPlayerDespawned.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerDespawned);
ZeusNet->OnPlayerStateUpdate.RemoveDynamic(this, &UZeusWorldSubsystem::HandlePlayerStateUpdate);
}
RemoteEntities.Reset();
LocalEntityId = 0;
Super::Deinitialize();
}
void UZeusWorldSubsystem::RegisterLocalEntity(const int64 EntityId, AActor* LocalActor)
{
if (EntityId == 0 || LocalActor == nullptr)
{
return;
}
LocalEntityId = EntityId;
RemoteEntities.Add(EntityId, TWeakObjectPtr<AActor>(LocalActor));
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));
UE_LOG(LogZMMO, Log, TEXT("ZeusWorldSubsystem: spawned remote EntityId=%d at (%s) yaw=%.1f t=%lld"),
EntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
}
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);
}
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>();
}