- ZMMOStatusWindowWidget (UCommonActivatableWidget, modo Menu): grid 2 colunas estilo RO. Esquerda: stats primários com botões + por stat (RequestStatAlloc). Direita: derivados ATK/MATK/DEF/MDEF/HIT/FLEE/CRIT/ASPD vindos do snapshot do server — cliente NUNCA recalcula (anti-cheat foundation). - Botão + envia C_STAT_ALLOC → server valida cost RO (floor((stat-1)/10)+2), aplica + recalcula derivados + SaveCharFull async, manda S_ATTRIBUTE_SNAPSHOT_FULL + reply. UI atualiza em tempo real via OnAttributesChanged. - ZMMOAttributeComponent ganha RequestStatAlloc + NotifyStatAllocReply + delegate OnStatAllocReply para feedback de rejeição. - ZMMOAttributeNetworkHandler: HandleStatAllocReply roteia pro componente do player local (reply não traz EntityId — sempre quem fez o request). - PlayerController: hotkey Alt+A (FInputChord legacy + BindKey, sem precisar de IA asset). ToggleStatusWindow via UUIInGameFlowSubsystem. - WBP_StatusWindow: layout 2 colunas via MCP set_widget_tree (43 widgets) + BgBorder escuro semi-transparente + centralizado no overlay. - DA_InGameScreenSet: StatusWindow → WBP_StatusWindow.
209 lines
6.2 KiB
C++
209 lines
6.2 KiB
C++
#include "ZMMOAttributeNetworkHandler.h"
|
|
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/World.h"
|
|
#include "GameFramework/GameStateBase.h"
|
|
#include "GameFramework/PlayerController.h"
|
|
#include "GameFramework/PlayerState.h"
|
|
#include "ZMMOAttributeComponent.h"
|
|
#include "ZMMOAttributeTypes.h"
|
|
#include "ZeusNetworkSubsystem.h"
|
|
|
|
namespace
|
|
{
|
|
// Helper privado — converte o payload binario do plugin pra USTRUCT
|
|
// Blueprint que o componente exibe.
|
|
FZMMOAttributesSnapshot ToSnapshot(const FZeusAttributesPayload& P)
|
|
{
|
|
FZMMOAttributesSnapshot S;
|
|
S.EntityId = P.EntityId;
|
|
S.ClassId = P.ClassId;
|
|
S.BaseLevel = P.BaseLevel;
|
|
S.BaseExp = P.BaseExp;
|
|
S.JobLevel = P.JobLevel;
|
|
S.JobExp = P.JobExp;
|
|
S.Str = P.Str;
|
|
S.Agi = P.Agi;
|
|
S.Vit = P.Vit;
|
|
S.Int = P.Int;
|
|
S.Dex = P.Dex;
|
|
S.Luk = P.Luk;
|
|
S.StatusPoint = P.StatusPoint;
|
|
S.SkillPoint = P.SkillPoint;
|
|
S.Hp = P.Hp;
|
|
S.MaxHp = P.MaxHp;
|
|
S.Sp = P.Sp;
|
|
S.MaxSp = P.MaxSp;
|
|
S.Money = P.Money;
|
|
S.Atk = P.Atk;
|
|
S.Matk = P.Matk;
|
|
S.Def = P.Def;
|
|
S.Mdef = P.Mdef;
|
|
S.Hit = P.Hit;
|
|
S.Flee = P.Flee;
|
|
S.CritX10 = P.CritX10;
|
|
S.Aspd = P.Aspd;
|
|
return S;
|
|
}
|
|
|
|
// Resolve `EntityId -> UZMMOAttributeComponent` via PlayerArray do GameState.
|
|
// AttributeComponent agora vive no PlayerState (vide AZMMOPlayerState +
|
|
// Component Registry em DefaultGame.ini). Itera apenas players (1 por
|
|
// conexao), nao 1000+ atores — O(N_players).
|
|
//
|
|
// Cada UZMMOAttributeComponent carrega seu proprio EntityId (seed em
|
|
// AZMMOPlayerCharacter::HandleLocalSpawnReady ou no primeiro snapshot).
|
|
UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId)
|
|
{
|
|
if (!World || EntityId == 0) { return nullptr; }
|
|
const AGameStateBase* GS = World->GetGameState();
|
|
if (!GS) { return nullptr; }
|
|
for (APlayerState* PS : GS->PlayerArray)
|
|
{
|
|
if (!PS) { continue; }
|
|
UZMMOAttributeComponent* Comp = PS->FindComponentByClass<UZMMOAttributeComponent>();
|
|
if (!Comp) { continue; }
|
|
if (Comp->GetSnapshot().EntityId == EntityId)
|
|
{
|
|
return Comp;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
bool UZMMOAttributeNetworkHandler::ShouldCreateSubsystem(UObject* Outer) const
|
|
{
|
|
// Cria apenas para mundos de gameplay (PIE/Game), nao para editor preview.
|
|
UWorld* World = Cast<UWorld>(Outer);
|
|
if (!World) { return false; }
|
|
return World->WorldType == EWorldType::Game || World->WorldType == EWorldType::PIE;
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
|
|
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
|
|
{
|
|
SnapshotFullHandle = Net->OnAttributeSnapshotFull.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull);
|
|
HpSpUpdateHandle = Net->OnHpSpUpdate.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleHpSpUpdate);
|
|
LevelUpHandle = Net->OnLevelUp.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleLevelUp);
|
|
StatAllocReplyHandle = Net->OnStatAllocReply.AddUObject(
|
|
this, &UZMMOAttributeNetworkHandler::HandleStatAllocReply);
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::Deinitialize()
|
|
{
|
|
if (UZeusNetworkSubsystem* Net = GetZeusNetSubsystem())
|
|
{
|
|
if (SnapshotFullHandle.IsValid())
|
|
{
|
|
Net->OnAttributeSnapshotFull.Remove(SnapshotFullHandle);
|
|
SnapshotFullHandle.Reset();
|
|
}
|
|
if (HpSpUpdateHandle.IsValid())
|
|
{
|
|
Net->OnHpSpUpdate.Remove(HpSpUpdateHandle);
|
|
HpSpUpdateHandle.Reset();
|
|
}
|
|
if (LevelUpHandle.IsValid())
|
|
{
|
|
Net->OnLevelUp.Remove(LevelUpHandle);
|
|
LevelUpHandle.Reset();
|
|
}
|
|
if (StatAllocReplyHandle.IsValid())
|
|
{
|
|
Net->OnStatAllocReply.Remove(StatAllocReplyHandle);
|
|
StatAllocReplyHandle.Reset();
|
|
}
|
|
}
|
|
Super::Deinitialize();
|
|
}
|
|
|
|
UZeusNetworkSubsystem* UZMMOAttributeNetworkHandler::GetZeusNetSubsystem() const
|
|
{
|
|
const UWorld* World = GetWorld();
|
|
if (!World) { return nullptr; }
|
|
UGameInstance* GI = World->GetGameInstance();
|
|
if (!GI) { return nullptr; }
|
|
return GI->GetSubsystem<UZeusNetworkSubsystem>();
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleAttributeSnapshotFull(const FZeusAttributesPayload& Payload)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, Payload.EntityId);
|
|
if (!Comp)
|
|
{
|
|
// Fallback chicken-and-egg: primeiro snapshot chega antes do EntityId
|
|
// estar seeded no componente. Aplica ao primeiro componente do
|
|
// PlayerArray que ainda tem EntityId=0. Apos o primeiro apply, o
|
|
// caminho normal por lookup funciona.
|
|
const AGameStateBase* GS = World->GetGameState();
|
|
if (GS)
|
|
{
|
|
for (APlayerState* PS : GS->PlayerArray)
|
|
{
|
|
if (!PS) { continue; }
|
|
UZMMOAttributeComponent* Candidate = PS->FindComponentByClass<UZMMOAttributeComponent>();
|
|
if (Candidate && Candidate->GetSnapshot().EntityId == 0)
|
|
{
|
|
Comp = Candidate;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Comp)
|
|
{
|
|
Comp->ApplySnapshot(ToSnapshot(Payload));
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleHpSpUpdate(int32 EntityId, int32 Hp, int32 Sp)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
|
|
if (Comp)
|
|
{
|
|
Comp->ApplyHpSpUpdate(Hp, Sp);
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleLevelUp(int32 EntityId, int32 NewBaseLevel, int32 StatusPointDelta)
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return; }
|
|
UZMMOAttributeComponent* Comp = FindComponentForEntity(World, EntityId);
|
|
if (Comp)
|
|
{
|
|
Comp->NotifyLevelUp(NewBaseLevel, StatusPointDelta);
|
|
}
|
|
}
|
|
|
|
void UZMMOAttributeNetworkHandler::HandleStatAllocReply(bool bAccepted, int32 Reason)
|
|
{
|
|
// S_ATTRIBUTE_STAT_ALLOC_OK nao traz EntityId no payload — sempre do
|
|
// player local que fez o request. Busca o AttributeComponent via PC.
|
|
if (UZMMOAttributeComponent* Comp = FindLocalPlayerAttributeComponent())
|
|
{
|
|
Comp->NotifyStatAllocReply(bAccepted, Reason);
|
|
}
|
|
}
|
|
|
|
UZMMOAttributeComponent* UZMMOAttributeNetworkHandler::FindLocalPlayerAttributeComponent() const
|
|
{
|
|
UWorld* World = GetWorld();
|
|
if (!World) { return nullptr; }
|
|
APlayerController* PC = World->GetFirstPlayerController();
|
|
if (!PC || !PC->PlayerState) { return nullptr; }
|
|
return PC->PlayerState->FindComponentByClass<UZMMOAttributeComponent>();
|
|
}
|