feat(char-spawn): Fase 4 client + UI WBPs paralelo (#2)
Co-authored-by: Mateus Rodrigues <mateuus27@outlook.com> Co-committed-by: Mateus Rodrigues <mateuus27@outlook.com>
This commit was merged in pull request #2.
This commit is contained in:
@@ -12,6 +12,11 @@ enum class EZMMOThemeKey : uint8
|
||||
HUD_HealthBar UMETA(DisplayName = "HUD - Health Bar"),
|
||||
HUD_ManaBar UMETA(DisplayName = "HUD - Mana Bar"),
|
||||
|
||||
// Boot
|
||||
Boot_Background UMETA(DisplayName = "Boot - Background"),
|
||||
Boot_Logo UMETA(DisplayName = "Boot - Logo"),
|
||||
Boot_Music UMETA(DisplayName = "Boot - Music"),
|
||||
|
||||
// Login / Character Select
|
||||
Login_Background UMETA(DisplayName = "Login - Background"),
|
||||
Login_Logo UMETA(DisplayName = "Login - Logo"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,6 +51,23 @@ struct ZMMO_API FUIStyle
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
|
||||
FUIStyleTooltip Tooltip;
|
||||
|
||||
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
|
||||
* WBP (ARQUITETURA.md §5). Preenchido em DT_UI_Styles.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style")
|
||||
FSlateBrush ScreenBackground;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "Styling/SlateBrush.h"
|
||||
#include "Layout/Margin.h"
|
||||
#include "Templates/SubclassOf.h"
|
||||
#include "Engine/Texture2D.h"
|
||||
#include "UIStyleTypes.h"
|
||||
#include "UIStyleTokens.generated.h"
|
||||
|
||||
@@ -171,7 +172,97 @@ struct ZMMO_API FUIStyleText
|
||||
float TitleLetterSpacing = 0.f; // informativo (RichText/futuro)
|
||||
};
|
||||
|
||||
/** Cores de uma variante de botão (primary/secondary/danger/ghost). */
|
||||
/**
|
||||
* Uma categoria tipográfica nomeada do tema. As categorias NÃO são fixas em
|
||||
* C++: o autor cria as que quiser ("Title", "Section", "Body", "Label", …)
|
||||
* no mapa FUIStyle.TextRoles do DT_UI_Styles. O UUICommonText_Base só
|
||||
* referencia o nome (dropdown vem do DT).
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUITextStyle
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text")
|
||||
FSlateFontInfo Font;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text")
|
||||
FLinearColor Color = FLinearColor(FColor(244, 240, 230));
|
||||
|
||||
/** Caixa-alta (ex.: labels/eyebrows). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text")
|
||||
bool bUppercase = false;
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// FUIStyleButton — padrão multi-map (Hyper-style). 3 sub-mapas com a MESMA
|
||||
// chave (nome da variante: "Primary", "Secondary", "Danger", "Ghost", ou
|
||||
// customizada). UUIButton_Base.Variant é FName e busca em cada sub-mapa.
|
||||
// O struct FUIStyleButtonVariant (composto, FLAT) permanece com a forma
|
||||
// antiga — assim o hook BP_ApplyUIStyle dos WBP_Master continua funcional.
|
||||
// =====================================================================
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleButtonBackground
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BgNormal = FLinearColor::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BgHover = FLinearColor::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BgPressed = FLinearColor::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BgDisabled = FLinearColor(0.f, 0.f, 0.f, 0.42f);
|
||||
|
||||
/** Textura opcional como background (Hyper-style). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
TObjectPtr<UTexture2D> BackgroundTexture = nullptr;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FMargin BackgroundMargin = FMargin(8.f);
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleButtonStroke
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BorderNormal = FLinearColor::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor BorderHover = FLinearColor::Transparent;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0"))
|
||||
float BorderWidth = 1.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0"))
|
||||
float CornerRadius = 8.f;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleButtonFont
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor TextColor = FLinearColor::White;
|
||||
|
||||
/** CommonTextStyle define fonte/tamanho/peso; TextColor sobrescreve a cor. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
TSubclassOf<UCommonTextStyle> TextStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* View FLAT composta da variante de botão (Background + Stroke + Font).
|
||||
* Mantida com a forma antiga (BgNormal/BgHover/.../TextStyle no topo) para
|
||||
* NÃO QUEBRAR o hook BP_ApplyUIStyle nos WBP_Master existentes. É construída
|
||||
* pelo UUIButton_Base::ResolveVariant a partir dos 3 sub-mapas do tema.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleButtonVariant
|
||||
{
|
||||
@@ -198,14 +289,21 @@ struct ZMMO_API FUIStyleButtonVariant
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FLinearColor TextColor = FLinearColor::White;
|
||||
|
||||
/**
|
||||
* Estilo de texto desta variante (padrão Hyper/CommonUI: CommonTextStyle).
|
||||
* Define fonte/tamanho/peso. A COR é sobreposta pelo TextColor acima
|
||||
* (FUIStyle manda na cor por tema). Permite Primary/Secondary/Danger/Ghost
|
||||
* com tipografias diferentes. Preenchido em DT_UI_Styles por tema.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
TSubclassOf<UCommonTextStyle> TextStyle;
|
||||
|
||||
// Campos extras vindos das sub-categorias (background texture + stroke):
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
|
||||
TObjectPtr<UTexture2D> BackgroundTexture = nullptr;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FMargin BackgroundMargin = FMargin(8.f);
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float BorderWidth = 1.f;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float CornerRadius = 8.f;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
@@ -213,74 +311,85 @@ struct ZMMO_API FUIStyleButton
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Defaults = paleta Aurora Arcana (styles.css .btn-*). Garante que mesmo
|
||||
* sem DataTable (preview de designer) o botão já saia estilizado.
|
||||
*/
|
||||
FUIStyleButton()
|
||||
{
|
||||
// .btn-primary — dourado, texto quase preto
|
||||
Primary.BgNormal = FLinearColor(FColor(201, 167, 90)); // gold
|
||||
Primary.BgHover = FLinearColor(FColor(231, 200, 115)); // gold-hi
|
||||
Primary.BgPressed = FLinearColor(FColor(201, 167, 90));
|
||||
Primary.BgDisabled = FLinearColor(FColor(201, 167, 90, 107));
|
||||
Primary.BorderNormal = FLinearColor(FColor(201, 167, 90));
|
||||
Primary.BorderHover = FLinearColor(FColor(231, 200, 115));
|
||||
Primary.TextColor = FLinearColor(FColor(10, 10, 18)); // ~#0A0A12
|
||||
// ---- Backgrounds (Primary/Secondary/Danger/Ghost) ----
|
||||
FUIStyleButtonBackground PriBg;
|
||||
PriBg.BgNormal = FLinearColor(FColor(201, 167, 90)); // gold
|
||||
PriBg.BgHover = FLinearColor(FColor(231, 200, 115)); // gold-hi
|
||||
PriBg.BgPressed = FLinearColor(FColor(201, 167, 90));
|
||||
PriBg.BgDisabled = FLinearColor(FColor(201, 167, 90, 107));
|
||||
Backgrounds.Add(TEXT("Primary"), PriBg);
|
||||
|
||||
// .btn-secondary — painel sólido + borda
|
||||
Secondary.BgNormal = FLinearColor(FColor(17, 24, 42)); // panel-2
|
||||
Secondary.BgHover = FLinearColor(FColor(22, 32, 58)); // #16203A
|
||||
Secondary.BgPressed = FLinearColor(FColor(17, 24, 42));
|
||||
Secondary.BorderNormal = FLinearColor(FColor(58, 75, 120)); // border
|
||||
Secondary.BorderHover = FLinearColor(FColor(78, 163, 255)); // blue
|
||||
Secondary.TextColor = FLinearColor(FColor(244, 240, 230));// text
|
||||
FUIStyleButtonBackground SecBg;
|
||||
SecBg.BgNormal = FLinearColor(FColor(17, 24, 42)); // panel-2
|
||||
SecBg.BgHover = FLinearColor(FColor(22, 32, 58));
|
||||
SecBg.BgPressed = FLinearColor(FColor(17, 24, 42));
|
||||
Backgrounds.Add(TEXT("Secondary"), SecBg);
|
||||
|
||||
// .btn-danger
|
||||
Danger.BgNormal = FLinearColor(FColor(217, 92, 92, 36)); // rgba(.14)
|
||||
Danger.BgHover = FLinearColor(FColor(217, 92, 92, 66)); // rgba(.26)
|
||||
Danger.BgPressed = FLinearColor(FColor(217, 92, 92, 36));
|
||||
Danger.BorderNormal = FLinearColor(FColor(217, 92, 92)); // danger
|
||||
Danger.BorderHover = FLinearColor(FColor(217, 92, 92));
|
||||
Danger.TextColor = FLinearColor(FColor(240, 182, 182)); // #F0B6B6
|
||||
FUIStyleButtonBackground DngBg;
|
||||
DngBg.BgNormal = FLinearColor(FColor(217, 92, 92, 36));
|
||||
DngBg.BgHover = FLinearColor(FColor(217, 92, 92, 66));
|
||||
DngBg.BgPressed = FLinearColor(FColor(217, 92, 92, 36));
|
||||
Backgrounds.Add(TEXT("Danger"), DngBg);
|
||||
|
||||
// .btn-ghost — transparente + borda suave
|
||||
Ghost.BgNormal = FLinearColor::Transparent;
|
||||
Ghost.BgHover = FLinearColor(FColor(22, 32, 58, 128));
|
||||
Ghost.BgPressed = FLinearColor::Transparent;
|
||||
Ghost.BorderNormal = FLinearColor(FColor(58, 75, 120, 140)); // border-soft
|
||||
Ghost.BorderHover = FLinearColor(FColor(78, 163, 255));
|
||||
Ghost.TextColor = FLinearColor(FColor(174, 182, 200)); // text-2
|
||||
FUIStyleButtonBackground GhoBg;
|
||||
GhoBg.BgNormal = FLinearColor::Transparent;
|
||||
GhoBg.BgHover = FLinearColor(FColor(22, 32, 58, 128));
|
||||
GhoBg.BgPressed = FLinearColor::Transparent;
|
||||
Backgrounds.Add(TEXT("Ghost"), GhoBg);
|
||||
|
||||
// ---- Strokes ----
|
||||
FUIStyleButtonStroke PriS;
|
||||
PriS.BorderNormal = FLinearColor(FColor(201, 167, 90));
|
||||
PriS.BorderHover = FLinearColor(FColor(231, 200, 115));
|
||||
Strokes.Add(TEXT("Primary"), PriS);
|
||||
|
||||
FUIStyleButtonStroke SecS;
|
||||
SecS.BorderNormal = FLinearColor(FColor(58, 75, 120));
|
||||
SecS.BorderHover = FLinearColor(FColor(78, 163, 255));
|
||||
Strokes.Add(TEXT("Secondary"), SecS);
|
||||
|
||||
FUIStyleButtonStroke DngS;
|
||||
DngS.BorderNormal = FLinearColor(FColor(217, 92, 92));
|
||||
DngS.BorderHover = FLinearColor(FColor(217, 92, 92));
|
||||
Strokes.Add(TEXT("Danger"), DngS);
|
||||
|
||||
FUIStyleButtonStroke GhoS;
|
||||
GhoS.BorderNormal = FLinearColor(FColor(58, 75, 120, 140));
|
||||
GhoS.BorderHover = FLinearColor(FColor(78, 163, 255));
|
||||
Strokes.Add(TEXT("Ghost"), GhoS);
|
||||
|
||||
// ---- Fonts ----
|
||||
FUIStyleButtonFont PriF; PriF.TextColor = FLinearColor(FColor(10, 10, 18));
|
||||
Fonts.Add(TEXT("Primary"), PriF);
|
||||
FUIStyleButtonFont SecF; SecF.TextColor = FLinearColor(FColor(244, 240, 230));
|
||||
Fonts.Add(TEXT("Secondary"), SecF);
|
||||
FUIStyleButtonFont DngF; DngF.TextColor = FLinearColor(FColor(240, 182, 182));
|
||||
Fonts.Add(TEXT("Danger"), DngF);
|
||||
FUIStyleButtonFont GhoF; GhoF.TextColor = FLinearColor(FColor(174, 182, 200));
|
||||
Fonts.Add(TEXT("Ghost"), GhoF);
|
||||
}
|
||||
|
||||
// .btn-primary — dourado, texto quase preto
|
||||
/** Cores/textura de fundo por variante (chave FName). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FUIStyleButtonVariant Primary;
|
||||
TMap<FName, FUIStyleButtonBackground> Backgrounds;
|
||||
|
||||
// .btn-secondary — painel sólido + borda
|
||||
/** Borda (cor, espessura, raio) por variante. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FUIStyleButtonVariant Secondary;
|
||||
TMap<FName, FUIStyleButtonStroke> Strokes;
|
||||
|
||||
// .btn-danger
|
||||
/** Tipografia (cor de texto, CommonTextStyle) por variante. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FUIStyleButtonVariant Danger;
|
||||
|
||||
// .btn-ghost — transparente + borda suave
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FUIStyleButtonVariant Ghost;
|
||||
TMap<FName, FUIStyleButtonFont> Fonts;
|
||||
|
||||
// Globais comuns às variantes:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
FMargin Padding = FMargin(22.f, 12.f);
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float MinHeight = 48.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float CornerRadius = 8.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float BorderWidth = 1.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button")
|
||||
float HoverScale = 1.02f;
|
||||
|
||||
@@ -444,3 +553,333 @@ struct ZMMO_API FUIStyleTooltip
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Tooltip")
|
||||
FLinearColor TextColor = FLinearColor(FColor(244, 240, 230)); // Text
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// FUIStyleSpinner — padrão multi-map (Hyper-style). Sub-mapas por categoria
|
||||
// (Colors, Layouts) com a MESMA CHAVE de variante. Permite criar variantes
|
||||
// como "Default", "Small", "Loading", etc.
|
||||
// =====================================================================
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleSpinnerColors
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
FLinearColor Color = FLinearColor(FColor(201, 167, 90)); // Gold
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
FLinearColor TrackColor = FLinearColor(FColor(58, 75, 120, 140));
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleSpinnerLayout
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
float Radius = 28.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
float Thickness = 3.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "1"))
|
||||
int32 NumberOfPieces = 8;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "0.05"))
|
||||
float Period = 0.9f;
|
||||
};
|
||||
|
||||
/** View FLAT composta da variante de Spinner. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleSpinnerVariant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
FUIStyleSpinnerColors Colors;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
FUIStyleSpinnerLayout Layout;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleSpinner
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FUIStyleSpinner()
|
||||
{
|
||||
Colors.Add(TEXT("Default"), FUIStyleSpinnerColors());
|
||||
Layouts.Add(TEXT("Default"), FUIStyleSpinnerLayout());
|
||||
}
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
TMap<FName, FUIStyleSpinnerColors> Colors;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner")
|
||||
TMap<FName, FUIStyleSpinnerLayout> Layouts;
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// FUIStyleCheckBox — padrão multi-map (Hyper-style). Sub-mapas por categoria
|
||||
// visual (Fills, Strokes, Layouts) com a MESMA CHAVE de variante. Cada
|
||||
// UUICheckBox_Base referencia o nome (FName Variant) e o C++ compõe.
|
||||
// =====================================================================
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleCheckBoxFill
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Preenchimento da caixa quando marcada. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor BoxColor = FLinearColor(FColor(201, 167, 90)); // Gold
|
||||
|
||||
/** Cor do "tique"/glifo de check. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor CheckColor = FLinearColor(FColor(18, 22, 34)); // Bg0
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor HoveredColor = FLinearColor(FColor(224, 196, 130));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor PressedColor = FLinearColor(FColor(178, 146, 74));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor DisabledColor = FLinearColor(FColor(90, 96, 112, 140));
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleCheckBoxStroke
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FLinearColor BorderColor = FLinearColor(FColor(58, 75, 120, 140));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "0"))
|
||||
float BorderThickness = 1.5f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "0"))
|
||||
float CornerRadius = 4.f;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleCheckBoxLayout
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Lado da caixa (px). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "8"))
|
||||
float BoxSize = 22.f;
|
||||
};
|
||||
|
||||
/** View FLAT composta da variante de CheckBox — usada pelo BP hook. */
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleCheckBoxVariant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FUIStyleCheckBoxFill Fill;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FUIStyleCheckBoxStroke Stroke;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
FUIStyleCheckBoxLayout Layout;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleCheckBox
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FUIStyleCheckBox()
|
||||
{
|
||||
Fills.Add(TEXT("Default"), FUIStyleCheckBoxFill());
|
||||
Strokes.Add(TEXT("Default"), FUIStyleCheckBoxStroke());
|
||||
Layouts.Add(TEXT("Default"), FUIStyleCheckBoxLayout());
|
||||
}
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
TMap<FName, FUIStyleCheckBoxFill> Fills;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
TMap<FName, FUIStyleCheckBoxStroke> Strokes;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox")
|
||||
TMap<FName, FUIStyleCheckBoxLayout> Layouts;
|
||||
};
|
||||
|
||||
// =====================================================================
|
||||
// FUIStyleInput — padrão multi-map (estilo Hyper): sub-mapas independentes
|
||||
// por categoria visual (Backgrounds, Strokes, Typography). O widget escolhe
|
||||
// um nome único de variante (ex.: "Box") que existe em CADA sub-mapa;
|
||||
// reaproveita-se sem dor (ex.: mesma Stroke em vários Backgrounds).
|
||||
// =====================================================================
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleInputBackground
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Brush completo do background (Image, Image Size, Matiz, Desenhar como,
|
||||
* Ladrilhos, Margin/9-slice, UV). Mesma UI de FSlateBrush exposta no UMG.
|
||||
* Se Brush.ResourceObject for nulo, pinta cor sólida (procedural — DrawAs
|
||||
* = Image sem textura).
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FSlateBrush Brush;
|
||||
|
||||
/** Tint multiplicado por cima do Brush em estado de hover. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FLinearColor HoveredColor = FLinearColor::White;
|
||||
|
||||
/** Tint multiplicado por cima do Brush em estado de focus. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FLinearColor FocusedColor = FLinearColor::White;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleInputStroke
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/**
|
||||
* Brush completo da borda (Image, Image Size, Matiz, Desenhar como,
|
||||
* Ladrilhos, Margin). Use textura 9-slice (DrawAs=Box) ou Image para
|
||||
* stroke decorada estilo Hyper Search.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FSlateBrush Brush;
|
||||
|
||||
/** Espessura procedural (efeito visual; usado por widgets que renderizam stroke vector). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0"))
|
||||
float BorderThickness = 1.f;
|
||||
|
||||
/** Raio do canto procedural (efeito visual; usado por widgets que renderizam RoundedBox). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0"))
|
||||
float CornerRadius = 6.f;
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleInputTypography
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FLinearColor TextColor = FLinearColor(FColor(244, 240, 230));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FLinearColor HintColor = FLinearColor(FColor(150, 160, 180, 200));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "1"))
|
||||
float Size = 16.f;
|
||||
|
||||
/** Fonte específica desta tipografia. Se vazia, usa FUIStyle.Text.BodyFont. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FSlateFontInfo FontOverride;
|
||||
};
|
||||
|
||||
/**
|
||||
* View "composta" da variante (Background + Stroke + Typography). Não é
|
||||
* editada diretamente no DT — é o que o UUIInput_Base monta ao resolver
|
||||
* a chave Variant nos 3 sub-mapas. Mantida aqui para uso no C++.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleInputVariant
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FUIStyleInputBackground Background;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FUIStyleInputStroke Stroke;
|
||||
|
||||
UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input")
|
||||
FUIStyleInputTypography Typography;
|
||||
};
|
||||
|
||||
/**
|
||||
* Campo de texto — padrão multi-map (Hyper-style). 3 sub-mapas independentes
|
||||
* com a MESMA CHAVE (nome da variante: "Box", "Outline", "Underline",
|
||||
* "Search", ou customizada). O UUIInput_Base usa Variant=FName e busca o
|
||||
* pedaço em cada sub-mapa; assim dá pra reaproveitar (ex.: a mesma Stroke
|
||||
* em variantes com Backgrounds diferentes).
|
||||
*
|
||||
* Construtor já popula com defaults bonitos inspirados na Login.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FUIStyleInput
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
FUIStyleInput()
|
||||
{
|
||||
auto MakeBg = [](const FLinearColor& Tint)
|
||||
{
|
||||
FUIStyleInputBackground B;
|
||||
B.Brush.TintColor = FSlateColor(Tint);
|
||||
B.Brush.DrawAs = ESlateBrushDrawType::Image;
|
||||
return B;
|
||||
};
|
||||
|
||||
// ---- Backgrounds ----
|
||||
Backgrounds.Add(TEXT("Box"), MakeBg(FLinearColor(FColor(9, 13, 24, 235))));
|
||||
Backgrounds.Add(TEXT("Outline"), MakeBg(FLinearColor(0, 0, 0, 0)));
|
||||
Backgrounds.Add(TEXT("Underline"), MakeBg(FLinearColor(0, 0, 0, 0)));
|
||||
Backgrounds.Add(TEXT("Search"), MakeBg(FLinearColor(FColor(17, 24, 42))));
|
||||
|
||||
auto MakeStroke = [](const FLinearColor& Tint, float Thickness, float Radius)
|
||||
{
|
||||
FUIStyleInputStroke S;
|
||||
S.Brush.TintColor = FSlateColor(Tint);
|
||||
S.Brush.DrawAs = ESlateBrushDrawType::Box;
|
||||
S.Brush.Margin = FMargin(4.f);
|
||||
S.BorderThickness = Thickness;
|
||||
S.CornerRadius = Radius;
|
||||
return S;
|
||||
};
|
||||
|
||||
// ---- Strokes ----
|
||||
Strokes.Add(TEXT("Box"), MakeStroke(FLinearColor(FColor(58, 75, 120, 140)), 1.f, 6.f));
|
||||
Strokes.Add(TEXT("Outline"), MakeStroke(FLinearColor(FColor(58, 75, 120)), 1.f, 6.f));
|
||||
Strokes.Add(TEXT("Underline"), MakeStroke(FLinearColor(FColor(58, 75, 120, 160)), 2.f, 0.f));
|
||||
Strokes.Add(TEXT("Search"), MakeStroke(FLinearColor(FColor(58, 75, 120, 140)), 1.f, 18.f));
|
||||
|
||||
// ---- Typography ----
|
||||
FUIStyleInputTypography DefaultType;
|
||||
DefaultType.TextColor = FLinearColor(FColor(244, 240, 230));
|
||||
DefaultType.HintColor = FLinearColor(FColor(150, 160, 180, 200));
|
||||
DefaultType.Size = 16.f;
|
||||
Typography.Add(TEXT("Box"), DefaultType);
|
||||
Typography.Add(TEXT("Outline"), DefaultType);
|
||||
Typography.Add(TEXT("Underline"), DefaultType);
|
||||
Typography.Add(TEXT("Search"), DefaultType);
|
||||
}
|
||||
|
||||
/** Cores/textura de fundo por variante (chave FName). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
TMap<FName, FUIStyleInputBackground> Backgrounds;
|
||||
|
||||
/** Borda (cor, espessura, arredondamento) por variante. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
TMap<FName, FUIStyleInputStroke> Strokes;
|
||||
|
||||
/** Tipografia (cor de texto/hint, tamanho, fonte) por variante. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input")
|
||||
TMap<FName, FUIStyleInputTypography> Typography;
|
||||
|
||||
/** Padding interno do campo (px, uniforme) — comum a todas variantes. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0"))
|
||||
float Padding = 10.f;
|
||||
|
||||
/** Altura mínima do campo (px). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0"))
|
||||
float MinHeight = 44.f;
|
||||
|
||||
/** Largura mínima PADRÃO do campo (px). Designer mostra esse tamanho. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0"))
|
||||
float MinWidth = 240.f;
|
||||
};
|
||||
|
||||
@@ -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. */
|
||||
|
||||
92
Source/ZMMO/Data/World/MapDef.h
Normal file
92
Source/ZMMO/Data/World/MapDef.h
Normal file
@@ -0,0 +1,92 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Engine/World.h"
|
||||
#include "ZoneRow.h"
|
||||
#include "MapDef.generated.h"
|
||||
|
||||
/**
|
||||
* FZMMOMapSpawn
|
||||
*
|
||||
* Ponto de spawn dentro de um mapa, identificado por `Tag`. Usado pelo
|
||||
* server pra decidir onde materializar um char quando ele entra:
|
||||
* - Char sem posicao salva: usa spawn com tag="default"
|
||||
* - Char saindo de raid: pode pedir tag="raid_return"
|
||||
* - Char morto: pode pedir tag="graveyard"
|
||||
*
|
||||
* ZeusEditorTools pode auto-popular o `Spawns[]` do FZMMOMapDef varrendo
|
||||
* todos APlayerStart (ou ator custom AZMMOMapSpawnPoint) presentes no
|
||||
* `.umap` do `ClientLevel` — botao "Sync from level".
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FZMMOMapSpawn
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** Identificador do spawn (ex.: "default", "raid_return"). "default" e obrigatorio. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
|
||||
FName Tag = TEXT("default");
|
||||
|
||||
/** Posicao em cm (espaco do mundo). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
|
||||
FVector PositionCm = FVector::ZeroVector;
|
||||
|
||||
/** Yaw em graus. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn")
|
||||
float YawDeg = 0.f;
|
||||
};
|
||||
|
||||
/**
|
||||
* FZMMOMapDef
|
||||
*
|
||||
* Definicao canonica de um mapa do MMO. Linha da DT_Maps no cliente.
|
||||
* Exportada para `Server/ZeusServerEngine/Config/DataTables/maps_config.json`
|
||||
* pelo plugin ZeusEditorTools — o JSON e a fonte que o WorldServer consome.
|
||||
*
|
||||
* Identidade:
|
||||
* - `MapId` (uint16) e a chave estavel cross-cliente/server. 0 = invalido
|
||||
* (detecta bug de leitura de DB / migration esquecida).
|
||||
* - RowName na DataTable e o nome interno (ex.: "TestWorld") — usado no
|
||||
* exporter como `name` do JSON. Nao duplicamos em campo `Name`.
|
||||
*
|
||||
* Wire economy:
|
||||
* - `S_CHAR_SELECT_OK` envia `mapId` como uint16 (2 bytes) em vez de
|
||||
* `mapName` (string), evitando ~10-30 bytes por handoff.
|
||||
*/
|
||||
USTRUCT(BlueprintType)
|
||||
struct ZMMO_API FZMMOMapDef : public FTableRowBase
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
/** ID estavel uint16. 0 = invalido. int32 aqui pois UE nao expoe uint16 em UPROPERTY. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map", meta = (ClampMin = "1", ClampMax = "65535"))
|
||||
int32 MapId = 0;
|
||||
|
||||
/** Nome de exibicao localizavel. Aparece em UI/loading screen. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map")
|
||||
FText DisplayName;
|
||||
|
||||
/** Asset do level no cliente. Exporter converte pra string canonica no JSON do server. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Level")
|
||||
TSoftObjectPtr<UWorld> ClientLevel;
|
||||
|
||||
/**
|
||||
* Pontos de spawn taggeados. Editavel inline na DataTable;
|
||||
* ZeusEditorTools pode auto-popular varrendo PlayerStart do .umap.
|
||||
* Pelo menos um com Tag="default" e esperado.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Spawns")
|
||||
TArray<FZMMOMapSpawn> Spawns;
|
||||
|
||||
/** Regras PvP/Safe/Dungeon. Reusa enum de FZoneRow. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map")
|
||||
EZoneRules Rules = EZoneRules::PvE;
|
||||
|
||||
/** Faixa de level recomendada — display only. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display")
|
||||
int32 RecommendedLevelMin = 1;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display")
|
||||
int32 RecommendedLevelMax = 99;
|
||||
};
|
||||
@@ -34,6 +34,20 @@ void AZMMOPlayerController::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
if (IsLocalPlayerController())
|
||||
{
|
||||
// Limpa estado de input herdado da UI do FrontEnd (UIManagerSubsystem
|
||||
// e LocalPlayerSubsystem — sobrevive ao travel, e o controller anterior
|
||||
// (AZMMOFrontEndPlayerController) tinha setado FInputModeUIOnly +
|
||||
// bShowMouseCursor=true). Sem este reset, WASD/Look podem nao chegar
|
||||
// ao pawn porque o Slate ainda esta com foco no widget antigo.
|
||||
FInputModeGameOnly InputMode;
|
||||
InputMode.SetConsumeCaptureMouseDown(true);
|
||||
SetInputMode(InputMode);
|
||||
bShowMouseCursor = false;
|
||||
FlushPressedKeys();
|
||||
}
|
||||
|
||||
if (ShouldUseTouchControls() && IsLocalPlayerController())
|
||||
{
|
||||
MobileControlsWidget = CreateWidget<UUserWidget>(this, MobileControlsWidgetClass);
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "ZMMO.h"
|
||||
#include "ZMMOWorldSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h"
|
||||
|
||||
DEFINE_LOG_CATEGORY(LogZMMOPlayer);
|
||||
|
||||
@@ -108,6 +109,31 @@ void AZMMOPlayerCharacter::BeginPlay()
|
||||
ResolveZeusNetworkSubsystem();
|
||||
BindZeusSpawnDelegate();
|
||||
TryRegisterLocalEntityFromCachedSpawn();
|
||||
|
||||
// Fase 4: reposiciona o pawn na pos salva no DB (vinda no S_CHAR_SELECT_OK
|
||||
// e memorizada no Flow). Substitui o PlayerStart default do level. Se nao
|
||||
// ha pose pendente (primeiro boot / debug PIE direto no level), mantem o
|
||||
// PlayerStart.
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
|
||||
{
|
||||
FVector PosCm = FVector::ZeroVector;
|
||||
float YawDeg = 0.0f;
|
||||
if (Flow->ConsumePendingSpawnPose(PosCm, YawDeg))
|
||||
{
|
||||
const FRotator NewRot(0.0f, YawDeg, 0.0f);
|
||||
SetActorLocationAndRotation(PosCm, NewRot, /*bSweep=*/false, nullptr, ETeleportType::TeleportPhysics);
|
||||
if (AController* C = GetController())
|
||||
{
|
||||
C->SetControlRotation(NewRot);
|
||||
}
|
||||
UE_LOG(LogZMMOPlayer, Log,
|
||||
TEXT("AZMMOPlayerCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"),
|
||||
*PosCm.ToString(), YawDeg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
|
||||
@@ -19,6 +19,15 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
RemoteEntityClasses.Add(EZMMOEntityType::NPC, AZMMOEntity::StaticClass());
|
||||
RemoteEntityClasses.Add(EZMMOEntityType::Object, AZMMOEntity::StaticClass());
|
||||
}
|
||||
// Bind + replay vivem em OnWorldBeginPlay (mundo pronto). Initialize roda
|
||||
// MUITO cedo no pipeline de LoadMap — antes do GameMode ser instanciado
|
||||
// e antes do WorldPartition terminar de carregar cells. Spawns dinamicos
|
||||
// feitos aqui podem ser perdidos durante a fase de setup posterior.
|
||||
}
|
||||
|
||||
void UZMMOWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld)
|
||||
{
|
||||
Super::OnWorldBeginPlay(InWorld);
|
||||
|
||||
if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem())
|
||||
{
|
||||
@@ -26,10 +35,26 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerDespawned);
|
||||
ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerStateUpdate);
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem bound to ZeusNetworkSubsystem delegates."));
|
||||
|
||||
// Catch-up race fix (Fase 3): broadcasts de proxies remotos chegam
|
||||
// enquanto o cliente novo ainda esta em OpenLevel. UZeusNetworkSubsystem
|
||||
// cacheia em PendingRemoteSpawnsByEntity. Aplicamos aqui — mundo ja
|
||||
// esta pronto (GameMode ativo, WorldPartition cells inicializadas).
|
||||
int32 ReplayCount = 0;
|
||||
ZeusNet->ForEachPendingRemoteSpawn(
|
||||
[this, &ReplayCount](const int32 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs)
|
||||
{
|
||||
HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs);
|
||||
++ReplayCount;
|
||||
});
|
||||
if (ReplayCount > 0)
|
||||
{
|
||||
UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at Initialize."));
|
||||
UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay."));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ class ZMMO_API UZMMOWorldSubsystem : public UWorldSubsystem
|
||||
|
||||
public:
|
||||
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
|
||||
virtual void OnWorldBeginPlay(UWorld& InWorld) override;
|
||||
virtual void Deinitialize() override;
|
||||
|
||||
/**
|
||||
|
||||
90
Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h
Normal file
90
Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h
Normal file
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
/**
|
||||
* Opcodes do CharServer (WebSocket) consumidos pelo cliente. Espelha
|
||||
* `Server/ZeusCharServer/src/protocol/CharOpcodes.ts` (faixa 2000-2099).
|
||||
*
|
||||
* Wire: string = uint16 LE (len) + UTF-8; inteiros LE. O cabeçalho de 32B
|
||||
* é responsabilidade do UZeusCharServerSubsystem (Send/OnRawMessage operam
|
||||
* sobre o payload já sem header).
|
||||
*/
|
||||
namespace ZMMOCharOp
|
||||
{
|
||||
// Autenticação — payload começa com uint8 method (ver ZMMOCharAuthMethod)
|
||||
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.
|
||||
*/
|
||||
namespace ZMMOCharAuthMethod
|
||||
{
|
||||
constexpr uint8 TOKEN = 0;
|
||||
constexpr uint8 PASSWORD = 1;
|
||||
}
|
||||
|
||||
/** Espelha `CharRejectReason` em CharOpcodes.ts. */
|
||||
namespace ZMMOCharRejectReason
|
||||
{
|
||||
constexpr uint16 Unknown = 0;
|
||||
constexpr uint16 InvalidToken = 1;
|
||||
constexpr uint16 AccountBanned = 2;
|
||||
constexpr uint16 AlreadyOnline = 3;
|
||||
constexpr uint16 InvalidCredentials = 4;
|
||||
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;
|
||||
}
|
||||
178
Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp
Normal file
178
Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp
Normal file
@@ -0,0 +1,178 @@
|
||||
#include "UIBootScreen_Base.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "UIButton_Base.h"
|
||||
#include "UISpinner_Base.h"
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "CommonTextBlock.h"
|
||||
#include "Components/Border.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "UI/UIStyleRow.h"
|
||||
|
||||
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).
|
||||
FUIStyle ResolveBootStyle(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("UIBootDesign"), false))
|
||||
{
|
||||
DesignStyle = Row->Style;
|
||||
bHas = true;
|
||||
}
|
||||
}
|
||||
return bHas ? DesignStyle : Fallback;
|
||||
}
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
RefreshUIStyle(); // preview do fundo no Designer (DT_UI_Styles "Default")
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::NativeOnActivated()
|
||||
{
|
||||
Super::NativeOnActivated(); // base: RefreshUIStyle + BP_OnScreenActivated
|
||||
|
||||
if (Btn_Start)
|
||||
{
|
||||
Btn_Start->OnClicked().AddUObject(this, &UUIBootScreen_Base::HandleStartClicked);
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* Char = GetCharServer();
|
||||
const bool bConnected = Char && Char->IsConnected();
|
||||
SetReady(bConnected);
|
||||
|
||||
if (Char && !bCharBound)
|
||||
{
|
||||
Char->OnConnected.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnected);
|
||||
Char->OnConnectionFailed.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed);
|
||||
bCharBound = true;
|
||||
}
|
||||
// A conexão em si é disparada pelo UUIFrontEndFlowSubsystem (estado Boot).
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::NativeOnDeactivated()
|
||||
{
|
||||
if (Btn_Start)
|
||||
{
|
||||
Btn_Start->OnClicked().RemoveAll(this);
|
||||
}
|
||||
if (bCharBound)
|
||||
{
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
Char->OnConnected.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnected);
|
||||
Char->OnConnectionFailed.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed);
|
||||
}
|
||||
bCharBound = false;
|
||||
}
|
||||
Super::NativeOnDeactivated();
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::RefreshUIStyle_Implementation()
|
||||
{
|
||||
Super::RefreshUIStyle_Implementation();
|
||||
|
||||
const FUIStyle Fallback;
|
||||
const FUIStyle AS = ResolveBootStyle(this, Fallback);
|
||||
|
||||
if (Border_Bg)
|
||||
{
|
||||
// Fundo data-driven do DT_UI_Styles (T_Boot_Background) — sem
|
||||
// hard-ref de textura no WBP (§5). Fallback p/ cor se vazio.
|
||||
if (AS.ScreenBackground.GetResourceObject() != nullptr)
|
||||
{
|
||||
Border_Bg->SetBrush(AS.ScreenBackground);
|
||||
}
|
||||
else
|
||||
{
|
||||
Border_Bg->SetBrushColor(AS.Palette.Bg0);
|
||||
}
|
||||
}
|
||||
if (Text_Status)
|
||||
{
|
||||
Text_Status->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurfaceMuted));
|
||||
}
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::SetReady(bool bReady)
|
||||
{
|
||||
if (Btn_Start)
|
||||
{
|
||||
// Enquanto conecta: OCULTO (Collapsed, sem ocupar layout). Conectado:
|
||||
// visível e interagível.
|
||||
Btn_Start->SetVisibility(
|
||||
bReady ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
|
||||
Btn_Start->SetIsInteractionEnabled(bReady);
|
||||
}
|
||||
if (Spinner_Connect)
|
||||
{
|
||||
Spinner_Connect->SetVisibility(
|
||||
bReady ? ESlateVisibility::Collapsed : ESlateVisibility::HitTestInvisible);
|
||||
}
|
||||
if (Text_Status)
|
||||
{
|
||||
Text_Status->SetText(bReady ? ReadyText : ConnectingText);
|
||||
}
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* UUIBootScreen_Base::GetCharServer() const
|
||||
{
|
||||
const UGameInstance* GI = GetGameInstance();
|
||||
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::HandleStartClicked()
|
||||
{
|
||||
UZeusCharServerSubsystem* Char = GetCharServer();
|
||||
if (!Char || !Char->IsConnected())
|
||||
{
|
||||
return; // botão só age quando conectado (defensivo)
|
||||
}
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
|
||||
{
|
||||
Flow->RequestEnterLogin();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::HandleCharConnected()
|
||||
{
|
||||
UE_LOG(LogZMMO, Log, TEXT("BootScreen: CharServer conectado — botão liberado."));
|
||||
SetReady(true);
|
||||
}
|
||||
|
||||
void UUIBootScreen_Base::HandleCharConnectionFailed(FString Reason)
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("BootScreen: falha ao conectar ao CharServer: %s"), *Reason);
|
||||
SetReady(false);
|
||||
if (Text_Status)
|
||||
{
|
||||
Text_Status->SetText(FailedText);
|
||||
}
|
||||
}
|
||||
73
Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.h
Normal file
73
Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UIActivatableScreen_Base.h"
|
||||
#include "UIBootScreen_Base.generated.h"
|
||||
|
||||
class UUIButton_Base;
|
||||
class UUISpinner_Base;
|
||||
class UCommonTextBlock;
|
||||
class UBorder;
|
||||
class UZeusCharServerSubsystem;
|
||||
|
||||
/**
|
||||
* Tela de Boot — primeira tela do front-end. Reconstruída a partir do mock
|
||||
* do Zeus UMG Forge com os widgets compartilhados (UI_Button_Master,
|
||||
* UI_Spinner_Master). Camada Abstract; o WBP concreto (WBP_Boot) herda
|
||||
* DIRETO desta classe (ARQUITETURA.md §3.3).
|
||||
*
|
||||
* O UUIFrontEndFlowSubsystem dispara a conexão ao ZeusCharServer (WebSocket)
|
||||
* ao entrar no estado Boot. Esta tela OBSERVA o UZeusCharServerSubsystem:
|
||||
* enquanto não conecta, mostra o spinner e o botão desabilitado; ao conectar,
|
||||
* libera o botão "Iniciar" (clique → Flow::RequestEnterLogin → Login).
|
||||
* Sem hard-ref de asset de tema (§5): cores vêm do FUIStyle.
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class ZMMO_API UUIBootScreen_Base : public UUIActivatableScreen_Base
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
virtual void NativePreConstruct() override;
|
||||
virtual void NativeOnActivated() override;
|
||||
virtual void NativeOnDeactivated() override;
|
||||
virtual void RefreshUIStyle_Implementation() override;
|
||||
|
||||
/** Texto exibido enquanto conecta / quando pronto / em erro. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
|
||||
FText ConnectingText = FText::FromString(TEXT("Conectando ao Gateway"));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
|
||||
FText ReadyText = FText::FromString(TEXT("Pronto"));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot")
|
||||
FText FailedText = FText::FromString(TEXT("Falha ao conectar — tentando novamente"));
|
||||
|
||||
// ---- Widgets do WBP (nomes esperados) ----
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUIButton_Base> Btn_Start;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUISpinner_Base> Spinner_Connect;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCommonTextBlock> Text_Status;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UBorder> Border_Bg;
|
||||
|
||||
private:
|
||||
void SetReady(bool bReady);
|
||||
UZeusCharServerSubsystem* GetCharServer() const;
|
||||
|
||||
UFUNCTION()
|
||||
void HandleStartClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCharConnected();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCharConnectionFailed(FString Reason);
|
||||
|
||||
bool bCharBound = false;
|
||||
};
|
||||
151
Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp
Normal file
151
Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp
Normal 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);
|
||||
}
|
||||
91
Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h
Normal file
91
Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h
Normal 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;
|
||||
};
|
||||
97
Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp
Normal file
97
Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "UICharacterCreatePage_Base.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "CharServerOpcodes.h"
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
#include "WireHelpers.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"
|
||||
|
||||
using ZMMOWire::WriteUuid16;
|
||||
using ZMMOWire::WriteUInt8;
|
||||
using ZMMOWire::WriteUInt16;
|
||||
using ZMMOWire::WriteUInt32;
|
||||
using ZMMOWire::WriteStringUtf8;
|
||||
|
||||
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();
|
||||
}
|
||||
58
Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h
Normal file
58
Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h
Normal 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;
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "Data/World/MapDef.h"
|
||||
#include "UIFrontEndScreenSet.h"
|
||||
#include "UIManagerSubsystem.h"
|
||||
#include "UIPrimaryGameLayout_Base.h"
|
||||
@@ -8,7 +9,9 @@
|
||||
#include "ZMMOGameInstance.h"
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "CommonActivatableWidget.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/LocalPlayer.h"
|
||||
#include "UObject/UObjectGlobals.h"
|
||||
@@ -54,22 +57,15 @@ void UUIFrontEndFlowSubsystem::StartFrontEnd()
|
||||
|
||||
SetState(EZMMOFrontEndState::Boot);
|
||||
|
||||
// Substitui o auto-connect do GameInstance: o fluxo dirige a conexão.
|
||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||
// Pré-login fala com o ZeusCharServer (WebSocket), NÃO com o world server
|
||||
// UDP. A tela Boot observa o CharServer e libera o botão ao conectar.
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
FString Host = TEXT("127.0.0.1");
|
||||
int32 Port = 27777;
|
||||
if (const UZMMOGameInstance* GI = Cast<UZMMOGameInstance>(GetGameInstance()))
|
||||
{
|
||||
Host = GI->ZeusServerHost;
|
||||
Port = GI->ZeusServerPort;
|
||||
}
|
||||
SetState(EZMMOFrontEndState::Connecting);
|
||||
Zeus->ConnectToZeusServer(Host, Port);
|
||||
Char->ConnectToDefaultCharServer(); // lê CharServerUrl de Project Settings
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusNetworkSubsystem indisponível; sem conexão."));
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusCharServerSubsystem indisponível; sem conexão de Boot."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +156,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;
|
||||
@@ -221,6 +221,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const
|
||||
return GI ? GI->GetSubsystem<UZeusNetworkSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const
|
||||
{
|
||||
const UGameInstance* GI = GetGameInstance();
|
||||
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
UUIManagerSubsystem* UUIFrontEndFlowSubsystem::GetUIManager() const
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
@@ -257,14 +263,19 @@ void UUIFrontEndFlowSubsystem::BindNetwork()
|
||||
{
|
||||
return;
|
||||
}
|
||||
// CharServer (WebSocket) dirige o pré-login: Boot→Login.
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
Char->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
||||
Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||
Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
||||
}
|
||||
// UDP (world server) só interessa para o handoff de travel.
|
||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||
{
|
||||
Zeus->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
||||
Zeus->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||
Zeus->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected);
|
||||
Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
||||
bNetBound = true;
|
||||
}
|
||||
bNetBound = true;
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
||||
@@ -273,11 +284,14 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
Char->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
||||
Char->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||
Char->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected);
|
||||
}
|
||||
if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork())
|
||||
{
|
||||
Zeus->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected);
|
||||
Zeus->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed);
|
||||
Zeus->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected);
|
||||
Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested);
|
||||
}
|
||||
bNetBound = false;
|
||||
@@ -285,25 +299,66 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork()
|
||||
|
||||
void UUIFrontEndFlowSubsystem::HandleConnected()
|
||||
{
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: conectado → Login."));
|
||||
SetState(EZMMOFrontEndState::Login);
|
||||
// NÃO auto-avança: a tela Boot libera o botão "Iniciar"; o usuário clica
|
||||
// (→ RequestEnterLogin). Mantém o estado Boot.
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer conectado (estado %s)."),
|
||||
*UEnum::GetValueAsString(CurrentState));
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::HandleConnectionFailed(FString Reason)
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão falhou (%s) → volta a Login."), *Reason);
|
||||
SetState(EZMMOFrontEndState::Login);
|
||||
// Permanece em Boot; a tela mostra erro e o CharServer pode re-tentar.
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão ao CharServer falhou (%s)."), *Reason);
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::HandleDisconnected()
|
||||
void UUIFrontEndFlowSubsystem::HandleCharDisconnected(int32 StatusCode, FString Reason)
|
||||
{
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: desconectado."));
|
||||
if (CurrentState != EZMMOFrontEndState::InWorld)
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer desconectou (code=%d, %s)."),
|
||||
StatusCode, *Reason);
|
||||
// Se caiu antes de entrar no mundo, volta a Boot para reconectar.
|
||||
if (CurrentState != EZMMOFrontEndState::InWorld &&
|
||||
CurrentState != EZMMOFrontEndState::EnteringWorld &&
|
||||
CurrentState != EZMMOFrontEndState::Boot)
|
||||
{
|
||||
SetState(EZMMOFrontEndState::Boot);
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
Char->ConnectToDefaultCharServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::RequestEnterLogin()
|
||||
{
|
||||
if (CurrentState == EZMMOFrontEndState::Boot)
|
||||
{
|
||||
SetState(EZMMOFrontEndState::Login);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -312,7 +367,15 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam
|
||||
|
||||
if (!MapPath.IsEmpty())
|
||||
{
|
||||
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath));
|
||||
// Forca ZMMOGameMode em qualquer mapa que venha do WorldServer.
|
||||
// World Settings do .umap podem ter override herdado de L_FrontEnd
|
||||
// (GameModeOverride=ZMMOFrontEndGameMode) — sobrescrevemos via URL
|
||||
// option pra nao depender de cada artista configurar.
|
||||
// Evolucao natural: quando o WorldServer enviar GameMode no
|
||||
// S_TRAVEL_TO_MAP (wire estendido), trocar este literal por
|
||||
// `?game=` + valor recebido do server.
|
||||
const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode");
|
||||
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,3 +400,70 @@ void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const FZMMOMapDef* UUIFrontEndFlowSubsystem::FindMapDef(int32 MapId) const
|
||||
{
|
||||
if (MapId <= 0) return nullptr;
|
||||
if (MapsTableAsset.IsNull())
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: MapsTableAsset nao configurado — adicione DT_Maps em DefaultGame.ini"));
|
||||
return nullptr;
|
||||
}
|
||||
const UDataTable* Table = MapsTableAsset.LoadSynchronous();
|
||||
if (!Table) return nullptr;
|
||||
|
||||
for (const TPair<FName, uint8*>& Row : Table->GetRowMap())
|
||||
{
|
||||
const FZMMOMapDef* Def = reinterpret_cast<const FZMMOMapDef*>(Row.Value);
|
||||
if (Def && Def->MapId == MapId)
|
||||
{
|
||||
return Def;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
FString UUIFrontEndFlowSubsystem::ResolveLevelPathByMapId(int32 MapId) const
|
||||
{
|
||||
const FZMMOMapDef* Def = FindMapDef(MapId);
|
||||
if (!Def) return FString();
|
||||
const FSoftObjectPath ObjPath = Def->ClientLevel.ToSoftObjectPath();
|
||||
if (!ObjPath.IsValid()) return FString();
|
||||
return ObjPath.GetLongPackageName();
|
||||
}
|
||||
|
||||
void UUIFrontEndFlowSubsystem::SetPendingSpawnPose(FVector PosCm, float YawDeg)
|
||||
{
|
||||
PendingSpawnPosCm = PosCm;
|
||||
PendingSpawnYawDeg = YawDeg;
|
||||
bHasPendingSpawnPose = true;
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose set pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
|
||||
PosCm.X, PosCm.Y, PosCm.Z, YawDeg);
|
||||
}
|
||||
|
||||
bool UUIFrontEndFlowSubsystem::ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg)
|
||||
{
|
||||
if (!bHasPendingSpawnPose) return false;
|
||||
OutPosCm = PendingSpawnPosCm;
|
||||
OutYawDeg = PendingSpawnYawDeg;
|
||||
bHasPendingSpawnPose = false;
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose consumido pos=(%.1f,%.1f,%.1f) yaw=%.1f"),
|
||||
OutPosCm.X, OutPosCm.Y, OutPosCm.Z, OutYawDeg);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UUIFrontEndFlowSubsystem::TravelToMapById(int32 MapId)
|
||||
{
|
||||
const FString MapPath = ResolveLevelPathByMapId(MapId);
|
||||
if (MapPath.IsEmpty())
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: TravelToMapById(%d) falhou — sem entrada no DT_Maps"), MapId);
|
||||
return false;
|
||||
}
|
||||
UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: TravelToMapById(%d) -> %s"), MapId, *MapPath);
|
||||
bTravelingToWorld = true;
|
||||
SetState(EZMMOFrontEndState::EnteringWorld);
|
||||
const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode");
|
||||
UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Subsystems/GameInstanceSubsystem.h"
|
||||
#include "UI/FrontEndTypes.h"
|
||||
#include "UIFrontEndFlowSubsystem.generated.h"
|
||||
@@ -8,6 +9,8 @@
|
||||
class UUIFrontEndScreenSet;
|
||||
class UUIManagerSubsystem;
|
||||
class UZeusNetworkSubsystem;
|
||||
class UZeusCharServerSubsystem;
|
||||
struct FZMMOMapDef;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOFrontEndStateChanged, EZMMOFrontEndState, NewState);
|
||||
|
||||
@@ -57,6 +60,64 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "FrontEnd")
|
||||
bool RequestBack();
|
||||
|
||||
/** Chamado pela tela Boot quando o usuário clica "Iniciar" (já conectado). */
|
||||
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();
|
||||
|
||||
/**
|
||||
* Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps.
|
||||
* Carrega o asset sync (PIE/dev OK; em prod considerar StreamableManager).
|
||||
* Retorna nullptr se MapId=0, DT_Maps nao configurado, ou mapId nao
|
||||
* encontrado.
|
||||
*/
|
||||
const FZMMOMapDef* FindMapDef(int32 MapId) const;
|
||||
|
||||
/**
|
||||
* Helper: resolve `MapId` no DT_Maps e retorna `ClientLevel.ToSoftObjectPath().GetLongPackageName()`
|
||||
* (ex.: `/Game/ThirdPerson/TestWorld`). String vazia se nao encontrado.
|
||||
*/
|
||||
UFUNCTION(BlueprintPure, Category = "FrontEnd|Maps")
|
||||
FString ResolveLevelPathByMapId(int32 MapId) const;
|
||||
|
||||
/**
|
||||
* Inicia transicao pra mundo dado um `MapId`: faz `OpenLevel` com
|
||||
* `?game=/Script/ZMMO.ZMMOGameMode`. No-op se `MapId` invalido ou
|
||||
* DT_Maps nao tem entrada.
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Maps")
|
||||
bool TravelToMapById(int32 MapId);
|
||||
|
||||
/**
|
||||
* Memoriza a pose autoritativa do char vinda no `S_CHAR_SELECT_OK`
|
||||
* (pos salva no DB + yaw). Sobrevive ao travel porque vivemos no
|
||||
* GameInstance. Lida pelo `AZMMOPlayerCharacter::BeginPlay` pra
|
||||
* reposicionar o pawn local em vez de spawnar no PlayerStart default
|
||||
* do level. Limpa apos consumo (a proxima sessao reenvia).
|
||||
*/
|
||||
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn")
|
||||
void SetPendingSpawnPose(FVector PosCm, float YawDeg);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn")
|
||||
bool ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg);
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "FrontEnd")
|
||||
FOnZMMOFrontEndStateChanged OnStateChanged;
|
||||
|
||||
@@ -65,10 +126,20 @@ protected:
|
||||
UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd")
|
||||
TSoftObjectPtr<UUIFrontEndScreenSet> ScreenSetAsset;
|
||||
|
||||
/**
|
||||
* DataTable com FZMMOMapDef. Source of truth client-side para
|
||||
* `MapId -> ClientLevel/spawns/displayName`. Configure em DefaultGame.ini:
|
||||
* [/Script/ZMMO.UIFrontEndFlowSubsystem]
|
||||
* MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps
|
||||
*/
|
||||
UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps")
|
||||
TSoftObjectPtr<UDataTable> MapsTableAsset;
|
||||
|
||||
private:
|
||||
void BindNetwork();
|
||||
void UnbindNetwork();
|
||||
UZeusNetworkSubsystem* GetZeusNetwork() const;
|
||||
UZeusCharServerSubsystem* GetCharServer() const;
|
||||
UUIManagerSubsystem* GetUIManager() const;
|
||||
UUIFrontEndScreenSet* GetScreenSet();
|
||||
|
||||
@@ -82,7 +153,7 @@ private:
|
||||
void HandleConnectionFailed(FString Reason);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleDisconnected();
|
||||
void HandleCharDisconnected(int32 StatusCode, FString Reason);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleServerTravelRequested(const FString& MapName, const FString& MapPath);
|
||||
@@ -100,6 +171,19 @@ private:
|
||||
UPROPERTY(Transient)
|
||||
EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
FString SelectedWorldId;
|
||||
|
||||
/** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */
|
||||
UPROPERTY(Transient)
|
||||
FVector PendingSpawnPosCm = FVector::ZeroVector;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
float PendingSpawnYawDeg = 0.f;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
bool bHasPendingSpawnPose = false;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
TObjectPtr<UUIFrontEndScreenSet> ScreenSet;
|
||||
|
||||
|
||||
340
Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.cpp
Normal file
340
Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.cpp
Normal file
@@ -0,0 +1,340 @@
|
||||
#include "UILoginScreen_Base.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "CharServerOpcodes.h"
|
||||
#include "UIButton_Base.h"
|
||||
#include "UICheckBox_Base.h"
|
||||
#include "UILabel_Base.h"
|
||||
#include "ZMMOLoginSaveGame.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "ZeusPacketWriter.h"
|
||||
#include "ZeusPacketReader.h"
|
||||
#include "CommonTextBlock.h"
|
||||
#include "Components/Border.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "UI/UIStyleRow.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Mesmo helper da Boot: runtime usa o tema ativo; design-time carrega a
|
||||
// row "Default" de DT_UI_Styles para o Designer ver o fundo.
|
||||
FUIStyle ResolveLoginStyle(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("UILoginDesign"), false))
|
||||
{
|
||||
DesignStyle = Row->Style;
|
||||
bHas = true;
|
||||
}
|
||||
}
|
||||
return bHas ? DesignStyle : Fallback;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
RefreshUIStyle(); // preview do fundo no Designer (DT_UI_Styles "Default")
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::NativeOnActivated()
|
||||
{
|
||||
Super::NativeOnActivated(); // base: RefreshUIStyle + BP_OnScreenActivated
|
||||
|
||||
if (Btn_Login)
|
||||
{
|
||||
Btn_Login->OnClicked().AddUObject(this, &UUILoginScreen_Base::HandleLoginClicked);
|
||||
}
|
||||
if (Btn_Back)
|
||||
{
|
||||
Btn_Back->OnClicked().AddUObject(this, &UUILoginScreen_Base::HandleBackClicked);
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* Char = GetCharServer();
|
||||
if (Char && !bRawBound)
|
||||
{
|
||||
Char->OnRawMessage.AddDynamic(this, &UUILoginScreen_Base::HandleCharRawMessage);
|
||||
bRawBound = true;
|
||||
}
|
||||
|
||||
bAwaitingAuth = false;
|
||||
const bool bConnected = Char && Char->IsConnected();
|
||||
SetBusy(false);
|
||||
if (Btn_Login)
|
||||
{
|
||||
Btn_Login->SetIsInteractionEnabled(bConnected);
|
||||
}
|
||||
SetStatus(bConnected ? IdleText : NotConnectedText);
|
||||
|
||||
// "Lembrar Acesso": tenta restaurar usuário salvo (async, não trava UI).
|
||||
LoadRememberedAccess();
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::NativeOnDeactivated()
|
||||
{
|
||||
if (Btn_Login)
|
||||
{
|
||||
Btn_Login->OnClicked().RemoveAll(this);
|
||||
}
|
||||
if (Btn_Back)
|
||||
{
|
||||
Btn_Back->OnClicked().RemoveAll(this);
|
||||
}
|
||||
if (bRawBound)
|
||||
{
|
||||
if (UZeusCharServerSubsystem* Char = GetCharServer())
|
||||
{
|
||||
Char->OnRawMessage.RemoveDynamic(this, &UUILoginScreen_Base::HandleCharRawMessage);
|
||||
}
|
||||
bRawBound = false;
|
||||
}
|
||||
Super::NativeOnDeactivated();
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::RefreshUIStyle_Implementation()
|
||||
{
|
||||
Super::RefreshUIStyle_Implementation();
|
||||
|
||||
const FUIStyle Fallback;
|
||||
const FUIStyle AS = ResolveLoginStyle(this, Fallback);
|
||||
|
||||
if (Border_Bg)
|
||||
{
|
||||
// Fundo data-driven (DT_UI_Styles) — sem hard-ref de textura no WBP
|
||||
// (§5). Fallback p/ cor se vazio.
|
||||
if (AS.ScreenBackground.GetResourceObject() != nullptr)
|
||||
{
|
||||
Border_Bg->SetBrush(AS.ScreenBackground);
|
||||
}
|
||||
else
|
||||
{
|
||||
Border_Bg->SetBrushColor(AS.Palette.Bg0);
|
||||
}
|
||||
}
|
||||
if (Text_Status)
|
||||
{
|
||||
Text_Status->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurfaceMuted));
|
||||
}
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* UUILoginScreen_Base::GetCharServer() const
|
||||
{
|
||||
const UGameInstance* GI = GetGameInstance();
|
||||
return GI ? GI->GetSubsystem<UZeusCharServerSubsystem>() : nullptr;
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::SetBusy(bool bBusy)
|
||||
{
|
||||
if (Btn_Login)
|
||||
{
|
||||
Btn_Login->SetIsInteractionEnabled(!bBusy);
|
||||
}
|
||||
if (Input_User)
|
||||
{
|
||||
Input_User->SetIsEnabled(!bBusy);
|
||||
}
|
||||
if (Input_Password)
|
||||
{
|
||||
Input_Password->SetIsEnabled(!bBusy);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::SetStatus(const FText& InText)
|
||||
{
|
||||
if (Text_Status)
|
||||
{
|
||||
Text_Status->SetText(InText);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::HandleLoginClicked()
|
||||
{
|
||||
if (bAwaitingAuth)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UZeusCharServerSubsystem* Char = GetCharServer();
|
||||
if (!Char || !Char->IsConnected())
|
||||
{
|
||||
SetStatus(NotConnectedText);
|
||||
return;
|
||||
}
|
||||
|
||||
const FString User = Input_User ? Input_User->GetText().ToString() : FString();
|
||||
const FString Pass = Input_Password ? Input_Password->GetText().ToString() : FString();
|
||||
if (User.TrimStartAndEnd().IsEmpty() || Pass.IsEmpty())
|
||||
{
|
||||
SetStatus(EmptyFieldsText);
|
||||
return;
|
||||
}
|
||||
|
||||
// "Lembrar Acesso": salva/apaga ANTES do envio do auth — atende a
|
||||
// expectativa "marquei → vai lembrar próxima vez" independente do
|
||||
// resultado da autenticação (senha errada ainda lembra do usuário,
|
||||
// padrão dos launchers comerciais).
|
||||
SaveOrClearRememberedAccess(User);
|
||||
|
||||
FZeusPacketWriter W;
|
||||
W.WriteUInt8(ZMMOCharAuthMethod::PASSWORD);
|
||||
W.WriteStringUtf8(User.TrimStartAndEnd());
|
||||
W.WriteStringUtf8(Pass);
|
||||
|
||||
const bool bSent = Char->SendCharRequest(
|
||||
ZMMOCharOp::C_CHAR_AUTH_REQUEST, W.GetBuffer());
|
||||
if (!bSent)
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("Login: falha ao enviar C_CHAR_AUTH_REQUEST."));
|
||||
SetStatus(NotConnectedText);
|
||||
return;
|
||||
}
|
||||
|
||||
bAwaitingAuth = true;
|
||||
SetBusy(true);
|
||||
SetStatus(AuthenticatingText);
|
||||
UE_LOG(LogZMMO, Log, TEXT("Login: C_CHAR_AUTH_REQUEST enviado (PASSWORD)."));
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::HandleBackClicked()
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
|
||||
{
|
||||
Flow->RequestBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload)
|
||||
{
|
||||
if (!bAwaitingAuth)
|
||||
{
|
||||
return; // só nos interessa a resposta do auth que disparamos
|
||||
}
|
||||
|
||||
if (Opcode == ZMMOCharOp::S_CHAR_AUTH_OK)
|
||||
{
|
||||
bAwaitingAuth = false;
|
||||
FZeusPacketReader R(Payload.GetData(), Payload.Num());
|
||||
FString AccountId, Username;
|
||||
R.ReadStringUtf8(AccountId);
|
||||
R.ReadStringUtf8(Username);
|
||||
uint64 ExpiresMs = 0;
|
||||
R.ReadUInt64(ExpiresMs);
|
||||
UE_LOG(LogZMMO, Log, TEXT("Login: S_CHAR_AUTH_OK (account=%s user=%s) → ServerSelect."),
|
||||
*AccountId, *Username);
|
||||
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>())
|
||||
{
|
||||
Flow->RequestEnterServerSelect();
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (Opcode == ZMMOCharOp::S_CHAR_AUTH_REJECT)
|
||||
{
|
||||
bAwaitingAuth = false;
|
||||
FZeusPacketReader R(Payload.GetData(), Payload.Num());
|
||||
uint16 Reason = 0;
|
||||
R.ReadUInt16(Reason);
|
||||
UE_LOG(LogZMMO, Warning, TEXT("Login: S_CHAR_AUTH_REJECT (reason=%u)."), Reason);
|
||||
SetBusy(false);
|
||||
|
||||
FText Message;
|
||||
switch (Reason)
|
||||
{
|
||||
case ZMMOCharRejectReason::InvalidCredentials: Message = InvalidCredentialsText; break;
|
||||
case ZMMOCharRejectReason::AccountLocked: Message = AccountLockedText; break;
|
||||
case ZMMOCharRejectReason::AccountSuspended: Message = AccountSuspendedText; break;
|
||||
case ZMMOCharRejectReason::AccountBanned: Message = AccountSuspendedText; break;
|
||||
case ZMMOCharRejectReason::CredentialsAuthDisabled: Message = CredentialsDisabledText; break;
|
||||
default:
|
||||
Message = FText::Format(
|
||||
NSLOCTEXT("ZMMO", "LoginRejectFmt", "{0} (cód. {1})"),
|
||||
RejectedText, FText::AsNumber(Reason));
|
||||
break;
|
||||
}
|
||||
SetStatus(Message);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::LoadRememberedAccess()
|
||||
{
|
||||
if (!UGameplayStatics::DoesSaveGameExist(
|
||||
UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FAsyncLoadGameFromSlotDelegate Delegate;
|
||||
Delegate.BindUFunction(this,
|
||||
FName(TEXT("HandleLoadLoginCacheCompleted")));
|
||||
UGameplayStatics::AsyncLoadGameFromSlot(
|
||||
UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex, Delegate);
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::HandleLoadLoginCacheCompleted(const FString& /*InSlotName*/,
|
||||
int32 /*InUserIndex*/, USaveGame* LoadedGame)
|
||||
{
|
||||
const UZMMOLoginSaveGame* Save = Cast<UZMMOLoginSaveGame>(LoadedGame);
|
||||
if (!Save || !Save->bRememberAccess || Save->SavedUsername.IsEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (Input_User)
|
||||
{
|
||||
Input_User->SetText(FText::FromString(Save->SavedUsername));
|
||||
}
|
||||
if (RememberAccess)
|
||||
{
|
||||
RememberAccess->SetIsChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILoginScreen_Base::SaveOrClearRememberedAccess(const FString& User)
|
||||
{
|
||||
const bool bRemember = RememberAccess ? RememberAccess->IsChecked() : false;
|
||||
if (bRemember)
|
||||
{
|
||||
UZMMOLoginSaveGame* Save = Cast<UZMMOLoginSaveGame>(
|
||||
UGameplayStatics::CreateSaveGameObject(UZMMOLoginSaveGame::StaticClass()));
|
||||
if (Save)
|
||||
{
|
||||
Save->SavedUsername = User.TrimStartAndEnd();
|
||||
Save->bRememberAccess = true;
|
||||
UGameplayStatics::AsyncSaveGameToSlot(
|
||||
Save, UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex);
|
||||
}
|
||||
}
|
||||
else if (UGameplayStatics::DoesSaveGameExist(
|
||||
UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex))
|
||||
{
|
||||
// Checkbox desmarcada: apaga save antigo (não vamos lembrar mais).
|
||||
UGameplayStatics::DeleteGameInSlot(
|
||||
UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex);
|
||||
}
|
||||
}
|
||||
119
Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.h
Normal file
119
Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.h
Normal file
@@ -0,0 +1,119 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "UIActivatableScreen_Base.h"
|
||||
#include "UILoginScreen_Base.generated.h"
|
||||
|
||||
class UUIButton_Base;
|
||||
class UUICheckBox_Base;
|
||||
class UUILabel_Base;
|
||||
class UCommonTextBlock;
|
||||
class UBorder;
|
||||
class USaveGame;
|
||||
class UZeusCharServerSubsystem;
|
||||
|
||||
/**
|
||||
* Tela de Login — reconstruída a partir do mock do Zeus UMG Forge
|
||||
* (WBP_LoginScreen) com os widgets compartilhados (UI_Button_Master,
|
||||
* UI_CheckBox_Master). Camada Abstract; o WBP concreto (WBP_Login) herda
|
||||
* DIRETO desta classe (ARQUITETURA.md §3.3).
|
||||
*
|
||||
* Auth: ao clicar "Entrar", envia C_CHAR_AUTH_REQUEST (opcode 2000) pelo
|
||||
* WebSocket do UZeusCharServerSubsystem como `uint8(PASSWORD) +
|
||||
* string(user) + string(password)`. CharServer chama AccountService.login
|
||||
* (Argon2id + lockout). S_CHAR_AUTH_OK → Flow::RequestEnterServerSelect;
|
||||
* reject (InvalidCredentials/AccountLocked/Suspended/etc.) → status
|
||||
* mapeado. Sem hard-ref de asset de tema (§5). Contas são criadas no
|
||||
* servidor via `npm run account`.
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class ZMMO_API UUILoginScreen_Base : public UUIActivatableScreen_Base
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
protected:
|
||||
virtual void NativePreConstruct() override;
|
||||
virtual void NativeOnActivated() override;
|
||||
virtual void NativeOnDeactivated() override;
|
||||
virtual void RefreshUIStyle_Implementation() override;
|
||||
|
||||
/** Textos de status/feedback da autenticação. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText IdleText = FText::FromString(TEXT("Acesso protegido pelo Farol da Manhã."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText AuthenticatingText = FText::FromString(TEXT("Autenticando…"));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText NotConnectedText = FText::FromString(TEXT("Sem conexão com o Gateway."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText EmptyFieldsText = FText::FromString(TEXT("Informe usuário e senha."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText RejectedText = FText::FromString(TEXT("Acesso negado pelo Gateway."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText InvalidCredentialsText = FText::FromString(TEXT("Usuário ou senha inválidos."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText AccountLockedText = FText::FromString(TEXT("Conta bloqueada por excesso de tentativas. Aguarde e tente novamente."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText AccountSuspendedText = FText::FromString(TEXT("Conta suspensa. Contate o suporte."));
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login")
|
||||
FText CredentialsDisabledText = FText::FromString(TEXT("Autenticação por credenciais indisponível no servidor."));
|
||||
|
||||
// ---- Widgets do WBP (nomes esperados) ----
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UBorder> Border_Bg;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUILabel_Base> Input_User;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUILabel_Base> Input_Password;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUICheckBox_Base> RememberAccess;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUIButton_Base> Btn_Login;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUIButton_Base> Btn_Back;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCommonTextBlock> Text_Status;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCommonTextBlock> Text_Version;
|
||||
|
||||
private:
|
||||
UZeusCharServerSubsystem* GetCharServer() const;
|
||||
void SetBusy(bool bBusy);
|
||||
void SetStatus(const FText& InText);
|
||||
|
||||
void HandleLoginClicked();
|
||||
void HandleBackClicked();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleCharRawMessage(int32 Opcode, const TArray<uint8>& Payload);
|
||||
|
||||
/** "Lembrar Acesso" — async load do cache local (LoginCache slot) e
|
||||
popula Input_User + Check_Remember se houver entrada salva. */
|
||||
void LoadRememberedAccess();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleLoadLoginCacheCompleted(const FString& InSlotName, int32 InUserIndex, USaveGame* LoadedGame);
|
||||
|
||||
/** Salva (se checkbox marcada) ou apaga (se desmarcada) o cache de Login.
|
||||
Chamado em HandleLoginClicked — antes do envio do auth, atende a
|
||||
expectativa "marquei → vai lembrar próxima vez" independente do
|
||||
resultado da autenticação. */
|
||||
void SaveOrClearRememberedAccess(const FString& User);
|
||||
|
||||
bool bRawBound = false;
|
||||
bool bAwaitingAuth = false;
|
||||
};
|
||||
92
Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp
Normal file
92
Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
74
Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h
Normal file
74
Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h
Normal 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;
|
||||
};
|
||||
331
Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp
Normal file
331
Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp
Normal file
@@ -0,0 +1,331 @@
|
||||
#include "UIServerSelectScreen_Base.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "CharServerOpcodes.h"
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
#include "UIServerCard_Base.h"
|
||||
#include "WireHelpers.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"
|
||||
|
||||
using ZMMOWire::ReadU8;
|
||||
using ZMMOWire::ReadU16;
|
||||
using ZMMOWire::ReadStringUtf8;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
82
Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h
Normal file
82
Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h
Normal 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;
|
||||
};
|
||||
419
Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp
Normal file
419
Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp
Normal file
@@ -0,0 +1,419 @@
|
||||
#include "UIUserLobbyScreen_Base.h"
|
||||
|
||||
#include "ZMMO.h"
|
||||
#include "CharServerOpcodes.h"
|
||||
#include "UIFrontEndFlowSubsystem.h"
|
||||
#include "UICharCard_Base.h"
|
||||
#include "UICharacterCreatePage_Base.h"
|
||||
#include "WireHelpers.h"
|
||||
#include "ZeusCharServerSubsystem.h"
|
||||
#include "ZeusNetworkSubsystem.h"
|
||||
#include "CommonTextBlock.h"
|
||||
#include "Components/PanelWidget.h"
|
||||
#include "Components/WidgetSwitcher.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "UI/Widgets/UIButton_Base.h"
|
||||
|
||||
using ZMMOWire::ReadU8;
|
||||
using ZMMOWire::ReadU16;
|
||||
using ZMMOWire::ReadU32;
|
||||
using ZMMOWire::ReadU64;
|
||||
using ZMMOWire::ReadFloat;
|
||||
using ZMMOWire::ReadStringUtf8;
|
||||
using ZMMOWire::ReadUuid16;
|
||||
using ZMMOWire::WriteUuid16;
|
||||
|
||||
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;
|
||||
uint16 mapId16 = 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)
|
||||
|| !ReadU16(Payload, Pos, mapId16)
|
||||
|| !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.MapId = static_cast<int32>(mapId16);
|
||||
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 + uint16 mapId + float x/y/z + float yaw +
|
||||
// string worldHost + uint16 worldPort + string handoffToken + string region
|
||||
int32 Pos = 0;
|
||||
uint64 CharId64 = 0; FString WorldHost, HandoffToken, Region;
|
||||
uint16 MapId16 = 0;
|
||||
float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0;
|
||||
if (!ReadU64(Payload, Pos, CharId64)
|
||||
|| !ReadU16(Payload, Pos, MapId16)
|
||||
|| !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 mapId=%u token=%s..."),
|
||||
*WorldHost, WorldPort, static_cast<uint32>(MapId16), *HandoffToken.Left(8));
|
||||
|
||||
UGameInstance* GI = GetGameInstance();
|
||||
if (!GI) return;
|
||||
UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem<UUIFrontEndFlowSubsystem>();
|
||||
if (Flow)
|
||||
{
|
||||
Flow->SetState(EZMMOFrontEndState::EnteringWorld);
|
||||
// Memoriza pose pra `AZMMOPlayerCharacter::BeginPlay` reposicionar
|
||||
// o pawn local na pos salva no DB (em vez do PlayerStart default).
|
||||
Flow->SetPendingSpawnPose(FVector(Px, Py, Pz), Yaw);
|
||||
}
|
||||
|
||||
// Fase 3: handoff UDP — apresenta o ticket no `C_CONNECT_REQUEST`. O
|
||||
// WorldServer valida via GETDEL no Valkey regional. WorldServer envia
|
||||
// `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado).
|
||||
// Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel —
|
||||
// ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel.
|
||||
if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem<UZeusNetworkSubsystem>())
|
||||
{
|
||||
ZeusNet->ConnectToZeusServerWithTicket(WorldHost, static_cast<int32>(WorldPort), HandoffToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
UE_LOG(LogZMMO, Error, TEXT("Lobby: ZeusNetworkSubsystem ausente — handoff abortado"));
|
||||
}
|
||||
|
||||
// Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo.
|
||||
// Se DT_Maps nao tem entrada (ou ainda nao foi criada no editor),
|
||||
// caimos no fallback do S_TRAVEL_TO_MAP do server (mapName/mapPath string).
|
||||
if (Flow && MapId16 != 0)
|
||||
{
|
||||
if (!Flow->TravelToMapById(static_cast<int32>(MapId16)))
|
||||
{
|
||||
UE_LOG(LogZMMO, Warning, TEXT("Lobby: mapId=%u nao resolvido no DT_Maps — esperando S_TRAVEL_TO_MAP do server."),
|
||||
static_cast<uint32>(MapId16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
113
Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h
Normal file
113
Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h
Normal 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;
|
||||
};
|
||||
136
Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h
Normal file
136
Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h
Normal file
@@ -0,0 +1,136 @@
|
||||
#pragma once
|
||||
|
||||
// Helpers de leitura/escrita binarios LE (UTF-8) compartilhados pelas telas
|
||||
// do FrontEnd (Boot/Login/ServerSelect/Lobby/CharCreate). Funcoes `inline`
|
||||
// pra evitar ODR violation quando dois .cpp caem no mesmo unity build do
|
||||
// UBT — definicoes em anonymous namespace cada um colidiam apos a
|
||||
// reorganizacao automatica de unities.
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
namespace ZMMOWire
|
||||
{
|
||||
inline bool ReadU8(const TArray<uint8>& Buf, int32& Pos, uint8& Out)
|
||||
{
|
||||
if (Pos + 1 > Buf.Num()) return false;
|
||||
Out = Buf[Pos];
|
||||
Pos += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
inline bool ReadU64(const TArray<uint8>& Buf, int32& Pos, uint64& Out)
|
||||
{
|
||||
if (Pos + 8 > Buf.Num()) return false;
|
||||
Out = 0;
|
||||
for (int32 i = 0; i < 8; ++i)
|
||||
{
|
||||
Out |= (static_cast<uint64>(Buf[Pos + i]) << (8 * i));
|
||||
}
|
||||
Pos += 8;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ReadFloat(const TArray<uint8>& Buf, int32& Pos, float& Out)
|
||||
{
|
||||
uint32 Raw = 0;
|
||||
if (!ReadU32(Buf, Pos, Raw)) return false;
|
||||
FMemory::Memcpy(&Out, &Raw, sizeof(float));
|
||||
return true;
|
||||
}
|
||||
|
||||
inline 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;
|
||||
}
|
||||
|
||||
/** Le 16 bytes raw e devolve UUID canonico "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". */
|
||||
inline bool ReadUuid16(const TArray<uint8>& Buf, int32& Pos, FString& Out)
|
||||
{
|
||||
if (Pos + 16 > Buf.Num()) return false;
|
||||
auto HexNibble = [](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++] = '-';
|
||||
HexNibble(Buf[Pos + I], &Raw[J]);
|
||||
J += 2;
|
||||
}
|
||||
Out = ANSI_TO_TCHAR(Raw);
|
||||
Pos += 16;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void WriteUInt8(TArray<uint8>& Buf, uint8 V) { Buf.Add(V); }
|
||||
|
||||
inline void WriteUInt16(TArray<uint8>& Buf, uint16 V)
|
||||
{
|
||||
Buf.Add(static_cast<uint8>(V & 0xFF));
|
||||
Buf.Add(static_cast<uint8>((V >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
inline void WriteUInt32(TArray<uint8>& Buf, uint32 V)
|
||||
{
|
||||
for (int32 I = 0; I < 4; ++I) Buf.Add(static_cast<uint8>((V >> (8 * I)) & 0xFF));
|
||||
}
|
||||
|
||||
inline 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);
|
||||
}
|
||||
|
||||
/** Aceita UUID canonico (com hifens) e escreve 16 bytes binarios. */
|
||||
inline bool WriteUuid16(TArray<uint8>& Buf, const FString& Uuid)
|
||||
{
|
||||
FString Hex = Uuid.Replace(TEXT("-"), TEXT(""));
|
||||
if (Hex.Len() != 32) return false;
|
||||
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;
|
||||
};
|
||||
for (int32 I = 0; I < 16; ++I)
|
||||
{
|
||||
const int32 H = HexVal(Hex[I * 2]);
|
||||
const int32 L = HexVal(Hex[I * 2 + 1]);
|
||||
if (H < 0 || L < 0) return false;
|
||||
Buf.Add(static_cast<uint8>((H << 4) | L));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
103
Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h
Normal file
103
Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h
Normal file
@@ -0,0 +1,103 @@
|
||||
#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;
|
||||
|
||||
/** ID do mapa (uint16). Resolvido via DT_Maps no cliente. 0 = invalido. */
|
||||
UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char")
|
||||
int32 MapId = 0;
|
||||
|
||||
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;
|
||||
};
|
||||
3
Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.cpp
Normal file
3
Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.cpp
Normal file
@@ -0,0 +1,3 @@
|
||||
#include "ZMMOLoginSaveGame.h"
|
||||
|
||||
const FString UZMMOLoginSaveGame::SlotName = TEXT("LoginCache");
|
||||
37
Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.h
Normal file
37
Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.h
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/SaveGame.h"
|
||||
#include "ZMMOLoginSaveGame.generated.h"
|
||||
|
||||
/**
|
||||
* Cache local da tela de Login. Persiste APENAS o nome do usuário (texto
|
||||
* plano — risco zero, é só rótulo) e o estado da checkbox "Lembrar Acesso".
|
||||
*
|
||||
* NÃO persistir senha aqui (CWE-256). Quando o auth real (HTTP/JWT) entrar,
|
||||
* adicionar `FString EncryptedRefreshToken` cifrado via DPAPI
|
||||
* (CryptProtectData no Windows / equivalente em outras plataformas) — o token
|
||||
* é opaco e revogável, padrão dominante em jogos comerciais
|
||||
* (Battle.net/Riot/Epic). Senha em si nunca toca o disco do cliente.
|
||||
*
|
||||
* Arquivo: [Project]/Saved/SaveGames/LoginCache_0.sav (binary blob das
|
||||
* UPROPERTY não-Transient via UE serializer).
|
||||
*/
|
||||
UCLASS()
|
||||
class ZMMO_API UZMMOLoginSaveGame : public USaveGame
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY()
|
||||
FString SavedUsername;
|
||||
|
||||
UPROPERTY()
|
||||
bool bRememberAccess = false;
|
||||
|
||||
/** Slot do arquivo .sav (sem extensão). */
|
||||
static const FString SlotName;
|
||||
|
||||
/** Index de usuário do SO — 0 para single-player local. */
|
||||
static constexpr int32 UserIndex = 0;
|
||||
};
|
||||
45
Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h
Normal file
45
Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h
Normal 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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
216
Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp
Normal file
216
Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp
Normal 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);
|
||||
}
|
||||
98
Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h
Normal file
98
Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h
Normal 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;
|
||||
};
|
||||
138
Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp
Normal file
138
Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp
Normal 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();
|
||||
}
|
||||
49
Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h
Normal file
49
Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h
Normal 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;
|
||||
};
|
||||
325
Source/ZMMO/Game/UI/Widgets/UIInput_Base.cpp
Normal file
325
Source/ZMMO/Game/UI/Widgets/UIInput_Base.cpp
Normal file
@@ -0,0 +1,325 @@
|
||||
#include "UIInput_Base.h"
|
||||
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Components/EditableTextBox.h"
|
||||
#include "Components/SizeBox.h"
|
||||
#include "Components/Image.h"
|
||||
#include "Styling/CoreStyle.h"
|
||||
#include "Styling/SlateBrush.h"
|
||||
#include "UI/UIStyleRow.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
// Mesmo helper das outras bases: runtime usa o tema ativo; design-time
|
||||
// carrega a row "Default" de DT_UI_Styles para o Designer ver igual.
|
||||
FUIStyle ResolveInputStyle(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("UIInputDesign"), false))
|
||||
{
|
||||
DesignStyle = Row->Style;
|
||||
bHas = true;
|
||||
}
|
||||
}
|
||||
return bHas ? DesignStyle : Fallback;
|
||||
}
|
||||
|
||||
// Pinta um UImage com o FSlateBrush vindo do DT (já tem Image, ImageSize,
|
||||
// TintColor, DrawAs, Tiling, Margin/9-slice, UV — tudo). Brush vazio
|
||||
// (sem ResourceObject e DrawAs!=NoDrawType) força Image procedural com
|
||||
// Tint da própria struct — evita brush nulo que vira magenta.
|
||||
void PaintImage(UImage* Img, const FSlateBrush& Brush)
|
||||
{
|
||||
if (!IsValid(Img)) { return; }
|
||||
FSlateBrush B = Brush;
|
||||
if (!B.GetResourceObject() && B.DrawAs != ESlateBrushDrawType::NoDrawType)
|
||||
{
|
||||
// Sem textura: pintura procedural (cor sólida).
|
||||
B.DrawAs = ESlateBrushDrawType::Image;
|
||||
}
|
||||
Img->SetBrush(B);
|
||||
// SetColorAndOpacity = identidade; cor já vem do Brush.TintColor.
|
||||
Img->SetColorAndOpacity(FLinearColor::White);
|
||||
}
|
||||
}
|
||||
|
||||
TArray<FString> UUIInput_Base::CollectVariantOptions()
|
||||
{
|
||||
TArray<FString> Options;
|
||||
for (const TCHAR* N : { TEXT("Box"), TEXT("Outline"), TEXT("Underline"), TEXT("Search") })
|
||||
{
|
||||
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("UIInputVariantOptions"), false))
|
||||
{
|
||||
for (const TPair<FName, FUIStyleInputBackground>& P : Row->Style.Input.Backgrounds) { Options.AddUnique(P.Key.ToString()); }
|
||||
for (const TPair<FName, FUIStyleInputStroke>& P : Row->Style.Input.Strokes) { Options.AddUnique(P.Key.ToString()); }
|
||||
for (const TPair<FName, FUIStyleInputTypography>& P : Row->Style.Input.Typography) { Options.AddUnique(P.Key.ToString()); }
|
||||
}
|
||||
}
|
||||
return Options;
|
||||
}
|
||||
|
||||
TArray<FString> UUIInput_Base::GetVariantOptions() const
|
||||
{
|
||||
return CollectVariantOptions();
|
||||
}
|
||||
|
||||
FUIStyleInputVariant UUIInput_Base::ResolveVariant(const FUIStyleInput& I) const
|
||||
{
|
||||
// Padrão multi-map (Hyper-style): compõe a variante buscando o nome
|
||||
// (Variant FName) em CADA sub-mapa independente. Se um sub-mapa não
|
||||
// tiver a chave, cai nos Defaults inline (sempre GC-safe — só
|
||||
// cores/floats no constructor de FUIStyleInput).
|
||||
static const FUIStyleInput Defaults;
|
||||
FUIStyleInputVariant V;
|
||||
|
||||
if (const FUIStyleInputBackground* E = I.Backgrounds.Find(Variant)) { V.Background = *E; }
|
||||
else if (const FUIStyleInputBackground* E2 = Defaults.Backgrounds.Find(Variant)) { V.Background = *E2; }
|
||||
else { V.Background = Defaults.Backgrounds.FindRef(TEXT("Box")); }
|
||||
|
||||
if (const FUIStyleInputStroke* E = I.Strokes.Find(Variant)) { V.Stroke = *E; }
|
||||
else if (const FUIStyleInputStroke* E2 = Defaults.Strokes.Find(Variant)) { V.Stroke = *E2; }
|
||||
else { V.Stroke = Defaults.Strokes.FindRef(TEXT("Box")); }
|
||||
|
||||
if (const FUIStyleInputTypography* E = I.Typography.Find(Variant)) { V.Typography = *E; }
|
||||
else if (const FUIStyleInputTypography* E2 = Defaults.Typography.Find(Variant)) { V.Typography = *E2; }
|
||||
else { V.Typography = Defaults.Typography.FindRef(TEXT("Box")); }
|
||||
|
||||
return V;
|
||||
}
|
||||
|
||||
void UUIInput_Base::ApplyStyle(const FUIStyleInputVariant& V, float InPadding,
|
||||
const FSlateFontInfo& ThemeFont, float FontSizePx)
|
||||
{
|
||||
// Background e Stroke (UImages) — seguro em design-time e runtime.
|
||||
if (IsValid(Background))
|
||||
{
|
||||
PaintImage(Background, V.Background.Brush);
|
||||
}
|
||||
if (IsValid(Stroke))
|
||||
{
|
||||
PaintImage(Stroke, V.Stroke.Brush);
|
||||
}
|
||||
|
||||
if (!IsValid(Input))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// IMPORTANTE: NÃO mexer no UEditableTextBox em design-time. Qualquer
|
||||
// chamada (SetForegroundColor / SetIsPassword / SetHintText / SetWidgetStyle)
|
||||
// dispara SEditableText::SynchronizeTextStyle → DetermineFont via FAttribute
|
||||
// delegate dangling → crash de "FSlateFontInfo::FSlateFontInfo() reading
|
||||
// 0xff..." nesta build da engine. Em design-time só pintamos Background/
|
||||
// Stroke (UImages, path estável). Preview do HintText/Password só em PIE.
|
||||
if (IsDesignTime())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// SÓ chamadas pontuais — NÃO chamar SetWidgetStyle (copia FEditableTextBoxStyle
|
||||
// inteiro incluindo TextStyle.Font). DetermineFont callback durante Prepass
|
||||
// do Slate dereferencia FSlateFontInfo dangling se a Font veio do DT com
|
||||
// FontObject UObject que ficou em estado intermediário → crash em
|
||||
// FSlateFontInfo::FSlateFontInfo() durante SEditableText::SynchronizeTextStyle.
|
||||
// Background brushes do EditableTextBox: configurar transparente direto no
|
||||
// asset UI_Input_Master no Designer (one-time setup) — o Background/Stroke
|
||||
// (UImages) atrás do EditableTextBox é quem pinta o visual via DT.
|
||||
Input->SetForegroundColor(V.Typography.TextColor);
|
||||
Input->SetIsPassword(bIsPassword);
|
||||
if (!HintText.IsEmpty())
|
||||
{
|
||||
Input->SetHintText(HintText);
|
||||
}
|
||||
|
||||
// Suprime warnings de parâmetros não usados (assinatura mantida pra
|
||||
// compatibilidade — InPadding/ThemeFont/FontSizePx podem ser usados
|
||||
// futuramente quando o caminho de SetWidgetStyle for re-habilitado de
|
||||
// forma segura).
|
||||
(void)InPadding;
|
||||
(void)ThemeFont;
|
||||
(void)FontSizePx;
|
||||
}
|
||||
|
||||
void UUIInput_Base::RefreshUIStyle()
|
||||
{
|
||||
const FUIStyle Fallback;
|
||||
const FUIStyle AS = ResolveInputStyle(this, Fallback);
|
||||
const FUIStyleInput& I = AS.Input;
|
||||
const FUIStyleInputVariant V = ResolveVariant(I);
|
||||
|
||||
// Tamanho via Width/Height Override (mais agressivo que MinDesired — força
|
||||
// tamanho exato). Resolução em 3 camadas:
|
||||
// 1. PreferredWidth/Height UPROPERTY > 0 → override explícito (designer)
|
||||
// 2. User mexeu DIRETO no Size_Root.WidthOverride (Designer) → preserva
|
||||
// 3. Senão → aplica do DT (I.MinWidth/MinHeight)
|
||||
if (IsValid(Size_Root))
|
||||
{
|
||||
auto ApplyDimension = [](USizeBox* Box, bool bIsWidth, float PreferredVal,
|
||||
float ThemeVal, float& LastThemeApplied)
|
||||
{
|
||||
const bool bHasManualFlag = bIsWidth ? Box->IsWidthOverride() : Box->IsHeightOverride();
|
||||
const float CurrentVal = bIsWidth ? Box->GetWidthOverride() : Box->GetHeightOverride();
|
||||
auto Setter = [Box, bIsWidth](float V)
|
||||
{
|
||||
if (bIsWidth) { Box->SetWidthOverride(V); }
|
||||
else { Box->SetHeightOverride(V); }
|
||||
};
|
||||
|
||||
if (PreferredVal > 0.f)
|
||||
{
|
||||
// (1) Override explícito via UPROPERTY.
|
||||
Setter(PreferredVal);
|
||||
LastThemeApplied = -1.f; // user controla via UPROPERTY a partir daqui
|
||||
}
|
||||
else if (bHasManualFlag && LastThemeApplied >= 0.f && CurrentVal != LastThemeApplied)
|
||||
{
|
||||
// (2) User editou direto no SizeBox no Designer — preserva.
|
||||
}
|
||||
else if (bHasManualFlag && LastThemeApplied < 0.f)
|
||||
{
|
||||
// (2b) Flag manual ativo mas C++ nunca aplicou nesta instância
|
||||
// (fresh load): assume que é override do user no asset — preserva.
|
||||
}
|
||||
else
|
||||
{
|
||||
// (3) Aplica do DT e marca como "C++ aplicou".
|
||||
Setter(ThemeVal);
|
||||
LastThemeApplied = ThemeVal;
|
||||
}
|
||||
};
|
||||
|
||||
ApplyDimension(Size_Root, /*bIsWidth=*/true, PreferredWidth, I.MinWidth, LastThemeWidthApplied);
|
||||
ApplyDimension(Size_Root, /*bIsWidth=*/false, PreferredHeight, I.MinHeight, LastThemeHeightApplied);
|
||||
}
|
||||
|
||||
// Size: 0 na instância → usa V.Typography.Size (do DT da variante).
|
||||
const float ChosenSize = (FontSize > 0.f) ? FontSize : V.Typography.Size;
|
||||
// Font: V.Typography.FontOverride se válida; senão FUIStyle.Text.BodyFont.
|
||||
const FSlateFontInfo& ThemeFont = V.Typography.FontOverride.HasValidFont()
|
||||
? V.Typography.FontOverride : AS.Text.BodyFont;
|
||||
ApplyStyle(V, I.Padding, ThemeFont, ChosenSize);
|
||||
|
||||
BP_ApplyInputStyle(I);
|
||||
}
|
||||
|
||||
FText UUIInput_Base::GetText() const
|
||||
{
|
||||
return IsValid(Input) ? Input->GetText() : FText::GetEmpty();
|
||||
}
|
||||
|
||||
void UUIInput_Base::SetText(FText InText)
|
||||
{
|
||||
if (IsValid(Input))
|
||||
{
|
||||
Input->SetText(InText);
|
||||
}
|
||||
}
|
||||
|
||||
void UUIInput_Base::SetHintTextRuntime(FText InHint)
|
||||
{
|
||||
HintText = InHint;
|
||||
if (IsValid(Input)) { Input->SetHintText(InHint); }
|
||||
}
|
||||
|
||||
void UUIInput_Base::SetIsPasswordRuntime(bool bInIsPassword)
|
||||
{
|
||||
bIsPassword = bInIsPassword;
|
||||
if (IsValid(Input)) { Input->SetIsPassword(bInIsPassword); }
|
||||
}
|
||||
|
||||
void UUIInput_Base::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUIInput_Base::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUIInput_Base::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (!bInputBound && IsValid(Input))
|
||||
{
|
||||
Input->OnTextChanged.AddDynamic(this, &UUIInput_Base::HandleTextChanged);
|
||||
Input->OnTextCommitted.AddDynamic(this, &UUIInput_Base::HandleTextCommitted);
|
||||
bInputBound = true;
|
||||
}
|
||||
|
||||
if (!bThemeBound)
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
|
||||
{
|
||||
Theme->OnThemeChanged.AddDynamic(this, &UUIInput_Base::HandleThemeChanged);
|
||||
bThemeBound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUIInput_Base::NativeDestruct()
|
||||
{
|
||||
if (bInputBound && IsValid(Input))
|
||||
{
|
||||
Input->OnTextChanged.RemoveDynamic(this, &UUIInput_Base::HandleTextChanged);
|
||||
Input->OnTextCommitted.RemoveDynamic(this, &UUIInput_Base::HandleTextCommitted);
|
||||
bInputBound = false;
|
||||
}
|
||||
if (bThemeBound)
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
|
||||
{
|
||||
Theme->OnThemeChanged.RemoveDynamic(this, &UUIInput_Base::HandleThemeChanged);
|
||||
}
|
||||
}
|
||||
bThemeBound = false;
|
||||
}
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UUIInput_Base::HandleThemeChanged(FName /*NewThemeId*/)
|
||||
{
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUIInput_Base::HandleTextChanged(const FText& Text)
|
||||
{
|
||||
OnTextChanged.Broadcast(Text);
|
||||
}
|
||||
|
||||
void UUIInput_Base::HandleTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
|
||||
{
|
||||
OnTextCommitted.Broadcast(Text, CommitMethod);
|
||||
}
|
||||
156
Source/ZMMO/Game/UI/Widgets/UIInput_Base.h
Normal file
156
Source/ZMMO/Game/UI/Widgets/UIInput_Base.h
Normal file
@@ -0,0 +1,156 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "CommonUserWidget.h"
|
||||
#include "UI/UIStyleTokens.h"
|
||||
#include "UI/UIStyleTypes.h"
|
||||
#include "UIInput_Base.generated.h"
|
||||
|
||||
class UEditableTextBox;
|
||||
class USizeBox;
|
||||
class UImage;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUIInputTextChanged, const FText&, Text);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FUIInputTextCommitted, const FText&, Text, ETextCommit::Type, CommitMethod);
|
||||
|
||||
/**
|
||||
* Campo de texto SÓ-INPUT (sem label). Padrão visual inspirado no UI_Search do
|
||||
* Hyper: SizeBox raiz → Overlay → Background (UImage) + Stroke (UImage) +
|
||||
* Input (UEditableTextBox). Os UImages recebem a aparência (cor ou textura
|
||||
* 9-slice via DT_UI_Styles); o EditableTextBox fica transparente e só
|
||||
* recebe fonte/cor de texto — assim a cor magenta default (brush vazio) é
|
||||
* eliminada.
|
||||
*
|
||||
* Variantes visuais são data-driven via FUIStyle.Input (Backgrounds/Strokes/
|
||||
* Typography) — chave FName (Box/Outline/Underline/Search/custom). Para
|
||||
* compor com rótulo + layout (Stacked/Inline), use UUILabel_Base.
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class ZMMO_API UUIInput_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 = "Input")
|
||||
FText GetText() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetText(FText InText);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetHintTextRuntime(FText InHint);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetIsPasswordRuntime(bool bInIsPassword);
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Input")
|
||||
FUIInputTextChanged OnTextChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Input")
|
||||
FUIInputTextCommitted OnTextCommitted;
|
||||
|
||||
/**
|
||||
* Tamanho da fonte do campo. 0 = usa o padrão do DT_UI_Styles
|
||||
* (FUIStyleInputTypography.Size). > 0 = sobrescreve aqui no Designer.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", meta = (ClampMin = "0"))
|
||||
float FontSize = 0.f;
|
||||
|
||||
/** Placeholder exibido quando vazio. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
|
||||
FText HintText;
|
||||
|
||||
/**
|
||||
* Variante visual do input (dropdown vem de FUIStyle.Input.Variants no
|
||||
* DT_UI_Styles + as 4 clássicas como fallback). Data-driven, sem enum.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input",
|
||||
meta = (GetOptions = "GetVariantOptions"))
|
||||
FName Variant = TEXT("Box");
|
||||
|
||||
/** Opções do dropdown de Variant (data-driven do DT_UI_Styles). */
|
||||
UFUNCTION()
|
||||
TArray<FString> GetVariantOptions() const;
|
||||
|
||||
/** Helper compartilhado p/ UUILabel_Base (mesma lista de variantes). */
|
||||
static TArray<FString> CollectVariantOptions();
|
||||
|
||||
/**
|
||||
* Largura preferida do campo (px). 0 = usa o padrão do DT
|
||||
* (FUIStyle.Input.MinWidth). > 0 = sobrescreve aqui no Designer.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0"))
|
||||
float PreferredWidth = 0.f;
|
||||
|
||||
/**
|
||||
* Altura preferida do campo (px). 0 = usa o padrão do DT
|
||||
* (FUIStyle.Input.MinHeight). > 0 = sobrescreve aqui no Designer.
|
||||
*/
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0"))
|
||||
float PreferredHeight = 0.f;
|
||||
|
||||
/** Campo de senha (oculta os caracteres). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
|
||||
bool bIsPassword = false;
|
||||
|
||||
protected:
|
||||
virtual void NativePreConstruct() override;
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
|
||||
/** Hook opcional: o WBP pode refinar além do que o C++ aplica. */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style",
|
||||
meta = (DisplayName = "Apply Input Style"))
|
||||
void BP_ApplyInputStyle(const FUIStyleInput& InputStyle);
|
||||
|
||||
// SizeBox raiz (controla PreferredWidth/MinHeight). Opcional: se o WBP
|
||||
// não tiver, fallback fica ileso (sem set de tamanho).
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<USizeBox> Size_Root;
|
||||
|
||||
// Background pintável (cor + textura 9-slice opcional).
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UImage> Background;
|
||||
|
||||
// Stroke / borda (cor + textura 9-slice opcional via FUIStyleInputStroke).
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UImage> Stroke;
|
||||
|
||||
// Campo de texto cru. Fica transparente — Background/Stroke pintam atrás.
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UEditableTextBox> Input;
|
||||
|
||||
private:
|
||||
FUIStyleInputVariant ResolveVariant(const FUIStyleInput& I) const;
|
||||
void ApplyStyle(const FUIStyleInputVariant& V, float InPadding,
|
||||
const FSlateFontInfo& ThemeFont, float FontSizePx);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleThemeChanged(FName NewThemeId);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleTextChanged(const FText& Text);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);
|
||||
|
||||
bool bThemeBound = false;
|
||||
bool bInputBound = false;
|
||||
|
||||
/**
|
||||
* Último valor que o C++ aplicou no Size_Root->WidthOverride a partir do
|
||||
* tema. Se o valor atual do SizeBox for diferente, presume-se que o user
|
||||
* editou manualmente (no Designer) — C++ deixa intacto. -1 = não aplicado
|
||||
* (próxima chamada aplica do tema).
|
||||
*/
|
||||
UPROPERTY(Transient)
|
||||
float LastThemeWidthApplied = -1.f;
|
||||
|
||||
UPROPERTY(Transient)
|
||||
float LastThemeHeightApplied = -1.f;
|
||||
};
|
||||
181
Source/ZMMO/Game/UI/Widgets/UILabel_Base.cpp
Normal file
181
Source/ZMMO/Game/UI/Widgets/UILabel_Base.cpp
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "UILabel_Base.h"
|
||||
|
||||
#include "UIInput_Base.h"
|
||||
#include "UICommonText_Base.h"
|
||||
#include "Components/VerticalBox.h"
|
||||
#include "Components/HorizontalBox.h"
|
||||
#include "CommonTextBlock.h"
|
||||
|
||||
UUIInput_Base* UUILabel_Base::ActiveInput() const
|
||||
{
|
||||
if (LabelLayout == EUIInputLabelLayout::Inline)
|
||||
{
|
||||
return IsValid(Input_Inline) ? Input_Inline : Input_Stacked;
|
||||
}
|
||||
return IsValid(Input_Stacked) ? Input_Stacked : Input_Inline;
|
||||
}
|
||||
|
||||
void UUILabel_Base::ApplyLayout()
|
||||
{
|
||||
// Só alterna VISIBILIDADE (nunca muta a árvore — isso crasha em
|
||||
// construção/thumbnail do editor).
|
||||
const bool bInline = (LabelLayout == EUIInputLabelLayout::Inline);
|
||||
if (IsValid(Box_Stacked))
|
||||
{
|
||||
Box_Stacked->SetVisibility(bInline
|
||||
? ESlateVisibility::Collapsed : ESlateVisibility::SelfHitTestInvisible);
|
||||
}
|
||||
if (IsValid(Row_Inline))
|
||||
{
|
||||
Row_Inline->SetVisibility(bInline
|
||||
? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILabel_Base::PropagateInputProps()
|
||||
{
|
||||
auto Apply = [this](UUIInput_Base* In)
|
||||
{
|
||||
if (!IsValid(In)) { return; }
|
||||
// Variant: controle de aparência do UILabel_Base — sempre propaga.
|
||||
In->Variant = Variant;
|
||||
// bIsPassword: sempre propaga (controle explícito).
|
||||
In->bIsPassword = bIsPassword;
|
||||
// Overrides "0/vazio = passthrough": só sobrescreve se o UILabel_Base
|
||||
// definiu valor explícito; assim o user pode setar PreferredWidth/Height,
|
||||
// FontSize, HintText DIRETO no UI_Input_Master filho e o pai não zera.
|
||||
if (FontSize > 0.f) { In->FontSize = FontSize; }
|
||||
if (!HintText.IsEmpty()) { In->HintText = HintText; }
|
||||
if (PreferredWidth > 0.f) { In->PreferredWidth = PreferredWidth; }
|
||||
if (PreferredHeight > 0.f) { In->PreferredHeight = PreferredHeight; }
|
||||
In->RefreshUIStyle();
|
||||
};
|
||||
Apply(Input_Stacked);
|
||||
Apply(Input_Inline);
|
||||
}
|
||||
|
||||
TArray<FString> UUILabel_Base::GetVariantOptions() const
|
||||
{
|
||||
return UUIInput_Base::CollectVariantOptions();
|
||||
}
|
||||
|
||||
TArray<FString> UUILabel_Base::GetLabelTextRoleOptions() const
|
||||
{
|
||||
return UUICommonText_Base::GetThemeTextRoleOptions();
|
||||
}
|
||||
|
||||
void UUILabel_Base::RefreshUIStyle()
|
||||
{
|
||||
auto SetupLabel = [this](UCommonTextBlock* L)
|
||||
{
|
||||
if (!L) { return; }
|
||||
if (!LabelText.IsEmpty()) { L->SetText(LabelText); }
|
||||
if (UUICommonText_Base* CT = Cast<UUICommonText_Base>(L))
|
||||
{
|
||||
if (CT->TextRole != LabelTextRole) { CT->TextRole = LabelTextRole; }
|
||||
CT->RefreshUIStyle();
|
||||
}
|
||||
};
|
||||
SetupLabel(Label_Stacked);
|
||||
SetupLabel(Label_Inline);
|
||||
|
||||
PropagateInputProps();
|
||||
}
|
||||
|
||||
FText UUILabel_Base::GetText() const
|
||||
{
|
||||
const UUIInput_Base* In = ActiveInput();
|
||||
return In ? In->GetText() : FText::GetEmpty();
|
||||
}
|
||||
|
||||
void UUILabel_Base::SetText(FText InText)
|
||||
{
|
||||
if (UUIInput_Base* In = ActiveInput())
|
||||
{
|
||||
In->SetText(InText);
|
||||
}
|
||||
}
|
||||
|
||||
void UUILabel_Base::SetHintTextRuntime(FText InHint)
|
||||
{
|
||||
HintText = InHint;
|
||||
if (IsValid(Input_Stacked)) { Input_Stacked->SetHintTextRuntime(InHint); }
|
||||
if (IsValid(Input_Inline)) { Input_Inline->SetHintTextRuntime(InHint); }
|
||||
}
|
||||
|
||||
void UUILabel_Base::SetIsPasswordRuntime(bool bInIsPassword)
|
||||
{
|
||||
bIsPassword = bInIsPassword;
|
||||
if (IsValid(Input_Stacked)) { Input_Stacked->SetIsPasswordRuntime(bInIsPassword); }
|
||||
if (IsValid(Input_Inline)) { Input_Inline->SetIsPasswordRuntime(bInIsPassword); }
|
||||
}
|
||||
|
||||
void UUILabel_Base::BindInputDelegates()
|
||||
{
|
||||
if (bInputBound) { return; }
|
||||
if (IsValid(Input_Stacked))
|
||||
{
|
||||
Input_Stacked->OnTextChanged.AddDynamic(this, &UUILabel_Base::HandleChildTextChanged);
|
||||
Input_Stacked->OnTextCommitted.AddDynamic(this, &UUILabel_Base::HandleChildTextCommitted);
|
||||
}
|
||||
if (IsValid(Input_Inline))
|
||||
{
|
||||
Input_Inline->OnTextChanged.AddDynamic(this, &UUILabel_Base::HandleChildTextChanged);
|
||||
Input_Inline->OnTextCommitted.AddDynamic(this, &UUILabel_Base::HandleChildTextCommitted);
|
||||
}
|
||||
bInputBound = true;
|
||||
}
|
||||
|
||||
void UUILabel_Base::UnbindInputDelegates()
|
||||
{
|
||||
if (!bInputBound) { return; }
|
||||
if (IsValid(Input_Stacked))
|
||||
{
|
||||
Input_Stacked->OnTextChanged.RemoveDynamic(this, &UUILabel_Base::HandleChildTextChanged);
|
||||
Input_Stacked->OnTextCommitted.RemoveDynamic(this, &UUILabel_Base::HandleChildTextCommitted);
|
||||
}
|
||||
if (IsValid(Input_Inline))
|
||||
{
|
||||
Input_Inline->OnTextChanged.RemoveDynamic(this, &UUILabel_Base::HandleChildTextChanged);
|
||||
Input_Inline->OnTextCommitted.RemoveDynamic(this, &UUILabel_Base::HandleChildTextCommitted);
|
||||
}
|
||||
bInputBound = false;
|
||||
}
|
||||
|
||||
void UUILabel_Base::HandleChildTextChanged(const FText& Text)
|
||||
{
|
||||
OnTextChanged.Broadcast(Text);
|
||||
}
|
||||
|
||||
void UUILabel_Base::HandleChildTextCommitted(const FText& Text, ETextCommit::Type CommitMethod)
|
||||
{
|
||||
OnTextCommitted.Broadcast(Text, CommitMethod);
|
||||
}
|
||||
|
||||
void UUILabel_Base::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
ApplyLayout();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUILabel_Base::SynchronizeProperties()
|
||||
{
|
||||
Super::SynchronizeProperties();
|
||||
ApplyLayout();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUILabel_Base::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
BindInputDelegates();
|
||||
ApplyLayout();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUILabel_Base::NativeDestruct()
|
||||
{
|
||||
UnbindInputDelegates();
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
131
Source/ZMMO/Game/UI/Widgets/UILabel_Base.h
Normal file
131
Source/ZMMO/Game/UI/Widgets/UILabel_Base.h
Normal file
@@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "CommonUserWidget.h"
|
||||
#include "UI/UIStyleTokens.h"
|
||||
#include "UI/UIStyleTypes.h"
|
||||
#include "UILabel_Base.generated.h"
|
||||
|
||||
class UUIInput_Base;
|
||||
class UCommonTextBlock;
|
||||
class UVerticalBox;
|
||||
class UHorizontalBox;
|
||||
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUILabelTextChanged, const FText&, Text);
|
||||
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FUILabelTextCommitted, const FText&, Text, ETextCommit::Type, CommitMethod);
|
||||
|
||||
/**
|
||||
* Composição "label + input" — dois arranjos pré-montados (Box_Stacked,
|
||||
* Row_Inline) onde cada Input_* é uma instância concreta de UI_Input_Master
|
||||
* (UUIInput_Base). Alterna VISIBILIDADE entre layouts (Stacked vs Inline);
|
||||
* nunca muta a árvore (mutar em construção/thumbnail crasha o editor).
|
||||
*
|
||||
* Encaminha a API de texto pro input ativo. Variant/HintText/FontSize/
|
||||
* PreferredWidth/bIsPassword são propagados pros DOIS filhos (preview
|
||||
* correto em qualquer LabelLayout).
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class ZMMO_API UUILabel_Base : public UCommonUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UFUNCTION(BlueprintCallable, Category = "UI Style")
|
||||
void RefreshUIStyle();
|
||||
|
||||
UFUNCTION(BlueprintPure, Category = "Input")
|
||||
FText GetText() const;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetText(FText InText);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetHintTextRuntime(FText InHint);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "Input")
|
||||
void SetIsPasswordRuntime(bool bInIsPassword);
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Input")
|
||||
FUILabelTextChanged OnTextChanged;
|
||||
|
||||
UPROPERTY(BlueprintAssignable, Category = "Input")
|
||||
FUILabelTextCommitted OnTextCommitted;
|
||||
|
||||
// ---- Rótulo ----
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label")
|
||||
FText LabelText;
|
||||
|
||||
/** Categoria tipográfica do rótulo (dropdown vem do DT_UI_Styles). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label",
|
||||
meta = (GetOptions = "GetLabelTextRoleOptions"))
|
||||
FName LabelTextRole = TEXT("Label");
|
||||
|
||||
UFUNCTION()
|
||||
TArray<FString> GetLabelTextRoleOptions() const;
|
||||
|
||||
/** Posição do rótulo (label acima x label à esquerda). */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label")
|
||||
EUIInputLabelLayout LabelLayout = EUIInputLabelLayout::Stacked;
|
||||
|
||||
// ---- Encaminhado pro input ----
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input",
|
||||
meta = (GetOptions = "GetVariantOptions"))
|
||||
FName Variant = TEXT("Box");
|
||||
|
||||
UFUNCTION()
|
||||
TArray<FString> GetVariantOptions() const;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
|
||||
FText HintText;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", meta = (ClampMin = "0"))
|
||||
float FontSize = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0"))
|
||||
float PreferredWidth = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0"))
|
||||
float PreferredHeight = 0.f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
|
||||
bool bIsPassword = false;
|
||||
|
||||
protected:
|
||||
virtual void NativePreConstruct() override;
|
||||
virtual void NativeConstruct() override;
|
||||
virtual void NativeDestruct() override;
|
||||
virtual void SynchronizeProperties() override;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UVerticalBox> Box_Stacked;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UHorizontalBox> Row_Inline;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCommonTextBlock> Label_Stacked;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUIInput_Base> Input_Stacked;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCommonTextBlock> Label_Inline;
|
||||
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UUIInput_Base> Input_Inline;
|
||||
|
||||
private:
|
||||
UUIInput_Base* ActiveInput() const;
|
||||
void ApplyLayout();
|
||||
void PropagateInputProps();
|
||||
void BindInputDelegates();
|
||||
void UnbindInputDelegates();
|
||||
|
||||
UFUNCTION()
|
||||
void HandleChildTextChanged(const FText& Text);
|
||||
|
||||
UFUNCTION()
|
||||
void HandleChildTextCommitted(const FText& Text, ETextCommit::Type CommitMethod);
|
||||
|
||||
bool bInputBound = false;
|
||||
};
|
||||
@@ -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 ----
|
||||
|
||||
135
Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp
Normal file
135
Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include "UISpinner_Base.h"
|
||||
|
||||
#include "ZMMOThemeSubsystem.h"
|
||||
#include "Engine/GameInstance.h"
|
||||
#include "Engine/DataTable.h"
|
||||
#include "Components/CircularThrobber.h"
|
||||
#include "UI/UIStyleRow.h"
|
||||
|
||||
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.
|
||||
FUIStyle ResolveSpinnerStyle(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("UISpinnerDesign"), false))
|
||||
{
|
||||
DesignStyle = Row->Style;
|
||||
bHas = true;
|
||||
}
|
||||
}
|
||||
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 FUIStyleSpinnerVariant V = ResolveSpinnerVariant(AS.Spinner, Variant);
|
||||
|
||||
if (Throbber)
|
||||
{
|
||||
Throbber->SetNumberOfPieces(FMath::Max(1, V.Layout.NumberOfPieces));
|
||||
Throbber->SetPeriod(FMath::Max(0.05f, V.Layout.Period));
|
||||
Throbber->SetRadius(V.Layout.Radius);
|
||||
}
|
||||
|
||||
BP_ApplySpinnerStyle(V);
|
||||
}
|
||||
|
||||
void UUISpinner_Base::NativePreConstruct()
|
||||
{
|
||||
Super::NativePreConstruct();
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUISpinner_Base::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
|
||||
if (!bThemeBound)
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
|
||||
{
|
||||
Theme->OnThemeChanged.AddDynamic(this, &UUISpinner_Base::HandleThemeChanged);
|
||||
bThemeBound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
RefreshUIStyle();
|
||||
}
|
||||
|
||||
void UUISpinner_Base::NativeDestruct()
|
||||
{
|
||||
if (bThemeBound)
|
||||
{
|
||||
if (const UGameInstance* GI = GetGameInstance())
|
||||
{
|
||||
if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem<UZMMOThemeSubsystem>())
|
||||
{
|
||||
Theme->OnThemeChanged.RemoveDynamic(this, &UUISpinner_Base::HandleThemeChanged);
|
||||
}
|
||||
}
|
||||
bThemeBound = false;
|
||||
}
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UUISpinner_Base::HandleThemeChanged(FName /*NewThemeId*/)
|
||||
{
|
||||
RefreshUIStyle();
|
||||
}
|
||||
60
Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h
Normal file
60
Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "CommonUserWidget.h"
|
||||
#include "UI/UIStyleTokens.h"
|
||||
#include "UISpinner_Base.generated.h"
|
||||
|
||||
class UCircularThrobber;
|
||||
|
||||
/**
|
||||
* Spinner/throbber indeterminado do ZMMO — mesmo padrão de UUIPanel_Base.
|
||||
* Fundação CommonUI (UCommonUserWidget). Camada Abstract; o WBP concreto
|
||||
* (UI_Spinner_Master) herda DIRETO desta classe C++ (UMG não encadeia
|
||||
* árvores — ARQUITETURA.md §3.3).
|
||||
*
|
||||
* Visual data-driven por FUIStyle.Spinner (UZMMOThemeSubsystem). C++ aplica
|
||||
* NumberOfPieces/Period/Radius no UCircularThrobber; cor/brush vão pelo hook
|
||||
* BP_ApplySpinnerStyle (o WBP tinge a imagem do throbber).
|
||||
*/
|
||||
UCLASS(Abstract, Blueprintable)
|
||||
class ZMMO_API UUISpinner_Base : public UCommonUserWidget
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
/** Re-resolve os tokens do tema ativo e reaplica no throbber. */
|
||||
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 (variante composta). */
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "UI Style",
|
||||
meta = (DisplayName = "Apply Spinner Style"))
|
||||
void BP_ApplySpinnerStyle(const FUIStyleSpinnerVariant& VariantStyle);
|
||||
|
||||
/** Throbber circular (nome esperado no WBP: "Throbber"). */
|
||||
UPROPERTY(meta = (BindWidgetOptional))
|
||||
TObjectPtr<UCircularThrobber> Throbber;
|
||||
|
||||
private:
|
||||
UFUNCTION()
|
||||
void HandleThemeChanged(FName NewThemeId);
|
||||
|
||||
bool bThemeBound = false;
|
||||
};
|
||||
@@ -27,6 +27,13 @@ void UZMMOThemeSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
||||
}
|
||||
|
||||
ResolveActiveTheme();
|
||||
|
||||
// Garantia: se ResolveActiveTheme não disparou ResolveActiveUIStyle (porque
|
||||
// ActiveTheme não mudou — caso comum quando DefaultTheme é nullptr e nada
|
||||
// resolveu), força a carga do DT_UI_Styles agora para não deixar
|
||||
// ActiveUIStyle como FUIStyle() default (brushes vazios → nada renderiza
|
||||
// em runtime). O fallback interno acha a linha "Default" do DT.
|
||||
ResolveActiveUIStyle(ActiveThemeId);
|
||||
}
|
||||
|
||||
void UZMMOThemeSubsystem::Deinitialize()
|
||||
|
||||
Reference in New Issue
Block a user