- ZeusCharacter/ZeusWorldSubsystem passam a usar UZeusNetworkingClientSubsystem: ENT_SELF (entidade própria), INPUT 6077 via EmitInput, spawn/despawn/delta rebindados pro subsystem novo (legacy vira fallback). - ZeusPlayerProxy consome velocidade/grounded/serverTimeMs (delta 0x02): anima, vira na direção do movimento e cola no chão (MOVE_Walking). - Fix overshoot/inércia: FlushInputAxisToServer considera velocidade residual (braking) e envia a 30Hz até o dono parar de fato; o proxy desacelera suave, sem extrapolar ~1m além nem snap-back. - Char-select -> ConnectWithTicket (handoff ticket) quando bUseZeusNetworkingV1. Inclui logs [InputDbg] (ZeusCharacter) e DEBUG-PROXY (ZeusPlayerProxy) mantidos para testes posteriores de input/jitter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
705 lines
24 KiB
C++
705 lines
24 KiB
C++
#include "ZeusCharacter.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 "GameplayTagContainer.h"
|
|
#include "GameplayTagsManager.h"
|
|
#include "ZeusGASComponent.h"
|
|
#include "ZeusAOIComponent.h"
|
|
#include "ZeusPlayerState.h"
|
|
#include "Blueprint/UserWidget.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "ZeusWorldSubsystem.h"
|
|
#include "ZeusNetworkSubsystem.h"
|
|
#include "ZeusNetworkingClientSubsystem.h"
|
|
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
|
|
|
|
DEFINE_LOG_CATEGORY(LogZeusPlayer);
|
|
|
|
AZeusCharacter::AZeusCharacter()
|
|
{
|
|
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;
|
|
|
|
// AOI do cliente (+ overlays de debug). Implementa IZeusAOIDebugTarget; o
|
|
// Zeus Admin Tools acha este componente no pawn e dirige os overlays.
|
|
AOIComponent = CreateDefaultSubobject<UZeusAOIComponent>(TEXT("ZeusAOIComponent"));
|
|
|
|
// AdminPanelClass NAO e' resolvido aqui via FClassFinder: na criacao do CDO
|
|
// (load do modulo) o asset registry pode nao estar pronto e o static cacheia
|
|
// null pra sempre. Resolvido lazy em ToggleAdminPanel (LoadClass em runtime).
|
|
|
|
// AttributeComponent migrou pro PlayerState (AZeusPlayerState + Component
|
|
// Registry config-driven). Pawn fica leve — so' movement/input/camera.
|
|
|
|
// Defaults visuais (mesh + AnimBP). Permitem ao motor spawnar AZeusCharacter
|
|
// directamente como DefaultPawnClass do AZeusGameMode 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(LogZeusPlayer, Warning, TEXT("Default mesh SKM_Quinn_Simple not found for AZeusCharacter."));
|
|
}
|
|
|
|
if (QuinnAnimBp.Succeeded())
|
|
{
|
|
MeshComponent->SetAnimationMode(EAnimationMode::AnimationBlueprint);
|
|
MeshComponent->SetAnimInstanceClass(QuinnAnimBp.Class);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZeusPlayer, Warning, TEXT("Default anim blueprint ABP_Unarmed not found for AZeusCharacter."));
|
|
}
|
|
}
|
|
|
|
// Input Actions defaults (Enhanced Input) — IMCs sao adicionados pelo
|
|
// AZeusPlayerController. 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"));
|
|
static ConstructorHelpers::FObjectFinder<UInputAction> DashActionAsset(
|
|
TEXT("/Game/Input/Actions/IA_Dash.IA_Dash"));
|
|
|
|
if (MoveActionAsset.Succeeded()) { MoveAction = MoveActionAsset.Object; }
|
|
if (LookActionAsset.Succeeded()) { LookAction = LookActionAsset.Object; }
|
|
if (MouseLookActionAsset.Succeeded()) { MouseLookAction = MouseLookActionAsset.Object; }
|
|
if (JumpActionAsset.Succeeded()) { JumpAction = JumpActionAsset.Object; }
|
|
if (DashActionAsset.Succeeded()) { DashAction = DashActionAsset.Object; }
|
|
}
|
|
|
|
void AZeusCharacter::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(LogZeusPlayer, Log,
|
|
TEXT("AZeusCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"),
|
|
*PosCm.ToString(), YawDeg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
UnbindZeusSpawnDelegate();
|
|
ZeusNetwork = nullptr;
|
|
Super::EndPlay(EndPlayReason);
|
|
}
|
|
|
|
void AZeusCharacter::Tick(const float DeltaSeconds)
|
|
{
|
|
Super::Tick(DeltaSeconds);
|
|
FlushInputAxisToServer(DeltaSeconds);
|
|
}
|
|
|
|
void AZeusCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
|
{
|
|
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
|
|
{
|
|
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Started, this, &AZeusCharacter::OnJumpPressed);
|
|
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AZeusCharacter::OnJumpReleased);
|
|
|
|
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AZeusCharacter::Move);
|
|
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Completed, this, &AZeusCharacter::MoveCompleted);
|
|
|
|
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &AZeusCharacter::Look);
|
|
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &AZeusCharacter::Look);
|
|
|
|
if (DashAction)
|
|
{
|
|
EnhancedInputComponent->BindAction(DashAction, ETriggerEvent::Triggered, this, &AZeusCharacter::OnDashTriggered);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogZeusPlayer, Error, TEXT("'%s' Failed to find an Enhanced Input component."), *GetNameSafe(this));
|
|
}
|
|
|
|
// Debug: F8 abre/fecha o Zeus Admin Tools (AIO Debug). Legacy BindKey
|
|
// convive com Enhanced Input. Trocar a tecla aqui se conflitar.
|
|
PlayerInputComponent->BindKey(EKeys::F8, IE_Pressed, this, &AZeusCharacter::ToggleAdminPanel);
|
|
}
|
|
|
|
void AZeusCharacter::ToggleAdminPanel()
|
|
{
|
|
APlayerController* PC = Cast<APlayerController>(GetController());
|
|
if (!PC)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Aberto -> fecha + devolve input pro jogo.
|
|
if (AdminPanelInstance && AdminPanelInstance->IsInViewport())
|
|
{
|
|
AdminPanelInstance->RemoveFromParent();
|
|
PC->SetInputMode(FInputModeGameOnly());
|
|
PC->bShowMouseCursor = false;
|
|
return;
|
|
}
|
|
|
|
// Lazy load (runtime): no F8 o asset ja' esta carregavel; evita o pitfall do
|
|
// FClassFinder na criacao do CDO. Path do generated class (_C).
|
|
// A UI vive no CONTENT DO PLUGIN (mount /ZeusAdminTools/), nao no /Game do projeto:
|
|
// o plugin ZeusAdminTools e' autocontido (C++ + UI no repo Server).
|
|
if (!AdminPanelClass)
|
|
{
|
|
AdminPanelClass = LoadClass<UUserWidget>(nullptr,
|
|
TEXT("/ZeusAdminTools/UI/WBP_AdminToolsAIO.WBP_AdminToolsAIO_C"));
|
|
}
|
|
if (!AdminPanelClass)
|
|
{
|
|
UE_LOG(LogZeusPlayer, Warning, TEXT("[AdminPanel] AdminPanelClass nao resolvido (WBP_AdminToolsAIO ausente?)."));
|
|
return;
|
|
}
|
|
|
|
if (!AdminPanelInstance)
|
|
{
|
|
AdminPanelInstance = CreateWidget<UUserWidget>(PC, AdminPanelClass);
|
|
}
|
|
if (AdminPanelInstance)
|
|
{
|
|
AdminPanelInstance->AddToViewport(1000);
|
|
|
|
// UIOnly: trava a acao do jogo enquanto o menu esta aberto e garante que
|
|
// os cliques vao 100% pra UI (no GameAndUI o viewport rouba o clique).
|
|
// O mundo continua renderizando + os overlays de debug seguem desenhando.
|
|
FInputModeUIOnly Mode;
|
|
Mode.SetWidgetToFocus(AdminPanelInstance->TakeWidget());
|
|
Mode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
|
|
PC->SetInputMode(Mode);
|
|
PC->bShowMouseCursor = true;
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::Move(const FInputActionValue& Value)
|
|
{
|
|
const FVector2D MovementVector = Value.Get<FVector2D>();
|
|
DoMove(MovementVector.X, MovementVector.Y);
|
|
}
|
|
|
|
void AZeusCharacter::MoveCompleted(const FInputActionValue& Value)
|
|
{
|
|
(void)Value;
|
|
DoMove(0.0f, 0.0f);
|
|
}
|
|
|
|
void AZeusCharacter::Look(const FInputActionValue& Value)
|
|
{
|
|
const FVector2D LookAxisVector = Value.Get<FVector2D>();
|
|
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
|
}
|
|
|
|
void AZeusCharacter::OnJumpPressed()
|
|
{
|
|
bPendingJumpPressed = true;
|
|
DoJumpStart();
|
|
}
|
|
|
|
void AZeusCharacter::OnJumpReleased()
|
|
{
|
|
bPendingJumpReleased = true;
|
|
DoJumpEnd();
|
|
}
|
|
|
|
void AZeusCharacter::OnDashTriggered()
|
|
{
|
|
// Resolve UZeusGASComponent do PlayerState. Component vive la' via
|
|
// AZeusPlayerState Component Registry (config-driven). Sem el, ASC ausente
|
|
// e RequestActivateAbilityByTag falha — log warning.
|
|
const APlayerState* PS = GetPlayerState();
|
|
if (!PS)
|
|
{
|
|
UE_LOG(LogZeusPlayer, Warning, TEXT("OnDashTriggered: PlayerState nullptr — ignorado"));
|
|
return;
|
|
}
|
|
UZeusGASComponent* Comp = PS->FindComponentByClass<UZeusGASComponent>();
|
|
if (!Comp)
|
|
{
|
|
UE_LOG(LogZeusPlayer, Warning, TEXT("OnDashTriggered: UZeusGASComponent nao achado"));
|
|
return;
|
|
}
|
|
|
|
// Tag canonica casa com row do DT + JSON do server. Hash FNV calculado
|
|
// dentro do componente (RequestActivateAbilityByTag).
|
|
const FGameplayTag DashTag = UGameplayTagsManager::Get().RequestGameplayTag(
|
|
FName(TEXT("Zeus.Ability.Movement.Dash")));
|
|
if (!DashTag.IsValid())
|
|
{
|
|
UE_LOG(LogZeusPlayer, Warning,
|
|
TEXT("OnDashTriggered: tag Zeus.Ability.Movement.Dash nao registrada no cliente"));
|
|
return;
|
|
}
|
|
|
|
const bool bSent = Comp->RequestActivateAbilityByTag(DashTag);
|
|
UE_LOG(LogZeusPlayer, Log, TEXT("OnDashTriggered -> RequestActivateAbilityByTag = %s"),
|
|
bSent ? TEXT("OK (C_ABILITY_TRY_ACTIVATE enviado)") : TEXT("FALHOU"));
|
|
}
|
|
|
|
void AZeusCharacter::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 AZeusCharacter::DoLook(const float Yaw, const float Pitch)
|
|
{
|
|
if (GetController() != nullptr)
|
|
{
|
|
AddControllerYawInput(Yaw);
|
|
AddControllerPitchInput(Pitch);
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::DoJumpStart()
|
|
{
|
|
Jump();
|
|
}
|
|
|
|
void AZeusCharacter::DoJumpEnd()
|
|
{
|
|
StopJumping();
|
|
}
|
|
|
|
void AZeusCharacter::ApplyEntitySnapshot(const FZeusEntitySnapshot& /*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 AZeusCharacter::SetEntityRelevant(bool /*bRelevant*/)
|
|
{
|
|
// Nunca despawnamos o jogador local via AOI.
|
|
}
|
|
|
|
void AZeusCharacter::ResolveZeusNetworkSubsystem()
|
|
{
|
|
if (ZeusNetwork && NetClient)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UGameInstance* GI = GetGameInstance();
|
|
if (!GI)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!ZeusNetwork)
|
|
{
|
|
ZeusNetwork = GI->GetSubsystem<UZeusNetworkSubsystem>();
|
|
}
|
|
if (!NetClient)
|
|
{
|
|
NetClient = GI->GetSubsystem<UZeusNetworkingClientSubsystem>();
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::BindZeusSpawnDelegate()
|
|
{
|
|
if (bSpawnDelegateBound || !IsLocallyControlled())
|
|
{
|
|
return;
|
|
}
|
|
if (!NetClient && !ZeusNetwork)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Self-entity (entityId do proprio char) vem do sistema de rede novo.
|
|
if (NetClient)
|
|
{
|
|
NetClient->OnSelfEntityAssigned.AddDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
|
|
}
|
|
// CHAR_INFO (nome/guild) ainda nao tem equivalente no sistema novo -> legacy.
|
|
if (ZeusNetwork)
|
|
{
|
|
ZeusNetwork->OnCharInfoReceived.AddDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
|
}
|
|
bSpawnDelegateBound = true;
|
|
}
|
|
|
|
void AZeusCharacter::UnbindZeusSpawnDelegate()
|
|
{
|
|
if (!bSpawnDelegateBound)
|
|
{
|
|
return;
|
|
}
|
|
if (NetClient)
|
|
{
|
|
NetClient->OnSelfEntityAssigned.RemoveDynamic(this, &AZeusCharacter::HandleSelfEntityAssigned);
|
|
}
|
|
if (ZeusNetwork)
|
|
{
|
|
ZeusNetwork->OnCharInfoReceived.RemoveDynamic(this, &AZeusCharacter::HandleZeusCharInfo);
|
|
}
|
|
bSpawnDelegateBound = false;
|
|
}
|
|
|
|
void AZeusCharacter::TryRegisterLocalEntityFromCachedSpawn()
|
|
{
|
|
// Self-entity vem do sistema de rede novo (ENT_SELF). O subsystem e'
|
|
// per-GameInstance e sobrevive ao OpenLevel, entao o entityId pode ja' ter
|
|
// chegado antes deste pawn existir -- puxa o cache agora. Caso ainda nao
|
|
// tenha chegado, o bind em OnSelfEntityAssigned cobre quando chegar.
|
|
// A pos/yaw vem do proprio pawn (ja posicionado via PendingSpawnPose do DB);
|
|
// o sistema novo so' carrega o entityId.
|
|
if (NetClient)
|
|
{
|
|
const int64 SelfId = NetClient->GetLocalEntityId();
|
|
if (SelfId != 0)
|
|
{
|
|
HandleLocalSpawnReady(SelfId, GetActorLocation(), GetActorRotation().Yaw, 0);
|
|
}
|
|
}
|
|
|
|
// Race fix CHAR_INFO (ainda legacy): o S_CHAR_INFO pode ter chegado antes do
|
|
// pawn existir; aplica o ultimo nome/guild cacheado.
|
|
TryApplyCachedCharInfo();
|
|
}
|
|
|
|
void AZeusCharacter::HandleSelfEntityAssigned(const int64 InEntityId)
|
|
{
|
|
// Sistema novo informou o entityId do proprio char (ENT_SELF). Reusa o mesmo
|
|
// caminho do spawn local legacy (seta ZeusEntityId + RegisterLocalEntity).
|
|
// pos/yaw vem do pawn (ja posicionado); o sistema novo so' traz o entityId.
|
|
HandleLocalSpawnReady(InEntityId, GetActorLocation(), GetActorRotation().Yaw, 0);
|
|
}
|
|
|
|
void AZeusCharacter::HandleZeusPlayerSpawned(const int64 InEntityId, const bool bIsLocal,
|
|
const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
|
{
|
|
if (!bIsLocal)
|
|
{
|
|
// Remote spawns sao tratados pelo `UZeusWorldSubsystem`; aqui so reagimos
|
|
// ao spawn do pawn possessivel local (S_SPAWN_PLAYER com bIsLocal=true).
|
|
return;
|
|
}
|
|
HandleLocalSpawnReady(InEntityId, PosCm, YawDeg, ServerTimeMs);
|
|
}
|
|
|
|
void AZeusCharacter::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 (ZeusEntityId == InEntityId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ZeusEntityId = InEntityId;
|
|
|
|
UE_LOG(LogZeusPlayer, Log,
|
|
TEXT("AZeusCharacter: local spawn captured EntityId=%lld pos=(%s) yaw=%.1f t=%lld"),
|
|
InEntityId, *PosCm.ToString(), YawDeg, ServerTimeMs);
|
|
|
|
if (UWorld* World = GetWorld())
|
|
{
|
|
if (UZeusWorldSubsystem* WorldSubsystem = World->GetSubsystem<UZeusWorldSubsystem>())
|
|
{
|
|
WorldSubsystem->RegisterLocalEntity(InEntityId, this);
|
|
}
|
|
}
|
|
|
|
// Identidade publica (EntityId) + seed no GAS Component. CharName/Guild
|
|
// vem separado via S_CHAR_INFO -> HandleZeusCharInfo. UZeusGASComponent
|
|
// vive como subobject do PlayerState (Component Registry) e recebe seed
|
|
// pra que o UZeusGASNetworkHandler consiga rotear o snapshot por
|
|
// EntityId desde o primeiro pacote.
|
|
if (APlayerState* PS = GetPlayerState())
|
|
{
|
|
if (AZeusPlayerState* ZeusPs = Cast<AZeusPlayerState>(PS))
|
|
{
|
|
ZeusPs->SetPublicIdentity(InEntityId, /*CharId*/ FString(),
|
|
/*BaseLevel*/ 1, /*ClassId*/ 0);
|
|
}
|
|
if (UZeusGASComponent* GASComp = PS->FindComponentByClass<UZeusGASComponent>())
|
|
{
|
|
GASComp->SeedEntityId(InEntityId);
|
|
}
|
|
}
|
|
|
|
// Aplica char info cacheada (caso S_CHAR_INFO tenha chegado antes do PS existir).
|
|
TryApplyCachedCharInfo();
|
|
|
|
// UI in-game e' responsabilidade do AZeusHUD (GameMode.HUDClass), nao do
|
|
// Pawn. AHUD::BeginPlay chama UUIInGameFlowSubsystem::StartInGame, que
|
|
// pushea WBP_HUD em UI.Layer.Game. UZeusHudWidget::NativeOnActivated auto-binda
|
|
// no UZeusGASComponent via PlayerState.
|
|
}
|
|
|
|
void AZeusCharacter::HandleZeusCharInfo(const int64 InEntityId, const FString& CharName, const FString& GuildName)
|
|
{
|
|
UE_LOG(LogZeusPlayer, Log,
|
|
TEXT("AZeusCharacter: S_CHAR_INFO recebido EntityId=%lld name=%s guild=%s"),
|
|
InEntityId, *CharName, *GuildName);
|
|
|
|
// 2026-06-03 fix: server envia S_CHAR_INFO de TODOS os players (proprio +
|
|
// catch-up de proxies pre-existentes). Sem este filtro, o nome do ultimo
|
|
// proxy recebido sobrescrevia o do pawn local (ex: player Mateuus loga e
|
|
// ve "Olatudook" em si mesmo porque o catch-up do Olatudook chegou depois
|
|
// do S_CHAR_INFO proprio). Proxies remotos sao roteados pelo registry no
|
|
// UZeusWorldSubsystem (futuro nameplate por EntityId).
|
|
if (ZeusEntityId != 0 && InEntityId != ZeusEntityId)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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 (AZeusPlayerState* ZeusPs = Cast<AZeusPlayerState>(PS))
|
|
{
|
|
ZeusPs->SetCharInfo(CharName, GuildName);
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::TryApplyCachedCharInfo()
|
|
{
|
|
if (!ZeusNetwork)
|
|
{
|
|
return;
|
|
}
|
|
int64 CachedEntityId = 0;
|
|
FString CachedCharName;
|
|
FString CachedGuildName;
|
|
if (!ZeusNetwork->TryGetLastLocalCharInfo(CachedEntityId, CachedCharName, CachedGuildName))
|
|
{
|
|
return;
|
|
}
|
|
if (APlayerState* PS = GetPlayerState())
|
|
{
|
|
if (AZeusPlayerState* ZeusPs = Cast<AZeusPlayerState>(PS))
|
|
{
|
|
ZeusPs->SetCharInfo(CachedCharName, CachedGuildName);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AZeusCharacter::FlushInputAxisToServer(const float DeltaSeconds)
|
|
{
|
|
ResolveZeusNetworkSubsystem();
|
|
// Sistema de rede novo (canonico) tem prioridade; legacy e' fallback.
|
|
const bool bV1 = (NetClient
|
|
&& NetClient->GetConnectionState() == EZeusV1ConnectionState::Accepted);
|
|
if (!bV1 && (!ZeusNetwork || !ZeusNetwork->IsConnected()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const float SendInterval = 1.0f / FMath::Max(1, InputSendRateHz);
|
|
SendAccumulatorSec += DeltaSeconds;
|
|
TimeSinceLastSendSec += DeltaSeconds;
|
|
|
|
// Movimento p/ efeitos de rate NAO e' so' o input (W/A/S/D): o CMC do dono
|
|
// continua desacelerando (braking) por ~0.3s depois de soltar a tecla. Se
|
|
// pararmos de enviar no rate rapido assim que o input zera, o servidor fica
|
|
// congelado na ultima `simpleVelCmS` alta e o proxy remoto extrapola ~1m alem
|
|
// (NewPos = Last.Pos + Last.Vel*ExtrapSec) ate o heartbeat corrigir -> overshoot
|
|
// + snap-back ("velocidade grande que demora a zerar" + jitter). Tratar a
|
|
// velocidade residual como movimento mantem o envio a InputSendRateHz ate o
|
|
// dono realmente parar, alimentando o proxy com a curva de desaceleracao.
|
|
const bool bHasInputAxis = !FMath::IsNearlyZero(PendingMoveForward) || !FMath::IsNearlyZero(PendingMoveRight);
|
|
const FVector OwnerVelNow = GetVelocity();
|
|
constexpr float kResidualSpeedSqCmS = 25.0f * 25.0f; // abaixo de 25 cm/s tratamos como parado
|
|
const bool bHasResidualVel = OwnerVelNow.SizeSquared() > kResidualSpeedSqCmS;
|
|
const bool bMovingInput = bHasInputAxis || bHasResidualVel;
|
|
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);
|
|
// DEBUG-INPUT (2026-06-12): log ~1x/s COM o PIE. Remover apos achar o gap.
|
|
{
|
|
static float DbgInputAcc = 0.0f;
|
|
DbgInputAcc += DeltaSeconds;
|
|
if (DbgInputAcc >= 1.0f)
|
|
{
|
|
DbgInputAcc = 0.0f;
|
|
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
|
|
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
|
|
UE_LOG(LogZeusPlayer, Warning,
|
|
TEXT("[PIE%d][InputDbg] bV1=%d state=%d moving=%d rate=%d should=%d hbReached=%d fwd=%.2f right=%.2f"),
|
|
PieId, bV1 ? 1 : 0,
|
|
NetClient ? static_cast<int32>(NetClient->GetConnectionState()) : -99,
|
|
bMovingInput ? 1 : 0, bRateReached ? 1 : 0, bShouldSend ? 1 : 0,
|
|
bHeartbeatReached ? 1 : 0, PendingMoveForward, PendingMoveRight);
|
|
}
|
|
}
|
|
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();
|
|
if (bV1)
|
|
{
|
|
// Sistema novo: INPUT 6077 com a pose absoluta (ADR 0040/0041).
|
|
const bool bSent = NetClient->EmitInput(
|
|
PendingMoveForward,
|
|
PendingMoveRight,
|
|
bPendingJumpPressed,
|
|
bPendingJumpReleased,
|
|
InputSequence,
|
|
ClientTimeMs,
|
|
ViewYawDeg,
|
|
PosCm,
|
|
FVector2D(Vel.X, Vel.Y),
|
|
bIsFalling,
|
|
static_cast<float>(Vel.Z));
|
|
// DEBUG-INPUT (2026-06-12): confirma chamada + retorno do SendPacket.
|
|
static float DbgEmitAcc = 0.0f;
|
|
DbgEmitAcc += DeltaSeconds;
|
|
if (DbgEmitAcc >= 1.0f)
|
|
{
|
|
DbgEmitAcc = 0.0f;
|
|
const int32 PieId = (GetWorld() && GetWorld()->GetOutermost())
|
|
? GetWorld()->GetOutermost()->GetPIEInstanceID() : -1;
|
|
UE_LOG(LogZeusPlayer, Warning,
|
|
TEXT("[PIE%d][InputDbg] EmitInput seq=%d sent=%d pos=%s"),
|
|
PieId, InputSequence, bSent ? 1 : 0, *PosCm.ToString());
|
|
}
|
|
}
|
|
else if (ZeusNetwork)
|
|
{
|
|
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;
|
|
}
|