Espelho do AttributeSystem do servidor (Game/MMO/Modules/AttributeSystem).
Sub-modulo C++ Source/ZMMOAttributes/ (Runtime, LoadingPhase=PreDefault)
isolando o cliente do AttributeSystem em pasta propria — pavimentando o
caminho para futuros Inventory/Skills/Combat virem como sub-modulos peer.
Componentes:
- FZMMOAttributesSnapshot (USTRUCT Blueprint, espelha
FZeusAttributesPayload do plugin ZeusNetwork)
- UZMMOAttributeComponent (UActorComponent ligado ao player):
ApplySnapshot / ApplyHpSpUpdate / NotifyLevelUp + 3 delegates
dinamicos (OnAttributesChanged / OnHpSpChanged / OnLevelUp) +
helpers GetHpRatio / GetSpRatio
- UZMMOAttributeNetworkHandler (UWorldSubsystem): ponte
UZeusNetworkSubsystem -> componente via lookup por EntityId
- UZMMOHudHpSpWidget (UUserWidget base): BindWidget (HpBar, SpBar) +
BindWidgetOptional (HpText, SpText, LevelText). NativeConstruct
binda nos delegates do componente; NativeDestruct desliga.
OnSnapshotApplied / OnHpSpDelta (BlueprintNativeEvent) com impl
C++ atualizando progress bars e textos.
Integracao no PlayerCharacter:
- CreateDefaultSubobject<UZMMOAttributeComponent> no ctor
- HudHpSpWidgetClass (TSubclassOf) resolvido via
ConstructorHelpers::FClassFinder em /Game/ZMMO/UI/HUD/WBP_HUD_HpSpBar
- HandleLocalSpawnReady seed do EntityId no componente + spawn do HUD
(CreateWidget + AddToViewport + BindToAttributeComponent)
- EndPlay remove widget do parent
WBP_HUD_HpSpBar (UMG, criado via MCP):
- Overlay root + SizeBox (320x120) + VerticalBox
- 5 widgets BindWidget: LevelText, HpBar (vermelho), HpText,
SpBar (azul), SpText
- Herda de UZMMOHudHpSpWidget
Verificacao (smoke test):
- PIE login -> server envia S_ATTRIBUTE_SNAPSHOT_FULL
- Cliente recebe via OnAttributeSnapshotFull -> handler roteia pra
componente do entity -> ApplySnapshot dispara OnAttributesChanged ->
widget atualiza ProgressBars + TextBlocks
- HUD aparece no canto inferior esquerdo da tela com valores
correspondentes ao DB
Refs:
- Plano: C:/Users/mateu/.claude/plans/temos-uma-lab-input-nested-diffie.md
- Server companion commit: feat(attributes): AttributeSystem modular
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
183 lines
5.5 KiB
C++
183 lines
5.5 KiB
C++
#include "ZMMOAttributeNetworkHandler.h"
|
|
|
|
#include "Engine/GameInstance.h"
|
|
#include "Engine/World.h"
|
|
#include "EngineUtils.h" // TActorIterator
|
|
#include "GameFramework/Actor.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;
|
|
}
|
|
|
|
// V1 — resolve por EntityId varrendo atores do mundo procurando um que
|
|
// tenha `UZMMOAttributeComponent`. Funciona para PIE com 1-2 players +
|
|
// proxies. Otimizacao futura: usar `UZMMOWorldSubsystem::GetActorByEntityId`
|
|
// (precisa expor) ou cache local de `EntityId -> UAttributeComponent*`.
|
|
//
|
|
// Convencao: `IZMMOEntityInterface::GetZMMOEntityId()` retorna o EntityId
|
|
// autoritativo do servidor. Nao depender da interface aqui para manter
|
|
// ZMMOAttributes sem `#include` do modulo ZMMO core — varremos `Tags`
|
|
// alternativamente, mas o caminho mais simples e' a propria component
|
|
// keys de `EntityId` no proprio componente (TODO Fase 2: armazenar
|
|
// EntityId no componente quando snapshot chega).
|
|
UZMMOAttributeComponent* FindComponentForEntity(UWorld* World, int32 EntityId)
|
|
{
|
|
if (!World || EntityId == 0) { return nullptr; }
|
|
for (TActorIterator<AActor> It(World); It; ++It)
|
|
{
|
|
AActor* Actor = *It;
|
|
if (!Actor) { continue; }
|
|
UZMMOAttributeComponent* Comp = Actor->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);
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
// V1 fallback: aplica ao primeiro UZMMOAttributeComponent encontrado
|
|
// (player local). Isso resolve o caso do primeiro snapshot chegar
|
|
// antes do `EntityId` estar registrado no componente (que so' acontece
|
|
// apos o primeiro snapshot — chicken-and-egg). Apos o primeiro apply,
|
|
// `Current.EntityId` ja' bate e o caminho normal funciona.
|
|
for (TActorIterator<AActor> It(World); It; ++It)
|
|
{
|
|
AActor* Actor = *It;
|
|
if (!Actor) { continue; }
|
|
Comp = Actor->FindComponentByClass<UZMMOAttributeComponent>();
|
|
if (Comp && Comp->GetSnapshot().EntityId == 0)
|
|
{
|
|
break;
|
|
}
|
|
Comp = nullptr;
|
|
}
|
|
}
|
|
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);
|
|
}
|
|
}
|