feat(frontend): Fase 1 + 1.5 — ServerSelect dinamico + Lobby (chars por mundo)

ServerSelect (Fase 1):
- UIServerSelectScreen_Base: subscribe OnRawMessage + C_WORLD_LIST_REQUEST
- WBP_ServerCard instanciado dinamicamente em GridPanel CardContainer (2 cols)
- UIServerCard_Base: SetFromEntry + OnCardPressed delegate + UI_Button_Master
- Push real-time S_WORLD_STATUS_UPDATE (opcode 2062) atualiza card in-place
- FZMMOWorldEntry struct, EZMMOWorldState enum no wire (offline/online/maint)
- Sem hardcap de slots — cards usam toda largura via ColumnFill weights

Lobby/CharSelect (Fase 1.5):
- UIUserLobbyScreen_Base + WBP_UserLobby (WidgetSwitcher: lista + create)
- C_CHAR_LIST_REQUEST filtrado por SelectedWorldId do FlowSubsystem
- UICharCard_Base + WBP_CharCard com Select/Delete + Accept/Cancel quando
  delete agendado; Text_DeleteCountdown atualiza em tempo real (timer 1s)
- UICharacterCreatePage_Base + WBP_CharacterCreate (Name + ComboBox class)
- Handlers S_CHAR_SELECT_OK (handoff parse), CHAR_CREATE_OK/REJECT,
  CHAR_DELETE_ACK/ACCEPT_ACK/CANCEL_ACK (refresh lista)

Auxiliares + fixes:
- ARQUITETURA_SERVER_SELECT.md + ARQUITETURA_CHARACTER_MODEL.md
- UIFrontEndFlowSubsystem: SelectedWorldId state + transicao Lobby
- DA_FrontEndScreenSet: Lobby -> WBP_UserLobby_C
- UICheckBox_Base + UICommonText_Base (componentes auxiliares)
- BSB_Button_Transparent.Disabled DrawAs=IMAGE -> NoDraw (fix bg branco)
- WBP_ServerCard layout: HBox para chips Region/Ping/Status, SizeBox 10x10
  no Dot verde (anti-overflow); SelfHitTestInvisible em containers (hover)
This commit is contained in:
2026-05-21 20:06:23 -03:00
parent 518a346cf5
commit 166650671f
41 changed files with 4060 additions and 78 deletions

View File

@@ -28,6 +28,15 @@ struct ZMMO_API FUIStyle
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleText Text;
/**
* Categorias tipográficas nomeadas (data-driven). O autor define as
* chaves aqui no DT_UI_Styles ("Title", "Section", "Body", "Label",
* "Dim", ou customizadas); o UUICommonText_Base escolhe pelo nome
* (dropdown populado a partir destas chaves — sem enum fixo em C++).
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
TMap<FName, FUITextStyle> TextRoles;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleButton Button;
@@ -46,6 +55,12 @@ struct ZMMO_API FUIStyle
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleSpinner Spinner;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleCheckBox CheckBox;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
FUIStyleInput Input;
/**
* Fundo de tela cheia do front-end (ex.: Boot/Login). Brush data-driven:
* a tela aplica no seu Border_Bg via RefreshUIStyle — nunca hard-ref no

View File

@@ -21,14 +21,12 @@ enum class EUITheme : uint8
RPG UMETA(DisplayName = "RPG")
};
/** Variante visual do botão (mapeia 1:1 com os campos de FUIStyleButton). */
/** Posição do rótulo no UUIInput_Base. */
UENUM(BlueprintType)
enum class EUIButtonVariant : uint8
enum class EUIInputLabelLayout : uint8
{
Primary UMETA(DisplayName = "Primary"),
Secondary UMETA(DisplayName = "Secondary"),
Danger UMETA(DisplayName = "Danger"),
Ghost UMETA(DisplayName = "Ghost")
Stacked UMETA(DisplayName = "Stacked (label acima)"),
Inline UMETA(DisplayName = "Inline (label à esquerda)")
};
/** Forma/silhueta de um botão — usada pelo widget para escolher dimensões. */

View File

@@ -5,8 +5,6 @@
/**
* Opcodes do CharServer (WebSocket) consumidos pelo cliente. Espelha
* `Server/ZeusCharServer/src/protocol/CharOpcodes.ts` (faixa 2000-2099).
* Mantido mínimo: só os opcodes que o front-end usa hoje (auth da Login).
* Ampliar conforme as telas (lista/criação/seleção de personagem) chegarem.
*
* Wire: string = uint16 LE (len) + UTF-8; inteiros LE. O cabeçalho de 32B
* é responsabilidade do UZeusCharServerSubsystem (Send/OnRawMessage operam
@@ -18,13 +16,40 @@ namespace ZMMOCharOp
constexpr int32 C_CHAR_AUTH_REQUEST = 2000;
constexpr int32 S_CHAR_AUTH_OK = 2001; // string(accountId)+string(user)+uint64(expiresMs)
constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason)
// Listagem/criação/seleção de personagem
constexpr int32 C_CHAR_LIST_REQUEST = 2010; // uint8 hasWorldFilter [+ uint8[16] worldId]
constexpr int32 S_CHAR_LIST = 2011;
constexpr int32 C_CHAR_CREATE = 2020; // uint8[16] worldId + slot + ...
constexpr int32 S_CHAR_CREATE_OK = 2021;
constexpr int32 S_CHAR_CREATE_REJECT= 2022;
// Delete agendado (rathena-style two-step)
constexpr int32 C_CHAR_DELETE_REQUEST = 2030;
constexpr int32 S_CHAR_DELETE_ACK = 2031;
constexpr int32 C_CHAR_DELETE_ACCEPT = 2032;
constexpr int32 S_CHAR_DELETE_ACCEPT_ACK = 2033;
constexpr int32 C_CHAR_DELETE_CANCEL = 2034;
constexpr int32 S_CHAR_DELETE_CANCEL_ACK = 2035;
constexpr int32 C_CHAR_SELECT = 2040;
constexpr int32 S_CHAR_SELECT_OK = 2041;
constexpr int32 S_CHAR_SELECT_REJECT= 2042;
// Listagem de mundos (Fase 1 do ARQUITETURA_SERVER_SELECT)
constexpr int32 C_WORLD_LIST_REQUEST = 2060; // payload vazio
constexpr int32 S_WORLD_LIST = 2061; // uint16 count + entries
/**
* Push do CharServer com update de UM mundo (state/pop/queueLen).
* Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state
*/
constexpr int32 S_WORLD_STATUS_UPDATE = 2062;
}
/**
* Método de autenticação enviado como uint8 prefix no payload do
* C_CHAR_AUTH_REQUEST. TOKEN mantém o fluxo Stub/JWT (handoff de outro
* serviço); PASSWORD chama AccountService.login direto no CharServer.
* Espelha `CharAuthMethod` em CharOpcodes.ts.
* C_CHAR_AUTH_REQUEST.
*/
namespace ZMMOCharAuthMethod
{
@@ -32,7 +57,7 @@ namespace ZMMOCharAuthMethod
constexpr uint8 PASSWORD = 1;
}
/** Espelha `CharRejectReason` em CharOpcodes.ts (subset usado no front-end). */
/** Espelha `CharRejectReason` em CharOpcodes.ts. */
namespace ZMMOCharRejectReason
{
constexpr uint16 Unknown = 0;
@@ -43,4 +68,23 @@ namespace ZMMOCharRejectReason
constexpr uint16 AccountLocked = 5;
constexpr uint16 AccountSuspended = 6;
constexpr uint16 CredentialsAuthDisabled = 7;
constexpr uint16 WorldOffline = 8;
constexpr uint16 WorldMaintenance = 9;
constexpr uint16 WorldFull = 10;
constexpr uint16 WorldRegionMismatch = 11;
constexpr uint16 WorldNotFound = 12;
constexpr uint16 NameInUse = 20;
constexpr uint16 InvalidName = 21;
constexpr uint16 SlotOccupied = 22;
constexpr uint16 CharNotFound = 23;
constexpr uint16 DeleteScheduled = 24;
constexpr uint16 DeleteNotScheduled = 25;
}
/** Estado do mundo no wire (uint8). Espelha `WireWorldState` em CharOpcodes.ts. */
namespace ZMMOWireWorldState
{
constexpr uint8 Offline = 0;
constexpr uint8 Online = 1;
constexpr uint8 Maintenance = 2;
}

View File

@@ -16,7 +16,7 @@ namespace
{
// Runtime usa o tema ativo; design-time carrega a row "Default" de
// DT_UI_Styles para o Designer ver o fundo (mesmo padrão de UUIPanel_Base).
const FUIStyle& ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
FUIStyle ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
@@ -28,22 +28,21 @@ namespace
}
}
}
static FUIStyle DesignStyle;
static bool bLoaded = false;
if (!bLoaded)
// Design-time relê o DT a cada chamada: editar o DT_UI_Styles e
// RECOMPILAR o WBP já reflete (sem reiniciar o editor).
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIBootDesign"), false))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIBootDesign"), false))
{
DesignStyle = Row->Style;
bLoaded = true;
}
DesignStyle = Row->Style;
bHas = true;
}
}
return bLoaded ? DesignStyle : Fallback;
return bHas ? DesignStyle : Fallback;
}
}
@@ -98,7 +97,7 @@ void UUIBootScreen_Base::RefreshUIStyle_Implementation()
Super::RefreshUIStyle_Implementation();
const FUIStyle Fallback;
const FUIStyle& AS = ResolveBootStyle(this, Fallback);
const FUIStyle AS = ResolveBootStyle(this, Fallback);
if (Border_Bg)
{

View File

@@ -0,0 +1,151 @@
#include "UICharCard_Base.h"
#include "ZMMO.h"
#include "CommonTextBlock.h"
#include "Engine/World.h"
#include "TimerManager.h"
#include "UI/Widgets/UIButton_Base.h"
void UUICharCard_Base::NativeConstruct()
{
Super::NativeConstruct();
if (!bBound)
{
if (Btn_Select) Btn_Select->OnClicked().AddUObject(this, &UUICharCard_Base::HandleSelectClicked);
if (Btn_Delete) Btn_Delete->OnClicked().AddUObject(this, &UUICharCard_Base::HandleDeleteClicked);
if (Btn_AcceptDelete) Btn_AcceptDelete->OnClicked().AddUObject(this, &UUICharCard_Base::HandleAcceptDeleteClicked);
if (Btn_CancelDelete) Btn_CancelDelete->OnClicked().AddUObject(this, &UUICharCard_Base::HandleCancelDeleteClicked);
bBound = true;
}
}
void UUICharCard_Base::NativeDestruct()
{
StopCountdownTimer();
if (bBound)
{
if (Btn_Select) Btn_Select->OnClicked().RemoveAll(this);
if (Btn_Delete) Btn_Delete->OnClicked().RemoveAll(this);
if (Btn_AcceptDelete) Btn_AcceptDelete->OnClicked().RemoveAll(this);
if (Btn_CancelDelete) Btn_CancelDelete->OnClicked().RemoveAll(this);
bBound = false;
}
Super::NativeDestruct();
}
void UUICharCard_Base::SetFromSummary(const FZMMOCharSummary& InSummary)
{
Summary = InSummary;
const bool bDeleteScheduled = Summary.DeleteScheduledAtMs > 0;
if (Text_Name)
{
Text_Name->SetText(FText::FromString(Summary.Name));
}
if (Text_Level)
{
Text_Level->SetText(FText::FromString(FString::Printf(TEXT("Lv %d / %d"), Summary.BaseLevel, Summary.JobLevel)));
}
if (Text_Class)
{
Text_Class->SetText(FText::FromString(FString::Printf(TEXT("Classe %d"), Summary.ClassId)));
}
const ESlateVisibility ActionVis = bDeleteScheduled ? ESlateVisibility::Collapsed : ESlateVisibility::Visible;
const ESlateVisibility ConfirmVis = bDeleteScheduled ? ESlateVisibility::Visible : ESlateVisibility::Collapsed;
if (Btn_Select) Btn_Select->SetVisibility(ActionVis);
if (Btn_Delete) Btn_Delete->SetVisibility(ActionVis);
if (Btn_AcceptDelete) Btn_AcceptDelete->SetVisibility(ConfirmVis);
if (Btn_CancelDelete) Btn_CancelDelete->SetVisibility(ConfirmVis);
if (Text_DeleteCountdown)
{
Text_DeleteCountdown->SetVisibility(bDeleteScheduled
? ESlateVisibility::HitTestInvisible
: ESlateVisibility::Collapsed);
}
if (bDeleteScheduled)
{
TickCountdown();
StartCountdownTimer();
}
else
{
StopCountdownTimer();
}
OnSummaryApplied(bDeleteScheduled);
}
void UUICharCard_Base::TickCountdown()
{
if (!Text_DeleteCountdown || Summary.DeleteScheduledAtMs <= 0) return;
// FDateTime::UtcNow().ToUnixTimestamp() retorna segundos UNIX UTC.
// Multiplicamos por 1000 pra equiparar com Summary.DeleteScheduledAtMs (ms).
const int64 NowMs = FDateTime::UtcNow().ToUnixTimestamp() * 1000LL;
const int64 RemainingMs = Summary.DeleteScheduledAtMs - NowMs;
if (RemainingMs <= 0)
{
Text_DeleteCountdown->SetText(FText::FromString(TEXT("Pronto para deletar")));
// Pode habilitar visualmente o Btn_AcceptDelete aqui se quiser feedback
// (ja esta enabled por default; futuramente pode mudar variant cor).
StopCountdownTimer();
return;
}
const int64 TotalSec = RemainingMs / 1000;
if (TotalSec >= 60)
{
const int64 Mins = TotalSec / 60;
const int64 Secs = TotalSec % 60;
Text_DeleteCountdown->SetText(FText::FromString(FString::Printf(
TEXT("Delete em %lldm %02llds"), static_cast<long long>(Mins), static_cast<long long>(Secs))));
}
else
{
Text_DeleteCountdown->SetText(FText::FromString(FString::Printf(
TEXT("Delete em %llds"), static_cast<long long>(TotalSec))));
}
}
void UUICharCard_Base::StartCountdownTimer()
{
UWorld* W = GetWorld();
if (!W) return;
if (CountdownTimerHandle.IsValid()) return;
W->GetTimerManager().SetTimer(CountdownTimerHandle, this,
&UUICharCard_Base::TickCountdown, 1.0f, true /* loop */, 1.0f /* first delay */);
}
void UUICharCard_Base::StopCountdownTimer()
{
if (!CountdownTimerHandle.IsValid()) return;
if (UWorld* W = GetWorld())
{
W->GetTimerManager().ClearTimer(CountdownTimerHandle);
}
CountdownTimerHandle.Invalidate();
}
void UUICharCard_Base::HandleSelectClicked()
{
OnCardSelected.Broadcast(Summary.CharId);
}
void UUICharCard_Base::HandleDeleteClicked()
{
OnCardDeleteRequest.Broadcast(Summary.CharId);
}
void UUICharCard_Base::HandleAcceptDeleteClicked()
{
OnCardDeleteAccept.Broadcast(Summary.CharId);
}
void UUICharCard_Base::HandleCancelDeleteClicked()
{
OnCardDeleteCancel.Broadcast(Summary.CharId);
}

View File

@@ -0,0 +1,91 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZMMOCharSummary.h"
#include "UICharCard_Base.generated.h"
class UCommonTextBlock;
class UBorder;
class UUIButton_Base;
class UWidget;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnCharCardSelected, FString, CharId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnCharCardDeleteRequest, FString, CharId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnCharCardDeleteAccept, FString, CharId);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FZMMOOnCharCardDeleteCancel, FString, CharId);
/**
* Card de personagem (instanciado dinamicamente pelo Lobby/CharSelect).
*
* Quando `Summary.DeleteScheduledAtMs > 0`, o card alterna os botoes:
* - Btn_Select → escondido
* - Btn_Delete → escondido
* - Btn_AcceptDelete → visivel (efetiva o delete depois da janela)
* - Btn_CancelDelete → visivel (cancela o agendamento)
*
* Visibilidade so e alterada se os widgets opcionais estiverem bindados —
* o WBP pode optar por mostrar/esconder via BP override.
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUICharCard_Base : public UUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|CharCard")
void SetFromSummary(const FZMMOCharSummary& InSummary);
UPROPERTY(BlueprintReadOnly, Category = "Zeus|CharCard")
FZMMOCharSummary Summary;
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCard")
FZMMOOnCharCardSelected OnCardSelected;
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCard")
FZMMOOnCharCardDeleteRequest OnCardDeleteRequest;
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCard")
FZMMOOnCharCardDeleteAccept OnCardDeleteAccept;
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCard")
FZMMOOnCharCardDeleteCancel OnCardDeleteCancel;
/**
* Hook BP — chamado apos SetFromSummary aplicar valores. Util pra
* ajustar visual quando delete esta agendado (badges, cores, etc).
*/
UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|CharCard")
void OnSummaryApplied(bool bIsDeleteScheduled);
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
void HandleSelectClicked();
void HandleDeleteClicked();
void HandleAcceptDeleteClicked();
void HandleCancelDeleteClicked();
/** Re-renderiza o texto do countdown (chamado pelo timer 1s). */
UFUNCTION()
void TickCountdown();
void StartCountdownTimer();
void StopCountdownTimer();
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UBorder> Card_Bg;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Name;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Level;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Class;
UPROPERTY(meta = (BindWidget)) TObjectPtr<UUIButton_Base> Btn_Select;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Delete;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_AcceptDelete;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_CancelDelete;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_DeleteCountdown;
private:
bool bBound = false;
FTimerHandle CountdownTimerHandle;
};

View File

@@ -0,0 +1,133 @@
#include "UICharacterCreatePage_Base.h"
#include "ZMMO.h"
#include "CharServerOpcodes.h"
#include "UIFrontEndFlowSubsystem.h"
#include "ZeusCharServerSubsystem.h"
#include "Components/EditableTextBox.h"
#include "Components/ComboBoxString.h"
#include "CommonTextBlock.h"
#include "Engine/GameInstance.h"
#include "UI/Widgets/UIButton_Base.h"
namespace
{
bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
{
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
if (Hex.Len() != 32) return false;
for (int32 i = 0; i < 16; ++i)
{
TCHAR hi = Hex[i * 2];
TCHAR lo = Hex[i * 2 + 1];
auto HexVal = [](TCHAR c) -> int32 {
if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0');
if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10;
if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10;
return -1;
};
int32 H = HexVal(hi);
int32 L = HexVal(lo);
if (H < 0 || L < 0) return false;
Buf.Add(static_cast<uint8>((H << 4) | L));
}
return true;
}
void WriteUInt8(TArray<uint8>& Buf, uint8 v) { Buf.Add(v); }
void WriteUInt16(TArray<uint8>& Buf, uint16 v)
{
Buf.Add(static_cast<uint8>(v & 0xFF));
Buf.Add(static_cast<uint8>((v >> 8) & 0xFF));
}
void WriteUInt32(TArray<uint8>& Buf, uint32 v)
{
for (int32 i = 0; i < 4; ++i) Buf.Add(static_cast<uint8>((v >> (8 * i)) & 0xFF));
}
void WriteStringUtf8(TArray<uint8>& Buf, const FString& S)
{
FTCHARToUTF8 Conv(*S);
const int32 Len = Conv.Length();
WriteUInt16(Buf, static_cast<uint16>(Len));
Buf.Append(reinterpret_cast<const uint8*>(Conv.Get()), Len);
}
}
void UUICharacterCreatePage_Base::NativeConstruct()
{
Super::NativeConstruct();
if (!bBound)
{
if (Btn_Confirm) Btn_Confirm->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleSubmitClicked);
if (Btn_Cancel) Btn_Cancel->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleCancelClicked);
bBound = true;
}
if (Text_Error) Text_Error->SetText(FText::GetEmpty());
}
void UUICharacterCreatePage_Base::NativeDestruct()
{
if (bBound)
{
if (Btn_Confirm) Btn_Confirm->OnClicked().RemoveAll(this);
if (Btn_Cancel) Btn_Cancel->OnClicked().RemoveAll(this);
bBound = false;
}
Super::NativeDestruct();
}
void UUICharacterCreatePage_Base::SubmitCreate()
{
const FString Name = Input_Name ? Input_Name->GetText().ToString() : FString();
if (Name.IsEmpty())
{
if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Informe um nome.")));
return;
}
UGameInstance* GI = GetGameInstance();
if (!GI) return;
UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>();
UZeusCharServerSubsystem* Char = GI->GetSubsystem<UZeusCharServerSubsystem>();
if (!Flow || !Char) return;
const FString WorldId = Flow->GetSelectedWorldId();
if (WorldId.IsEmpty())
{
if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Mundo nao selecionado.")));
return;
}
int32 ClassId = 0;
if (Combo_Class)
{
const FString Sel = Combo_Class->GetSelectedOption();
LexFromString(ClassId, *Sel);
}
// Slot: simples, sempre 0 por enquanto (servidor valida unicidade na conta).
const uint8 SlotIdx = 0;
TArray<uint8> Payload;
if (!WriteUuid16(Payload, WorldId))
{
if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("WorldId invalido.")));
return;
}
WriteUInt8(Payload, SlotIdx);
WriteStringUtf8(Payload, Name);
WriteUInt16(Payload, static_cast<uint16>(ClassId));
WriteUInt32(Payload, 0); // hair
WriteUInt32(Payload, 0); // hairColor
WriteUInt32(Payload, 0); // skinColor
WriteUInt8(Payload, 0); // bodyType
Char->SendCharRequest(ZMMOCharOp::C_CHAR_CREATE, Payload);
if (Text_Error) Text_Error->SetText(FText::GetEmpty());
OnRequestSent.Broadcast();
}
void UUICharacterCreatePage_Base::CancelCreate()
{
OnCancelled.Broadcast();
}

View File

@@ -0,0 +1,58 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UICharacterCreatePage_Base.generated.h"
class UEditableTextBox;
class UComboBoxString;
class UCommonTextBlock;
class UUIButton_Base;
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnCharCreateRequestSent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FZMMOOnCharCreateCancelled);
/**
* Page de criacao de personagem dentro do Lobby. Form minimo: nome + classId
* (combo) + appearance default. Quando confirma, monta o payload do
* C_CHAR_CREATE e envia via UZeusCharServerSubsystem. O Lobby host escuta
* S_CHAR_CREATE_OK/REJECT.
*
* Wire C_CHAR_CREATE (espelha char-create.service.ts):
* uint8[16] worldId + uint8 slot + string name + uint16 classId
* + uint32 hair + uint32 hairColor + uint32 skinColor + uint8 bodyType
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUICharacterCreatePage_Base : public UUserWidget
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|CharCreate")
void SubmitCreate();
UFUNCTION(BlueprintCallable, Category = "Zeus|CharCreate")
void CancelCreate();
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCreate")
FZMMOOnCharCreateRequestSent OnRequestSent;
UPROPERTY(BlueprintAssignable, Category = "Zeus|CharCreate")
FZMMOOnCharCreateCancelled OnCancelled;
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
void HandleSubmitClicked() { SubmitCreate(); }
void HandleCancelClicked() { CancelCreate(); }
UPROPERTY(meta = (BindWidget)) TObjectPtr<UEditableTextBox> Input_Name;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UComboBoxString> Combo_Class;
UPROPERTY(meta = (BindWidget)) TObjectPtr<UUIButton_Base> Btn_Confirm;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Cancel;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Error;
private:
bool bBound = false;
};

View File

@@ -154,6 +154,10 @@ bool UUIFrontEndFlowSubsystem::RequestBack()
{
switch (CurrentState)
{
case EZMMOFrontEndState::Login:
// Volta à Boot (CharServer segue conectado; só re-mostra a tela).
SetState(EZMMOFrontEndState::Boot);
return true;
case EZMMOFrontEndState::ServerSelect:
SetState(EZMMOFrontEndState::Login);
return true;
@@ -330,6 +334,29 @@ void UUIFrontEndFlowSubsystem::RequestEnterLogin()
}
}
void UUIFrontEndFlowSubsystem::RequestEnterServerSelect()
{
if (CurrentState == EZMMOFrontEndState::Login)
{
SetState(EZMMOFrontEndState::ServerSelect);
}
}
void UUIFrontEndFlowSubsystem::SetSelectedWorldId(const FString& WorldId)
{
SelectedWorldId = WorldId;
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: mundo selecionado = %s"), *SelectedWorldId);
}
void UUIFrontEndFlowSubsystem::ClearSelectedWorld()
{
if (!SelectedWorldId.IsEmpty())
{
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando mundo selecionado (%s)"), *SelectedWorldId);
SelectedWorldId.Reset();
}
}
void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath)
{
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: travel pedido pelo servidor → %s (%s)."), *MapName, *MapPath);

View File

@@ -62,6 +62,24 @@ public:
UFUNCTION(BlueprintCallable, Category = "FrontEnd")
void RequestEnterLogin();
/** Chamado pela tela Login após autenticação OK no CharServer. */
UFUNCTION(BlueprintCallable, Category = "FrontEnd")
void RequestEnterServerSelect();
/**
* Memoriza o mundo escolhido na ServerSelect (UUID v4 string). Lido pelo
* CharSelect/CharCreate na hora de filtrar/criar personagens. Limpo no
* logout/back para ServerSelect.
*/
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds")
void SetSelectedWorldId(const FString& WorldId);
UFUNCTION(BlueprintPure, Category = "FrontEnd|Worlds")
FString GetSelectedWorldId() const { return SelectedWorldId; }
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds")
void ClearSelectedWorld();
UPROPERTY(BlueprintAssignable, Category = "FrontEnd")
FOnZMMOFrontEndStateChanged OnStateChanged;
@@ -106,6 +124,9 @@ private:
UPROPERTY(Transient)
EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None;
UPROPERTY(Transient)
FString SelectedWorldId;
UPROPERTY(Transient)
TObjectPtr<UUIFrontEndScreenSet> ScreenSet;

View File

@@ -0,0 +1,92 @@
#include "UIServerCard_Base.h"
#include "ZMMO.h"
#include "CommonTextBlock.h"
#include "Components/Border.h"
#include "Components/ProgressBar.h"
#include "UI/Widgets/UIButton_Base.h"
void UUIServerCard_Base::NativePreConstruct()
{
Super::NativePreConstruct();
// Reaplica tema do botao antes de qualquer SetIsEnabled — garante que o
// background da variante "Primary" pinta tanto em Normal quanto Disabled.
if (Btn_Enter)
{
Btn_Enter->RefreshUIStyle();
}
}
void UUIServerCard_Base::NativeConstruct()
{
Super::NativeConstruct();
if (Btn_Enter && !bBound)
{
Btn_Enter->OnClicked().AddUObject(this, &UUIServerCard_Base::HandleEnterClicked);
bBound = true;
}
if (Btn_Enter)
{
Btn_Enter->RefreshUIStyle();
}
}
void UUIServerCard_Base::NativeDestruct()
{
if (Btn_Enter && bBound)
{
Btn_Enter->OnClicked().RemoveAll(this);
bBound = false;
}
Super::NativeDestruct();
}
void UUIServerCard_Base::SetFromEntry(const FZMMOWorldEntry& InEntry)
{
Entry = InEntry;
if (Text_Name)
{
Text_Name->SetText(FText::FromString(Entry.WorldName));
}
if (Text_Pop)
{
Text_Pop->SetText(FText::FromString(FString::Printf(TEXT("%d/%d"), Entry.Population, Entry.Capacity)));
}
if (Text_Status)
{
Text_Status->SetText(FormatStatus(Entry.State));
}
if (Bar_Pop)
{
const float Ratio = (Entry.Capacity > 0)
? FMath::Clamp(static_cast<float>(Entry.Population) / static_cast<float>(Entry.Capacity), 0.f, 1.f)
: 0.f;
Bar_Pop->SetPercent(Ratio);
}
// Botao "Selecionar" sempre habilitado — mesmo com world offline,
// o usuario pode entrar no Lobby pra ver a lista de personagens, criar/
// deletar. O gate de "entrar no mundo" fica no proprio Lobby (botao
// "Entrar no Servidor"), que checa World.State antes do handoff.
if (Btn_Enter)
{
Btn_Enter->RefreshUIStyle();
}
}
void UUIServerCard_Base::HandleEnterClicked()
{
OnCardPressed.Broadcast(Entry.WorldId, Entry.State);
}
FText UUIServerCard_Base::FormatStatus_Implementation(uint8 State) const
{
switch (State)
{
case 1: return NSLOCTEXT("ZMMO.ServerSelect", "StatusOnline", "Online");
case 2: return NSLOCTEXT("ZMMO.ServerSelect", "StatusMaintenance", "Manutencao");
case 0:
default: return NSLOCTEXT("ZMMO.ServerSelect", "StatusOffline", "Offline");
}
}

View File

@@ -0,0 +1,74 @@
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "ZMMOWorldEntry.h"
#include "UIServerCard_Base.generated.h"
class UCommonTextBlock;
class UBorder;
class UUIButton_Base;
class UProgressBar;
/**
* Card de servidor (mundo) instanciado dinamicamente pela ServerSelect.
*
* Recebe um `FZMMOWorldEntry` e renderiza nome/pop/status/bar; ao clicar
* em "Entrar" dispara `OnCardPressed.Broadcast(WorldId, State)`. A tela
* dona (UUIServerSelectScreen_Base) escuta esse delegate.
*
* WBP filho (`WBP_ServerCard`) precisa expor (via nomes de variavel):
* - Card_Bg : Border (opcional - background com estado)
* - Text_Name : CommonTextBlock
* - Text_Pop : CommonTextBlock
* - Text_Status : CommonTextBlock
* - Btn_Enter : Button
* - Bar_Pop : ProgressBar (opcional)
*/
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FZMMOOnServerCardPressed, FString, WorldId, uint8, State);
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIServerCard_Base : public UUserWidget
{
GENERATED_BODY()
public:
/**
* Aplica os dados de um mundo no card.
*/
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerCard")
void SetFromEntry(const FZMMOWorldEntry& Entry);
/** Dados atuais (copia). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerCard")
FZMMOWorldEntry Entry;
/**
* Disparado quando o botao "Entrar" do card e clicado.
* State e o wire state (0=Offline, 1=Online, 2=Maintenance).
*/
UPROPERTY(BlueprintAssignable, Category = "Zeus|ServerCard")
FZMMOOnServerCardPressed OnCardPressed;
protected:
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
void HandleEnterClicked();
/** Texto i18n correspondente ao state. Subclasse pode override em BP. */
UFUNCTION(BlueprintNativeEvent, Category = "Zeus|ServerCard")
FText FormatStatus(uint8 State) const;
virtual FText FormatStatus_Implementation(uint8 State) const;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UBorder> Card_Bg;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Name;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Pop;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Status;
UPROPERTY(meta = (BindWidget)) TObjectPtr<UUIButton_Base> Btn_Enter;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UProgressBar> Bar_Pop;
private:
bool bBound = false;
};

View File

@@ -0,0 +1,360 @@
#include "UIServerSelectScreen_Base.h"
#include "ZMMO.h"
#include "ZMMOThemeSubsystem.h"
#include "CharServerOpcodes.h"
#include "UIFrontEndFlowSubsystem.h"
#include "UIServerCard_Base.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonTextBlock.h"
#include "Components/Border.h"
#include "Components/GridPanel.h"
#include "Components/GridSlot.h"
#include "Components/PanelWidget.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "UI/UIStyleRow.h"
namespace
{
FUIStyle ResolveServerSelectStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIServerSelectDesign"), false))
{
DesignStyle = Row->Style;
bHas = true;
}
}
return bHas ? DesignStyle : Fallback;
}
// ---- Wire reader (LE) ----
bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
{
if (Pos + 1 > Buf.Num()) return false;
Out = Buf[Pos];
Pos += 1;
return true;
}
bool ReadU16(const TArray<uint8>& Buf, int32& Pos, uint16& Out)
{
if (Pos + 2 > Buf.Num()) return false;
Out = static_cast<uint16>(Buf[Pos]) | (static_cast<uint16>(Buf[Pos + 1]) << 8);
Pos += 2;
return true;
}
bool ReadStringUtf8(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
uint16 Len = 0;
if (!ReadU16(Buf, Pos, Len)) return false;
if (Pos + Len > Buf.Num()) return false;
if (Len == 0)
{
Out.Reset();
}
else
{
Out = FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buf.GetData() + Pos), Len));
}
Pos += Len;
return true;
}
}
void UUIServerSelectScreen_Base::NativePreConstruct()
{
Super::NativePreConstruct();
RefreshUIStyle();
}
void UUIServerSelectScreen_Base::NativeOnActivated()
{
Super::NativeOnActivated();
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: ativada."));
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
if (!bSubscribed)
{
Char->OnRawMessage.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCharRawMessage);
bSubscribed = true;
}
}
RequestWorldList();
}
void UUIServerSelectScreen_Base::NativeOnDeactivated()
{
if (bSubscribed)
{
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->OnRawMessage.RemoveDynamic(this, &UUIServerSelectScreen_Base::HandleCharRawMessage);
}
bSubscribed = false;
}
Super::NativeOnDeactivated();
}
void UUIServerSelectScreen_Base::RefreshUIStyle_Implementation()
{
Super::RefreshUIStyle_Implementation();
const FUIStyle Fallback;
const FUIStyle AS = ResolveServerSelectStyle(this, Fallback);
if (Border_Bg)
{
if (AS.ScreenBackground.GetResourceObject() != nullptr)
{
Border_Bg->SetBrush(AS.ScreenBackground);
}
else
{
Border_Bg->SetBrushColor(AS.Palette.Bg0);
}
}
if (Text_Title)
{
Text_Title->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurface));
}
}
void UUIServerSelectScreen_Base::RequestWorldList()
{
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected())
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CharServer indisponivel — pulando RequestWorldList"));
return;
}
TArray<uint8> EmptyPayload;
Char->SendCharRequest(ZMMOCharOp::C_WORLD_LIST_REQUEST, EmptyPayload);
}
void UUIServerSelectScreen_Base::SelectWorldAndProceed(const FString& WorldId)
{
if (WorldId.IsEmpty())
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: SelectWorldAndProceed com WorldId vazio."));
return;
}
UGameInstance* GI = GetGameInstance();
if (!GI)
{
return;
}
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->SetSelectedWorldId(WorldId);
Flow->SetState(EZMMOFrontEndState::Lobby);
}
}
void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload)
{
if (Opcode == ZMMOCharOp::S_WORLD_LIST)
{
ParseWorldList(Payload);
}
else if (Opcode == ZMMOCharOp::S_WORLD_STATUS_UPDATE)
{
ApplyStatusUpdate(Payload);
}
}
void UUIServerSelectScreen_Base::ApplyStatusUpdate(const TArray<uint8>& Payload)
{
// Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state
int32 Pos = 0;
FString WorldId;
uint16 Pop = 0;
uint16 QueueLen = 0;
uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, WorldId)
|| !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen)
|| !ReadU8(Payload, Pos, State))
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_STATUS_UPDATE malformado"));
return;
}
int32 Idx = INDEX_NONE;
for (int32 i = 0; i < Worlds.Num(); ++i)
{
if (Worlds[i].WorldId == WorldId)
{
Idx = i;
break;
}
}
if (Idx == INDEX_NONE)
{
// Mundo desconhecido (ainda nao recebemos via S_WORLD_LIST). Pede lista
// completa pra trazer dados estaticos (name/host/region/capacity).
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: STATUS_UPDATE de mundo desconhecido (%s) — pedindo lista"), *WorldId);
RequestWorldList();
return;
}
FZMMOWorldEntry& Entry = Worlds[Idx];
Entry.Population = static_cast<int32>(Pop);
Entry.QueueLen = static_cast<int32>(QueueLen);
Entry.State = State;
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: world %s atualizado (state=%d pop=%d)"),
*WorldId, State, Entry.Population);
// Atualiza apenas o card afetado (preserva animacoes/scroll dos demais).
if (SpawnedCards.IsValidIndex(Idx) && SpawnedCards[Idx])
{
SpawnedCards[Idx]->SetFromEntry(Entry);
}
OnWorldListReceived();
}
void UUIServerSelectScreen_Base::ParseWorldList(const TArray<uint8>& Payload)
{
int32 Pos = 0;
uint16 Count = 0;
if (!ReadU16(Payload, Pos, Count))
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_LIST malformado (sem count)"));
return;
}
TArray<FZMMOWorldEntry> NewWorlds;
NewWorlds.Reserve(Count);
for (uint16 i = 0; i < Count; ++i)
{
FZMMOWorldEntry Entry;
FString RegionStr;
uint16 Port = 0;
uint16 Cap = 0;
uint16 Pop = 0;
uint16 QueueLen = 0;
uint8 State = 0;
if (!ReadStringUtf8(Payload, Pos, Entry.WorldId)
|| !ReadStringUtf8(Payload, Pos, Entry.WorldName)
|| !ReadStringUtf8(Payload, Pos, RegionStr)
|| !ReadStringUtf8(Payload, Pos, Entry.Host)
|| !ReadU16(Payload, Pos, Port)
|| !ReadU16(Payload, Pos, Cap)
|| !ReadU16(Payload, Pos, Pop)
|| !ReadU16(Payload, Pos, QueueLen)
|| !ReadU8(Payload, Pos, State))
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: S_WORLD_LIST malformado (entry %d)"), i);
return;
}
Entry.Region = RegionStr;
Entry.Port = static_cast<int32>(Port);
Entry.Capacity = static_cast<int32>(Cap);
Entry.Population = static_cast<int32>(Pop);
Entry.QueueLen = static_cast<int32>(QueueLen);
Entry.State = State;
NewWorlds.Add(MoveTemp(Entry));
}
Worlds = MoveTemp(NewWorlds);
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: recebidos %d mundos"), Worlds.Num());
RebuildCards();
OnWorldListReceived();
}
void UUIServerSelectScreen_Base::RebuildCards()
{
if (!CardContainer)
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CardContainer nao bindado — cards nao serao exibidos"));
return;
}
if (!CardClass)
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CardClass nao configurada — defina no WBP_ServerSelect Defaults"));
return;
}
// Limpa cards antigos (delegate + remove do container).
for (UUIServerCard_Base* Old : SpawnedCards)
{
if (Old)
{
Old->OnCardPressed.RemoveDynamic(this, &UUIServerSelectScreen_Base::HandleCardPressed);
}
}
SpawnedCards.Reset();
CardContainer->ClearChildren();
UGridPanel* AsGrid = Cast<UGridPanel>(CardContainer);
const int32 GridColumns = 2; // bate com o mock (ColumnFill[0]/[1])
int32 Index = 0;
for (const FZMMOWorldEntry& W : Worlds)
{
UUIServerCard_Base* Card = CreateWidget<UUIServerCard_Base>(this, CardClass);
if (!Card)
{
UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: falha ao criar card para %s"), *W.WorldName);
continue;
}
Card->SetFromEntry(W);
Card->OnCardPressed.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCardPressed);
CardContainer->AddChild(Card);
// Quando o container e um GridPanel, distribui em grid (row, col) — bate
// com o layout do mock (2 colunas, N linhas).
if (AsGrid)
{
if (UGridSlot* GS = Cast<UGridSlot>(Card->Slot))
{
GS->SetRow(Index / GridColumns);
GS->SetColumn(Index % GridColumns);
GS->SetHorizontalAlignment(HAlign_Fill);
GS->SetVerticalAlignment(VAlign_Fill);
GS->SetPadding(FMargin(12.f, 12.f, 12.f, 12.f));
}
}
SpawnedCards.Add(Card);
++Index;
}
}
void UUIServerSelectScreen_Base::HandleCardPressed(FString WorldId, uint8 State)
{
// O ServerSelect permite escolher qualquer mundo (online/maintenance/
// offline) — o usuario entra no Lobby pra gerenciar chars desse mundo.
// O bloqueio real (handoff -> world) acontece no botao "Entrar no
// Servidor" do Lobby, que checa World.State antes do C_CHAR_SELECT.
UE_LOG(LogZMMO, Log, TEXT("ServerSelect: mundo %s selecionado (state=%d)"), *WorldId, State);
SelectWorldAndProceed(WorldId);
}
UZeusCharServerSubsystem* UUIServerSelectScreen_Base::GetCharServer() const
{
if (const UGameInstance* GI = GetGameInstance())
{
return GI->GetSubsystem<UZeusCharServerSubsystem>();
}
return nullptr;
}

View File

@@ -0,0 +1,82 @@
#pragma once
#include "CoreMinimal.h"
#include "UIActivatableScreen_Base.h"
#include "ZMMOWorldEntry.h"
#include "UIServerSelectScreen_Base.generated.h"
class UCommonTextBlock;
class UBorder;
class UPanelWidget;
class UUIServerCard_Base;
class UZeusCharServerSubsystem;
/**
* Tela de Server Select — Fase 1 do ARQUITETURA_SERVER_SELECT.
*
* Fluxo:
* 1. NativeOnActivated subscreve OnRawMessage do UZeusCharServerSubsystem
* 2. Envia C_WORLD_LIST_REQUEST (payload vazio)
* 3. ParseWorldList monta `Worlds`
* 4. RebuildCards apaga filhos de `CardContainer` e instancia 1 WBP_ServerCard
* por mundo, bindando `OnCardPressed -> SelectWorldAndProceed`
* 5. Dispara `OnWorldListReceived` (BIE) pra extensoes em BP
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIServerSelectScreen_Base : public UUIActivatableScreen_Base
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect")
void RequestWorldList();
UFUNCTION(BlueprintCallable, Category = "Zeus|ServerSelect")
void SelectWorldAndProceed(const FString& WorldId);
UPROPERTY(BlueprintReadOnly, Category = "Zeus|ServerSelect")
TArray<FZMMOWorldEntry> Worlds;
UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|ServerSelect")
void OnWorldListReceived();
protected:
virtual void NativePreConstruct() override;
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
virtual void RefreshUIStyle_Implementation() override;
UFUNCTION()
void HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload);
UFUNCTION()
void HandleCardPressed(FString WorldId, uint8 State);
UZeusCharServerSubsystem* GetCharServer() const;
void ParseWorldList(const TArray<uint8>& Payload);
void ApplyStatusUpdate(const TArray<uint8>& Payload);
void RebuildCards();
/** Background e titulo (mantidos para RefreshUIStyle). */
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UBorder> Border_Bg;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Title;
/**
* Container onde os cards sao instanciados (ScrollBox/VerticalBox/
* UniformGridPanel/WrapBox — qualquer UPanelWidget serve).
*/
UPROPERTY(meta = (BindWidget)) TObjectPtr<UPanelWidget> CardContainer;
/**
* Classe do card a instanciar. Configurada no WBP_ServerSelect
* (Defaults) ou via Project Settings se for global.
*/
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Zeus|ServerSelect")
TSubclassOf<UUIServerCard_Base> CardClass;
private:
bool bSubscribed = false;
UPROPERTY(Transient)
TArray<TObjectPtr<UUIServerCard_Base>> SpawnedCards;
};

View File

@@ -0,0 +1,468 @@
#include "UIUserLobbyScreen_Base.h"
#include "ZMMO.h"
#include "CharServerOpcodes.h"
#include "UIFrontEndFlowSubsystem.h"
#include "UICharCard_Base.h"
#include "UICharacterCreatePage_Base.h"
#include "ZeusCharServerSubsystem.h"
#include "CommonTextBlock.h"
#include "Components/PanelWidget.h"
#include "Components/WidgetSwitcher.h"
#include "Engine/GameInstance.h"
#include "UI/Widgets/UIButton_Base.h"
namespace
{
bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
{
if (Pos + 1 > Buf.Num()) return false;
Out = Buf[Pos]; Pos += 1; return true;
}
bool ReadU16(const TArray<uint8>& Buf, int32& Pos, uint16& Out)
{
if (Pos + 2 > Buf.Num()) return false;
Out = static_cast<uint16>(Buf[Pos]) | (static_cast<uint16>(Buf[Pos + 1]) << 8);
Pos += 2; return true;
}
bool ReadU32(const TArray<uint8>& Buf, int32& Pos, uint32& Out)
{
if (Pos + 4 > Buf.Num()) return false;
Out = static_cast<uint32>(Buf[Pos])
| (static_cast<uint32>(Buf[Pos + 1]) << 8)
| (static_cast<uint32>(Buf[Pos + 2]) << 16)
| (static_cast<uint32>(Buf[Pos + 3]) << 24);
Pos += 4; return true;
}
bool ReadU64(const TArray<uint8>& Buf, int32& Pos, uint64& Out)
{
if (Pos + 8 > Buf.Num()) return false;
Out = 0;
for (int i = 0; i < 8; ++i) Out |= (static_cast<uint64>(Buf[Pos + i]) << (8 * i));
Pos += 8; return true;
}
bool ReadFloat(const TArray<uint8>& Buf, int32& Pos, float& Out)
{
uint32 raw;
if (!ReadU32(Buf, Pos, raw)) return false;
FMemory::Memcpy(&Out, &raw, sizeof(float));
return true;
}
bool ReadStringUtf8(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
uint16 Len = 0;
if (!ReadU16(Buf, Pos, Len)) return false;
if (Pos + Len > Buf.Num()) return false;
if (Len == 0) { Out.Reset(); return true; }
Out = FString(FUTF8ToTCHAR(reinterpret_cast<const ANSICHAR*>(Buf.GetData() + Pos), Len));
Pos += Len;
return true;
}
bool ReadUuid16(const TArray<uint8>& Buf, int32& Pos, FString& Out)
{
if (Pos + 16 > Buf.Num()) return false;
// 16 bytes raw -> 8-4-4-4-12
auto Hex = [](uint8 b, ANSICHAR* dst)
{
static const ANSICHAR* H = "0123456789abcdef";
dst[0] = H[(b >> 4) & 0xF];
dst[1] = H[b & 0xF];
};
ANSICHAR raw[37]; raw[36] = 0;
int32 j = 0;
for (int32 i = 0; i < 16; ++i)
{
if (i == 4 || i == 6 || i == 8 || i == 10) raw[j++] = '-';
Hex(Buf[Pos + i], &raw[j]); j += 2;
}
Out = ANSI_TO_TCHAR(raw);
Pos += 16;
return true;
}
bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
{
// Espera xxxxxxxx-xxxx-...
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
if (Hex.Len() != 32) return false;
for (int32 i = 0; i < 16; ++i)
{
TCHAR hi = Hex[i * 2];
TCHAR lo = Hex[i * 2 + 1];
auto HexVal = [](TCHAR c) -> int32 {
if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0');
if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10;
if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10;
return -1;
};
int32 H = HexVal(hi);
int32 L = HexVal(lo);
if (H < 0 || L < 0) return false;
Buf.Add(static_cast<uint8>((H << 4) | L));
}
return true;
}
}
void UUIUserLobbyScreen_Base::NativeOnActivated()
{
Super::NativeOnActivated();
UE_LOG(LogZMMO, Log, TEXT("Lobby: ativada."));
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
if (!bSubscribed)
{
Char->OnRawMessage.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage);
bSubscribed = true;
}
}
if (!bButtonsBound)
{
if (Btn_Back) Btn_Back->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::BackToServerSelect);
if (Btn_Create) Btn_Create->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::ShowCharCreatePage);
bButtonsBound = true;
}
ShowCharSelectPage();
RequestCharList();
}
void UUIUserLobbyScreen_Base::NativeOnDeactivated()
{
if (bSubscribed)
{
if (UZeusCharServerSubsystem* Char = GetCharServer())
{
Char->OnRawMessage.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage);
}
bSubscribed = false;
}
Super::NativeOnDeactivated();
}
void UUIUserLobbyScreen_Base::RequestCharList()
{
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char || !Char->IsConnected()) return;
FString WorldId;
if (UGameInstance* GI = GetGameInstance())
{
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
WorldId = Flow->GetSelectedWorldId();
}
}
// Wire: uint8 hasFilter + (uint8[16] worldId se hasFilter==1)
TArray<uint8> Payload;
if (WorldId.IsEmpty())
{
Payload.Add(0);
}
else
{
Payload.Add(1);
if (!WriteUuid16(Payload, WorldId))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: SelectedWorldId invalido (%s) — pedindo sem filtro"), *WorldId);
Payload.Reset();
Payload.Add(0);
}
}
Char->SendCharRequest(ZMMOCharOp::C_CHAR_LIST_REQUEST, Payload);
}
void UUIUserLobbyScreen_Base::ShowCharSelectPage()
{
if (PageSwitcher) PageSwitcher->SetActiveWidgetIndex(0);
}
void UUIUserLobbyScreen_Base::ShowCharCreatePage()
{
if (PageSwitcher) PageSwitcher->SetActiveWidgetIndex(1);
}
void UUIUserLobbyScreen_Base::BackToServerSelect()
{
UGameInstance* GI = GetGameInstance();
if (!GI) return;
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->ClearSelectedWorld();
Flow->SetState(EZMMOFrontEndState::ServerSelect);
}
}
void UUIUserLobbyScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload)
{
switch (Opcode)
{
case ZMMOCharOp::S_CHAR_LIST: ParseCharList(Payload); break;
case ZMMOCharOp::S_CHAR_SELECT_OK: HandleCharSelectOk(Payload); break;
case ZMMOCharOp::S_CHAR_SELECT_REJECT: HandleCharSelectReject(Payload); break;
case ZMMOCharOp::S_CHAR_CREATE_OK: HandleCharCreateOk(Payload); break;
case ZMMOCharOp::S_CHAR_CREATE_REJECT: HandleCharCreateReject(Payload); break;
case ZMMOCharOp::S_CHAR_DELETE_ACK: HandleCharDeleteAck(Payload); break;
case ZMMOCharOp::S_CHAR_DELETE_ACCEPT_ACK: HandleCharDeleteAcceptAck(Payload); break;
case ZMMOCharOp::S_CHAR_DELETE_CANCEL_ACK: HandleCharDeleteCancelAck(Payload); break;
default: break;
}
}
void UUIUserLobbyScreen_Base::ParseCharList(const TArray<uint8>& Payload)
{
int32 Pos = 0;
uint16 Count = 0;
if (!ReadU16(Payload, Pos, Count))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_LIST malformado (sem count)"));
return;
}
TArray<FZMMOCharSummary> New;
New.Reserve(Count);
for (uint16 i = 0; i < Count; ++i)
{
FZMMOCharSummary E;
uint64 charId64 = 0;
uint8 slot = 0;
uint16 classId = 0;
uint32 baseLvl = 0, baseExp = 0, jobLvl = 0, jobExp = 0;
uint16 s = 0, a = 0, v = 0, in_ = 0, d = 0, l = 0;
uint32 hp = 0, maxHp = 0, sp = 0, maxSp = 0, money = 0;
float px = 0, py = 0, pz = 0, yaw = 0;
uint32 hair = 0, hairColor = 0, skinColor = 0;
uint8 bodyType = 0;
uint64 deleteAt = 0;
if (!ReadU64(Payload, Pos, charId64)
|| !ReadUuid16(Payload, Pos, E.WorldId)
|| !ReadU8(Payload, Pos, slot)
|| !ReadStringUtf8(Payload, Pos, E.Name)
|| !ReadU16(Payload, Pos, classId)
|| !ReadU32(Payload, Pos, baseLvl)
|| !ReadU32(Payload, Pos, baseExp)
|| !ReadU32(Payload, Pos, jobLvl)
|| !ReadU32(Payload, Pos, jobExp)
|| !ReadU16(Payload, Pos, s) || !ReadU16(Payload, Pos, a) || !ReadU16(Payload, Pos, v)
|| !ReadU16(Payload, Pos, in_) || !ReadU16(Payload, Pos, d) || !ReadU16(Payload, Pos, l)
|| !ReadU32(Payload, Pos, hp) || !ReadU32(Payload, Pos, maxHp)
|| !ReadU32(Payload, Pos, sp) || !ReadU32(Payload, Pos, maxSp)
|| !ReadU32(Payload, Pos, money)
|| !ReadStringUtf8(Payload, Pos, E.MapName)
|| !ReadFloat(Payload, Pos, px) || !ReadFloat(Payload, Pos, py) || !ReadFloat(Payload, Pos, pz)
|| !ReadFloat(Payload, Pos, yaw)
|| !ReadU32(Payload, Pos, hair) || !ReadU32(Payload, Pos, hairColor) || !ReadU32(Payload, Pos, skinColor)
|| !ReadU8(Payload, Pos, bodyType)
|| !ReadU64(Payload, Pos, deleteAt))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_LIST malformado (entry %d)"), i);
return;
}
E.CharId = FString::Printf(TEXT("%llu"), static_cast<unsigned long long>(charId64));
E.Slot = slot; E.ClassId = classId;
E.BaseLevel = baseLvl; E.BaseExp = static_cast<int32>(baseExp);
E.JobLevel = jobLvl; E.JobExp = static_cast<int32>(jobExp);
E.Stats.Str = s; E.Stats.Agi = a; E.Stats.Vit = v;
E.Stats.Int = in_; E.Stats.Dex = d; E.Stats.Luk = l;
E.Hp = hp; E.MaxHp = maxHp; E.Sp = sp; E.MaxSp = maxSp;
E.Money = static_cast<int32>(money);
E.Position = FVector(px, py, pz); E.YawDeg = yaw;
E.Appearance.Hair = hair; E.Appearance.HairColor = hairColor;
E.Appearance.SkinColor = skinColor; E.Appearance.BodyType = bodyType;
E.DeleteScheduledAtMs = static_cast<int64>(deleteAt);
New.Add(MoveTemp(E));
}
Chars = MoveTemp(New);
UE_LOG(LogZMMO, Log, TEXT("Lobby: recebidos %d chars"), Chars.Num());
RebuildCards();
OnCharListReceived();
}
void UUIUserLobbyScreen_Base::RebuildCards()
{
if (!CardContainer || !CharCardClass) return;
for (UUICharCard_Base* Old : SpawnedCards)
{
if (Old)
{
Old->OnCardSelected.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCardSelected);
Old->OnCardDeleteRequest.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCardDeleteRequest);
Old->OnCardDeleteAccept.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCardAcceptDeleteRequest);
Old->OnCardDeleteCancel.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCardCancelDeleteRequest);
}
}
SpawnedCards.Reset();
CardContainer->ClearChildren();
for (const FZMMOCharSummary& C : Chars)
{
UUICharCard_Base* Card = CreateWidget<UUICharCard_Base>(this, CharCardClass);
if (!Card) continue;
Card->SetFromSummary(C);
Card->OnCardSelected.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCardSelected);
Card->OnCardDeleteRequest.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCardDeleteRequest);
Card->OnCardDeleteAccept.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCardAcceptDeleteRequest);
Card->OnCardDeleteCancel.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCardCancelDeleteRequest);
CardContainer->AddChild(Card);
SpawnedCards.Add(Card);
}
}
void UUIUserLobbyScreen_Base::HandleCardSelected(FString CharId)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: char %s selecionado — enviando C_CHAR_SELECT"), *CharId);
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char) return;
uint64 CharId64 = 0;
LexFromString(CharId64, *CharId);
// Wire: uint64 charId
TArray<uint8> Payload;
for (int32 i = 0; i < 8; ++i) Payload.Add(static_cast<uint8>((CharId64 >> (8 * i)) & 0xFF));
Char->SendCharRequest(ZMMOCharOp::C_CHAR_SELECT, Payload);
}
void UUIUserLobbyScreen_Base::HandleCardDeleteRequest(FString CharId)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: DELETE_REQUEST char %s"), *CharId);
SendCharIdOpcode(ZMMOCharOp::C_CHAR_DELETE_REQUEST, CharId);
}
void UUIUserLobbyScreen_Base::HandleCardAcceptDeleteRequest(FString CharId)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: DELETE_ACCEPT char %s"), *CharId);
SendCharIdOpcode(ZMMOCharOp::C_CHAR_DELETE_ACCEPT, CharId);
}
void UUIUserLobbyScreen_Base::HandleCardCancelDeleteRequest(FString CharId)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: DELETE_CANCEL char %s"), *CharId);
SendCharIdOpcode(ZMMOCharOp::C_CHAR_DELETE_CANCEL, CharId);
}
void UUIUserLobbyScreen_Base::SendCharIdOpcode(int32 Opcode, const FString& CharId)
{
UZeusCharServerSubsystem* Char = GetCharServer();
if (!Char) return;
uint64 CharId64 = 0;
LexFromString(CharId64, *CharId);
TArray<uint8> Payload;
for (int32 i = 0; i < 8; ++i) Payload.Add(static_cast<uint8>((CharId64 >> (8 * i)) & 0xFF));
Char->SendCharRequest(Opcode, Payload);
}
void UUIUserLobbyScreen_Base::HandleCharDeleteAck(const TArray<uint8>& Payload)
{
// Wire: uint64 charId + uint16 reason + uint64 effectiveAtMs
int32 Pos = 0; uint64 CharId64 = 0; uint16 Reason = 0; uint64 EffectiveAtMs = 0;
if (!ReadU64(Payload, Pos, CharId64) || !ReadU16(Payload, Pos, Reason) || !ReadU64(Payload, Pos, EffectiveAtMs))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_DELETE_ACK malformado"));
return;
}
if (Reason == 0)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: delete agendado char=%llu (effective=%llu)"),
static_cast<unsigned long long>(CharId64), static_cast<unsigned long long>(EffectiveAtMs));
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: DELETE_ACK rejeitado (reason=%d)"), Reason);
}
RequestCharList();
}
void UUIUserLobbyScreen_Base::HandleCharDeleteAcceptAck(const TArray<uint8>& Payload)
{
int32 Pos = 0; uint64 CharId64 = 0; uint16 Reason = 0;
ReadU64(Payload, Pos, CharId64); ReadU16(Payload, Pos, Reason);
if (Reason == 0)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: char %llu DELETADO"), static_cast<unsigned long long>(CharId64));
}
else
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: DELETE_ACCEPT rejeitado (reason=%d)"), Reason);
}
RequestCharList();
}
void UUIUserLobbyScreen_Base::HandleCharDeleteCancelAck(const TArray<uint8>& Payload)
{
int32 Pos = 0; uint64 CharId64 = 0; uint16 Reason = 0;
ReadU64(Payload, Pos, CharId64); ReadU16(Payload, Pos, Reason);
if (Reason == 0)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: delete cancelado char %llu"), static_cast<unsigned long long>(CharId64));
}
RequestCharList();
}
void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray<uint8>& Payload)
{
// Wire: uint64 charId + string mapName + float x/y/z + float yaw +
// string worldHost + uint16 worldPort + string handoffToken + string region
int32 Pos = 0;
uint64 CharId64 = 0; FString MapName, WorldHost, HandoffToken, Region;
float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0;
if (!ReadU64(Payload, Pos, CharId64)
|| !ReadStringUtf8(Payload, Pos, MapName)
|| !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz)
|| !ReadFloat(Payload, Pos, Yaw)
|| !ReadStringUtf8(Payload, Pos, WorldHost)
|| !ReadU16(Payload, Pos, WorldPort)
|| !ReadStringUtf8(Payload, Pos, HandoffToken)
|| !ReadStringUtf8(Payload, Pos, Region))
{
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado"));
return;
}
UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d map=%s token=%s..."),
*WorldHost, WorldPort, *MapName, *HandoffToken.Left(8));
UGameInstance* GI = GetGameInstance();
if (!GI) return;
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
{
Flow->SetState(EZMMOFrontEndState::EnteringWorld);
}
// TODO Fase 1.5C: invocar UZeusNetworkSubsystem->ConnectToWorld(host,port,token)
// quando o WorldServer aceitar tickets do CharServer (handshake estendido).
}
void UUIUserLobbyScreen_Base::HandleCharSelectReject(const TArray<uint8>& Payload)
{
int32 Pos = 0;
uint16 Reason = 0;
ReadU16(Payload, Pos, Reason);
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_REJECT reason=%d"), Reason);
}
void UUIUserLobbyScreen_Base::HandleCharCreateOk(const TArray<uint8>& /*Payload*/)
{
UE_LOG(LogZMMO, Log, TEXT("Lobby: S_CHAR_CREATE_OK — refresh lista"));
ShowCharSelectPage();
RequestCharList();
}
void UUIUserLobbyScreen_Base::HandleCharCreateReject(const TArray<uint8>& Payload)
{
int32 Pos = 0;
uint16 Reason = 0;
ReadU16(Payload, Pos, Reason);
UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_CREATE_REJECT reason=%d"), Reason);
}
UZeusCharServerSubsystem* UUIUserLobbyScreen_Base::GetCharServer() const
{
if (const UGameInstance* GI = GetGameInstance())
{
return GI->GetSubsystem<UZeusCharServerSubsystem>();
}
return nullptr;
}

View File

@@ -0,0 +1,113 @@
#pragma once
#include "CoreMinimal.h"
#include "UIActivatableScreen_Base.h"
#include "ZMMOCharSummary.h"
#include "UIUserLobbyScreen_Base.generated.h"
class UCommonTextBlock;
class UBorder;
class UPanelWidget;
class UWidgetSwitcher;
class UUIButton_Base;
class UUICharCard_Base;
class UUICharacterCreatePage_Base;
class UZeusCharServerSubsystem;
/**
* Tela Lobby (host) — Fase 1.5 do ARQUITETURA_SERVER_SELECT.
*
* Fluxo:
* 1. NativeOnActivated: pega `SelectedWorldId` do FlowSubsystem, envia
* C_CHAR_LIST_REQUEST filtrado por esse mundo
* 2. Parse S_CHAR_LIST -> Chars[]; instancia 1 WBP_CharCard por entry
* 3. Card "Selecionar" -> C_CHAR_SELECT(charId) -> S_CHAR_SELECT_OK ->
* handoff UDP pro WorldServer
* 4. Botao "Criar Personagem" -> mostra page interna `CharacterCreate`
* 5. Botao "Voltar" -> ClearSelectedWorld + Flow.SetState(ServerSelect)
*
* Pages internas via UWidgetSwitcher:
* - PageIndex 0: Lista de chars (CardContainer)
* - PageIndex 1: Form de criar personagem (CreatePage)
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUIUserLobbyScreen_Base : public UUIActivatableScreen_Base
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable, Category = "Zeus|Lobby")
void RequestCharList();
UFUNCTION(BlueprintCallable, Category = "Zeus|Lobby")
void ShowCharSelectPage();
UFUNCTION(BlueprintCallable, Category = "Zeus|Lobby")
void ShowCharCreatePage();
UFUNCTION(BlueprintCallable, Category = "Zeus|Lobby")
void BackToServerSelect();
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Lobby")
TArray<FZMMOCharSummary> Chars;
UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|Lobby")
void OnCharListReceived();
protected:
virtual void NativeOnActivated() override;
virtual void NativeOnDeactivated() override;
UFUNCTION()
void HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload);
UFUNCTION()
void HandleCardSelected(FString CharId);
UFUNCTION()
void HandleCardDeleteRequest(FString CharId);
UZeusCharServerSubsystem* GetCharServer() const;
void ParseCharList(const TArray<uint8>& Payload);
void HandleCharSelectOk(const TArray<uint8>& Payload);
void HandleCharSelectReject(const TArray<uint8>& Payload);
void HandleCharCreateOk(const TArray<uint8>& Payload);
void HandleCharCreateReject(const TArray<uint8>& Payload);
void HandleCharDeleteAck(const TArray<uint8>& Payload);
void HandleCharDeleteAcceptAck(const TArray<uint8>& Payload);
void HandleCharDeleteCancelAck(const TArray<uint8>& Payload);
UFUNCTION()
void HandleCardAcceptDeleteRequest(FString CharId);
UFUNCTION()
void HandleCardCancelDeleteRequest(FString CharId);
void SendCharIdOpcode(int32 Opcode, const FString& CharId);
void RebuildCards();
/** Container de cards (ScrollBox/GridPanel/UniformGridPanel). */
UPROPERTY(meta = (BindWidget)) TObjectPtr<UPanelWidget> CardContainer;
/** Switcher entre Lista (0) e Criar (1). */
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UWidgetSwitcher> PageSwitcher;
/** Page de criacao (WBP_CharacterCreate) — opcional, pode estar embedado. */
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUICharacterCreatePage_Base> CharCreatePage;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_Title;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UCommonTextBlock> Text_WorldName;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Back;
UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr<UUIButton_Base> Btn_Create;
/** Classe do card a instanciar. */
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Zeus|Lobby")
TSubclassOf<UUICharCard_Base> CharCardClass;
private:
bool bSubscribed = false;
bool bButtonsBound = false;
UPROPERTY(Transient)
TArray<TObjectPtr<UUICharCard_Base>> SpawnedCards;
};

View File

@@ -0,0 +1,102 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOCharSummary.generated.h"
/** Stats primarios do personagem (espelha `CharStats` em char.types.ts). */
USTRUCT(BlueprintType)
struct FZMMOCharStats
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Str = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Agi = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Vit = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Int = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Dex = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Luk = 1;
};
/** Aparencia (espelha `CharAppearance` em char.types.ts). */
USTRUCT(BlueprintType)
struct FZMMOCharAppearance
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Hair = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 HairColor = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 SkinColor = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 BodyType = 0;
};
/**
* Resumo de personagem recebido do CharServer via S_CHAR_LIST.
* Espelha `CharSummary` em `Server/ZeusCharServer/src/types/char.types.ts`.
*/
USTRUCT(BlueprintType)
struct FZMMOCharSummary
{
GENERATED_BODY()
/** charId — BIGINT UNSIGNED no banco; armazenado como FString aqui pra preservar 64 bits. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString CharId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString WorldId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Slot = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString Name;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 ClassId = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 BaseLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 BaseExp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 JobLevel = 1;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 JobExp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FZMMOCharStats Stats;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Hp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 MaxHp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Sp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 MaxSp = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int32 Money = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FString MapName;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FVector Position = FVector::ZeroVector;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
float YawDeg = 0.f;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
FZMMOCharAppearance Appearance;
/** Epoch ms em que o delete vai efetivar. 0 = nao agendado. */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
int64 DeleteScheduledAtMs = 0;
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include "CoreMinimal.h"
#include "ZMMOWorldEntry.generated.h"
/**
* Entrada de mundo (server) recebida do CharServer via S_WORLD_LIST.
* Espelha `WorldRecord` em `Server/ZeusCharServer/src/types/world.types.ts`.
*
* State usa o wire raw (0=Offline, 1=Online, 2=Maintenance — ver
* ZMMOWireWorldState em CharServerOpcodes.h).
*/
USTRUCT(BlueprintType)
struct FZMMOWorldEntry
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString WorldId;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString WorldName;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString Region;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
FString Host;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Port = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Capacity = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 Population = 0;
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
int32 QueueLen = 0;
/** 0=Offline, 1=Online, 2=Maintenance (vide ZMMOWireWorldState). */
UPROPERTY(BlueprintReadOnly, Category = "Zeus|World")
uint8 State = 0;
};

View File

@@ -33,16 +33,68 @@ namespace
}
}
const FUIStyleButtonVariant& UUIButton_Base::ResolveVariant(const FUIStyleButton& Button) const
FUIStyleButtonVariant UUIButton_Base::ResolveVariant(const FUIStyleButton& Button) const
{
switch (Variant)
// Compõe FLAT a partir dos 3 sub-mapas do tema (Backgrounds/Strokes/Fonts).
// Fallback inline → Defaults (constructor de FUIStyleButton popula
// Primary/Secondary/Danger/Ghost com defaults Aurora Arcana — GC-safe).
static const FUIStyleButton Defaults;
FUIStyleButtonVariant V;
const FUIStyleButtonBackground* BG = Button.Backgrounds.Find(Variant);
if (!BG) { BG = Defaults.Backgrounds.Find(Variant); }
if (!BG) { BG = Defaults.Backgrounds.Find(TEXT("Primary")); }
if (BG)
{
case EUIButtonVariant::Secondary: return Button.Secondary;
case EUIButtonVariant::Danger: return Button.Danger;
case EUIButtonVariant::Ghost: return Button.Ghost;
case EUIButtonVariant::Primary:
default: return Button.Primary;
V.BgNormal = BG->BgNormal;
V.BgHover = BG->BgHover;
V.BgPressed = BG->BgPressed;
V.BgDisabled = BG->BgDisabled;
V.BackgroundTexture = BG->BackgroundTexture;
V.BackgroundMargin = BG->BackgroundMargin;
}
const FUIStyleButtonStroke* ST = Button.Strokes.Find(Variant);
if (!ST) { ST = Defaults.Strokes.Find(Variant); }
if (!ST) { ST = Defaults.Strokes.Find(TEXT("Primary")); }
if (ST)
{
V.BorderNormal = ST->BorderNormal;
V.BorderHover = ST->BorderHover;
V.BorderWidth = ST->BorderWidth;
V.CornerRadius = ST->CornerRadius;
}
const FUIStyleButtonFont* F = Button.Fonts.Find(Variant);
if (!F) { F = Defaults.Fonts.Find(Variant); }
if (!F) { F = Defaults.Fonts.Find(TEXT("Primary")); }
if (F)
{
V.TextColor = F->TextColor;
V.TextStyle = F->TextStyle;
}
return V;
}
TArray<FString> UUIButton_Base::GetVariantOptions() const
{
TArray<FString> Options;
for (const TCHAR* N : { TEXT("Primary"), TEXT("Secondary"), TEXT("Danger"), TEXT("Ghost") })
{
Options.Add(N);
}
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIButtonVariantOptions"), false))
{
for (const TPair<FName, FUIStyleButtonBackground>& P : Row->Style.Button.Backgrounds) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleButtonStroke>& P : Row->Style.Button.Strokes) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleButtonFont>& P : Row->Style.Button.Fonts) { Options.AddUnique(P.Key.ToString()); }
}
}
return Options;
}
// ---- Hyper "Set Button Text" ----
@@ -229,17 +281,18 @@ void UUIButton_Base::ApplyVisualState(EUIButtonVisual State)
Brush.TintColor = FSlateColor(Fill);
if (bRoundedBackground)
{
const float R = AS.Button.CornerRadius;
// CornerRadius/BorderWidth agora vêm da variante composta (Stroke).
const float R = V.CornerRadius;
Brush.DrawAs = ESlateBrushDrawType::RoundedBox;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(R, R, R, R), FSlateColor(Outline), AS.Button.BorderWidth);
FVector4(R, R, R, R), FSlateColor(Outline), V.BorderWidth);
Brush.OutlineSettings.RoundingType = ESlateBrushRoundingType::FixedRadius;
}
else
{
Brush.DrawAs = ESlateBrushDrawType::Box;
Brush.OutlineSettings = FSlateBrushOutlineSettings(
FVector4(0, 0, 0, 0), FSlateColor(Outline), AS.Button.BorderWidth);
FVector4(0, 0, 0, 0), FSlateColor(Outline), V.BorderWidth);
}
Background->SetBrush(Brush);
}

View File

@@ -42,8 +42,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
EUIButtonShape ButtonTypeSelection = EUIButtonShape::Rectangle;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
EUIButtonVariant Variant = EUIButtonVariant::Primary;
/**
* Variante visual do botão (dropdown vem de FUIStyle.Button.Backgrounds/
* Strokes/Fonts no DT_UI_Styles + as 4 clássicas como fallback).
* Data-driven via FName, sem enum fixo (padrão Hyper-style).
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button",
meta = (GetOptions = "GetVariantOptions"))
FName Variant = TEXT("Primary");
/** Opções do dropdown de Variant (data-driven do DT_UI_Styles). */
UFUNCTION()
TArray<FString> GetVariantOptions() const;
/** Hyper "Transform Policy = To Upper": força o texto em maiúsculas. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Button")
@@ -174,7 +184,7 @@ protected:
meta = (DisplayName = "Apply UI Style"))
void BP_ApplyUIStyle(const FUIStyleButton& Button, const FUIStyleButtonVariant& VariantStyle);
const FUIStyleButtonVariant& ResolveVariant(const FUIStyleButton& Button) const;
FUIStyleButtonVariant ResolveVariant(const FUIStyleButton& Button) const;
// ---- Widgets visuais opcionais (nomes espelham o Hyper) ----
UPROPERTY(meta = (BindWidgetOptional))

View File

@@ -0,0 +1,216 @@
#include "UICheckBox_Base.h"
#include "ZMMOThemeSubsystem.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "Components/CheckBox.h"
#include "CommonTextBlock.h"
#include "UICommonText_Base.h"
#include "UI/UIStyleRow.h"
namespace
{
// Mesmo padrão de UUISpinner_Base: runtime usa o tema ativo; design-time
// carrega a row "Default" de DT_UI_Styles para o Designer ver igual.
FUIStyle ResolveCheckBoxStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
// Design-time relê o DT a cada chamada: editar o DT_UI_Styles e
// RECOMPILAR o WBP já reflete (sem reiniciar o editor).
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UICheckBoxDesign"), false))
{
DesignStyle = Row->Style;
bHas = true;
}
}
return bHas ? DesignStyle : Fallback;
}
}
TArray<FString> UUICheckBox_Base::GetLabelTextRoleOptions() const
{
return UUICommonText_Base::GetThemeTextRoleOptions();
}
TArray<FString> UUICheckBox_Base::GetVariantOptions() const
{
TArray<FString> Options;
Options.Add(TEXT("Default"));
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UICheckBoxVariantOptions"), false))
{
for (const TPair<FName, FUIStyleCheckBoxFill>& P : Row->Style.CheckBox.Fills) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleCheckBoxStroke>& P : Row->Style.CheckBox.Strokes) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleCheckBoxLayout>& P : Row->Style.CheckBox.Layouts) { Options.AddUnique(P.Key.ToString()); }
}
}
return Options;
}
static FUIStyleCheckBoxVariant ResolveCheckBoxVariant(const FUIStyleCheckBox& C, FName VariantName)
{
static const FUIStyleCheckBox Defaults;
FUIStyleCheckBoxVariant V;
if (const FUIStyleCheckBoxFill* E = C.Fills.Find(VariantName)) { V.Fill = *E; }
else if (const FUIStyleCheckBoxFill* E2 = Defaults.Fills.Find(VariantName)) { V.Fill = *E2; }
else { V.Fill = Defaults.Fills.FindRef(TEXT("Default")); }
if (const FUIStyleCheckBoxStroke* E = C.Strokes.Find(VariantName)) { V.Stroke = *E; }
else if (const FUIStyleCheckBoxStroke* E2 = Defaults.Strokes.Find(VariantName)) { V.Stroke = *E2; }
else { V.Stroke = Defaults.Strokes.FindRef(TEXT("Default")); }
if (const FUIStyleCheckBoxLayout* E = C.Layouts.Find(VariantName)) { V.Layout = *E; }
else if (const FUIStyleCheckBoxLayout* E2 = Defaults.Layouts.Find(VariantName)) { V.Layout = *E2; }
else { V.Layout = Defaults.Layouts.FindRef(TEXT("Default")); }
return V;
}
void UUICheckBox_Base::RefreshUIStyle()
{
const FUIStyle Fallback;
const FUIStyle AS = ResolveCheckBoxStyle(this, Fallback);
const FUIStyleCheckBoxVariant V = ResolveCheckBoxVariant(AS.CheckBox, Variant);
if (Label && !LabelText.IsEmpty())
{
Label->SetText(LabelText);
}
if (UUICommonText_Base* CT = Cast<UUICommonText_Base>(Label))
{
if (CT->TextRole != LabelTextRole)
{
CT->TextRole = LabelTextRole;
}
CT->RefreshUIStyle();
}
if (CheckBox)
{
FCheckBoxStyle S = CheckBox->GetWidgetStyle();
const FVector2D Box(V.Layout.BoxSize, V.Layout.BoxSize);
auto Paint = [&Box](FSlateBrush& Brush, const FLinearColor& Tint)
{
Brush.TintColor = FSlateColor(Tint);
Brush.ImageSize = Box;
};
Paint(S.UncheckedImage, V.Stroke.BorderColor);
Paint(S.UncheckedHoveredImage, V.Fill.HoveredColor);
Paint(S.UncheckedPressedImage, V.Fill.PressedColor);
Paint(S.CheckedImage, V.Fill.BoxColor);
Paint(S.CheckedHoveredImage, V.Fill.HoveredColor);
Paint(S.CheckedPressedImage, V.Fill.PressedColor);
Paint(S.UndeterminedImage, V.Stroke.BorderColor);
Paint(S.UndeterminedHoveredImage, V.Fill.HoveredColor);
Paint(S.UndeterminedPressedImage, V.Fill.PressedColor);
S.ForegroundColor = FSlateColor(V.Fill.CheckColor);
S.BorderBackgroundColor = FSlateColor(V.Fill.DisabledColor);
CheckBox->SetWidgetStyle(S);
}
BP_ApplyCheckBoxStyle(V);
}
bool UUICheckBox_Base::IsChecked() const
{
return CheckBox ? CheckBox->IsChecked() : false;
}
void UUICheckBox_Base::SetIsChecked(bool bInChecked)
{
if (CheckBox)
{
CheckBox->SetIsChecked(bInChecked);
}
}
void UUICheckBox_Base::NativePreConstruct()
{
Super::NativePreConstruct();
RefreshUIStyle();
}
void UUICheckBox_Base::SynchronizeProperties()
{
Super::SynchronizeProperties();
// Live no Designer: edita LabelText/LabelTextRole nos Detalhes e o canvas
// reflete na hora (sem precisar recompilar o WBP).
RefreshUIStyle();
}
void UUICheckBox_Base::NativeConstruct()
{
Super::NativeConstruct();
if (CheckBox && !bCheckBound)
{
CheckBox->OnCheckStateChanged.AddDynamic(this, &UUICheckBox_Base::HandleCheckStateChanged);
bCheckBound = true;
}
if (!bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.AddDynamic(this, &UUICheckBox_Base::HandleThemeChanged);
bThemeBound = true;
}
}
}
RefreshUIStyle();
}
void UUICheckBox_Base::NativeDestruct()
{
if (bCheckBound && CheckBox)
{
CheckBox->OnCheckStateChanged.RemoveDynamic(this, &UUICheckBox_Base::HandleCheckStateChanged);
bCheckBound = false;
}
if (bThemeBound)
{
if (const UGameInstance* GI = GetGameInstance())
{
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
Theme->OnThemeChanged.RemoveDynamic(this, &UUICheckBox_Base::HandleThemeChanged);
}
}
bThemeBound = false;
}
Super::NativeDestruct();
}
void UUICheckBox_Base::HandleThemeChanged(FName /*NewThemeId*/)
{
RefreshUIStyle();
}
void UUICheckBox_Base::HandleCheckStateChanged(bool bIsChecked)
{
OnCheckStateChanged.Broadcast(bIsChecked);
}

View File

@@ -0,0 +1,98 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonUserWidget.h"
#include "UI/UIStyleTokens.h"
#include "UICheckBox_Base.generated.h"
class UCheckBox;
class UCommonTextBlock;
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUICheckBoxStateChanged, bool, bIsChecked);
/**
* Checkbox compartilhado do ZMMO — mesmo padrão de UUISpinner_Base /
* UUIPanel_Base. Fundação CommonUI (UCommonUserWidget). Camada Abstract; o
* WBP concreto (UI_CheckBox_Master) herda DIRETO desta classe C++ (UMG não
* encadeia árvores — ARQUITETURA.md §3.3).
*
* Visual data-driven por FUIStyle.CheckBox (UZMMOThemeSubsystem). C++ aplica
* o que dá no UCheckBox; cor/brush finos vão pelo hook BP_ApplyCheckBoxStyle
* (o WBP tinge os brushes do estilo do UCheckBox).
*/
UCLASS(Abstract, Blueprintable)
class ZMMO_API UUICheckBox_Base : public UCommonUserWidget
{
GENERATED_BODY()
public:
/** Re-resolve os tokens do tema ativo e reaplica. */
UFUNCTION(BlueprintCallable, Category = "UI Style")
void RefreshUIStyle();
UFUNCTION(BlueprintPure, Category = "CheckBox")
bool IsChecked() const;
UFUNCTION(BlueprintCallable, Category = "CheckBox")
void SetIsChecked(bool bInChecked);
/** Disparado quando o usuário (ou código) muda o estado da caixa. */
UPROPERTY(BlueprintAssignable, Category = "CheckBox")
FUICheckBoxStateChanged OnCheckStateChanged;
/** Rótulo ao lado da caixa. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CheckBox")
FText LabelText;
/**
* Categoria tipográfica do rótulo (dropdown vem do DT_UI_Styles).
* Exposto na raiz: dá pra trocar aqui sem selecionar o Label interno.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CheckBox",
meta = (GetOptions = "GetLabelTextRoleOptions"))
FName LabelTextRole = TEXT("Label");
/** Opções do dropdown de LabelTextRole (data-driven do DT_UI_Styles). */
UFUNCTION()
TArray<FString> GetLabelTextRoleOptions() const;
/**
* Variante visual do checkbox (dropdown vem de FUIStyle.CheckBox.Fills/
* Strokes/Layouts no DT_UI_Styles). Data-driven, sem enum fixo.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "CheckBox",
meta = (GetOptions = "GetVariantOptions"))
FName Variant = TEXT("Default");
UFUNCTION()
TArray<FString> GetVariantOptions() const;
protected:
virtual void NativePreConstruct() override;
virtual void SynchronizeProperties() override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Hook opcional: o WBP aplica cor/brush no estilo do UCheckBox. */
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style",
meta = (DisplayName = "Apply CheckBox Style"))
void BP_ApplyCheckBoxStyle(const FUIStyleCheckBoxVariant& VariantStyle);
/** Caixa de check (nome esperado no WBP: "CheckBox"). */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCheckBox> CheckBox;
/** Rótulo opcional ao lado (nome esperado no WBP: "Label"). */
UPROPERTY(meta = (BindWidgetOptional))
TObjectPtr<UCommonTextBlock> Label;
private:
UFUNCTION()
void HandleThemeChanged(FName NewThemeId);
UFUNCTION()
void HandleCheckStateChanged(bool bIsChecked);
bool bThemeBound = false;
bool bCheckBound = false;
};

View File

@@ -0,0 +1,138 @@
#include "UICommonText_Base.h"
#include "ZMMOThemeSubsystem.h"
#include "Engine/GameInstance.h"
#include "Engine/DataTable.h"
#include "Styling/CoreStyle.h"
#include "UI/UIStyleRow.h"
namespace
{
const TCHAR* GStylesDT = TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles");
// Retorna POR VALOR (cópia fresca): nunca persiste ponteiro de FontObject
// num static não-rastreado por GC (causa AV ao hashear a fonte no preview).
FUIStyle ResolveTextStyle(const UWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
if (const UGameInstance* GI = Widget->GetGameInstance())
{
if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
{
return Theme->GetActiveUIStyle();
}
}
}
// Design-time relê o DT a cada chamada. Local (não-static): a fonte
// fica viva apenas durante o RefreshUIStyle (o UObject UFont é mantido
// pelo DataTable carregado — sem ponteiro pendurado entre frames).
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr, GStylesDT))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UITextDesign"), false))
{
DesignStyle = Row->Style;
bHas = true;
}
}
return bHas ? DesignStyle : Fallback;
}
// Fallback: categorias clássicas a partir de FUIStyle.Text (já configurado
// com as FF_ fonts no DT) enquanto o mapa TextRoles não for preenchido.
bool LegacyRole(const FUIStyle& AS, FName Role, FSlateFontInfo& OutFont,
FLinearColor& OutColor, bool& bOutUpper)
{
const FUIStyleText& T = AS.Text;
const FUIStylePalette& P = AS.Palette;
const FString R = Role.ToString();
bOutUpper = false;
if (R.Equals(TEXT("Title"), ESearchCase::IgnoreCase)) { OutFont = T.TitleFont; OutColor = P.Text; return true; }
if (R.Equals(TEXT("Section"), ESearchCase::IgnoreCase)) { OutFont = T.SectionFont; OutColor = P.Text; return true; }
if (R.Equals(TEXT("Button"), ESearchCase::IgnoreCase)) { OutFont = T.ButtonFont; OutColor = P.Text; return true; }
if (R.Equals(TEXT("Label"), ESearchCase::IgnoreCase)) { OutFont = T.LabelFont; OutColor = P.Text2; bOutUpper = T.bLabelUppercase; return true; }
if (R.Equals(TEXT("Dim"), ESearchCase::IgnoreCase)) { OutFont = T.BodyFont; OutColor = P.TextDim; return true; }
if (R.Equals(TEXT("Body"), ESearchCase::IgnoreCase)) { OutFont = T.BodyFont; OutColor = P.Text; return true; }
return false;
}
}
void UUICommonText_Base::RefreshUIStyle()
{
const FUIStyle Fallback;
const FUIStyle AS = ResolveTextStyle(this, Fallback);
FSlateFontInfo RoleFont;
FLinearColor RoleColor = AS.Palette.Text;
bool bUpper = false;
// 1) Mapa data-driven (autor define no DT_UI_Styles).
if (const FUITextStyle* E = AS.TextRoles.Find(TextRole))
{
RoleFont = E->Font;
RoleColor = E->Color;
bUpper = E->bUppercase;
}
// 2) Fallback p/ categorias clássicas de FUIStyle.Text.
else if (!LegacyRole(AS, TextRole, RoleFont, RoleColor, bUpper))
{
RoleFont = AS.Text.BodyFont;
RoleColor = AS.Palette.Text;
}
// Nunca deixa sem fonte (evita o "A BASIC LATIN" gigante/quebrado).
if (RoleFont.FontObject == nullptr && RoleFont.CompositeFont == nullptr)
{
RoleFont = FCoreStyle::GetDefaultFontStyle("Regular", 14);
}
SetFont(RoleFont);
SetColorAndOpacity(FSlateColor(RoleColor));
if (bUpper)
{
const FText Cur = GetText();
if (!Cur.IsEmpty())
{
SetText(FText::FromString(Cur.ToString().ToUpper()));
}
}
}
TArray<FString> UUICommonText_Base::GetTextRoleOptions() const
{
return GetThemeTextRoleOptions();
}
TArray<FString> UUICommonText_Base::GetThemeTextRoleOptions()
{
TArray<FString> Options;
// Categorias clássicas sempre disponíveis (fallback garantido).
for (const TCHAR* N : { TEXT("Title"), TEXT("Section"), TEXT("Body"),
TEXT("Button"), TEXT("Label"), TEXT("Dim") })
{
Options.Add(N);
}
// + chaves definidas pelo autor no DT_UI_Styles (data-driven).
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr, GStylesDT))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UITextRoleOptions"), false))
{
for (const TPair<FName, FUITextStyle>& Pair : Row->Style.TextRoles)
{
Options.AddUnique(Pair.Key.ToString());
}
}
}
return Options;
}
void UUICommonText_Base::SynchronizeProperties()
{
Super::SynchronizeProperties();
RefreshUIStyle();
}

View File

@@ -0,0 +1,49 @@
#pragma once
#include "CoreMinimal.h"
#include "CommonTextBlock.h"
#include "UICommonText_Base.generated.h"
/**
* Texto compartilhado do ZMMO. Subclasse de UCommonTextBlock (leaf, usado
* direto na árvore). A categoria tipográfica NÃO é um enum fixo: é um nome
* (FName) resolvido contra o mapa FUIStyle.TextRoles do DT_UI_Styles via
* UZMMOThemeSubsystem — o dropdown do Details é populado a partir do DT
* (GetOptions). Data-driven, sem struct/enum pré-definido (pedido do autor).
*
* Fallback: se a chave não existir no mapa, cai nas categorias clássicas de
* FUIStyle.Text (Title/Section/Body/Button/Label/Dim) — assim já renderiza
* com as fontes do tema (FF_Cinzel/FF_Rajdhani) mesmo antes do mapa ser
* preenchido no DT.
*/
UCLASS(Blueprintable)
class ZMMO_API UUICommonText_Base : public UCommonTextBlock
{
GENERATED_BODY()
public:
/**
* Nome da categoria tipográfica (chave de FUIStyle.TextRoles no
* DT_UI_Styles). O dropdown é populado pelo DT — ver GetTextRoleOptions.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI Style",
meta = (GetOptions = "GetTextRoleOptions"))
FName TextRole = TEXT("Body");
/** Re-resolve o tema ativo e reaplica fonte/cor (chamar em troca de tema). */
UFUNCTION(BlueprintCallable, Category = "UI Style")
void RefreshUIStyle();
/** Opções do dropdown de TextRole — lidas do DT_UI_Styles (data-driven). */
UFUNCTION()
TArray<FString> GetTextRoleOptions() const;
/**
* Mesma lista (clássicas + chaves do DT) reutilizável por outras bases
* que expõem um TextRole na raiz (ex.: UUICheckBox_Base, UUIInput_Base).
*/
static TArray<FString> GetThemeTextRoleOptions();
protected:
virtual void SynchronizeProperties() override;
};

View File

@@ -9,7 +9,7 @@
namespace
{
const FUIStyle& ResolvePanelStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
FUIStyle ResolvePanelStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
@@ -24,22 +24,21 @@ namespace
// Design-time (sem GameInstance/subsystem): carrega o DT_UI_Styles
// direto e usa a row "Default", para o Designer pintar/mostrar os
// brushes igual ao runtime (caso contrário FUIStyle vem vazio).
static FUIStyle DesignStyle;
static bool bDesignLoaded = false;
if (!bDesignLoaded)
// Design-time relê o DT a cada chamada: editar o DT_UI_Styles e
// RECOMPILAR o WBP já reflete (sem reiniciar o editor).
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIPanelDesign"), false))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UIPanelDesign"), false))
{
DesignStyle = Row->Style;
bDesignLoaded = true;
}
DesignStyle = Row->Style;
bHas = true;
}
}
return bDesignLoaded ? DesignStyle : Fallback;
return bHas ? DesignStyle : Fallback;
}
}
@@ -50,7 +49,7 @@ void UUIPanel_Base::RefreshUIStyle()
return;
}
const FUIStyle Fallback;
const FUIStyle& AS = ResolvePanelStyle(this, Fallback);
const FUIStyle AS = ResolvePanelStyle(this, Fallback);
const FUIStylePanel& P = AS.Panel;
// ---- Modo TEXTURA (padrão Hyper): brush por EUIPanelTexture ----

View File

@@ -10,7 +10,7 @@ namespace
{
// Mesmo padrão de UUIPanel_Base: runtime usa o tema ativo; design-time
// carrega a row "Default" de DT_UI_Styles para o Designer ver igual.
const FUIStyle& ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
FUIStyle ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback)
{
if (Widget)
{
@@ -22,40 +22,71 @@ namespace
}
}
}
static FUIStyle DesignStyle;
static bool bDesignLoaded = false;
if (!bDesignLoaded)
// Design-time relê o DT a cada chamada: editar o DT_UI_Styles e
// RECOMPILAR o WBP já reflete (sem reiniciar o editor).
FUIStyle DesignStyle;
bool bHas = false;
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UISpinnerDesign"), false))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UISpinnerDesign"), false))
{
DesignStyle = Row->Style;
bDesignLoaded = true;
}
DesignStyle = Row->Style;
bHas = true;
}
}
return bDesignLoaded ? DesignStyle : Fallback;
return bHas ? DesignStyle : Fallback;
}
}
static FUIStyleSpinnerVariant ResolveSpinnerVariant(const FUIStyleSpinner& Sp, FName VariantName)
{
static const FUIStyleSpinner Defaults;
FUIStyleSpinnerVariant V;
if (const FUIStyleSpinnerColors* E = Sp.Colors.Find(VariantName)) { V.Colors = *E; }
else if (const FUIStyleSpinnerColors* E2 = Defaults.Colors.Find(VariantName)) { V.Colors = *E2; }
else { V.Colors = Defaults.Colors.FindRef(TEXT("Default")); }
if (const FUIStyleSpinnerLayout* E = Sp.Layouts.Find(VariantName)) { V.Layout = *E; }
else if (const FUIStyleSpinnerLayout* E2 = Defaults.Layouts.Find(VariantName)) { V.Layout = *E2; }
else { V.Layout = Defaults.Layouts.FindRef(TEXT("Default")); }
return V;
}
TArray<FString> UUISpinner_Base::GetVariantOptions() const
{
TArray<FString> Options;
Options.Add(TEXT("Default"));
if (const UDataTable* DT = LoadObject<UDataTable>(nullptr,
TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles")))
{
if (const FUIStyleRow* Row = DT->FindRow<FUIStyleRow>(
FName(TEXT("Default")), TEXT("UISpinnerVariantOptions"), false))
{
for (const TPair<FName, FUIStyleSpinnerColors>& P : Row->Style.Spinner.Colors) { Options.AddUnique(P.Key.ToString()); }
for (const TPair<FName, FUIStyleSpinnerLayout>& P : Row->Style.Spinner.Layouts) { Options.AddUnique(P.Key.ToString()); }
}
}
return Options;
}
void UUISpinner_Base::RefreshUIStyle()
{
const FUIStyle Fallback;
const FUIStyle& AS = ResolveSpinnerStyle(this, Fallback);
const FUIStyleSpinner& S = AS.Spinner;
const FUIStyle AS = ResolveSpinnerStyle(this, Fallback);
const FUIStyleSpinnerVariant V = ResolveSpinnerVariant(AS.Spinner, Variant);
if (Throbber)
{
Throbber->SetNumberOfPieces(FMath::Max(1, S.NumberOfPieces));
Throbber->SetPeriod(FMath::Max(0.05f, S.Period));
Throbber->SetRadius(S.Radius);
Throbber->SetNumberOfPieces(FMath::Max(1, V.Layout.NumberOfPieces));
Throbber->SetPeriod(FMath::Max(0.05f, V.Layout.Period));
Throbber->SetRadius(V.Layout.Radius);
}
// Cor/brush: o WBP aplica no throbber (mesma divisão de UUIPanel_Base).
BP_ApplySpinnerStyle(S);
BP_ApplySpinnerStyle(V);
}
void UUISpinner_Base::NativePreConstruct()

View File

@@ -27,15 +27,26 @@ public:
UFUNCTION(BlueprintCallable, Category = "UI Style")
void RefreshUIStyle();
/**
* Variante visual do spinner (dropdown vem de FUIStyle.Spinner.Colors/
* Layouts no DT_UI_Styles). Data-driven, sem enum fixo.
*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spinner",
meta = (GetOptions = "GetVariantOptions"))
FName Variant = TEXT("Default");
UFUNCTION()
TArray<FString> GetVariantOptions() const;
protected:
virtual void NativePreConstruct() override;
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
/** Hook opcional: o WBP aplica cor/brush do spinner (FUIStyleSpinner). */
/** Hook opcional: o WBP aplica cor/brush do spinner (variante composta). */
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style",
meta = (DisplayName = "Apply Spinner Style"))
void BP_ApplySpinnerStyle(const FUIStyleSpinner& Spinner);
void BP_ApplySpinnerStyle(const FUIStyleSpinnerVariant& VariantStyle);
/** Throbber circular (nome esperado no WBP: "Throbber"). */
UPROPERTY(meta = (BindWidgetOptional))