From 166650671fd812a7152dd4742b022380e655f471 Mon Sep 17 00:00:00 2001 From: Mateus Rodrigues Date: Thu, 21 May 2026 20:06:23 -0300 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20Fase=201=20+=201.5=20?= =?UTF-8?q?=E2=80=94=20ServerSelect=20dinamico=20+=20Lobby=20(chars=20por?= =?UTF-8?q?=20mundo)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ServerSelect (Fase 1): - UIServerSelectScreen_Base: subscribe OnRawMessage + C_WORLD_LIST_REQUEST - WBP_ServerCard instanciado dinamicamente em GridPanel CardContainer (2 cols) - UIServerCard_Base: SetFromEntry + OnCardPressed delegate + UI_Button_Master - Push real-time S_WORLD_STATUS_UPDATE (opcode 2062) atualiza card in-place - FZMMOWorldEntry struct, EZMMOWorldState enum no wire (offline/online/maint) - Sem hardcap de slots — cards usam toda largura via ColumnFill weights Lobby/CharSelect (Fase 1.5): - UIUserLobbyScreen_Base + WBP_UserLobby (WidgetSwitcher: lista + create) - C_CHAR_LIST_REQUEST filtrado por SelectedWorldId do FlowSubsystem - UICharCard_Base + WBP_CharCard com Select/Delete + Accept/Cancel quando delete agendado; Text_DeleteCountdown atualiza em tempo real (timer 1s) - UICharacterCreatePage_Base + WBP_CharacterCreate (Name + ComboBox class) - Handlers S_CHAR_SELECT_OK (handoff parse), CHAR_CREATE_OK/REJECT, CHAR_DELETE_ACK/ACCEPT_ACK/CANCEL_ACK (refresh lista) Auxiliares + fixes: - ARQUITETURA_SERVER_SELECT.md + ARQUITETURA_CHARACTER_MODEL.md - UIFrontEndFlowSubsystem: SelectedWorldId state + transicao Lobby - DA_FrontEndScreenSet: Lobby -> WBP_UserLobby_C - UICheckBox_Base + UICommonText_Base (componentes auxiliares) - BSB_Button_Transparent.Disabled DrawAs=IMAGE -> NoDraw (fix bg branco) - WBP_ServerCard layout: HBox para chips Region/Ping/Status, SizeBox 10x10 no Dot verde (anti-overflow); SelfHitTestInvisible em containers (hover) --- ARQUITETURA.md | 23 +- ARQUITETURA_CHARACTER_MODEL.md | 891 ++++++++++++++++++ ARQUITETURA_SERVER_SELECT.md | 593 ++++++++++++ .../UI/FrontEnd/DA_FrontEndScreenSet.uasset | Bin 1752 -> 2192 bytes .../UI/FrontEnd/Lobby/WBP_CharCard.uasset | Bin 0 -> 32934 bytes .../FrontEnd/Lobby/WBP_CharacterCreate.uasset | Bin 0 -> 36097 bytes .../UI/FrontEnd/Lobby/WBP_UserLobby.uasset | Bin 0 -> 42017 bytes .../ServerSelect/WBP_ServerCard.uasset | Bin 0 -> 88573 bytes Content/ZMMO/UI/FrontEnd/WBP_Boot.uasset | Bin 173015 -> 212894 bytes Content/ZMMO/UI/FrontEnd/WBP_Login.uasset | Bin 186612 -> 126933 bytes .../UI/FrontEnd/WBP_PrimaryGameLayout.uasset | Bin 28642 -> 30730 bytes .../ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset | Bin 0 -> 117979 bytes .../ZMMO/UI/Shared/UI_Button_Master.uasset | Bin 36709 -> 33801 bytes Source/ZMMO/Data/UI/UIStyleRow.h | 15 + Source/ZMMO/Data/UI/UIStyleTypes.h | 10 +- .../ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h | 56 +- .../Game/UI/FrontEnd/UIBootScreen_Base.cpp | 27 +- .../ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp | 151 +++ .../ZMMO/Game/UI/FrontEnd/UICharCard_Base.h | 91 ++ .../FrontEnd/UICharacterCreatePage_Base.cpp | 133 +++ .../UI/FrontEnd/UICharacterCreatePage_Base.h | 58 ++ .../UI/FrontEnd/UIFrontEndFlowSubsystem.cpp | 27 + .../UI/FrontEnd/UIFrontEndFlowSubsystem.h | 21 + .../Game/UI/FrontEnd/UIServerCard_Base.cpp | 92 ++ .../ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h | 74 ++ .../UI/FrontEnd/UIServerSelectScreen_Base.cpp | 360 +++++++ .../UI/FrontEnd/UIServerSelectScreen_Base.h | 82 ++ .../UI/FrontEnd/UIUserLobbyScreen_Base.cpp | 468 +++++++++ .../Game/UI/FrontEnd/UIUserLobbyScreen_Base.h | 113 +++ .../ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h | 102 ++ Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h | 45 + Source/ZMMO/Game/UI/Widgets/UIButton_Base.cpp | 73 +- Source/ZMMO/Game/UI/Widgets/UIButton_Base.h | 16 +- .../ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp | 216 +++++ Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h | 98 ++ .../Game/UI/Widgets/UICommonText_Base.cpp | 138 +++ .../ZMMO/Game/UI/Widgets/UICommonText_Base.h | 49 + Source/ZMMO/Game/UI/Widgets/UIPanel_Base.cpp | 27 +- .../ZMMO/Game/UI/Widgets/UISpinner_Base.cpp | 71 +- Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h | 15 +- ZMMO.uproject | 3 +- 41 files changed, 4060 insertions(+), 78 deletions(-) create mode 100644 ARQUITETURA_CHARACTER_MODEL.md create mode 100644 ARQUITETURA_SERVER_SELECT.md create mode 100644 Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharCard.uasset create mode 100644 Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharacterCreate.uasset create mode 100644 Content/ZMMO/UI/FrontEnd/Lobby/WBP_UserLobby.uasset create mode 100644 Content/ZMMO/UI/FrontEnd/ServerSelect/WBP_ServerCard.uasset create mode 100644 Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp create mode 100644 Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h create mode 100644 Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h create mode 100644 Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp create mode 100644 Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h create mode 100644 Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp create mode 100644 Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h diff --git a/ARQUITETURA.md b/ARQUITETURA.md index 3d2e7fc..79a9ec6 100644 --- a/ARQUITETURA.md +++ b/ARQUITETURA.md @@ -7,6 +7,10 @@ Última actualização: 2026-05-11. +> **Extensões da arquitetura** (documentos normativos paralelos, escopo específico): +> - [`ARQUITETURA_SERVER_SELECT.md`](ARQUITETURA_SERVER_SELECT.md) — Server Select, lista de mundos, handoff CharServer↔WorldServer, multi-region, queue, Valkey. Roadmap em 7 fases. +> - [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md) — Stats primários (STR/AGI/VIT/INT/DEX/LUK), classes (Aprendiz → especializações), fórmulas de stats derivados (ATK/MATK/DEF/...), `jobs.yml` data-driven, stat allocation server-side. Espelha rathena. + > **Sumário de mudanças (2026-05-11):** adicionada secção de **temas sazonais > de UI** (`UI/Themes/`), com tutorial §4.7, regra de "no hard ref a textura > em widget" em §5 e política de activação em §1.10. @@ -211,7 +215,8 @@ Content/ │ │ │ Chat, Party, Guild, Trade, Quest, Map, │ │ │ Shared, Icons │ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_Master, -│ │ │ UI_Panel_Master, UI_Spinner_Master) +│ │ │ UI_Panel_Master, UI_Spinner_Master, +│ │ │ UI_CheckBox_Master, UI_Input_Master) │ │ ├── FrontEnd/ ← WBP_PrimaryGameLayout, DA_FrontEndScreenSet, │ │ │ telas refinadas (Boot/Login/… vindas do Forge) │ │ ├── Fonts/ ← Font_*/FF_* (Cinzel, Rajdhani) @@ -270,10 +275,15 @@ Source/ZMMO/ │ └── UI/ ← runtime de UI (não é contrato de dados) │ ├── ZMMOThemeSubsystem.* ← resolução de tema │ ├── Widgets/ ← UUIButton_Base, UUIPanel_Base, -│ │ UUISpinner_Base +│ │ UUISpinner_Base, UUICheckBox_Base, +│ │ UUIInput_Base (variantes Box/Outline/ +│ │ Underline/Search), UUICommonText_Base +│ │ (EUITextRole → FUIStyle.Text) │ └── FrontEnd/ ← infra de navegação CommonUI: │ UUIActivatableScreen_Base, │ UUIBootScreen_Base (1ª tela), +│ UUILoginScreen_Base (2ª tela), +│ CharServerOpcodes.h (faixa 2000-2099), │ UUIPrimaryGameLayout_Base, │ UUIManagerSubsystem (LocalPlayer), │ UUIFrontEndFlowSubsystem (GameInstance), @@ -634,6 +644,15 @@ CharServer: libera o botão "Iniciar" no `OnConnected`; o clique chama `UUIFrontEndFlowSubsystem::RequestEnterLogin()` → estado Login. Tela nunca segura hard-ref de asset de tema (cores via `FUIStyle`; §5). +**Login:** a tela `UUILoginScreen_Base` autentica pelo MESMO WebSocket do +CharServer enviando `C_CHAR_AUTH_REQUEST` (opcode 2000, ver +`CharServerOpcodes.h`) com um token. Escopo atual = **dev**: token `dev:` +aceito pelo `StubTokenValidator` (senha ignorada no stub). `S_CHAR_AUTH_OK` +→ `RequestEnterServerSelect()` → estado ServerSelect; `S_CHAR_AUTH_REJECT` +→ mensagem de erro na própria tela. Auth real (HTTP `/auth/login` + JWT) +fica para tarefa dedicada. "Voltar" → `RequestBack()` (Login→Boot, CharServer +permanece conectado). + 1. Prototipar a tela em HTML/CSS em `Tools/Templates/MMO_Widget/` (ou usar os templates já existentes). 2. Rodar o Zeus UMG Forge (HTML→UMG). Saída crua: `Content/AutoCreated//`. diff --git a/ARQUITETURA_CHARACTER_MODEL.md b/ARQUITETURA_CHARACTER_MODEL.md new file mode 100644 index 0000000..819c3a1 --- /dev/null +++ b/ARQUITETURA_CHARACTER_MODEL.md @@ -0,0 +1,891 @@ +# Arquitetura: Character Model — Stats, Jobs, Fórmulas + +> **Status:** documento normativo paralelo ao `ARQUITETURA.md`. Define como o modelo de personagem do ZMMO funciona — atributos, classes, derivações, autoridade. Espelha as decisões do `.bases/rathena/rathena-master` mas adaptado ao stack TS + UE5.7 do Zeus. + +> **Escopo:** persistência + autoridade + onde tudo é calculado. **NÃO cobre** balanceamento de gameplay (decisão de game design — vai num doc de design separado). + +## Context + +O ZMMO usa modelo de atributos estilo Ragnarok Online — 6 stats primários que o jogador aloca pontos (STR/AGI/VIT/INT/DEX/LUK), com stats derivados calculados a partir deles + level + classe + equip. Jogador nasce **Aprendiz** (Novice) e especializa em outras classes ao chegar a critérios (job level + quest de mudança). + +Este doc fixa: +1. O que persiste no MySQL (CharServer) vs o que vive em memória (WorldServer). +2. Como classes são definidas (data-driven, não tabela DB). +3. Como atributos primários geram derivados (fórmulas espelhando rathena). +4. Onde acontece o cálculo (autoridade). +5. Como fluxos de criação/alocação/level up funcionam end-to-end. + +--- + +## 1. Princípios + +### 1.1 Persistência flat — TUDO na tabela `characters` + +Padrão da indústria emuladora (rathena, TrinityCore, AzerothCore, MaNGOS). Ver `.bases/rathena/rathena-master/sql-files/main.sql:209-296`. + +- ✅ 1 query lê o char completo (spawn, list, transfer). +- ✅ Writeback é 1 UPDATE única. +- ❌ Sem entidade `character_stats` separada (1:1 FK seria JOIN inútil — stats não são reutilizáveis entre chars, é relacionamento estritamente 1:1). + +### 1.2 Stats primários persistem; derivados são sempre recalculados + +| Categoria | Persiste no MySQL | Recalcula em runtime (WorldServer) | +|---|---|---| +| **Primários** (STR/AGI/VIT/INT/DEX/LUK) | ✅ | — | +| **Progressão** (base_level, base_exp, job_level, job_exp) | ✅ | — | +| **Pool atual** (hp, sp) | ✅ | — | +| **Pool máximo** (max_hp, max_sp) | ✅ (cache) | Recalcula em level up / equip change | +| **Pontos não-gastos** (status_point, skill_point) | ✅ | — | +| **Class id** | ✅ | — | +| **Moeda** (zeny) | ✅ | — | +| **Derivados** (ATK, MATK, DEF, MDEF, hit, flee, crit, aspd) | ❌ NUNCA | ✅ Toda hora via `StatusCalc` | +| **Regen rates** (hp_regen, sp_regen) | ❌ | ✅ | +| **Buffs/debuffs ativos** | ❌ | ✅ (efêmero) | + +**Por que NUNCA persistir derivados:** mudou um equip → derivado obsoleto. Aplicou buff → obsoleto. Cresceu de level → obsoleto. Salvar deriva inválida toda hora — bug fest silenciosa. Padrão rathena: `status_calc_pc_` em `.bases/rathena/rathena-master/src/map/status.cpp:4996` recalcula a `struct status_data` inteira a cada evento relevante. + +### 1.3 Classes (jobs) são data-driven — arquivo YAML, não tabela DB + +Padrão rathena: `db/re/job_stats.yml` carregado no boot. Hot-reloadable. + +- ✅ Versionado no git (mudança de balanceamento = commit auditável) +- ✅ Sem SELECT repetido em todo spawn +- ✅ Game designer edita arquivo, não roda SQL + +**No Zeus:** `Server/ZeusCharServer/data/jobs.yml` (carregado no boot do WorldServer e do CharServer). `characters.class_id` referencia lógica, não FK física. + +### 1.4 Autoridade + +| Componente | Quem decide | Quem persiste | +|---|---|---| +| Criar char (stats iniciais, class=Aprendiz) | CharServer (valida) | CharServer → MySQL | +| Stat allocation (gastar `status_point` em STR/etc.) | CharServer **ou** WorldServer | CharServer → MySQL (via writeback) | +| Subir de level / ganhar EXP | WorldServer (kill mob, quest) | WorldServer → writeback → CharServer → MySQL | +| Mudar classe (Aprendiz → Espadachim) | WorldServer (cumpre quest) | WorldServer → writeback → CharServer → MySQL | +| Recalcular ATK/MATK/DEF/etc. | WorldServer (memória, toda hora) | NÃO persiste | +| Casting de skill | WorldServer | NÃO persiste (efêmero) | +| Resultado de skill (HP perdido, item dropado) | WorldServer | WorldServer → writeback | + +**Regra:** o estado "definitivo" do char vive no MySQL do CharServer. O WorldServer só **simula** com cópia em memória + writeback periódico (60s) e em eventos críticos (level up, item raro, logout). + +--- + +## 2. Stats primários + +### 2.1 Os 6 stats clássicos + +| Stat | Nome PT-BR | Influencia diretamente | +|---|---|---| +| **STR** | Força | ATK (físico melee), peso carregável | +| **AGI** | Agilidade | ASPD (velocidade de ataque), Flee (esquiva) | +| **VIT** | Vitalidade | MaxHP, DEF (defesa física), resist status | +| **INT** | Inteligência | MaxSP, MATK (mágico), MDEF | +| **DEX** | Destreza | Hit (precisão), ATK (à distância — arco/arma de DEX), cast time | +| **LUK** | Sorte | CRIT (crítico), Perfect Dodge, drop rate raros | + +Persistidos como `SMALLINT UNSIGNED` (0-65535), default 1, cap configurável (rathena default 99 pré-renewal, 130 renewal — Zeus pode escolher). + +### 2.2 Stats primários adicionais (opcional, futuro) + +Rathena Renewal adiciona 6 "trait stats" para classes 4ª: POW, STA, WIS, SPL, CON, CRT. **Não escopo da Fase 1** — adicionar quando classes 4ª entrarem (Fase de gameplay avançado). + +### 2.3 Cost de alocar +1 stat + +Padrão rathena: `cost = (stat_atual - 1) / 10 + 2`. + +``` +str=1 → custo de +1 stat = (0/10)+2 = 2 pontos +str=10 → custo = (9/10)+2 = 2 +str=11 → custo = (10/10)+2 = 3 +str=20 → custo = (19/10)+2 = 3 +str=21 → custo = 4 +... +str=99 → custo = (98/10)+2 = 11 +``` + +Validação **SEMPRE server-side**: +```ts +const cost = Math.floor((current - 1) / 10) + 2; +if (char.status_point < cost) reject('NotEnoughPoints'); +UPDATE characters SET str = str+1, status_point = status_point - cost; +``` + +--- + +## 3. Stats derivados (calculados em runtime no WorldServer) + +Lista mínima a implementar. Fórmulas espelham rathena (`status_calc_misc`, `status_calc_pc_sub`, `status_base_atk` em `src/map/status.cpp`). + +### 3.1 ATK físico (base, sem equip) + +Renewal (Zeus alvo): +``` +base_atk = floor(str + (str/10)^2 + dex/5 + luk/3 + base_level/4) +``` + +Para armas de DEX (arco, instrumento, chicote, armas de fogo): troca STR ↔ DEX no cálculo principal. + +Total = `base_atk + weapon_atk + refine_bonus + cards_atk + status_atk_buffs`. + +Ref: `.bases/rathena/rathena-master/src/map/status.cpp:2424` (`status_base_atk`). + +### 3.2 MATK (mágico) + +``` +matk_min = floor(int + int/7^2 + dex/5 + luk/3 + base_level/4) +matk_max = floor(int + int/5^2 + dex/5 + luk/3 + base_level/4) +``` + +### 3.3 DEF (defesa física) + +``` +def2 = floor(vit + (vit/2)^2/30) +def_total = def2 + def_from_equip + def_from_refine +``` + +Renewal usa "soft DEF + hard DEF" — definir convenção no roadmap. + +### 3.4 MDEF (defesa mágica) + +``` +mdef2 = floor(int + vit/5 + dex/5) +mdef_total = mdef2 + mdef_from_equip +``` + +### 3.5 HIT (precisão) + +``` +hit = base_level + dex + luk/3 + skill_bonus + equip_bonus +``` + +### 3.6 FLEE (esquiva) + +``` +flee = base_level + agi + luk/5 + skill_bonus + equip_bonus +``` + +### 3.7 CRIT (crítico) + +``` +crit = 1 + luk/3 + skill_bonus + equip_bonus +``` + +(Em rathena multiplica por 10 internamente para resolução; converte na display.) + +### 3.8 ASPD (velocidade de ataque) + +``` +aspd = base_aspd_da_arma - (agi + dex/4) * fator_de_classe +``` + +`base_aspd_da_arma` vem do `jobs.yml` (cada job tem array `BaseASPD` por tipo de arma). + +### 3.9 MaxHP / MaxSP — três camadas (cuidado para não persistir bônus) + +Esta é a parte mais delicada. **Há três camadas** de MaxHP/MaxSP, cada uma com regra diferente: + +| Camada | Onde vive | Quando recalcula | Persiste no DB? | +|---|---|---|---| +| **1. Base** | `characters.max_hp` / `max_sp` (cache) | Level up, job change, stat allocation (VIT/INT permanentes) | ✅ Sim | +| **2. Bônus de equipamento** (anel VIT+5, armor MaxHP+200) | WorldServer em memória | Equip / desequip de item | ❌ Nunca — recalcula do `character_inventory` no spawn | +| **3. Bônus de buff** (skill "Endure" +100 MaxHP por 60s) | WorldServer em memória (`status_change`) | Buff start / expire | ❌ Nunca — efêmero, expira sozinho | + +**MaxHP efetivo (mostrado pro player) = camada 1 + camada 2 + camada 3.** Sempre calculado em runtime, nunca persistido. + +**Fórmula da camada 1 (base):** +``` +max_hp_base = floor(jobs.yml[class].HpFactor * base_level + jobs.yml[class].HpIncrease) * (1 + vit_base/100) +max_sp_base = floor(jobs.yml[class].SpFactor * base_level + jobs.yml[class].SpIncrease) * (1 + int_base/100) +``` + +Note: usa `vit_base` (`characters.vit`), **não** `vit_efetivo` (com bônus de equip). Bônus de equip não recalcula o cache base — entra como camada 2 separada. + +**Triggers que escrevem `characters.max_hp` (camada 1):** +- ✅ Level up (`base_level += 1`) +- ✅ Job change (`HpFactor` novo) +- ✅ Stat allocation permanente (`vit_base += 1`) + +**Triggers que NÃO escrevem `characters.max_hp` (só atualizam camadas 2/3 em memória):** +- ❌ Equip de item com VIT+5 ou MaxHP+200 +- ❌ Desequip de item +- ❌ Buff aplicado (skill, food, gravação) +- ❌ Buff expirou + +**Por que essa separação importa (cenário de bug se misturar):** + +Suponha que você persistisse `max_hp` total (base + equip + buff): +1. Player equipa anel VIT+5 → bônus +50 MaxHP → `max_hp=1500` salvo no DB (era 1450 base). +2. WorldServer crasha. +3. Spawn: lê `max_hp=1500` do DB. +4. Recarrega equip do inventory mas o WorldServer "esqueceu" qual era o bônus do anel — só vê 1500. +5. Player desequipa o anel → bug: continua com 1500. + +Pior com buff: writeback periódico salva `max_hp` durante buff ativo → buff expira mas DB tem o valor inflado → spawn dá HP zombie. + +**Padrão rathena** ([src/map/status.cpp:status_calc_pc_](.bases/rathena/rathena-master/src/map/status.cpp)): salva só camada 1. Camadas 2 e 3 são sempre recalculadas do equip + buffs ativos no spawn. Buffs não persistem (morrem no logout). + +**Estrutura em memória no WorldServer (template):** +```cpp +struct CharRuntimeStatus { + // --- Camada 1 (do DB; persistido) --- + uint32 max_hp_base; + uint32 max_sp_base; + uint16 str_base, agi_base, vit_base, int_base, dex_base, luk_base; + + // --- Camada 2 (do inventory; não persiste) --- + int32 equip_max_hp_bonus, equip_max_sp_bonus; + int16 equip_str_bonus, equip_agi_bonus, /*...*/ equip_luk_bonus; + + // --- Camada 3 (de status_change ativos; não persiste) --- + int32 buff_max_hp_bonus, buff_max_sp_bonus; + int16 buff_str_bonus, /*...*/; + + // --- Computados on-demand (camada 1 + 2 + 3) --- + uint32 EffectiveMaxHp() const { return max_hp_base + equip_max_hp_bonus + buff_max_hp_bonus; } + uint16 EffectiveVit() const { return vit_base + equip_vit_bonus + buff_vit_bonus; } + // ... análogo pros outros stats +}; +``` + +Cliente sempre vê o efetivo (`S_CHAR_HP_UPDATE { effective_hp, effective_max_hp }`). Cliente nunca vê o base nem os bônus separados — só a soma final. + +--- + +## 4. Schema `jobs.yml` + +Arquivo: `Server/ZeusCharServer/data/jobs.yml`. + +```yaml +version: 1 +jobs: + - id: 0 + name: Novice # nome técnico (inglês, estável) + display_name_ptbr: Aprendiz + parent_job: null + max_base_level: 99 + max_job_level: 10 + hp_factor: 35 # rathena Novice: ~35 + hp_increase: 0 + sp_factor: 10 + sp_increase: 0 + max_weight: 20000 + base_aspd: + bare: 2000 + dagger: 1900 + sword: 2000 + starting_hp: 40 + starting_sp: 11 + starting_status_points: 0 + starting_skill_points: 0 + allowed_weapons: [bare, dagger] + skills: + - id: NV_BASIC + max_level: 10 + - id: NV_FIRST_AID + max_level: 1 + + - id: 1 + name: Swordman + display_name_ptbr: Espadachim + parent_job: Novice # precisa ter sido Novice antes + requirements: + base_level: 1 # 1 (na verdade qualquer; Ragnarok exige job_level 10 Novice — definir) + job_level: 10 # do Novice + quest: "swordman_test" # quest id no questdb + max_base_level: 99 + max_job_level: 50 + hp_factor: 70 + hp_increase: 200 + sp_factor: 20 + sp_increase: 200 + bonus_stats: # auto-stats ao subir job level + - job_level: 2 + str: 1 + - job_level: 6 + vit: 1 + # ... + allowed_weapons: [bare, dagger, sword, two_hand_sword, axe] + skills: + - id: SM_BASH + max_level: 10 + # ... + + # Mago, Arqueiro, Mercador, Ladrão, Acólito... +``` + +**Notas:** +- IDs estáveis (não mude) — `characters.class_id` referencia direto. +- Mudar campo `hp_factor` ou `bonus_stats` rebalanceia o jogo retroativamente. **OK** — RuneScape/PoE/MMOs fazem isso o tempo todo. +- Hot-reload: endpoint admin `POST /admin/jobs/reload` recarrega `jobs.yml` em runtime sem restart (Fase 6+; Fase 1 só load no boot). + +--- + +## 5. Fluxos + +### 5.1 Criação de personagem (Aprendiz) + +``` +Cliente → C_CHAR_CREATE { name, world_id, class_id=NOVICE, appearance } + +CharServer: + 1. Validar (name único globalmente, world_id existe, slot disponível, class_id=NOVICE — não pode criar direto em outra classe) + 2. Lookup jobs.yml[NOVICE] → { starting_hp, starting_sp, hp_factor, sp_factor, ... } + 3. INSERT characters ( + account_id, world_id, slot, name, class_id=NOVICE, + base_level=1, base_exp=0, job_level=1, job_exp=0, + str=1, agi=1, vit=1, int=1, dex=1, luk=1, + hp=starting_hp, max_hp=starting_hp, sp=starting_sp, max_sp=starting_sp, + status_point=0, skill_point=0, + zeny=0, appearance, + map_name="zmmo_starting_village", pos_x=..., pos_y=..., pos_z=..., yaw_deg=0 + ) + 4. → S_CHAR_CREATE_OK +``` + +**Nota:** o player **sempre** nasce Aprendiz. Outras classes só via job change in-game (Fase pós-Fase 3). + +### 5.2 Stat allocation (após level up) + +``` +Cliente → C_CHAR_STAT_ALLOC { stat: "str", amount: 1 } + +CharServer (ou WorldServer; ver §6 — autoridade): + 1. Lookup char no cache/DB + 2. cost = floor((char.str - 1) / 10) + 2 + 3. amount * cost ≤ char.status_point? else REJECT('NotEnoughPoints') + 4. char.str + amount > MAX_STAT(jobs.yml[class].max_stat)? else REJECT('StatCapped') + 5. UPDATE characters SET str = str + 1, status_point = status_point - cost, version = version + 1 + 6. WorldServer: status_calc_pc(char) → recalcula derivados → manda S_CHAR_STAT_UPDATE pro cliente +``` + +### 5.3 Level up + +``` +WorldServer (em kill mob / complete quest): + 1. char.base_exp += mob.exp_reward + 2. enquanto char.base_exp >= exp_table[char.base_level + 1]: + char.base_exp -= exp_table[char.base_level + 1] + char.base_level += 1 + char.status_point += status_per_level(char.base_level) // ex.: 3 base + level/4 + max_hp = recalc(char, jobs.yml) + max_sp = recalc(char, jobs.yml) + char.hp = max_hp // heal full no level up (padrão Ragnarok) + char.sp = max_sp + 3. POST /interserver/characters/{id}/checkpoint (writeback imediato — level up é crítico) + 4. Enviar S_CHAR_LEVEL_UP pro cliente +``` + +Mesmo padrão pra job_exp/job_level (com skill_point em vez de status_point). + +### 5.4 Job change (Aprendiz → Espadachim, etc.) + +``` +WorldServer (player cumpriu quest): + 1. Validar requirements: jobs.yml[Swordman].requirements.{base_level, job_level, quest} + 2. char.class_id = SWORDMAN + 3. char.job_level = 1 + 4. char.job_exp = 0 + 5. (Opcional Ragnarok-style: status_point NÃO zera; player mantém stats alocados) + 6. recalc max_hp/max_sp (novo HpFactor/SpFactor) + 7. POST /interserver/characters/{id}/checkpoint + 8. S_CHAR_JOB_CHANGE +``` + +### 5.5 Logout — writeback final + +``` +WorldServer: + POST /interserver/characters/{id}/checkpoint { + str, agi, vit, int, dex, luk, + base_level, base_exp, job_level, job_exp, + hp, max_hp, sp, max_sp, + status_point, skill_point, zeny, + map_name, pos_x, pos_y, pos_z, yaw_deg, + appearance, + version: current_version + } +``` + +Conflict (409) → recarrega + merge + retry. + +--- + +## 5.6 Loadouts (build switching) — Modelo A: equip + skills, stats permanentes + +### Premissa + +**Stats primários (str/agi/vit/int/dex/luk) são permanentes** uma vez alocados — padrão Ragnarok clássico. Permitir trocar stats via loadout livre destruiria o significado de cada ponto investido. + +Loadouts no ZMMO guardam **apenas equip e skills** — exatamente o padrão FFXIV Gearset / WoW Equipment Manager. Cobre o caso de uso real (alternar entre setup PvE/PvP/farm sem reequipar manualmente todos os slots), sem virar free respec. + +### Stat reset (separado de loadouts) + +Reset de stats é uma operação **rara e cara**, fora do sistema de loadouts. Caminhos previstos (a decidir no game design): +- Item raro "Reset Stone" (drop de boss / cash shop). +- NPC específico que cobra moeda (zeny alto ou moeda especial). +- Privilégio VIP (limite mensal). + +Mecânica: zera `str/agi/vit/int/dex/luk` aos valores base do job, e converte tudo investido em `status_point` (player realoca do zero). WorldServer endpoint dedicado, audit log obrigatório. + +### Schema `character_loadouts` + +```sql +CREATE TABLE character_loadouts ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + character_id BIGINT UNSIGNED NOT NULL, + slot_index TINYINT UNSIGNED NOT NULL, -- 0..N-1 (N = MAX_LOADOUTS, ex.: 10) + name VARCHAR(32) NOT NULL, -- "Tank", "DPS PvE", "PvP Build" + + -- Snapshot de equip (referência a instâncias específicas no inventory, + -- FK pra character_inventory.id que entra em frente futura). + equip_head_id BIGINT UNSIGNED NULL, + equip_body_id BIGINT UNSIGNED NULL, + equip_weapon_id BIGINT UNSIGNED NULL, + equip_shield_id BIGINT UNSIGNED NULL, + equip_garment_id BIGINT UNSIGNED NULL, + equip_footgear_id BIGINT UNSIGNED NULL, + equip_acc_left_id BIGINT UNSIGNED NULL, + equip_acc_right_id BIGINT UNSIGNED NULL, + equip_head_top_id BIGINT UNSIGNED NULL, + equip_head_mid_id BIGINT UNSIGNED NULL, + equip_head_bottom_id BIGINT UNSIGNED NULL, + -- (Slots espelham o padrão rathena: weapon, shield, garment, footgear, + -- acc esquerda, acc direita, head top/mid/bottom) + + -- Snapshot de hotbar de skills (JSON: { slot_index: skill_id }) + skill_hotbar JSON NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + FOREIGN KEY (character_id) REFERENCES characters(id) ON DELETE CASCADE, + UNIQUE KEY uniq_char_slot (character_id, slot_index), + INDEX idx_loadouts_character (character_id) +); +``` + +**Notas:** +- **`characters` continua flat e inalterado** — não há `active_loadout_id`. Stats + equip atuais vivem em `characters`. Loadouts são *snapshots salvos*, não "estado ativo". +- Não inclui stats — modelo A. Loadout não é respec. +- Não inclui skills aprendidas (essas ficam em `character_skills` quando a feature entrar) — só a `skill_hotbar` (quais skills estão nas teclas de atalho). +- Referências de equip apontam pra **instâncias específicas** do item (`character_inventory.id`), não pra item template id genérico. Se o player vendeu o item depois de salvar a build, o slot vira NULL na hora de aplicar + warning UI. + +### Fluxos de loadout + +**Salvar build atual como preset:** +``` +Cliente → C_LOADOUT_SAVE { slot_index, name } +WorldServer: + Pega equip atual + skill_hotbar do char em memória + INSERT/UPDATE character_loadouts SET ... + POST /interserver/characters/{id}/loadouts/save (writeback imediato) + S_LOADOUT_SAVED { slot_index } +``` + +**Aplicar loadout (trocar equip + hotbar):** +``` +Cliente → C_LOADOUT_APPLY { slot_index } +WorldServer: + SELECT * FROM character_loadouts WHERE character_id=$id AND slot_index=$slot + Para cada equip_*_id no loadout: + Verifica se item ainda existe em character_inventory + pertence ao char + Se sim: equipa + Se não: slot vira NULL + retorna warning lista de slots "perdidos" + Aplica skill_hotbar + Recalcula stats derivados (StatusCalc) + S_LOADOUT_APPLIED { applied_slot, missing_slots? } +``` + +**Listar loadouts (CharSelect ou in-world):** +``` +Cliente → C_LOADOUT_LIST +WorldServer: + SELECT slot_index, name FROM character_loadouts WHERE character_id=$id ORDER BY slot_index + S_LOADOUT_LIST [{ slot_index, name, summary }] +``` + +**Deletar loadout:** +``` +Cliente → C_LOADOUT_DELETE { slot_index } +DELETE FROM character_loadouts WHERE character_id=$id AND slot_index=$slot +``` + +### Comportamento na seleção de char (CharSelect) + +Aplicar loadout só funciona **in-world**, não na CharSelect — porque depende do inventory carregado, contexto de combate, etc. CharSelect só lista qual foi a última usada (estado UX no cliente). WorldServer aplica o loadout ativo automaticamente no spawn se cliente indicou via parâmetro do C_CHAR_SELECT (opcional — pode também simplesmente carregar o último equip salvo em `characters` direto). + +### Autoridade + +Mesmo princípio dos stats: **WorldServer é dono**. Cliente envia comando, WorldServer valida + aplica + persiste via canal C. + +### Eager loading no spawn (resposta à sua dúvida específica) + +**Pergunta original:** "ao pegar Character retorna também Stats — fica mais fácil". + +Como decidimos modelo A (stats permanentes em `characters`, sem entidade separada), **não há eager loading de stats** — já vêm na mesma row. + +Para loadouts: +- **CharServer (TypeORM):** anota relacionamento com `{ eager: true }` ou faz `.relations(['loadouts'])` em queries específicas. Suporte nativo. +- **WorldServer (`ZeusPersistence` C++):** hoje o `Repository` é single-table. **Recomendação imediata:** 2 queries separadas no service de spawn (`Repo.Find(id)` + `Repo.FindWhere(character_id=id)`). Performance idêntica a JOIN para single char. Simples. + - **Roadmap (`ZeusPersistence` v2):** estender com `Repository::Include()` (estilo EF Core). Vale o trabalho quando aparecer a 3ª relação eager (loadouts + inventory + skills + buffs). Adiar até lá. + +### Roadmap + +Loadouts entram em **frente própria de gameplay**, depois das Fases A-C do Character Model e da feature de inventory. Não bloqueia Fase 1 do ServerSelect — schema é puro additive (não muda `characters`). + +--- + +## 6. Autoridade — onde stat allocation acontece? + +Duas escolhas defensáveis: + +| Onde | Vantagens | Desvantagens | +|---|---|---| +| **CharServer** (recebe `C_CHAR_STAT_ALLOC` direto do cliente) | Simples; muda DB direto sem RPC | Cliente in-world tem que avisar WorldServer "stats mudaram" → recalcular | +| **WorldServer** (cliente sempre fala com mundo) | WorldServer já tem char na memória, recalcula imediato | Stat allocation só funciona logged in (sem alocação fora do mundo) | + +**Recomendação:** **WorldServer.** Razões: +1. Stat allocation só faz sentido in-world (precisa de UI de char, contexto de combate). +2. WorldServer tem stats derivados em memória — UPDATE de STR força recálculo imediato. +3. CharServer não precisa entender game logic de cost (`(s-1)/10+2`) — fica isolado. +4. Padrão rathena: map-server (= WorldServer) faz `pc_statusup` ([rathena/src/map/pc.cpp](.bases/rathena/rathena-master/src/map/pc.cpp)). + +Fluxo: cliente envia opcode UDP pro WorldServer → valida + UPDATE em memória + writeback diferido (não imediato — junto com o próximo checkpoint periódico de 60s). Level up é exceção (writeback imediato). + +--- + +## 7. Anti-cheat e validação server-side + +**Toda** mudança de stat passa por validação no servidor: + +- Cliente envia `C_CHAR_STAT_ALLOC { stat, amount }` → servidor valida + faz, nunca cliente diz "novo valor". +- HP/SP nunca vêm do cliente — só do WorldServer (autoridade). +- EXP é WorldServer who-says. +- Zeny: transações sempre server-side (trade, NPC shop, drop). + +Cliente apenas **renderiza** o estado que o servidor empurra (`S_CHAR_STAT_UPDATE`, `S_CHAR_HP_UPDATE`, etc.). + +Audit log entries (Fase 3 do ServerSelect doc): toda mudança de zeny>1000, level up, mudança de classe, item raro recebido → `POST /interserver/audit`. + +--- + +## 8. Roadmap de implementação + +### Fase A — Schema + criação de char Aprendiz (parte da Fase 1 do ServerSelect) + +- Schema `characters` com todos os campos (já especificado no `ARQUITETURA_SERVER_SELECT.md`). +- `jobs.yml` apenas com `Novice` (Aprendiz) — outras classes ficam pra B. +- CharServer carrega `jobs.yml` no boot. +- `C_CHAR_CREATE` valida `class_id == NOVICE` e usa defaults do yml. +- Cliente UI: tela de criação com nome + appearance (sem stat allocation aqui — Aprendiz nasce com todos os stats=1). + +### Fase B — Classes adicionais + job change + +- `jobs.yml` expande para Espadachim, Mago, Arqueiro, Mercador, Ladrão, Acólito (6 first jobs clássicos do Ragnarok). +- Quest de mudança de classe (mínimo: NPC simples; quest system real fica pra C). +- WorldServer endpoint pra mudar class. + +### Fase C — Stat allocation + status_point/skill_point granting + +- WorldServer recebe `C_CHAR_STAT_ALLOC` UDP opcode. +- Validação cost = `(s-1)/10+2`. +- `S_CHAR_STAT_UPDATE` push pro cliente. +- Skill point granting tied to job_level up. + +### Fase D — Stats derivados completos + StatusCalc framework + +- Implementar `StatusCalc::ComputeAll(char, equip, buffs)` no WorldServer C++. +- ATK/MATK/DEF/MDEF/hit/flee/crit/aspd. +- Push de update no equip change, buff aplicado, level up. + +### Fase E — Classes 2ª (advanced) e além + +- Especialização (Knight, Wizard, Hunter, etc. — segunda promoção do Ragnarok). +- Eventualmente classes 3ª (Renewal) e trait stats (POW/STA/WIS/SPL/CON/CRT). + +### Fase F — Loadouts (build switching, Modelo A) + +**Pré-requisito:** inventory implementado (FK de `character_loadouts.equip_*_id` aponta pra `character_inventory.id`). + +- Schema `character_loadouts` (já especificado em §5.6). +- WorldServer endpoints: `C_LOADOUT_SAVE/APPLY/LIST/DELETE`. +- Cliente UI: tela "Builds" com até N slots, botões salvar/aplicar/renomear/deletar, preview de equip. +- Audit log em apply (Fase 3 do ServerSelect doc). + +### Fase G — Stat reset (item / NPC / VIP) + +- Endpoint `C_CHAR_STAT_RESET` no WorldServer. +- Validação: tem o item de reset? Tem o zeny do NPC? Tem privilégio VIP + cooldown? +- Lógica: zera str/agi/vit/int/dex/luk pros base do job; converte total investido em `status_point`. +- Audit log obrigatório (write-heavy, sensível a abuso). + +**A se conecta com a Fase 1 do ServerSelect.** B-G são frentes próprias de gameplay, fora do escopo de network/handoff. + +--- + +## 9. Decisões consolidadas + +1. **Stats primários flat** na tabela `characters` (str/agi/vit/int/dex/luk + level/exp + hp/sp/max_hp/max_sp + status_point/skill_point + zeny + class_id). **Sem entidade separada.** +2. **Stats derivados NUNCA persistidos** — sempre recalculados em runtime no WorldServer (`StatusCalc::ComputeAll`). +3. **Jobs data-driven** via `Server/ZeusCharServer/data/jobs.yml` (versionado no git, hot-reloadable). `characters.class_id` é lookup lógico. +4. **Player nasce Aprendiz** (`class_id=NOVICE`); especialização via quest in-game (Fase B). +5. **Stat allocation acontece no WorldServer** (não CharServer). Validação `cost = (s-1)/10 + 2` server-side. +6. **Anti-cheat:** toda mudança de estado é server-authoritative. Cliente apenas renderiza. +7. **Fórmulas espelham rathena** (`status_calc_pc_` em `src/map/status.cpp:4996`) — adapta valores pra balanceamento Zeus depois. +8. **MaxHP/MaxSP são persistidos como cache** (recalcula em level up / equip change, salva pra evitar recalcular no spawn). +9. **Writeback granular:** stat allocation = writeback diferido (junto do checkpoint 60s); level up + job change = writeback imediato. +10. **Loadouts/build switching (Modelo A — Ragnarok clássico):** tabela `character_loadouts` separada (1:N com `characters`). Guarda **apenas equip + skill_hotbar**, NÃO stats. Stats permanecem permanentes; reset só via item raro / NPC / VIP (operação dedicada, fora do sistema de loadouts). Aplicar loadout = trocar equip e hotbar, server-side, in-world only. +11. **Sem `active_loadout_id` em `characters`:** estado vivo permanece em `characters` (flat). Loadouts são snapshots salvos pra reaplicar sob demanda. +12. **Eager loading:** TypeORM já suporta. No `ZeusPersistence` C++ usa 2 queries separadas por enquanto; `Include()` fluent fica como roadmap quando houver 3+ relações eager. + +--- + +## 10. Lições do rathena — o que adotar e o que evitar + +Análise direta do código do rathena (versão master atual em `.bases/rathena/rathena-master/`). Lista o que vale levar pro modelo moderno do Zeus e o que é legado a evitar. + +### 10.1 Três estruturas de status, não duas + +Rathena tem **3 structs separadas** convivendo em `map_session_data` ([src/map/pc.hpp:382-388](.bases/rathena/rathena-master/src/map/pc.hpp)): + +```cpp +class map_session_data : public block_list { + ... + struct mmo_charstatus status; // (1) PERSISTED — SQL <-> RAM + struct status_data base_status; // (2) BASE — RAM only + struct status_data battle_status; // (3) BATTLE — RAM only + status_change sc; // (Buffs/debuffs ativos) + ... +}; +``` + +| Camada | Conteúdo | Persiste no DB? | Recalcula quando | +|---|---|---|---| +| **(1) `mmo_charstatus status`** (PERSISTED) | Stats primários permanentes, level/exp, HP/SP atuais, equip slots, skill levels aprendidos, zeny, pos | ✅ Sim — espelha SQL `char` table | Em writeback periódico (a partir do battle_status) | +| **(2) `status_data base_status`** (BASE) | Stats primários + bônus de equip + bônus de skills passivas. ATK/MATK/DEF/etc. SEM buffs ativos. | ❌ Não | Equip change, level up, mudança de skill, mudança de job, stat allocation. Via `status_calc_pc_` ([src/map/status.cpp:4996](.bases/rathena/rathena-master/src/map/status.cpp)) | +| **(3) `status_data battle_status`** (BATTLE) | base_status **+** todos os buffs/debuffs ativos (`status_change`). É o valor "final" usado em combat e mostrado pro cliente. | ❌ Não | Cada vez que um buff aplica/expira, cada hit (debuffs proc). Via `status_calc_bl_main` ([src/map/status.cpp:5836](.bases/rathena/rathena-master/src/map/status.cpp)) | + +**Por que três e não duas:** separar equip+passivas (camada 2, muda raro) de buffs (camada 3, muda toda hora) permite recalcular APENAS camada 3 quando um buff entra/sai. Sem isso, todo buff força recálculo completo de equipamento — caro em PvP/raids com muitos buffs simultâneos. + +**Adaptação Zeus C++ (WorldServer):** + +```cpp +struct CharRuntimeState { + PersistedStatus persisted; // copia do MySQL via ZeusPersistence (camada 1) + BaseStatusData base_status; // calculado de persisted + equip + passivas (camada 2) + BaseStatusData battle_status; // base_status + status_change (camada 3) + StatusChangeContainer sc; // lista de buffs ativos com expire_tick +}; +``` + +`battle_status` é o que vai pro `S_CHAR_STAT_UPDATE` pro cliente. Cliente nunca vê `base_status` nem `persisted` direto. + +### 10.2 Recompute total > delta incremental + +Rathena **zera `base_status` inteiro** (`memset` em status.cpp:3816) e recompila tudo do zero a cada `status_calc_pc_`. Não tenta "subtrair o equip antigo, somar o novo" — pull-not-push. + +```cpp +// rathena status.cpp:3816 (simplificado) +memset(&base_status->max_hp, 0, sizeof(struct status_data) - sizeof(hp/sp/ap)); +// reaplica defaults do job +// reaplica equipped items +// reaplica skills passivas +// recalcula derivados +``` + +**Por que:** delta incremental cria drift sutil. Um bug de "esqueci subtrair na hora de desequipar" e o stat fica inflado pra sempre. Recompute total é O(N) por evento mas N é pequeno (~10 slots de equip + ~30 skills passivas) e elimina bug class inteira. + +**Adotar:** mesma estratégia no `WorldServer::StatusCalc::RecomputeBase(char)`. Em C++ moderno usar `base_status = BaseStatusData{};` (move-assign do default) em vez de `memset` (porque structs com membros não-trivially-copyable quebram). + +### 10.3 HP/SP atual nunca é zerado no recálculo + +Truque clássico do rathena (status.cpp:3816): o `memset` começa em `&base_status->max_hp`, **pulando** os primeiros membros (`hp`, `sp`, `ap`). HP atual sobrevive ao recálculo de base_status. + +**Razão:** equipar um anel não pode resetar o HP do char pra zero. Mas pode mudar `max_hp` — se `max_hp` aumentou, HP continua o atual; se diminuiu, capa em `min(hp, max_hp)`. + +**Adotar:** ordenar membros do struct em C++ pra ter HP/SP/AP no topo, e adotar regra `RecomputeBase()` sem tocar nesses 3. Ou explicitamente preservar: + +```cpp +const auto saved_hp = base_status.hp, saved_sp = base_status.sp; +base_status = BaseStatusData{}; +base_status.hp = saved_hp; base_status.sp = saved_sp; +ApplyJobDefaults(); ApplyEquip(); ApplyPassives(); ComputeDerived(); +base_status.hp = std::min(base_status.hp, base_status.max_hp); // cap +``` + +### 10.4 Recursion guard obrigatório no recálculo + +Rathena ([status.cpp:3756-3764](.bases/rathena/rathena-master/src/map/status.cpp)): + +```cpp +static int32 calculating = 0; +if (++calculating > 10) return -1; +// ... recálculo +--calculating; +``` + +**Razão:** recálculo de stat pode trigger skill que aplica buff que dispara recálculo. Sem guard = stack overflow em corner cases (combos de auto-cast, item triggers cascading). + +**Adotar:** flag thread-local `bool recalculating = false` ou contador. Se já está em recálculo, retornar erro/skip em vez de reentrar. Importante mesmo em arquiteturas modernas — buffs reativos a hits criam loops circulares fácil. + +### 10.5 Recalc parcial via bitflags (otimização avançada) + +Rathena tem ([status.hpp](.bases/rathena/rathena-master/src/map/status.hpp)): + +```cpp +enum scb_flag { + SCB_NONE = 0, SCB_BASE, SCB_STR, SCB_AGI, /*...*/, SCB_WATK, SCB_BATK, + SCB_MAX +}; +status_calc_bl_main(bl, std::bitset{ SCB_STR | SCB_WATK }); +``` + +Quando só STR muda (e nada mais), recalcula apenas WATK derivado (que depende de STR). Performance no PvP de alto nível. + +**Adotar:** **NÃO na Fase 1**. Implementar como otimização depois quando profile mostrar `RecomputeBase` aparecendo no top do flame chart. Pra MMOs com <1000 players concurrent, recompute total é OK. + +### 10.6 Buffs em estrutura separada com expiração por timer + +Rathena `status_change` ([src/map/status.hpp](.bases/rathena/rathena-master/src/map/status.hpp)): + +```cpp +struct status_change_entry { + int32 timer; // handle do scheduler + int32 val1-4; // payload (varia por tipo) + t_tick tick; // duração total +}; +struct status_change { + status_change_entry *data[SC_MAX]; // indexado por SCType +}; +``` + +Cada SC (status change) tem timer próprio que dispara `status_change_end` quando expira. Engine de timer global gerencia. **Buffs NUNCA persistem em SQL** — morrem em logout (regra mais comum) ou DC (alguns servers preservam alguns SC em rejoin rápido). + +**Adotar:** +```cpp +struct StatusChangeEntry { + StatusChangeType type; + int32 stack_value; // SC ID + payload + TickT expire_at; // absolute deadline +}; +class StatusChangeContainer { + std::unordered_map active; + void Tick(TickT now) { /* expira os terminados, dispara recalc se necessário */ } +}; +``` + +Buffs morrem em logout — padrão da indústria. Exceções (food buffs, gravações permanentes, equip-while-stationary) tratadas via cases especiais no game design depois. + +### 10.7 Item bonus data-driven, não hardcoded + +Rathena tem `pc_bonus` ([src/map/pc.cpp](.bases/rathena/rathena-master/src/map/pc.cpp)) com switch case gigante: + +```cpp +void pc_bonus(map_session_data *sd, int32 type, int32 val) { + switch (type) { + case SP_STR: sd->base_status.str += val; break; + case SP_AGI: sd->base_status.agi += val; break; + case SP_MAXHP: sd->base_status.max_hp += val; break; + // ... ~200 cases + } +} +``` + +E `item_db.yml` declara em YAML quais bonuses cada item dá: +```yaml +- Id: 1109 + Name: Sword + Type: Weapon + Script: | + bonus bAtk, 25; + bonus bStr, 1; +``` + +**O bom:** items são data-driven (designer edita YAML, não código). +**O ruim:** switch case com 200 cases é horror de manter. Bug em um case afeta um stat só, difícil de testar. + +**Adotar (versão moderna):** +```cpp +using BonusHandler = void(*)(BaseStatusData&, int32 val); +inline const std::unordered_map kBonusHandlers = { + { BonusType::Str, [](auto& s, int32 v){ s.str += v; } }, + { BonusType::Agi, [](auto& s, int32 v){ s.agi += v; } }, + { BonusType::MaxHp, [](auto& s, int32 v){ s.max_hp += v; } }, + // ... +}; +void ApplyBonus(BaseStatusData& s, BonusType type, int32 val) { + if (auto it = kBonusHandlers.find(type); it != kBonusHandlers.end()) { + it->second(s, val); + } +} +``` + +Mesma capacidade data-driven, sem switch gigante. Testes unitários ficam triviais (testa cada handler isolado). + +### 10.8 Save state separado do battle state + +Rathena periodicamente faz `pc_makesavestatus(sd)` que **copia** `sd->battle_status.hp/sp/ap` → `sd->status.hp/sp/ap`. Só depois isso vai pro CharServer/SQL. + +**Razão:** `battle_status` tem cap inflado por buff (max_hp +500 de buff). `status.hp` deve refletir o valor real que faz sentido persistir (HP atual, capa de `status.max_hp` que é o base puro sem buff). + +**Adotar:** writeback NUNCA salva valores afetados por buff ativo. Save HP é o `min(battle.hp, base.max_hp)`. Save MaxHP é `base.max_hp`. Garante que log out + log in com buff expirado não dá HP zombie. + +### 10.9 O que NÃO levar do rathena + +Práticas que são legado e dão mais trabalho do que valem em projeto novo: + +| Anti-pattern | Por que evitar | Alternativa moderna | +|---|---|---| +| **`battle_config` global com 500+ flags** | Estado mutável global, difícil de testar, conflito em multi-tenant | Struct `BalanceConfig` injetada por DI; YAML de balance versionado | +| **Arrays C fixos (`MAX_SKILL`, `MAX_INVENTORY`)** | Crash se exceder; memória desperdiçada se vazio | `std::vector` ou `std::array` com bounds-check em debug | +| **`map_session_data : block_list`** (herda) | Mistura "dados do char" com "ator com posição no mundo" | Separar `CharacterData` (POD) de `WorldActor` (componente de posição/movimento) | +| **Job class em bitfield** (`JOBL_BABY`, `MAPID_SUMMONER`) | Decisão de packet do RO original. Manutenção horrível | `enum class JobId` + `struct JobTraits { bool is_baby; bool can_summon; }` data-driven em `jobs.yml` | +| **Recursive call preemption por contador** | Workaround pra event model mal-formado | ECS / actor model não-reentrante; quando reentrância é necessária, fila de eventos diferida | +| **`pc_calc_skilltree` muta skill tree do char** | Mistura "skills aprendidas" (persistente) com "efeitos passivos ativos" (derivado) | Separar `LearnedSkills` (no DB) de `ActivePassives` (calculado em memória) | +| **`status_change->data[SC_MAX]` array linkedlist** | Lookup linear, memória pré-alocada | `std::unordered_map` ou `boost::flat_map` para hot path | +| **Hot-reload via `@reloadbattleconf`** in-game commands | Não tem versionamento, vira PvP-zoeira "ajustei config no live" | Hot-reload via endpoint admin autenticado + audit log obrigatório | +| **SQL queries em string puro** (sem ORM) no map server | SQL injection era real; manutenção horrível | ZeusPersistence (já fizemos) ou TypeORM no CharServer | +| **Globals com função inicializadora (`do_init_status`)** | Side effects no static init | Singleton com lifetime explícito, `StatusEngine engine; engine.Init(config);` | + +### 10.10 Resumo do que vai pro Zeus + +**Adotar:** +1. **3 structs**: `PersistedStatus` (SQL), `BaseStatusData` (RAM, equip+passivas), `BattleStatusData` (RAM, +buffs). +2. **Recompute total** em camadas 2 e 3 — não delta incremental. +3. **HP/SP atual preservado** em recálculo de base. +4. **Recursion guard** no `RecomputeBase`/`RecomputeBattle`. +5. **`StatusChange` container** com expiração por timer; buffs nunca persistem. +6. **Bonus handler table** (não switch case). +7. **Save state separado** do battle state no writeback. + +**Adiar (otimização):** +- Recalc parcial via bitflags (vale com >1000 concurrent). + +**Evitar:** +- battle_config global; arrays C fixos; herança block_list; job em bits; SQL crú no map server; hot-reload sem audit. + +--- + +## 11. Não cobertos aqui + +Por design, estes temas têm docs próprios (futuros): + +- **Inventory e items** (slots, refine, cards, sockets) — `ARQUITETURA_INVENTORY.md` (a criar). +- **Skill system** (skill tree, cooldowns, animations) — `ARQUITETURA_SKILLS.md` (a criar). +- **Quests** — `ARQUITETURA_QUESTS.md` (a criar). +- **Combat resolution** (damage formula, element table, race modifier) — `ARQUITETURA_COMBAT.md` (a criar). +- **Game balance** (números concretos: quanto STR dá quanto ATK em level N) — `DESIGN_BALANCE.md` (decisão de game design, não arquitetura). +- **Party/guild** — `ARQUITETURA_SOCIAL.md` (a criar). + +Este documento só fixa **estrutura e autoridade** — o que vai onde, quem decide o quê. + +--- + +## Referências + +- rathena `db/re/job_stats.yml` — schema base de jobs ([.bases/rathena/rathena-master/db/re/job_stats.yml](Server/.bases/rathena/rathena-master/db/re/job_stats.yml)) +- rathena `src/map/status.cpp:status_calc_pc_` — recálculo de status ([linha 4996](Server/.bases/rathena/rathena-master/src/map/status.cpp)) +- rathena `src/map/status.cpp:status_base_atk` — fórmula de ATK base ([linha 2424](Server/.bases/rathena/rathena-master/src/map/status.cpp)) +- rathena `sql-files/main.sql:209-296` — tabela `char` flat ([main.sql](Server/.bases/rathena/rathena-master/sql-files/main.sql)) +- rathena `src/map/pc.cpp:pc_statusup` — server-side stat allocation +- [`ARQUITETURA_SERVER_SELECT.md`](ARQUITETURA_SERVER_SELECT.md) — schema de `characters` e fluxos de criação no contexto de mundos diff --git a/ARQUITETURA_SERVER_SELECT.md b/ARQUITETURA_SERVER_SELECT.md new file mode 100644 index 0000000..899ccf6 --- /dev/null +++ b/ARQUITETURA_SERVER_SELECT.md @@ -0,0 +1,593 @@ +# Arquitetura: Server Select + Mundos + Handoff CharServer↔WorldServer + +> **Status:** plano de implementação + extensão da arquitetura. Quando aprovado, este documento vira `Clients/ZMMO/Docs/ARQUITETURA_SERVER_SELECT.md` (ou path equivalente em `Server/`) — é referência permanente, não plano descartável. + +## Context + +Hoje a tela de Server Select carrega após o login mas é um mock visual sem dados reais: + +- `worldHost`/`worldPort` no `S_CHAR_SELECT_OK` está hardcoded em `127.0.0.1:27777`. +- `handoffToken` é gerado aleatório no CharServer mas **nada valida no WorldServer**. +- Não existe tabela `worlds` no schema; não existe opcode pra listar mundos. +- `characters` não tem `world_id` — qualquer char joga em qualquer mundo. +- WorldServer não reporta população/status; CharServer não sabe quem está online onde. + +Esta frente desenha a arquitetura "de verdade": múltiplos mundos cadastrados, criação de personagem amarrada ao mundo escolhido, handoff seguro com ticket validado, e uma base que escale pra multi-node + multi-region. Cobre desde o desenho conceitual até execução em fases pequenas e verificáveis. + +--- + +## Princípios arquiteturais (consolidado, após autocrítica) + +### 1. Separação de responsabilidades + +| Responsabilidade | Quem é dono | Justificativa | +|---|---|---| +| **Identidade do jogador** (account, JWT, sessões) | CharServer + MySQL global | Auth precisa ser único cross-region. JWT habilita multi-node sem state replication. | +| **Persistência de personagens** (chars, inventário, level) | CharServer + MySQL global | Habilita transfer, cross-world social, char list multi-mundo. Padrão AccelByte/FFXIV. | +| **Estado vivo de mundos** (pop, queue, status) | WorldServer → Valkey (regional) | Source-of-truth do mundo é quem hospeda o mundo. CharServer só lê e propaga. | +| **Simulação autoritativa** (movimento, combate, IA) | WorldServer C++ Unreal | Latência baixa + tick fixo + autoridade contra cheat. | +| **Tickets de handoff** (single-use, TTL curto) | CharServer emite, Valkey armazena, WorldServer consome | Padrão GameLift: lobby decide quem entra. | + +### 2. Quatro canais de comunicação (sem sobreposição) + +CharServer já roda dois listeners simultâneos (`buildServer` em `core/server.ts`): **uWebSockets.js** na porta 7100 (cliente Unreal, path `/zeus`) e **Fastify HTTPS** na porta 7101 (control plane). É a base do padrão AccelByte: WSS pra push real-time pro cliente, HTTPS pra chamadas pontuais e tráfego servidor-a-servidor. + +| Canal | Quem fala com quem | Protocolo | Onde está | Uso | +|---|---|---|---|---| +| **A** | Cliente ↔ CharServer | WebSocket (WSS) binário Zeus | `wss://...:7100/zeus` | Login, char list/create/select, lista de mundos, push de status, queue updates, chat (futuro) | +| **B** | Cliente ↔ WorldServer | UDP binário Zeus | `UZeusNetworkSubsystem` ↔ `ZeusNet/UdpServer` | Handshake + gameplay autoritativo. Cliente apresenta ticket no `C_CONNECT_REQUEST` | +| **C** | CharServer ↔ WorldServer | HTTPS REST (Fastify) | `:7101/interserver/*` | Claim, character RPC, audit, kick (mTLS na Fase 6) | +| **D** | Admin/CLI ↔ CharServer | HTTPS REST + CLI direto MySQL | `:7101/admin/*` e CLI `npm run world` | CRUD de mundos, métricas, health | + +**Regras invioláveis:** +- Cliente Unreal **NUNCA** fala HTTP com o CharServer. Tudo é WSS via opcodes Zeus. +- Cliente Unreal **NUNCA** consulta o WorldServer pra ver pop/status. Sempre via CharServer (canal A). +- WorldServer **NUNCA** escreve direto no MySQL — sempre via canal C (validação + auditoria). + +### 3. Pop tracking: três triggers combinados (não escolher um — usar todos) + +WorldServer é o único que sabe a verdade sobre pop/queue/state. Reporta via **Valkey** (Fase 2+); na Fase 1 reporta via HTTP POST como fallback temporário. + +| Trigger | O que faz | Frequência | +|---|---|---| +| **Event-driven** (player entrou/saiu) | `HINCRBY world:{id}:status pop ±1` + `PUBLISH world:events` | Imediato | +| **State-change** (online↔maintenance↔offline) | `HSET world:{id}:status state=...` + `PUBLISH world:events` | Imediato | +| **Periodic heartbeat** | `HSET world:{id}:status ...` + `PEXPIRE 30000` (reset TTL) | A cada 5s | + +**Por que os três:** events dão real-time; periodic reconcilia se um PUBLISH foi perdido; TTL detecta world morto automaticamente em ~30s sem código extra. + +### 4. Topologia geo-distribuída + +``` + ┌────────────────────────────────────────┐ + │ MySQL Primary (writes globais) │ + │ ex.: us-east-1 ou Hetzner DE │ + └──────────┬─────────────────────────────┘ + │ async replication (<100ms) + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + read-replica read-replica read-replica + (NA) (SA) (EU) + │ │ │ +┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐ +│ CharServer │ │ CharServer │ │ CharServer │ +│ node × N │ │ node × N │ │ node × N │ +│ Valkey (NA) │ │ Valkey (SA) │ │ Valkey (EU) │ +└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + na.zeusmmo… sa.zeusmmo… eu.zeusmmo… + ▲ ▲ ▲ + WorldServers WorldServers WorldServers + na região NA na região SA na região EU + ▲ ▲ ▲ + Clientes NA Clientes SA Clientes EU + (GeoDNS/CDN) (GeoDNS/CDN) (GeoDNS/CDN) +``` + +**Regras:** +1. WorldServer fala **apenas** com CharServer da sua região (`CharServerEndpoint` no `server.json`). +2. Cliente conecta no DNS regional via GeoDNS. Não cruza região durante sessão. +3. MySQL writes globais (primary central); reads na replica regional. +4. Valkey é por-região (estado vivo é local — sem cross-region sync). +5. Lista de mundos é regional por default (padrão FFXIV — cliente vê só os mundos da `account.region`). +6. `ZEUS_REGION` env var em cada CharServer node; `region` no `worlds.region` deve bater com a região do CharServer que recebe o claim do WorldServer. + +| Região | DNS público | MySQL replica | Valkey cluster | +|---|---|---|---| +| Brasil/SA | `sa.zeusmmo.com.br` | sa-east-1 | cluster SA | +| North America | `na.zeusmmo.com.br` | us-east-1 | cluster NA | +| Europe | `eu.zeusmmo.com.br` | eu-central-1 | cluster EU | +| Asia | `as.zeusmmo.com.br` | ap-southeast-1 | cluster AS | + +--- + +## Componentes-chave + +### Identidade do mundo: admin-create + WorldServer-claim + +Híbrido (estilo AccelByte Armada). Três alternativas e por que escolhemos esta: + +| Padrão | Veredito | +|---|---| +| Auto-register em runtime | ✗ Restart cria ID novo, órfana chars com FK | +| Pre-config puro (estilo WoW) | ✗ Sem prova de identidade — qualquer processo manda heartbeat falso | +| **Admin-create + claim com secret** | ✓ ID estável + prova de posse + auditoria | + +**Fluxo:** + +1. Admin via CLI: `npm run world -- create --name=Aurora --host=... --port=... --region=br --cap=500` → CharServer gera `world_id` (UUID) + `world_secret` (32 bytes base64url). Output mostra os dois **uma vez**. `secret_hash` (Argon2id) é persistido em `worlds.secret_hash`. +2. Admin copia `world_id` + `world_secret` pro `Config/server.json` do WorldServer. +3. WorldServer no boot: `POST /interserver/worlds/{id}/claim { secret }`. CharServer valida Argon2id contra `secret_hash` + region match. Sucesso → `state=online`. Falha → `403` + WorldServer aborta startup. +4. WorldServer começa heartbeat (Fase 1 HTTP; Fase 2+ Valkey direto). +5. WorldServer crashado → TTL Valkey expira em 30s → `state=offline` automático. + +### Ticket de handoff: opaque token single-use + +**Hoje:** `handoffToken` é random sem validação. Inseguro. + +**Padrão escolhido:** opaque token 32 bytes (crypto.randomBytes), **TTL 10s** (não 30 — janela menor pra replay), armazenado em Valkey, consumido via `GETDEL` no WorldServer (single-use). + +``` +CharServer ao emitir: + payload = { account_id, char_id, world_id, expires_at } + SETEX ticket:{token} 10 + INCR world:{world_id}:reserved ← reserva slot (anti-overbooking) + responde S_CHAR_SELECT_OK(worldHost, worldPort, token) + +Cliente: + conecta UDP no World, envia C_CONNECT_REQUEST(nonce) + ticket + +WorldServer: + GETDEL ticket:{token} ← consome + valida: world_id==EU? char_id válido? não expirou? + se OK: + DECR world:{world_id}:reserved ← libera reserva + HINCRBY world:{world_id}:status pop 1 + PUBLISH world:events ... + fetch char data via canal C → spawn + se NOK: + S_CONNECT_REJECT +``` + +**Por que opaque + Valkey lookup (não JWT):** revogação trivial (DEL), single-use natural (GETDEL), payload menor no UDP (32 bytes vs ~200 do JWT). Round-trip Valkey de ~3-5ms é desprezível no handshake (acontece 1x por sessão de horas). + +### Single sign-on enforcement (sem char duplicado in-world) + +Sem isso, dois clientes podem entrar com mesmo char → item dup, corrupção. Acontece em **todo** MMO sem proteção. + +``` +CharServer ao emitir ticket (antes do SETEX): + active = GET char:active:{char_id} + se active existe (account já está in-world em algum mundo): + POST /interserver/worlds/{active.world_id}/kick { char_id, reason: "logged_in_elsewhere" } + aguarda confirmação (com timeout 3s) + se WorldServer não respondeu: DEL char:active:{char_id} forçado + prossegue com ticket + +WorldServer ao spawnar: + SETEX char:active:{char_id} 120 '{"world_id":...,"node_id":...}' + refresh a cada 60s (renova TTL) + +WorldServer ao despawnar (logout/crash): + DEL char:active:{char_id} +``` + +### Concurrency control no writeback (sem silent corruption) + +Cenário: WorldServer cacheou char na memória; admin via SQL muda level. Próximo writeback do WorldServer sobrescreve. Solução padrão = optimistic locking via `version`. + +```sql +characters: version INT UNSIGNED DEFAULT 0 +``` + +``` +WorldServer carrega char: + GET /interserver/characters/{id} → guarda version=N + +WorldServer writeback: + POST /interserver/characters/{id}/checkpoint { pos, inv, ..., version: N } + CharServer SQL: + UPDATE characters SET ..., version=N+1 WHERE id=$id AND version=$N + se affected_rows=0: + 409 Conflict + WorldServer recarrega + faz merge + retry +``` + +### Capacity com reserva (anti-overbooking) + +`pop >= cap` ingênuo causa overbooking: 50 tickets emitidos não conectaram ainda; pop reporta 450/500; entra mais 50; quando todos conectarem → 501/500. Bug clássico de New World/WoW launch. + +**Solução padrão:** ticket emitido **reserva slot** em Valkey: + +``` +Capacidade real = pop + reserved < cap + pop = HGET world:{id}:status pop + reserved = GET world:{id}:reserved (decai com expiração natural) + +Emit ticket: + if pop + reserved >= cap: enfileira (Fase 4) ou reject WorldFull + else: SETEX ticket:{token} 10 ... + INCR world:{id}:reserved + +WorldServer consome ticket: + DECR world:{id}:reserved + INCR pop + +Ticket expira sem ser consumido (timeout): + CharServer monitora via keyspace notifications ou cleanup periódico → DECR world:{id}:reserved +``` + +### QueryPort: dois usos distintos (separar sempre) + +| Uso | Quem consome | Como Zeus faz | Quando | +|---|---|---|---| +| **(A)** Estado vivo pro cliente do jogo | Cliente Unreal logado no CharServer | HTTPS interno (Fase 1) → push Valkey (Fase 2+). Cliente sempre via WSS/CharServer | Implementado nesta frente | +| **(B)** Visibilidade externa pública | Steam server browser, Battlemetrics, GameTracker | UDP A2S (protocolo Valve Source), porta 27015, rate-limited | Roadmap Fase 7 (Steam launch) | + +WorldServer **não usa QueryPort UDP** pra (A) — gasto desnecessário e expõe estado interno. (B) é uma listener UDP separado, anônimo, rate-limited. + +--- + +## Schema MySQL (Fase 1) + +### Tabela `worlds` + +```typescript +@Entity({ name: 'worlds' }) +export class WorldEntity { + @ZUuid({ primary: true }) + id!: Buffer; // UUID v4 binary(16) + + @Column({ type: 'varchar', length: 64, unique: true }) + name!: string; + + @ZEnum(['na', 'sa', 'eu', 'br', 'as']) + region!: AccountRegion; + + @Column({ type: 'varchar', length: 255 }) + host!: string; + + @Column({ type: 'smallint', unsigned: true }) + port!: number; + + @Column({ type: 'smallint', unsigned: true, default: 500 }) + capacity!: number; + + @ZEnum(['online', 'maintenance', 'offline'], { default: 'offline' }) + state!: WorldState; + + /** Hash Argon2id do world_secret. */ + @Column({ name: 'secret_hash', type: 'varchar', length: 255 }) + secretHash!: string; + + @Column({ name: 'last_claim_at', type: 'datetime', nullable: true }) + lastClaimAt!: Date | null; + + @Column({ name: 'last_claim_node', type: 'varchar', length: 64, nullable: true }) + lastClaimNode!: string | null; + + @CreateDateColumn() createdAt!: Date; + @UpdateDateColumn() updatedAt!: Date; +} +``` + +### Alteração em `characters` + +A entity atual ([Character.entity.ts](Server/ZeusCharServer/src/database/entities/MMO/Character.entity.ts)) tem só campos básicos (id, account_id, slot, name, class_id, level, exp, posição, appearance). A Fase 1 adiciona: + +```sql +ALTER TABLE characters + -- Roteamento de mundo + ADD COLUMN world_id BINARY(16) NULL, + ADD CONSTRAINT fk_characters_world FOREIGN KEY (world_id) REFERENCES worlds(id) ON DELETE RESTRICT, + ADD INDEX idx_characters_account_world (account_id, world_id), + + -- Concurrency control (writeback Fase 3) + ADD COLUMN version INT UNSIGNED NOT NULL DEFAULT 0, + + -- Stats primários estilo Ragnarok (ver ARQUITETURA_CHARACTER_MODEL.md) + ADD COLUMN str SMALLINT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN agi SMALLINT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN vit SMALLINT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN `int` SMALLINT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN dex SMALLINT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN luk SMALLINT UNSIGNED NOT NULL DEFAULT 1, + + -- Progressão (já tem level/exp = base; adicionar job_level/job_exp) + ADD COLUMN job_level INT UNSIGNED NOT NULL DEFAULT 1, + ADD COLUMN job_exp BIGINT UNSIGNED NOT NULL DEFAULT 0, + + -- HP/SP atuais e máximos (max é cache; recalcula em level/equip change) + ADD COLUMN hp INT UNSIGNED NOT NULL DEFAULT 40, + ADD COLUMN max_hp INT UNSIGNED NOT NULL DEFAULT 40, + ADD COLUMN sp INT UNSIGNED NOT NULL DEFAULT 11, + ADD COLUMN max_sp INT UNSIGNED NOT NULL DEFAULT 11, + + -- Pontos não-gastos (allocação no level up) + ADD COLUMN status_point INT UNSIGNED NOT NULL DEFAULT 0, + ADD COLUMN skill_point INT UNSIGNED NOT NULL DEFAULT 0, + + -- Moeda + ADD COLUMN zeny BIGINT UNSIGNED NOT NULL DEFAULT 0; +``` + +**Notas:** +- `world_id` é nullable (sem chars em prod hoje — evita downtime futuro). Validação no service: novos chars devem ter `world_id`. +- Estrutura flat segue rathena (`.bases/rathena/rathena-master/sql-files/main.sql:209-296`) — padrão da indústria emuladora. Sem entidade `character_stats` separada (1:1 FK seria JOIN inútil). +- Defaults vêm da row Aprendiz no `jobs.yml` (ver doc Character Model). +- **Stats derivados (ATK/MATK/DEF/MDEF/hit/flee/crit/aspd) NÃO entram aqui** — são sempre recalculados em runtime no WorldServer via fórmulas. Persistir é bug fest (esquece atualizar quando muda equip/buff/level). +- `version` (optimistic locking) usado em writeback da Fase 3. + +> **Para o detalhamento do modelo de personagem** (stats primários vs derivados, fórmulas de ATK/MATK/DEF, jobs.yml, stat allocation, leveling curve, classes Aprendiz → especialização), ver doc dedicado [`ARQUITETURA_CHARACTER_MODEL.md`](ARQUITETURA_CHARACTER_MODEL.md). + +### Schema Valkey (Fase 2+) + +``` +world:{id}:status HSET { pop, cap, queue_len, state, updated_at } TTL 30s (renovado a cada 5s) +world:{id}:reserved INTEGER (sem TTL — decai por DECR) +world:events PUB/SUB channel (transitions + player_in/out) +queue:world:{id} ZSET { account_id → timestamp_ms } (Fase 4, Valkey Streams) +char:active:{char_id} STRING '{world_id,node_id}' TTL 120s (renovado pelo WS) +ticket:{token} STRING '{account_id,char_id,world_id,expires_at}' TTL 10s, GETDEL +session:{jti} HSET { account_id, node_id, ip, expires_at } TTL = JWT TTL (Fase 5) +``` + +--- + +## Roadmap em fases + +### Fase 1 — Lista de mundos estática + char por mundo (sem Valkey) + +**Objetivo:** ServerSelect mostra mundos reais; CharSelect filtra por mundo; cada novo char nasce vinculado. + +**MySQL:** +- Migration `worlds` + `characters.world_id` + `characters.version`. +- CLI `npm run world -- create/list/set-state/rotate-secret`. + +**CharServer (TS):** +- `WorldRepository` + `WorldService` (list/get/setState/claim/heartbeat). +- Opcodes novos: + - `C_WORLD_LIST_REQUEST=2060` / `S_WORLD_LIST=2061` (region-aware — filtra por `ZEUS_REGION`). + - Estender `C_CHAR_LIST_REQUEST` com `world_id`. + - Estender `C_CHAR_CREATE` com `world_id` obrigatório. +- Endpoints HTTPS (canal C): + - `POST /interserver/worlds/{id}/claim { secret }` — Argon2id check + region check → `state=online`. **Rate-limited** (1 req/min por IP — anti crash-loop hammering). + - `POST /interserver/worlds/{id}/heartbeat { pop, queue_len, state }` — fallback HTTP antes do Valkey (UPDATE em `worlds.last_heartbeat_at` ou tabela `worlds_live` auxiliar). +- `S_CHAR_SELECT` valida `char.world_id == world.id` e `world.state == online`. Reject reasons novos: + - `WorldOffline=8`, `WorldMaintenance=9`, `WorldFull=10`, `WorldRegionMismatch=11`. +- Devolve `worldHost/Port` do mundo escolhido (não mais hardcoded). + +**Cliente Unreal:** +- Novo estado `EZMMOFrontEndState::CharSelect` entre ServerSelect e ingame. +- `UUIServerSelectScreen_Base`: pede lista, popula cards, clique → `SelectedWorldId` no `UUIFrontEndFlowSubsystem` + transição CharSelect. +- `UUICharSelectScreen_Base` (NOVO): lista filtrada por `SelectedWorldId`, criar/deletar/entrar. +- `CharServerOpcodes.h`: novos opcodes + reasons. + +**Métricas Prometheus (mínimas):** +- `char_auth_total{result}`, `char_select_total{result}`, `world_list_request_total`. + +**Files críticos:** +- `Server/ZeusCharServer/src/database/entities/MMO/World.entity.ts` (NOVO) +- `Server/ZeusCharServer/src/database/entities/MMO/Character.entity.ts` (add `world_id`, `version`) +- `Server/ZeusCharServer/src/services/world.service.ts` (NOVO) +- `Server/ZeusCharServer/src/http/routes/interserver.ts` (estender) +- `Server/ZeusCharServer/src/services/char-{list,create,select}.service.ts` +- `Server/ZeusCharServer/scripts/world-cli.ts` (NOVO) +- `Server/ZeusCharServer/src/protocol/CharOpcodes.ts` +- `Clients/ZMMO/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.{h,cpp}` +- `Clients/ZMMO/Source/ZMMO/Game/UI/FrontEnd/UICharSelectScreen_Base.{h,cpp}` (NOVO) +- `Clients/ZMMO/Source/ZMMO/Data/UI/FrontEndTypes.h` + +### Fase 2 — Valkey + heartbeat real + +**Objetivo:** pop/state real-time no ServerSelect; UI atualiza sem refresh. + +**Setup:** +- `docker-compose.yml` com `valkey/valkey:7-alpine`. +- `ioredis` no CharServer (API 100% compatível com Valkey 7.2). +- Cliente Valkey C++ no WorldServer (`hiredis` — C, popular; ou `cpp_redis` — header-only, mais moderno; decidir na Fase 2). + +**WorldServer (C++):** +- Worker thread separada (não bloqueia game loop): + - On player_connected/disconnected: `HINCRBY` + `PUBLISH`. + - On state_change: `HSET state` + `PUBLISH`. + - Periodic 5s: refresh `HSET` completo + `PEXPIRE 30000`. +- `WorldId` validado contra `worlds` no boot via HTTP claim (Fase 1). + +**CharServer (TS):** +- `ValkeyService` singleton (ioredis client + subscriber). +- `WorldService.getLiveStatus(id)` lê do Valkey (fallback `state=offline` se key não existe). +- Novo opcode `S_WORLD_STATUS_UPDATE=2062` — push pra cliente quando recebe `world:events`. +- `S_WORLD_LIST` agora inclui `pop/queue_len/state` do Valkey no payload inicial. + +**Cliente:** +- ServerSelect: subscribe na ativação; listener `S_WORLD_STATUS_UPDATE` atualiza cards em tempo real. + +**Métricas:** `world_heartbeat_age_seconds{world_id}` (alerta >30s), `world_population{world_id}`. + +### Fase 3 — Ticket validado + writeback de char + concurrency + +**Objetivo:** handoff seguro + persistência real do gameplay sem corrupção. + +**CharServer:** +- `TicketService.issue({account_id, char_id, world_id})`: `crypto.randomBytes(32)` → `SETEX ticket:{token} 10` + `INCR world:{id}:reserved`. +- `S_CHAR_SELECT_OK` envia ticket emitido (substitui o random anterior). +- Endpoints novos (canal C): + - `GET /interserver/characters/{id}` — full state (pos, inv, stats, **version**). + - `POST /interserver/characters/{id}/checkpoint { ..., version }` — optimistic UPDATE; 409 se conflict. + - `POST /interserver/audit { type, account_id, char_id, payload }` — audit log (level up, item raro, trade). + +**WorldServer:** +- Handshake UDP lê ticket → `GETDEL ticket:{token}` no Valkey → valida campos. +- Se válido: `DECR world:{id}:reserved` + `INCR pop` + `GET /interserver/characters/{id}` → spawn. +- Worker 60s + on logout: `POST /interserver/characters/{id}/checkpoint`. Em 409 → recarrega + merge + retry. + +**Cliente:** transparente (só passa o ticket que recebeu). + +**Métricas:** `ticket_{issued,consumed,expired}_total`, `character_writeback_{ok,conflict}_total`. + +### Fase 3.5 — Single sign-on enforcement + +**Objetivo:** impedir char duplicado in-world (item dup, corrupção). + +**CharServer:** +- Antes de emitir ticket: `GET char:active:{char_id}`. Se existe e é mundo diferente → `POST /interserver/worlds/{active.world_id}/kick { char_id }` com timeout 3s. Se timeout → `DEL char:active:{char_id}` forçado (assumir world morreu) + log warn. + +**WorldServer:** +- On spawn: `SETEX char:active:{char_id} 120 '{world_id, node_id}'`. +- Worker 60s: refresh TTL (`PEXPIRE`). +- On despawn/logout/crash recover: `DEL char:active:{char_id}`. +- Endpoint `POST /admin/kick { char_id, reason }`: força disconnect daquele char. + +**Métricas:** `sso_kick_total{reason=logged_in_elsewhere|stale}`. + +### Fase 4 — Queue com Valkey Streams (não PUB/SUB) + +**Objetivo:** quando `pop + reserved >= cap`, enfileirar em vez de rejeitar. + +**Por que Streams e não PUB/SUB:** PUB/SUB é fire-and-forget; CharServer reiniciando perde eventos `slot-free` → jogadores ficam eternamente na fila. Streams (XADD/XREAD/XACK) persiste mensagens, consumer pode catch-up após restart. + +**CharServer:** +- Em `S_CHAR_SELECT` com pop cheio: `ZADD queue:world:{id} {ts_ms} {account_id}` + responde `S_CHAR_QUEUED(position, ETA)`. +- Worker push 5s: `S_QUEUE_UPDATE` pra cada um na fila (com posição via `ZRANK`). +- Consumer da stream `world:slot-free:{id}`: `ZPOPMIN` da queue → emite ticket → `S_CHAR_QUEUE_READY(ticket, worldHost, worldPort)`. +- Cleanup de zombie entries: WS disconnect → `ZREM` do account_id em todas as filas. + +**WorldServer:** +- Em logout/disconnect: `XADD world:slot-free:{id} * world_id={id}` (Valkey Stream). + +**Cliente:** tela de fila (overlay), opção "Cancelar" → `C_CHAR_QUEUE_CANCEL`. + +**Métricas:** `queue_length{world_id}`, `queue_wait_seconds_p99{world_id}`. + +### Fase 5 — Multi-node CharServer + +**Objetivo:** rodar N instâncias atrás de LB dentro de uma região. + +- Sticky session por cookie no gateway (HAProxy/Nginx) — canal A WSS. +- `session:{jti}` em Valkey HSET — revogação JWT global via `PUBLISH session:revoke`. +- Health endpoint pro LB; readiness probe diferenciado. +- **Connection draining**: SIGTERM handler seta `draining=true` flag, recusa novas conexões WSS mas mantém existentes até timeout (60s). + +**Métricas:** `ws_connections_active{node_id}`, `node_draining{node_id}`. + +### Fase 6 — Multi-region + +**Objetivo:** roll-out NA/SA/EU/AS com DB global. + +- MySQL Primary central + read replicas regionais (async replication). +- Valkey cluster por região (sem cross-region sync — cada região é uma ilha de estado vivo). +- DNS regional via GeoDNS (Route53/Cloudflare) ou subdomain estático. +- WorldServer `CharServerEndpoint` aponta pro hostname regional. +- Worlds filtrados por região automaticamente (`WHERE region = $ZEUS_REGION`). +- Read-your-own-writes: flag `READ_PRIMARY=1` no service layer pra reads críticos imediatamente após write. +- World transfer cross-region: endpoint admin `POST /admin/characters/{id}/transfer { target_world_id }`. +- **mTLS interserver** (canal C) — cada WorldServer com cert único, CharServer valida chain. Substitui shared secret pra cross-region. +- **JWT key rotation** via JWKS endpoint (`GET /.well-known/jwks.json`) com `kid` header. +- **Liveness probe externa** (não só heartbeat self-reported): CharServer faz GET `/health` no WorldServer a cada 30s — detecta congelado (heartbeat fluindo + game thread morta). +- **Disaster recovery:** GeoDNS roteia pra região mais próxima em catástrofe. Usuários re-loginam. + +### Fase 7 — WorldServer QueryPort A2S público (sob demanda) + +**Objetivo:** expor WorldServer pra Steam server browser, Battlemetrics, GameTracker. + +- Listener UDP separado (porta 27015 default), independente do gameplay UDP. +- Protocolo Valve A2S: `A2S_INFO` (0x54), `A2S_PLAYER` (0x55), `A2S_RULES` (0x56) com challenge anti-spoofing (0x41, obrigatório desde 2020). +- Rate-limit por IP (token bucket 30 req/s). +- Registro no Steam Master Server (`hl2master.steampowered.com:27011`). +- **Nunca compartilha identidade com canal C** — stateless, anônimo, jamais expõe `world_secret`. +- Flag `enable_query_port` no `server.json` (default off em dev/CI). +- Ref: https://developer.valvesoftware.com/wiki/Server_queries + +--- + +## Decisões consolidadas + +1. **CharServer dono dos `characters`** (MySQL global). Coluna `world_id` (FK) + `version` (optimistic locking). +2. **WorldServer stateless** quanto a persistência. Cache em memória + writeback ~60s. +3. **Lista de mundos:** `worlds` no MySQL (durável). Estado vivo (pop/queue/state) em Valkey (Fase 2+). +4. **Heartbeat WorldServer:** três triggers combinados (event + state-change + periodic 5s) escrevendo direto no Valkey (Fase 2+); HTTP POST como fallback na Fase 1. +5. **Mundo em manutenção:** sempre acessível em ServerSelect/CharSelect; botão "Entrar" rejeita. +6. **Char list filtrada por mundo:** escolha do mundo na ServerSelect → CharSelect com chars desse mundo. Criação inclui `world_id`. +7. **Ticket de handoff:** opaque 32-byte token, TTL 10s, `GETDEL` no WorldServer, com **reserva de slot** (`INCR world:{id}:reserved`). +8. **SSO enforcement:** `char:active:{char_id}` em Valkey + kick old session via canal C antes de emitir ticket novo. +9. **Concurrency control:** coluna `version` em `characters`, optimistic UPDATE no writeback (409 → retry com merge). +10. **Queue:** MVP sem fila (Fase 1-3 → reject `WorldFull`); **Fase 4 implementa via Valkey Streams** (não PUB/SUB) com cleanup de zombies + reserva integrada. +11. **Valkey** (fork BSD do Redis 7.2 mantido pela Linux Foundation): a partir da Fase 2. `ioredis` no Node funciona sem mudança. Docker `valkey/valkey:7-alpine`. +12. **`world_id`:** admin-create + WorldServer-claim com Argon2id secret (padrão AccelByte Armada). Rate-limited (anti crash-loop). +13. **Topologia geo-distribuída:** Valkey + CharServer por região; MySQL primary global + read replicas regionais. Multi-region é Fase 6. +14. **QueryPort:** UDP A2S público apenas pra visibilidade externa (Steam/Battlemetrics) na Fase 7. Estado pro cliente do jogo sempre via WSS/CharServer. +15. **mTLS interserver:** Fase 6 (substitui shared secret pra cross-region). +16. **JWT key rotation:** JWKS endpoint na Fase 6. +17. **Métricas Prometheus:** lista mínima em cada fase (`char_auth_total`, `world_heartbeat_age_seconds`, `ticket_*_total`, `queue_length`, etc.). + +--- + +## Verificação por fase + +**Fase 1:** +- `npm run world -- create --name="Aurora" --host=127.0.0.1 --port=27777 --region=br --cap=500` → imprime `world_id` + `world_secret`. +- `npm run world -- create --name="Volcanus" --host=127.0.0.1 --port=27778 --region=na --cap=500`. +- `npm run world -- list` mostra ambos com `state=offline`. +- WorldServer com `world_id`+`world_secret` no `Config/server.json` sobe → `POST /interserver/worlds/{id}/claim` → `204` + log "claimed". `state` no DB vira `online`. +- Claim com secret errado → `403` + abort. +- PIE login → ServerSelect: 2 cards reais. +- Clique em Aurora → CharSelect com chars de `world_id=Aurora` (filtrado). +- Criar Personagem → INSERT com `world_id=Aurora`. +- `npm run world -- set-state maintenance` → entrar → reject `WorldMaintenance` (CharSelect ainda funciona). + +**Fase 2:** +- `docker-compose up -d valkey` sobe Valkey. +- `valkey-cli HGETALL world:{id}:status` mostra pop fresh. +- Player conecta → ServerSelect: pop incrementa em tempo real sem refresh. +- Matar WorldServer → 30s depois ServerSelect mostra "Offline". + +**Fase 3:** +- WorldServer log "ticket validado, account=X char=Y". +- Reuse de mesmo ticket → reject. +- Editar inv in-game → 60s depois `SELECT inv FROM characters` mostra novo state. +- Editar level via SQL direto durante gameplay → writeback do WorldServer dá 409 → recarrega + merge. + +**Fase 3.5:** +- Logar com char Y no PIE 1. +- Logar mesmo char Y no PIE 2 → CharServer kicka PIE 1 antes de emitir ticket. `S_DISCONNECT` no PIE 1 com motivo "logged_in_elsewhere". + +**Fase 4:** +- `npm run world -- set-state ...` setar `cap=1`. +- Player A entra → ok. +- Player B entra → fila posição 1. +- Player A logout → Player B recebe ticket + entra. +- Restart CharServer durante fila → Player B continua na fila (Streams persistiu). + +**Fase 5:** +- 2 instâncias `npm run dev` em portas diferentes + nginx LB. +- Login → matar node ativo → reconnect cai no outro → sessão preserva (Valkey). +- `kill -TERM` no node ativo → drena (sem cortar conexão imediatamente) → desce após timeout. + +**Fase 6:** +- Subir CharServer SA + CharServer NA + Valkey SA + Valkey NA + read replicas. +- WorldServer SA claim em CharServer SA: ok. +- WorldServer SA tentar claim em CharServer NA: 403 region mismatch. +- Cliente conecta `sa.zeusmmo.com.br`: vê só mundos SA. + +--- + +## Notas e riscos (após autocrítica) + +- **Live Coding ≠ UCLASS nova**: cada nova classe C++ no cliente exige rebuild completo + restart do editor. Planejar fazer todas as classes novas de uma fase no mesmo build. +- **Reconnect WSS no cliente:** `UZeusCharServerSubsystem` hoje não tem retry/reconnect. Gap não-trivial — implementar antes da Fase 5 (multi-node faz failover frequente). +- **WorldServer congelado vs morto:** heartbeat continua fluindo se game thread trava mas worker thread vive. Liveness probe externa (Fase 6) detecta isso. Fase 1-5 fica como risco aceito. +- **Argon2id no claim:** custo 50-200ms por hash. WorldServer faz 1x no boot — irrelevante. Crash-loop hammering → rate limit no endpoint (1/min/IP). +- **Replication lag MySQL:** assumido <100ms (verdade em RDS/Aurora). VPS amador pode ser 1-5s. Medir antes da Fase 6. +- **Limite de capacity por mundo:** ~500-1500 players com WorldServer monolítico. Acima disso precisa **world sharding interno** (zone servers) — Fase 8+ se necessário. Documentar como limite atual. +- **Chat global, party, friends:** fora desta frente. Entram como **serviço paralelo no CharServer** (canal A, opcodes 3000+) depois da Fase 3. +- **Anti-cheat client-side (EAC/BattlEye/VAC):** decisão comercial pós-launch. Fora de escopo. +- **Area of Interest no WorldServer:** indispensável pra >100 players concorrentes. Não bloqueia Fase 1-5 (dev tem 1-10 players). Documentar como pré-requisito de teste de carga. +- **CI/CD multi-region:** Fase 6 pressupõe Kubernetes ou equivalente. Decisão de ops, fora desta arquitetura. + +--- + +## O que esta frente entrega vs roadmap completo + +**Esta frente (Fases 1-3.5):** ServerSelect funcional + char por mundo + handoff seguro + SSO + writeback sem corrupção. Suficiente pra dev real com 1-50 players concorrentes em 1 região, 1-5 mundos. + +**Roadmap longo (Fases 4-7):** queue, multi-node, multi-region, A2S público. Cada uma incremental — não quebra as anteriores. + +**Fora do roadmap (frentes paralelas):** chat global/party/friends, anti-cheat client-side, world sharding interno (>1000 players), CI/CD K8s, observability completa (tracing distribuído), disaster recovery automatizado. diff --git a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset index 8f52edb2f7731f8e51026dd6395b53270f65be86..5cdc89e190fabb4560fa922b86e74776060041a6 100644 GIT binary patch delta 887 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 literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharacterCreate.uasset b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharacterCreate.uasset new file mode 100644 index 0000000000000000000000000000000000000000..14f2b126fd011d84f3fb8262f49de039a97f548c GIT binary patch literal 36097 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

D_e%<4#_gAZFcfg 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 literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/FrontEnd/ServerSelect/WBP_ServerCard.uasset b/Content/ZMMO/UI/FrontEnd/ServerSelect/WBP_ServerCard.uasset new file mode 100644 index 0000000000000000000000000000000000000000..be9ff2a195d849649008937cfcb4116dc969569e GIT binary patch literal 88573 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 z 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�NvjZ|Q?u6s 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 D?g-$V delta 17316 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 b$FVshE(8kPF_d5oucGEdbzT6Bx>x-Jv6>+- diff --git a/Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset b/Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset new file mode 100644 index 0000000000000000000000000000000000000000..3103b010915c022b1385016eee77f803e185538a GIT binary patch literal 117979 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

5tVDq%VNa4?0>em9hYopevE#39`uDl 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 F{|{%ln<4-J literal 0 HcmV?d00001 diff --git a/Content/ZMMO/UI/Shared/UI_Button_Master.uasset b/Content/ZMMO/UI/Shared/UI_Button_Master.uasset index 8f4859b4b733818668edb834d54edaee355d5574..781eb17bd31bb5416a5e042a807f2ccf012992b5 100644 GIT binary patch delta 7087 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 Fe*-eSw)_A9 delta 8673 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 N)?g~7?3Ww!{s&N?U(x^o diff --git a/Source/ZMMO/Data/UI/UIStyleRow.h b/Source/ZMMO/Data/UI/UIStyleRow.h index 2a395c6..44ff112 100644 --- a/Source/ZMMO/Data/UI/UIStyleRow.h +++ b/Source/ZMMO/Data/UI/UIStyleRow.h @@ -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 TextRoles; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") FUIStyleButton Button; @@ -46,6 +55,12 @@ struct ZMMO_API FUIStyle UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") FUIStyleSpinner Spinner; + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FUIStyleCheckBox CheckBox; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FUIStyleInput Input; + /** * Fundo de tela cheia do front-end (ex.: Boot/Login). Brush data-driven: * a tela aplica no seu Border_Bg via RefreshUIStyle — nunca hard-ref no diff --git a/Source/ZMMO/Data/UI/UIStyleTypes.h b/Source/ZMMO/Data/UI/UIStyleTypes.h index ff641cb..826a9cd 100644 --- a/Source/ZMMO/Data/UI/UIStyleTypes.h +++ b/Source/ZMMO/Data/UI/UIStyleTypes.h @@ -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. */ diff --git a/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h b/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h index d5016b6..3b32cf0 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h +++ b/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h @@ -5,8 +5,6 @@ /** * Opcodes do CharServer (WebSocket) consumidos pelo cliente. Espelha * `Server/ZeusCharServer/src/protocol/CharOpcodes.ts` (faixa 2000-2099). - * Mantido mínimo: só os opcodes que o front-end usa hoje (auth da Login). - * Ampliar conforme as telas (lista/criação/seleção de personagem) chegarem. * * Wire: string = uint16 LE (len) + UTF-8; inteiros LE. O cabeçalho de 32B * é responsabilidade do UZeusCharServerSubsystem (Send/OnRawMessage operam @@ -18,13 +16,40 @@ namespace ZMMOCharOp constexpr int32 C_CHAR_AUTH_REQUEST = 2000; constexpr int32 S_CHAR_AUTH_OK = 2001; // string(accountId)+string(user)+uint64(expiresMs) constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason) + + // Listagem/criação/seleção de personagem + constexpr int32 C_CHAR_LIST_REQUEST = 2010; // uint8 hasWorldFilter [+ uint8[16] worldId] + constexpr int32 S_CHAR_LIST = 2011; + + constexpr int32 C_CHAR_CREATE = 2020; // uint8[16] worldId + slot + ... + constexpr int32 S_CHAR_CREATE_OK = 2021; + constexpr int32 S_CHAR_CREATE_REJECT= 2022; + + // Delete agendado (rathena-style two-step) + constexpr int32 C_CHAR_DELETE_REQUEST = 2030; + constexpr int32 S_CHAR_DELETE_ACK = 2031; + constexpr int32 C_CHAR_DELETE_ACCEPT = 2032; + constexpr int32 S_CHAR_DELETE_ACCEPT_ACK = 2033; + constexpr int32 C_CHAR_DELETE_CANCEL = 2034; + constexpr int32 S_CHAR_DELETE_CANCEL_ACK = 2035; + + constexpr int32 C_CHAR_SELECT = 2040; + constexpr int32 S_CHAR_SELECT_OK = 2041; + constexpr int32 S_CHAR_SELECT_REJECT= 2042; + + // Listagem de mundos (Fase 1 do ARQUITETURA_SERVER_SELECT) + constexpr int32 C_WORLD_LIST_REQUEST = 2060; // payload vazio + constexpr int32 S_WORLD_LIST = 2061; // uint16 count + entries + /** + * Push do CharServer com update de UM mundo (state/pop/queueLen). + * Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state + */ + constexpr int32 S_WORLD_STATUS_UPDATE = 2062; } /** * Método de autenticação enviado como uint8 prefix no payload do - * C_CHAR_AUTH_REQUEST. TOKEN mantém o fluxo Stub/JWT (handoff de outro - * serviço); PASSWORD chama AccountService.login direto no CharServer. - * Espelha `CharAuthMethod` em CharOpcodes.ts. + * C_CHAR_AUTH_REQUEST. */ namespace ZMMOCharAuthMethod { @@ -32,7 +57,7 @@ namespace ZMMOCharAuthMethod constexpr uint8 PASSWORD = 1; } -/** Espelha `CharRejectReason` em CharOpcodes.ts (subset usado no front-end). */ +/** Espelha `CharRejectReason` em CharOpcodes.ts. */ namespace ZMMOCharRejectReason { constexpr uint16 Unknown = 0; @@ -43,4 +68,23 @@ namespace ZMMOCharRejectReason constexpr uint16 AccountLocked = 5; constexpr uint16 AccountSuspended = 6; constexpr uint16 CredentialsAuthDisabled = 7; + constexpr uint16 WorldOffline = 8; + constexpr uint16 WorldMaintenance = 9; + constexpr uint16 WorldFull = 10; + constexpr uint16 WorldRegionMismatch = 11; + constexpr uint16 WorldNotFound = 12; + constexpr uint16 NameInUse = 20; + constexpr uint16 InvalidName = 21; + constexpr uint16 SlotOccupied = 22; + constexpr uint16 CharNotFound = 23; + constexpr uint16 DeleteScheduled = 24; + constexpr uint16 DeleteNotScheduled = 25; +} + +/** Estado do mundo no wire (uint8). Espelha `WireWorldState` em CharOpcodes.ts. */ +namespace ZMMOWireWorldState +{ + constexpr uint8 Offline = 0; + constexpr uint8 Online = 1; + constexpr uint8 Maintenance = 2; } diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp index bbd44c8..a92af2e 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp @@ -16,7 +16,7 @@ namespace { // Runtime usa o tema ativo; design-time carrega a row "Default" de // DT_UI_Styles para o Designer ver o fundo (mesmo padrão de UUIPanel_Base). - const FUIStyle& ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + FUIStyle ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback) { if (Widget) { @@ -28,22 +28,21 @@ namespace } } } - static FUIStyle DesignStyle; - static bool bLoaded = false; - if (!bLoaded) + // Design-time relê o DT a cada chamada: editar o DT_UI_Styles e + // RECOMPILAR o WBP já reflete (sem reiniciar o editor). + FUIStyle DesignStyle; + bool bHas = false; + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) { - if (const UDataTable* DT = LoadObject(nullptr, - TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UIBootDesign"), false)) { - if (const FUIStyleRow* Row = DT->FindRow( - FName(TEXT("Default")), TEXT("UIBootDesign"), false)) - { - DesignStyle = Row->Style; - bLoaded = true; - } + DesignStyle = Row->Style; + bHas = true; } } - return bLoaded ? DesignStyle : Fallback; + return bHas ? DesignStyle : Fallback; } } @@ -98,7 +97,7 @@ void UUIBootScreen_Base::RefreshUIStyle_Implementation() Super::RefreshUIStyle_Implementation(); const FUIStyle Fallback; - const FUIStyle& AS = ResolveBootStyle(this, Fallback); + const FUIStyle AS = ResolveBootStyle(this, Fallback); if (Border_Bg) { diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp new file mode 100644 index 0000000..8263248 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.cpp @@ -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(Mins), static_cast(Secs)))); + } + else + { + Text_DeleteCountdown->SetText(FText::FromString(FString::Printf( + TEXT("Delete em %llds"), static_cast(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); +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h new file mode 100644 index 0000000..5656114 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharCard_Base.h @@ -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 Card_Bg; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Name; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Level; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Class; + UPROPERTY(meta = (BindWidget)) TObjectPtr Btn_Select; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_Delete; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_AcceptDelete; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_CancelDelete; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_DeleteCountdown; + +private: + bool bBound = false; + + FTimerHandle CountdownTimerHandle; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp new file mode 100644 index 0000000..0b578f3 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp @@ -0,0 +1,133 @@ +#include "UICharacterCreatePage_Base.h" + +#include "ZMMO.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "ZeusCharServerSubsystem.h" +#include "Components/EditableTextBox.h" +#include "Components/ComboBoxString.h" +#include "CommonTextBlock.h" +#include "Engine/GameInstance.h" +#include "UI/Widgets/UIButton_Base.h" + +namespace +{ + bool WriteUuid16(TArray& Buf, const FString& Uuid) + { + FString Hex = Uuid.Replace(TEXT("-"), TEXT("")); + if (Hex.Len() != 32) return false; + for (int32 i = 0; i < 16; ++i) + { + TCHAR hi = Hex[i * 2]; + TCHAR lo = Hex[i * 2 + 1]; + auto HexVal = [](TCHAR c) -> int32 { + if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0'); + if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10; + if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10; + return -1; + }; + int32 H = HexVal(hi); + int32 L = HexVal(lo); + if (H < 0 || L < 0) return false; + Buf.Add(static_cast((H << 4) | L)); + } + return true; + } + + void WriteUInt8(TArray& Buf, uint8 v) { Buf.Add(v); } + void WriteUInt16(TArray& Buf, uint16 v) + { + Buf.Add(static_cast(v & 0xFF)); + Buf.Add(static_cast((v >> 8) & 0xFF)); + } + void WriteUInt32(TArray& Buf, uint32 v) + { + for (int32 i = 0; i < 4; ++i) Buf.Add(static_cast((v >> (8 * i)) & 0xFF)); + } + void WriteStringUtf8(TArray& Buf, const FString& S) + { + FTCHARToUTF8 Conv(*S); + const int32 Len = Conv.Length(); + WriteUInt16(Buf, static_cast(Len)); + Buf.Append(reinterpret_cast(Conv.Get()), Len); + } +} + +void UUICharacterCreatePage_Base::NativeConstruct() +{ + Super::NativeConstruct(); + if (!bBound) + { + if (Btn_Confirm) Btn_Confirm->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleSubmitClicked); + if (Btn_Cancel) Btn_Cancel->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleCancelClicked); + bBound = true; + } + if (Text_Error) Text_Error->SetText(FText::GetEmpty()); +} + +void UUICharacterCreatePage_Base::NativeDestruct() +{ + if (bBound) + { + if (Btn_Confirm) Btn_Confirm->OnClicked().RemoveAll(this); + if (Btn_Cancel) Btn_Cancel->OnClicked().RemoveAll(this); + bBound = false; + } + Super::NativeDestruct(); +} + +void UUICharacterCreatePage_Base::SubmitCreate() +{ + const FString Name = Input_Name ? Input_Name->GetText().ToString() : FString(); + if (Name.IsEmpty()) + { + if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Informe um nome."))); + return; + } + + UGameInstance* GI = GetGameInstance(); + if (!GI) return; + UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem(); + UZeusCharServerSubsystem* Char = GI->GetSubsystem(); + 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 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(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(); +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h new file mode 100644 index 0000000..68f3e51 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.h @@ -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 Input_Name; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Combo_Class; + UPROPERTY(meta = (BindWidget)) TObjectPtr Btn_Confirm; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_Cancel; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Error; + +private: + bool bBound = false; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp index 9a0f13a..22bde20 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -154,6 +154,10 @@ bool UUIFrontEndFlowSubsystem::RequestBack() { switch (CurrentState) { + case EZMMOFrontEndState::Login: + // Volta à Boot (CharServer segue conectado; só re-mostra a tela). + SetState(EZMMOFrontEndState::Boot); + return true; case EZMMOFrontEndState::ServerSelect: SetState(EZMMOFrontEndState::Login); return true; @@ -330,6 +334,29 @@ void UUIFrontEndFlowSubsystem::RequestEnterLogin() } } +void UUIFrontEndFlowSubsystem::RequestEnterServerSelect() +{ + if (CurrentState == EZMMOFrontEndState::Login) + { + SetState(EZMMOFrontEndState::ServerSelect); + } +} + +void UUIFrontEndFlowSubsystem::SetSelectedWorldId(const FString& WorldId) +{ + SelectedWorldId = WorldId; + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: mundo selecionado = %s"), *SelectedWorldId); +} + +void UUIFrontEndFlowSubsystem::ClearSelectedWorld() +{ + if (!SelectedWorldId.IsEmpty()) + { + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando mundo selecionado (%s)"), *SelectedWorldId); + SelectedWorldId.Reset(); + } +} + void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath) { UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: travel pedido pelo servidor → %s (%s)."), *MapName, *MapPath); diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 7224f5d..53d6cc3 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -62,6 +62,24 @@ public: UFUNCTION(BlueprintCallable, Category = "FrontEnd") void RequestEnterLogin(); + /** Chamado pela tela Login após autenticação OK no CharServer. */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd") + void RequestEnterServerSelect(); + + /** + * Memoriza o mundo escolhido na ServerSelect (UUID v4 string). Lido pelo + * CharSelect/CharCreate na hora de filtrar/criar personagens. Limpo no + * logout/back para ServerSelect. + */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") + void SetSelectedWorldId(const FString& WorldId); + + UFUNCTION(BlueprintPure, Category = "FrontEnd|Worlds") + FString GetSelectedWorldId() const { return SelectedWorldId; } + + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") + void ClearSelectedWorld(); + UPROPERTY(BlueprintAssignable, Category = "FrontEnd") FOnZMMOFrontEndStateChanged OnStateChanged; @@ -106,6 +124,9 @@ private: UPROPERTY(Transient) EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None; + UPROPERTY(Transient) + FString SelectedWorldId; + UPROPERTY(Transient) TObjectPtr ScreenSet; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp new file mode 100644 index 0000000..8a357ac --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.cpp @@ -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(Entry.Population) / static_cast(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"); + } +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h new file mode 100644 index 0000000..564c314 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerCard_Base.h @@ -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 Card_Bg; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Name; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Pop; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Status; + UPROPERTY(meta = (BindWidget)) TObjectPtr Btn_Enter; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Bar_Pop; + +private: + bool bBound = false; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp new file mode 100644 index 0000000..c624a48 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp @@ -0,0 +1,360 @@ +#include "UIServerSelectScreen_Base.h" + +#include "ZMMO.h" +#include "ZMMOThemeSubsystem.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "UIServerCard_Base.h" +#include "ZeusCharServerSubsystem.h" +#include "CommonTextBlock.h" +#include "Components/Border.h" +#include "Components/GridPanel.h" +#include "Components/GridSlot.h" +#include "Components/PanelWidget.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "UI/UIStyleRow.h" + +namespace +{ + FUIStyle ResolveServerSelectStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + return Theme->GetActiveUIStyle(); + } + } + } + FUIStyle DesignStyle; + bool bHas = false; + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UIServerSelectDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } + + // ---- Wire reader (LE) ---- + bool ReadU8(const TArray& Buf, int32& Pos, uint8& Out) + { + if (Pos + 1 > Buf.Num()) return false; + Out = Buf[Pos]; + Pos += 1; + return true; + } + + bool ReadU16(const TArray& Buf, int32& Pos, uint16& Out) + { + if (Pos + 2 > Buf.Num()) return false; + Out = static_cast(Buf[Pos]) | (static_cast(Buf[Pos + 1]) << 8); + Pos += 2; + return true; + } + + bool ReadStringUtf8(const TArray& Buf, int32& Pos, FString& Out) + { + uint16 Len = 0; + if (!ReadU16(Buf, Pos, Len)) return false; + if (Pos + Len > Buf.Num()) return false; + if (Len == 0) + { + Out.Reset(); + } + else + { + Out = FString(FUTF8ToTCHAR(reinterpret_cast(Buf.GetData() + Pos), Len)); + } + Pos += Len; + return true; + } +} + +void UUIServerSelectScreen_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + RefreshUIStyle(); +} + +void UUIServerSelectScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + UE_LOG(LogZMMO, Log, TEXT("ServerSelect: ativada.")); + + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + if (!bSubscribed) + { + Char->OnRawMessage.AddDynamic(this, &UUIServerSelectScreen_Base::HandleCharRawMessage); + bSubscribed = true; + } + } + + RequestWorldList(); +} + +void UUIServerSelectScreen_Base::NativeOnDeactivated() +{ + if (bSubscribed) + { + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnRawMessage.RemoveDynamic(this, &UUIServerSelectScreen_Base::HandleCharRawMessage); + } + bSubscribed = false; + } + Super::NativeOnDeactivated(); +} + +void UUIServerSelectScreen_Base::RefreshUIStyle_Implementation() +{ + Super::RefreshUIStyle_Implementation(); + + const FUIStyle Fallback; + const FUIStyle AS = ResolveServerSelectStyle(this, Fallback); + + if (Border_Bg) + { + if (AS.ScreenBackground.GetResourceObject() != nullptr) + { + Border_Bg->SetBrush(AS.ScreenBackground); + } + else + { + Border_Bg->SetBrushColor(AS.Palette.Bg0); + } + } + if (Text_Title) + { + Text_Title->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurface)); + } +} + +void UUIServerSelectScreen_Base::RequestWorldList() +{ + UZeusCharServerSubsystem* Char = GetCharServer(); + if (!Char || !Char->IsConnected()) + { + UE_LOG(LogZMMO, Warning, TEXT("ServerSelect: CharServer indisponivel — pulando RequestWorldList")); + return; + } + TArray 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()) + { + Flow->SetSelectedWorldId(WorldId); + Flow->SetState(EZMMOFrontEndState::Lobby); + } +} + +void UUIServerSelectScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray& 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& 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(Pop); + Entry.QueueLen = static_cast(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& 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 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(Port); + Entry.Capacity = static_cast(Cap); + Entry.Population = static_cast(Pop); + Entry.QueueLen = static_cast(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(CardContainer); + const int32 GridColumns = 2; // bate com o mock (ColumnFill[0]/[1]) + + int32 Index = 0; + for (const FZMMOWorldEntry& W : Worlds) + { + UUIServerCard_Base* Card = CreateWidget(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(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(); + } + return nullptr; +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h new file mode 100644 index 0000000..e9c4895 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.h @@ -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 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& Payload); + + UFUNCTION() + void HandleCardPressed(FString WorldId, uint8 State); + + UZeusCharServerSubsystem* GetCharServer() const; + void ParseWorldList(const TArray& Payload); + void ApplyStatusUpdate(const TArray& Payload); + void RebuildCards(); + + /** Background e titulo (mantidos para RefreshUIStyle). */ + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Border_Bg; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Title; + + /** + * Container onde os cards sao instanciados (ScrollBox/VerticalBox/ + * UniformGridPanel/WrapBox — qualquer UPanelWidget serve). + */ + UPROPERTY(meta = (BindWidget)) TObjectPtr 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 CardClass; + +private: + bool bSubscribed = false; + + UPROPERTY(Transient) + TArray> SpawnedCards; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp new file mode 100644 index 0000000..35e3e64 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp @@ -0,0 +1,468 @@ +#include "UIUserLobbyScreen_Base.h" + +#include "ZMMO.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "UICharCard_Base.h" +#include "UICharacterCreatePage_Base.h" +#include "ZeusCharServerSubsystem.h" +#include "CommonTextBlock.h" +#include "Components/PanelWidget.h" +#include "Components/WidgetSwitcher.h" +#include "Engine/GameInstance.h" +#include "UI/Widgets/UIButton_Base.h" + +namespace +{ + bool ReadU8(const TArray& Buf, int32& Pos, uint8& Out) + { + if (Pos + 1 > Buf.Num()) return false; + Out = Buf[Pos]; Pos += 1; return true; + } + bool ReadU16(const TArray& Buf, int32& Pos, uint16& Out) + { + if (Pos + 2 > Buf.Num()) return false; + Out = static_cast(Buf[Pos]) | (static_cast(Buf[Pos + 1]) << 8); + Pos += 2; return true; + } + bool ReadU32(const TArray& Buf, int32& Pos, uint32& Out) + { + if (Pos + 4 > Buf.Num()) return false; + Out = static_cast(Buf[Pos]) + | (static_cast(Buf[Pos + 1]) << 8) + | (static_cast(Buf[Pos + 2]) << 16) + | (static_cast(Buf[Pos + 3]) << 24); + Pos += 4; return true; + } + bool ReadU64(const TArray& Buf, int32& Pos, uint64& Out) + { + if (Pos + 8 > Buf.Num()) return false; + Out = 0; + for (int i = 0; i < 8; ++i) Out |= (static_cast(Buf[Pos + i]) << (8 * i)); + Pos += 8; return true; + } + bool ReadFloat(const TArray& Buf, int32& Pos, float& Out) + { + uint32 raw; + if (!ReadU32(Buf, Pos, raw)) return false; + FMemory::Memcpy(&Out, &raw, sizeof(float)); + return true; + } + bool ReadStringUtf8(const TArray& 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(Buf.GetData() + Pos), Len)); + Pos += Len; + return true; + } + bool ReadUuid16(const TArray& Buf, int32& Pos, FString& Out) + { + if (Pos + 16 > Buf.Num()) return false; + // 16 bytes raw -> 8-4-4-4-12 + auto Hex = [](uint8 b, ANSICHAR* dst) + { + static const ANSICHAR* H = "0123456789abcdef"; + dst[0] = H[(b >> 4) & 0xF]; + dst[1] = H[b & 0xF]; + }; + ANSICHAR raw[37]; raw[36] = 0; + int32 j = 0; + for (int32 i = 0; i < 16; ++i) + { + if (i == 4 || i == 6 || i == 8 || i == 10) raw[j++] = '-'; + Hex(Buf[Pos + i], &raw[j]); j += 2; + } + Out = ANSI_TO_TCHAR(raw); + Pos += 16; + return true; + } + bool WriteUuid16(TArray& Buf, const FString& Uuid) + { + // Espera xxxxxxxx-xxxx-... + FString Hex = Uuid.Replace(TEXT("-"), TEXT("")); + if (Hex.Len() != 32) return false; + for (int32 i = 0; i < 16; ++i) + { + TCHAR hi = Hex[i * 2]; + TCHAR lo = Hex[i * 2 + 1]; + auto HexVal = [](TCHAR c) -> int32 { + if (c >= TEXT('0') && c <= TEXT('9')) return c - TEXT('0'); + if (c >= TEXT('a') && c <= TEXT('f')) return c - TEXT('a') + 10; + if (c >= TEXT('A') && c <= TEXT('F')) return c - TEXT('A') + 10; + return -1; + }; + int32 H = HexVal(hi); + int32 L = HexVal(lo); + if (H < 0 || L < 0) return false; + Buf.Add(static_cast((H << 4) | L)); + } + return true; + } +} + +void UUIUserLobbyScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + UE_LOG(LogZMMO, Log, TEXT("Lobby: ativada.")); + + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + if (!bSubscribed) + { + Char->OnRawMessage.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage); + bSubscribed = true; + } + } + + if (!bButtonsBound) + { + if (Btn_Back) Btn_Back->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::BackToServerSelect); + if (Btn_Create) Btn_Create->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::ShowCharCreatePage); + bButtonsBound = true; + } + + ShowCharSelectPage(); + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::NativeOnDeactivated() +{ + if (bSubscribed) + { + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnRawMessage.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage); + } + bSubscribed = false; + } + Super::NativeOnDeactivated(); +} + +void UUIUserLobbyScreen_Base::RequestCharList() +{ + UZeusCharServerSubsystem* Char = GetCharServer(); + if (!Char || !Char->IsConnected()) return; + + FString WorldId; + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + WorldId = Flow->GetSelectedWorldId(); + } + } + + // Wire: uint8 hasFilter + (uint8[16] worldId se hasFilter==1) + TArray 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()) + { + Flow->ClearSelectedWorld(); + Flow->SetState(EZMMOFrontEndState::ServerSelect); + } +} + +void UUIUserLobbyScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray& 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& 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 New; + New.Reserve(Count); + + for (uint16 i = 0; i < Count; ++i) + { + FZMMOCharSummary E; + uint64 charId64 = 0; + uint8 slot = 0; + uint16 classId = 0; + uint32 baseLvl = 0, baseExp = 0, jobLvl = 0, jobExp = 0; + uint16 s = 0, a = 0, v = 0, in_ = 0, d = 0, l = 0; + uint32 hp = 0, maxHp = 0, sp = 0, maxSp = 0, money = 0; + float px = 0, py = 0, pz = 0, yaw = 0; + uint32 hair = 0, hairColor = 0, skinColor = 0; + uint8 bodyType = 0; + uint64 deleteAt = 0; + + if (!ReadU64(Payload, Pos, charId64) + || !ReadUuid16(Payload, Pos, E.WorldId) + || !ReadU8(Payload, Pos, slot) + || !ReadStringUtf8(Payload, Pos, E.Name) + || !ReadU16(Payload, Pos, classId) + || !ReadU32(Payload, Pos, baseLvl) + || !ReadU32(Payload, Pos, baseExp) + || !ReadU32(Payload, Pos, jobLvl) + || !ReadU32(Payload, Pos, jobExp) + || !ReadU16(Payload, Pos, s) || !ReadU16(Payload, Pos, a) || !ReadU16(Payload, Pos, v) + || !ReadU16(Payload, Pos, in_) || !ReadU16(Payload, Pos, d) || !ReadU16(Payload, Pos, l) + || !ReadU32(Payload, Pos, hp) || !ReadU32(Payload, Pos, maxHp) + || !ReadU32(Payload, Pos, sp) || !ReadU32(Payload, Pos, maxSp) + || !ReadU32(Payload, Pos, money) + || !ReadStringUtf8(Payload, Pos, E.MapName) + || !ReadFloat(Payload, Pos, px) || !ReadFloat(Payload, Pos, py) || !ReadFloat(Payload, Pos, pz) + || !ReadFloat(Payload, Pos, yaw) + || !ReadU32(Payload, Pos, hair) || !ReadU32(Payload, Pos, hairColor) || !ReadU32(Payload, Pos, skinColor) + || !ReadU8(Payload, Pos, bodyType) + || !ReadU64(Payload, Pos, deleteAt)) + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_LIST malformado (entry %d)"), i); + return; + } + E.CharId = FString::Printf(TEXT("%llu"), static_cast(charId64)); + E.Slot = slot; E.ClassId = classId; + E.BaseLevel = baseLvl; E.BaseExp = static_cast(baseExp); + E.JobLevel = jobLvl; E.JobExp = static_cast(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(money); + E.Position = FVector(px, py, pz); E.YawDeg = yaw; + E.Appearance.Hair = hair; E.Appearance.HairColor = hairColor; + E.Appearance.SkinColor = skinColor; E.Appearance.BodyType = bodyType; + E.DeleteScheduledAtMs = static_cast(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(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 Payload; + for (int32 i = 0; i < 8; ++i) Payload.Add(static_cast((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 Payload; + for (int32 i = 0; i < 8; ++i) Payload.Add(static_cast((CharId64 >> (8 * i)) & 0xFF)); + Char->SendCharRequest(Opcode, Payload); +} + +void UUIUserLobbyScreen_Base::HandleCharDeleteAck(const TArray& 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(CharId64), static_cast(EffectiveAtMs)); + } + else + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: DELETE_ACK rejeitado (reason=%d)"), Reason); + } + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::HandleCharDeleteAcceptAck(const TArray& 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(CharId64)); + } + else + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: DELETE_ACCEPT rejeitado (reason=%d)"), Reason); + } + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::HandleCharDeleteCancelAck(const TArray& 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(CharId64)); + } + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::HandleCharSelectOk(const TArray& Payload) +{ + // Wire: uint64 charId + string mapName + float x/y/z + float yaw + + // string worldHost + uint16 worldPort + string handoffToken + string region + int32 Pos = 0; + uint64 CharId64 = 0; FString MapName, WorldHost, HandoffToken, Region; + float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0; + if (!ReadU64(Payload, Pos, CharId64) + || !ReadStringUtf8(Payload, Pos, MapName) + || !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz) + || !ReadFloat(Payload, Pos, Yaw) + || !ReadStringUtf8(Payload, Pos, WorldHost) + || !ReadU16(Payload, Pos, WorldPort) + || !ReadStringUtf8(Payload, Pos, HandoffToken) + || !ReadStringUtf8(Payload, Pos, Region)) + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado")); + return; + } + UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d map=%s token=%s..."), + *WorldHost, WorldPort, *MapName, *HandoffToken.Left(8)); + + UGameInstance* GI = GetGameInstance(); + if (!GI) return; + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->SetState(EZMMOFrontEndState::EnteringWorld); + } + // TODO Fase 1.5C: invocar UZeusNetworkSubsystem->ConnectToWorld(host,port,token) + // quando o WorldServer aceitar tickets do CharServer (handshake estendido). +} + +void UUIUserLobbyScreen_Base::HandleCharSelectReject(const TArray& 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& /*Payload*/) +{ + UE_LOG(LogZMMO, Log, TEXT("Lobby: S_CHAR_CREATE_OK — refresh lista")); + ShowCharSelectPage(); + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::HandleCharCreateReject(const TArray& 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(); + } + return nullptr; +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h new file mode 100644 index 0000000..077c0f7 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.h @@ -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 Chars; + + UFUNCTION(BlueprintImplementableEvent, Category = "Zeus|Lobby") + void OnCharListReceived(); + +protected: + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + + UFUNCTION() + void HandleCharRawMessage(int32 Opcode, const TArray& Payload); + + UFUNCTION() + void HandleCardSelected(FString CharId); + + UFUNCTION() + void HandleCardDeleteRequest(FString CharId); + + UZeusCharServerSubsystem* GetCharServer() const; + void ParseCharList(const TArray& Payload); + void HandleCharSelectOk(const TArray& Payload); + void HandleCharSelectReject(const TArray& Payload); + void HandleCharCreateOk(const TArray& Payload); + void HandleCharCreateReject(const TArray& Payload); + void HandleCharDeleteAck(const TArray& Payload); + void HandleCharDeleteAcceptAck(const TArray& Payload); + void HandleCharDeleteCancelAck(const TArray& 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 CardContainer; + + /** Switcher entre Lista (0) e Criar (1). */ + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr PageSwitcher; + + /** Page de criacao (WBP_CharacterCreate) — opcional, pode estar embedado. */ + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr CharCreatePage; + + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_Title; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Text_WorldName; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_Back; + UPROPERTY(meta = (BindWidgetOptional)) TObjectPtr Btn_Create; + + /** Classe do card a instanciar. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Zeus|Lobby") + TSubclassOf CharCardClass; + +private: + bool bSubscribed = false; + bool bButtonsBound = false; + + UPROPERTY(Transient) + TArray> SpawnedCards; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h b/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h new file mode 100644 index 0000000..be21bff --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h @@ -0,0 +1,102 @@ +#pragma once + +#include "CoreMinimal.h" +#include "ZMMOCharSummary.generated.h" + +/** Stats primarios do personagem (espelha `CharStats` em char.types.ts). */ +USTRUCT(BlueprintType) +struct FZMMOCharStats +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Str = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Agi = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Vit = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Int = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Dex = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Luk = 1; +}; + +/** Aparencia (espelha `CharAppearance` em char.types.ts). */ +USTRUCT(BlueprintType) +struct FZMMOCharAppearance +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Hair = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 HairColor = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 SkinColor = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 BodyType = 0; +}; + +/** + * Resumo de personagem recebido do CharServer via S_CHAR_LIST. + * Espelha `CharSummary` em `Server/ZeusCharServer/src/types/char.types.ts`. + */ +USTRUCT(BlueprintType) +struct FZMMOCharSummary +{ + GENERATED_BODY() + + /** charId — BIGINT UNSIGNED no banco; armazenado como FString aqui pra preservar 64 bits. */ + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString CharId; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString WorldId; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Slot = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString Name; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 ClassId = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 BaseLevel = 1; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 BaseExp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 JobLevel = 1; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 JobExp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FZMMOCharStats Stats; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Hp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 MaxHp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Sp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 MaxSp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Money = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString MapName; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FVector Position = FVector::ZeroVector; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + float YawDeg = 0.f; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FZMMOCharAppearance Appearance; + + /** Epoch ms em que o delete vai efetivar. 0 = nao agendado. */ + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int64 DeleteScheduledAtMs = 0; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h b/Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h new file mode 100644 index 0000000..34b0b8c --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/ZMMOWorldEntry.h @@ -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; +}; diff --git a/Source/ZMMO/Game/UI/Widgets/UIButton_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UIButton_Base.cpp index 862852f..51bb889 100644 --- a/Source/ZMMO/Game/UI/Widgets/UIButton_Base.cpp +++ b/Source/ZMMO/Game/UI/Widgets/UIButton_Base.cpp @@ -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 UUIButton_Base::GetVariantOptions() const +{ + TArray Options; + for (const TCHAR* N : { TEXT("Primary"), TEXT("Secondary"), TEXT("Danger"), TEXT("Ghost") }) + { + Options.Add(N); + } + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UIButtonVariantOptions"), false)) + { + for (const TPair& P : Row->Style.Button.Backgrounds) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& P : Row->Style.Button.Strokes) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& 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); } diff --git a/Source/ZMMO/Game/UI/Widgets/UIButton_Base.h b/Source/ZMMO/Game/UI/Widgets/UIButton_Base.h index e7367d7..cdc0415 100644 --- a/Source/ZMMO/Game/UI/Widgets/UIButton_Base.h +++ b/Source/ZMMO/Game/UI/Widgets/UIButton_Base.h @@ -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 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)) diff --git a/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp new file mode 100644 index 0000000..ebbae17 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.cpp @@ -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()) + { + 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(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UICheckBoxDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } +} + +TArray UUICheckBox_Base::GetLabelTextRoleOptions() const +{ + return UUICommonText_Base::GetThemeTextRoleOptions(); +} + +TArray UUICheckBox_Base::GetVariantOptions() const +{ + TArray Options; + Options.Add(TEXT("Default")); + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UICheckBoxVariantOptions"), false)) + { + for (const TPair& P : Row->Style.CheckBox.Fills) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& P : Row->Style.CheckBox.Strokes) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& 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(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()) + { + 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()) + { + 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); +} diff --git a/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h b/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h new file mode 100644 index 0000000..2f03931 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UICheckBox_Base.h @@ -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 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 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 CheckBox; + + /** Rótulo opcional ao lado (nome esperado no WBP: "Label"). */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Label; + +private: + UFUNCTION() + void HandleThemeChanged(FName NewThemeId); + + UFUNCTION() + void HandleCheckStateChanged(bool bIsChecked); + + bool bThemeBound = false; + bool bCheckBound = false; +}; diff --git a/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp new file mode 100644 index 0000000..0e28645 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.cpp @@ -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()) + { + 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(nullptr, GStylesDT)) + { + if (const FUIStyleRow* Row = DT->FindRow( + 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 UUICommonText_Base::GetTextRoleOptions() const +{ + return GetThemeTextRoleOptions(); +} + +TArray UUICommonText_Base::GetThemeTextRoleOptions() +{ + TArray 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(nullptr, GStylesDT)) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UITextRoleOptions"), false)) + { + for (const TPair& Pair : Row->Style.TextRoles) + { + Options.AddUnique(Pair.Key.ToString()); + } + } + } + return Options; +} + +void UUICommonText_Base::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + RefreshUIStyle(); +} diff --git a/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h b/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h new file mode 100644 index 0000000..8890987 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UICommonText_Base.h @@ -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 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 GetThemeTextRoleOptions(); + +protected: + virtual void SynchronizeProperties() override; +}; diff --git a/Source/ZMMO/Game/UI/Widgets/UIPanel_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UIPanel_Base.cpp index 055514e..7ec2f4f 100644 --- a/Source/ZMMO/Game/UI/Widgets/UIPanel_Base.cpp +++ b/Source/ZMMO/Game/UI/Widgets/UIPanel_Base.cpp @@ -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(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) { - if (const UDataTable* DT = LoadObject(nullptr, - TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UIPanelDesign"), false)) { - if (const FUIStyleRow* Row = DT->FindRow( - 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 ---- diff --git a/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp index ed93bb7..4357bea 100644 --- a/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp +++ b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp @@ -10,7 +10,7 @@ namespace { // Mesmo padrão de UUIPanel_Base: runtime usa o tema ativo; design-time // carrega a row "Default" de DT_UI_Styles para o Designer ver igual. - const FUIStyle& ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + FUIStyle ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback) { if (Widget) { @@ -22,40 +22,71 @@ namespace } } } - static FUIStyle DesignStyle; - static bool bDesignLoaded = false; - if (!bDesignLoaded) + // Design-time relê o DT a cada chamada: editar o DT_UI_Styles e + // RECOMPILAR o WBP já reflete (sem reiniciar o editor). + FUIStyle DesignStyle; + bool bHas = false; + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) { - if (const UDataTable* DT = LoadObject(nullptr, - TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UISpinnerDesign"), false)) { - if (const FUIStyleRow* Row = DT->FindRow( - FName(TEXT("Default")), TEXT("UISpinnerDesign"), false)) - { - DesignStyle = Row->Style; - bDesignLoaded = true; - } + DesignStyle = Row->Style; + bHas = true; } } - return bDesignLoaded ? DesignStyle : Fallback; + return bHas ? DesignStyle : Fallback; } } +static FUIStyleSpinnerVariant ResolveSpinnerVariant(const FUIStyleSpinner& Sp, FName VariantName) +{ + static const FUIStyleSpinner Defaults; + FUIStyleSpinnerVariant V; + + if (const FUIStyleSpinnerColors* E = Sp.Colors.Find(VariantName)) { V.Colors = *E; } + else if (const FUIStyleSpinnerColors* E2 = Defaults.Colors.Find(VariantName)) { V.Colors = *E2; } + else { V.Colors = Defaults.Colors.FindRef(TEXT("Default")); } + + if (const FUIStyleSpinnerLayout* E = Sp.Layouts.Find(VariantName)) { V.Layout = *E; } + else if (const FUIStyleSpinnerLayout* E2 = Defaults.Layouts.Find(VariantName)) { V.Layout = *E2; } + else { V.Layout = Defaults.Layouts.FindRef(TEXT("Default")); } + + return V; +} + +TArray UUISpinner_Base::GetVariantOptions() const +{ + TArray Options; + Options.Add(TEXT("Default")); + if (const UDataTable* DT = LoadObject(nullptr, + TEXT("/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles"))) + { + if (const FUIStyleRow* Row = DT->FindRow( + FName(TEXT("Default")), TEXT("UISpinnerVariantOptions"), false)) + { + for (const TPair& P : Row->Style.Spinner.Colors) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& P : Row->Style.Spinner.Layouts) { Options.AddUnique(P.Key.ToString()); } + } + } + return Options; +} + void UUISpinner_Base::RefreshUIStyle() { const FUIStyle Fallback; - const FUIStyle& AS = ResolveSpinnerStyle(this, Fallback); - const FUIStyleSpinner& S = AS.Spinner; + const FUIStyle AS = ResolveSpinnerStyle(this, Fallback); + const FUIStyleSpinnerVariant V = ResolveSpinnerVariant(AS.Spinner, Variant); if (Throbber) { - Throbber->SetNumberOfPieces(FMath::Max(1, S.NumberOfPieces)); - Throbber->SetPeriod(FMath::Max(0.05f, S.Period)); - Throbber->SetRadius(S.Radius); + Throbber->SetNumberOfPieces(FMath::Max(1, V.Layout.NumberOfPieces)); + Throbber->SetPeriod(FMath::Max(0.05f, V.Layout.Period)); + Throbber->SetRadius(V.Layout.Radius); } - // Cor/brush: o WBP aplica no throbber (mesma divisão de UUIPanel_Base). - BP_ApplySpinnerStyle(S); + BP_ApplySpinnerStyle(V); } void UUISpinner_Base::NativePreConstruct() diff --git a/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h index f88a787..ed13dbb 100644 --- a/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h +++ b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h @@ -27,15 +27,26 @@ public: UFUNCTION(BlueprintCallable, Category = "UI Style") void RefreshUIStyle(); + /** + * Variante visual do spinner (dropdown vem de FUIStyle.Spinner.Colors/ + * Layouts no DT_UI_Styles). Data-driven, sem enum fixo. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spinner", + meta = (GetOptions = "GetVariantOptions")) + FName Variant = TEXT("Default"); + + UFUNCTION() + TArray GetVariantOptions() const; + protected: virtual void NativePreConstruct() override; virtual void NativeConstruct() override; virtual void NativeDestruct() override; - /** Hook opcional: o WBP aplica cor/brush do spinner (FUIStyleSpinner). */ + /** Hook opcional: o WBP aplica cor/brush do spinner (variante composta). */ UFUNCTION(BlueprintImplementableEvent, Category = "UI Style", meta = (DisplayName = "Apply Spinner Style")) - void BP_ApplySpinnerStyle(const FUIStyleSpinner& Spinner); + void BP_ApplySpinnerStyle(const FUIStyleSpinnerVariant& VariantStyle); /** Throbber circular (nome esperado no WBP: "Throbber"). */ UPROPERTY(meta = (BindWidgetOptional)) diff --git a/ZMMO.uproject b/ZMMO.uproject index c53cef7..b4a7881 100644 --- a/ZMMO.uproject +++ b/ZMMO.uproject @@ -28,7 +28,8 @@ "Enabled": true, "SupportedTargetPlatforms": [ "Win64" - ] + ], + "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/product/362651520df94e4fa65492dbcba44ae2" }, { "Name": "ZeusNetwork",