#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(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); CameraBoom->TargetArmLength = 400.0f; CameraBoom->bUsePawnControlRotation = true; FollowCamera = CreateDefaultSubobject(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 QuinnMesh( TEXT("/Game/Characters/Mannequins/Meshes/SKM_Quinn_Simple.SKM_Quinn_Simple")); static ConstructorHelpers::FClassFinder 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 MoveActionAsset( TEXT("/Game/Input/Actions/IA_Move.IA_Move")); static ConstructorHelpers::FObjectFinder LookActionAsset( TEXT("/Game/Input/Actions/IA_Look.IA_Look")); static ConstructorHelpers::FObjectFinder MouseLookActionAsset( TEXT("/Game/Input/Actions/IA_MouseLook.IA_MouseLook")); static ConstructorHelpers::FObjectFinder 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()) { 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(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(); 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(); 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(); } 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; } 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. TryApplyCachedCharInfo(); } void AZMMOPlayerCharacter::HandleZeusPlayerSpawned(const int64 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 int64 InEntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs) { if (InEntityId == 0) { return; } // PR-HANDOFF-007 — InEntityId é int64 (era int32) if (ZMMOEntityId == InEntityId) { return; } ZMMOEntityId = InEntityId; UE_LOG(LogZMMOPlayer, Log, TEXT("AZMMOPlayerCharacter: local spawn captured EntityId=%lld pos=(%s) yaw=%.1f t=%lld"), InEntityId, *PosCm.ToString(), YawDeg, ServerTimeMs); if (UWorld* World = GetWorld()) { if (UZMMOWorldSubsystem* WorldSubsystem = World->GetSubsystem()) { WorldSubsystem->RegisterLocalEntity(InEntityId, 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(PS)) { ZMMOPs->SetPublicIdentity(InEntityId, /*CharId*/ FString(), /*BaseLevel*/ 1, /*ClassId*/ 0); } if (UZMMOAttributeComponent* AttrComp = PS->FindComponentByClass()) { 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 int64 InEntityId, const FString& CharName, const FString& GuildName) { UE_LOG(LogZMMOPlayer, Log, TEXT("AZMMOPlayerCharacter: S_CHAR_INFO recebido EntityId=%lld 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(PS)) { ZMMOPs->SetCharInfo(CharName, GuildName); } } void AZMMOPlayerCharacter::TryApplyCachedCharInfo() { if (!ZeusNetwork) { return; } int64 CachedEntityId = 0; FString CachedCharName; FString CachedGuildName; if (!ZeusNetwork->TryGetLastLocalCharInfo(CachedEntityId, CachedCharName, CachedGuildName)) { return; } if (APlayerState* PS = GetPlayerState()) { if (AZMMOPlayerState* ZMMOPs = Cast(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(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(GetController()->GetControlRotation().Yaw) : static_cast(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(Vel.Z)); SendAccumulatorSec = 0.0f; TimeSinceLastSendSec = 0.0f; bPendingJumpPressed = false; bPendingJumpReleased = false; bPreviousFalling = bIsFalling; }