Files
ZMMO/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp
Mateus Rodrigues 90484876f0 feat(player): CharName do DB no PlayerState + header do Status Window
- PlayerState ganha CharName (Replicated) + SetCharInfo(CharName, GuildName)
- ZMMOPlayerCharacter assina FZeusOnCharInfoReceived; TryApplyCachedCharInfo
  resolve race se S_CHAR_INFO chega antes do pawn possuir
- UIPlayerStatus_Window header le PlayerState->CharName (fallback p/ GetPlayerName)
  em vez do "Mateuus-61E9B19A4FB2" generico

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 02:24:29 -03:00

491 lines
16 KiB
C++

#include "ZMMOPlayerCharacter.h"
#include "Animation/AnimInstance.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "EnhancedInputComponent.h"
#include "Engine/GameInstance.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/World.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Controller.h"
#include "GameFramework/SpringArmComponent.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "GameFramework/PlayerState.h"
#include "Subsystems/WorldSubsystem.h"
#include "UObject/ConstructorHelpers.h"
#include "ZMMO.h"
#include "ZMMOAttributeComponent.h"
#include "ZMMOPlayerState.h"
#include "ZMMOWorldSubsystem.h"
#include "ZeusNetworkSubsystem.h"
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
DEFINE_LOG_CATEGORY(LogZMMOPlayer);
AZMMOPlayerCharacter::AZMMOPlayerCharacter()
{
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
GetCapsuleComponent()->InitCapsuleSize(42.0f, 96.0f);
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
UCharacterMovementComponent* CMC = GetCharacterMovement();
CMC->bOrientRotationToMovement = true;
CMC->RotationRate = FRotator(0.0f, 500.0f, 0.0f);
CMC->JumpZVelocity = 500.0f;
CMC->AirControl = 0.35f;
CMC->MaxWalkSpeed = 500.0f;
CMC->MinAnalogWalkSpeed = 20.0f;
CMC->BrakingDecelerationWalking = 2000.0f;
CMC->BrakingDecelerationFalling = 1500.0f;
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 400.0f;
CameraBoom->bUsePawnControlRotation = true;
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
FollowCamera->bUsePawnControlRotation = false;
// AttributeComponent migrou pro PlayerState (AZMMOPlayerState + Component
// Registry config-driven). Pawn fica leve — so' movement/input/camera.
// Defaults visuais (mesh + AnimBP) — alinhados ao ZClientMMO. Permitem ao
// motor spawnar AZMMOPlayerCharacter directamente como DefaultPawnClass do
// AZMMOGameMode sem exigir um BP filho. Um BP filho continua opcional para
// trocar mesh/AnimBP por mapa.
if (USkeletalMeshComponent* MeshComponent = GetMesh())
{
static ConstructorHelpers::FObjectFinder<USkeletalMesh> QuinnMesh(
TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple"));
static ConstructorHelpers::FClassFinder<UAnimInstance> QuinnAnimBp(
TEXT("/Game/Characters/Mannequins/Anims/Unarmed/ABP_Unarmed"));
if (QuinnMesh.Succeeded())
{
MeshComponent->SetSkeletalMesh(QuinnMesh.Object);
MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, -90.0f));
MeshComponent->SetRelativeRotation(FRotator(0.0f, -90.0f, 0.0f));
}
else
{
UE_LOG(LogZMMOPlayer, Warning, TEXT("Default mesh SKM_Quinn_Simple not found for AZMMOPlayerCharacter."));
}
if (QuinnAnimBp.Succeeded())
{
MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
}
else
{
UE_LOG(LogZMMOPlayer, Warning, TEXT("Default anim blueprint ABP_Unarmed not found for AZMMOPlayerCharacter."));
}
}
// Input Actions defaults (Enhanced Input) — IMCs sao adicionados pelo
// AZMMOPlayerController. As IAs aqui resolvem o asset por path; um BP filho
// pode sobrescrever caso queira inputs custom.
static ConstructorHelpers::FObjectFinder<UInputAction> MoveActionAsset(
TEXT("/Game/Input/Actions/IA_Move.IA_Move"));
static ConstructorHelpers::FObjectFinder<UInputAction> LookActionAsset(
TEXT("/Game/Input/Actions/IA_Look.IA_Look"));
static ConstructorHelpers::FObjectFinder<UInputAction> MouseLookActionAsset(
TEXT("/Game/Input/Actions/IA_MouseLook.IA_MouseLook"));
static ConstructorHelpers::FObjectFinder<UInputAction> JumpActionAsset(
TEXT("/Game/Input/Actions/IA_Jump.IA_Jump"));
if (MoveActionAsset.Succeeded()) { MoveAction = MoveActionAsset.Object; }
if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; }
if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; }
if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; }
}
void AZMMOPlayerCharacter::BeginPlay()
{
Super::BeginPlay();
ResolveZeusNetworkSubsystem();
BindZeusSpawnDelegate();
TryRegisterLocalEntityFromCachedSpawn();
// Fase 4: reposiciona o pawn na pos salva no DB (vinda no S_CHAR_SELECT_OK
// e memorizada no Flow). Substitui o PlayerStart default do level. Se nao
// ha pose pendente (primeiro boot / debug PIE direto no level), mantem o
// PlayerStart.
if (const UGameInstance* GI = GetGameInstance())
{
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
FVector PosCm = FVector::ZeroVector;
float YawDeg = 0.0f;
if (Flow->ConsumePendingSpawnPose(PosCm, YawDeg))
{
const FRotator NewRot(0.0f, YawDeg, 0.0f);
SetActorLocationAndRotation(PosCm, NewRot, /*bSweep=*/false, nullptr, ETeleportType::TeleportPhysics);
if (AController* C = GetController())
{
C->SetControlRotation(NewRot);
}
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"),
*PosCm.ToString(), YawDeg);
}
}
}
}
void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
UnbindZeusSpawnDelegate();
ZeusNetwork = nullptr;
Super::EndPlay(EndPlayReason);
}
void AZMMOPlayerCharacter::Tick(const float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
FlushInputAxisToServer(DeltaSeconds);
}
void AZMMOPlayerCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
{
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZMMOPlayerCharacter::OnJumpPressed);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::OnJumpReleased);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Move);
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AZMMOPlayerCharacter::MoveCompleted);
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AZMMOPlayerCharacter::Look);
}
else
{
UE_LOG(LogZMMOPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this));
}
}
void AZMMOPlayerCharacter::Move(const FInputActionValue& Value)
{
const FVector2D MovementVector = Value.Get<FVector2D>();
DoMove(MovementVector.X, MovementVector.Y);
}
void AZMMOPlayerCharacter::MoveCompleted(const FInputActionValue& Value)
{
(void)Value;
DoMove(0.0f, 0.0f);
}
void AZMMOPlayerCharacter::Look(const FInputActionValue& Value)
{
const FVector2D LookAxisVector = Value.Get<FVector2D>();
DoLook(LookAxisVector.X, LookAxisVector.Y);
}
void AZMMOPlayerCharacter::OnJumpPressed()
{
bPendingJumpPressed = true;
DoJumpStart();
}
void AZMMOPlayerCharacter::OnJumpReleased()
{
bPendingJumpReleased = true;
DoJumpEnd();
}
void AZMMOPlayerCharacter::DoMove(const float Right, const float Forward)
{
PendingMoveRight = FMath::Clamp(Right, -1.0f, 1.0f);
PendingMoveForward = FMath::Clamp(Forward, -1.0f, 1.0f);
if (GetController() != nullptr)
{
const FRotator Rotation = GetController()->GetControlRotation();
const FRotator YawRotation(0.0f, Rotation.Yaw, 0.0f);
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(ForwardDirection, Forward);
AddMovementInput(RightDirection, Right);
}
}
void AZMMOPlayerCharacter::DoLook(const float Yaw, const float Pitch)
{
if (GetController() != nullptr)
{
AddControllerYawInput(Yaw);
AddControllerPitchInput(Pitch);
}
}
void AZMMOPlayerCharacter::DoJumpStart()
{
Jump();
}
void AZMMOPlayerCharacter::DoJumpEnd()
{
StopJumping();
}
void AZMMOPlayerCharacter::ApplyEntitySnapshot(const FZMMOEntitySnapshot& /*Snapshot*/)
{
// Cliente local solto: o servidor nao reconcilia posicao em V0 (ADR 0038).
// Quando colisao de objetos chegar, podemos ligar uma reconciliacao opcional
// horizontal aqui (XY only), preservando o Z do CMC local.
}
void AZMMOPlayerCharacter::SetEntityRelevant(bool /*bRelevant*/)
{
// Nunca despawnamos o jogador local via AOI.
}
void AZMMOPlayerCharacter::ResolveZeusNetworkSubsystem()
{
if (ZeusNetwork)
{
return;
}
UGameInstance* GI = GetGameInstance();
if (!GI)
{
return;
}
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
}
void AZMMOPlayerCharacter::BindZeusSpawnDelegate()
{
if (bSpawnDelegateBound || !IsLocallyControlled())
{
return;
}
if (!ZeusNetwork)
{
return;
}
ZeusNetwork->OnPlayerSpawned.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZMMOPlayerCharacter::HandleZeusCharInfo);
bSpawnDelegateBound = true;
}
void AZMMOPlayerCharacter::UnbindZeusSpawnDelegate()
{
if (!bSpawnDelegateBound || !ZeusNetwork)
{
bSpawnDelegateBound = false;
return;
}
ZeusNetwork->OnPlayerSpawned.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusPlayerSpawned);
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZMMOPlayerCharacter::HandleZeusCharInfo);
bSpawnDelegateBound = false;
}
void AZMMOPlayerCharacter::TryRegisterLocalEntityFromCachedSpawn()
{
if (!ZeusNetwork)
{
return;
}
int32 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.
TryApplyCachedCharInfo();
}
void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int32 InEntityId, const bool bIsLocal,
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
{
if (!bIsLocal)
{
// Remote spawns sao tratados pelo `UZMMOWorldSubsystem`; aqui so reagimos
// ao spawn do pawn possessivel local (S_SPAWN_PLAYER com bIsLocal=true).
return;
}
HandleLocalSpawnReady(InEntityId, PosCm, YawDeg, ServerTimeMs);
}
void AZMMOPlayerCharacter::HandleLocalSpawnReady(const int32 InEntityId, const FVector PosCm,
const float YawDeg, const int64 ServerTimeMs)
{
if (InEntityId == 0)
{
return;
}
const int64 EntityIdAsInt64 = static_cast<int64>(InEntityId);
if (ZMMOEntityId == EntityIdAsInt64)
{
return;
}
ZMMOEntityId = EntityIdAsInt64;
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: local spawn captured EntityId=%d pos=(%s) yaw=%.1f t=%lld"),
InEntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
if (UWorld* World = GetWorld())
{
if (UZMMOWorldSubsystem* WorldSubsystem = World->GetSubsystem<UZMMOWorldSubsystem>())
{
WorldSubsystem->RegisterLocalEntity(EntityIdAsInt64, this);
}
}
// Identidade publica (EntityId) + seed no AttributeComponent. CharName/Guild
// vem separado via S_CHAR_INFO -> HandleZeusCharInfo. AttributeComponent vive
// como subobject e recebe seed pra que o ZMMOAttributeNetworkHandler consiga
// rotear o snapshot por EntityId desde o primeiro pacote.
if (APlayerState* PS = GetPlayerState())
{
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetPublicIdentity(EntityIdAsInt64, /*CharId*/ FString(),
/*BaseLevel*/ 1, /*ClassId*/ 0);
}
if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass<UZMMOAttributeComponent>())
{
AttrComp->SeedEntityId(InEntityId);
}
}
// Aplica char info cacheada (caso S_CHAR_INFO tenha chegado antes do PS existir).
TryApplyCachedCharInfo();
// UI in-game e' responsabilidade do AZMMOHUD (GameMode.HUDClass), nao do
// Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que
// pushea WBP_HUD em UI.Layer.Game. UZMMOHudWidget::NativeOnActivated auto-binda
// no UZMMOAttributeComponent via PlayerState.
}
void AZMMOPlayerCharacter::HandleZeusCharInfo(const int32 InEntityId, const FString& CharName, const FString& GuildName)
{
UE_LOG(LogZMMOPlayer, Log,
TEXT("AZMMOPlayerCharacter: S_CHAR_INFO recebido EntityId=%d name=%s guild=%s"),
InEntityId, *CharName, *GuildName);
APlayerState* PS = GetPlayerState();
if (!PS)
{
// Race fix: pawn pode nao ter PlayerState ainda. Subsystem ja cacheou
// (LastLocalCharInfoCache); TryApplyCachedCharInfo aplica em
// HandleLocalSpawnReady ou em outra oportunidade.
return;
}
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetCharInfo(CharName, GuildName);
}
}
void AZMMOPlayerCharacter::TryApplyCachedCharInfo()
{
if (!ZeusNetwork)
{
return;
}
int32 CachedEntityId = 0;
FString CachedCharName;
FString CachedGuildName;
if (!ZeusNetwork->TryGetLastLocalCharInfo(CachedEntityId, CachedCharName, CachedGuildName))
{
return;
}
if (APlayerState* PS = GetPlayerState())
{
if (AZMMOPlayerState* ZMMOPs = Cast<AZMMOPlayerState>(PS))
{
ZMMOPs->SetCharInfo(CachedCharName, CachedGuildName);
}
}
}
void AZMMOPlayerCharacter::FlushInputAxisToServer(const float DeltaSeconds)
{
ResolveZeusNetworkSubsystem();
if (!ZeusNetwork || !ZeusNetwork->IsConnected())
{
return;
}
const float SendInterval = 1.0f / FMath::Max(1, InputSendRateHz);
SendAccumulatorSec += DeltaSeconds;
TimeSinceLastSendSec += DeltaSeconds;
const bool bMovingInput = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
const bool bJumpEvent = bPendingJumpPressed || bPendingJumpReleased;
const bool bRateReached = SendAccumulatorSec >= SendInterval;
const bool bHeartbeatReached = TimeSinceLastSendSec >= HeartbeatIntervalSec;
// IsFalling é estado contínuo do CMC (espelhado em PlayerStatePayload::grounded
// para alimentar o MovementMode dos proxies). Forçar envio em transições
// walking↔falling garante que a animação de queda/pulo dispare nos outros
// clientes mesmo quando estamos em rate limit / sem input de movimento.
const UCharacterMovementComponent* CMCForFalling = GetCharacterMovement();
const bool bIsFalling = CMCForFalling != nullptr && CMCForFalling->IsFalling();
const bool bFallingChanged = (bIsFalling != bPreviousFalling);
const bool bShouldSend = bJumpEvent || bFallingChanged || (bMovingInput && bRateReached) || (!bMovingInput && bHeartbeatReached);
if (!bShouldSend)
{
return;
}
++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));
// 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));
SendAccumulatorSec = 0.0f;
TimeSinceLastSendSec = 0.0f;
bPendingJumpPressed = false;
bPendingJumpReleased = false;
bPreviousFalling = bIsFalling;
}