diff --git a/.gitignore b/.gitignore index b157a4c..730b96b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ desktop.ini # Misc *.log *.pdb +.vscode/ diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..90d6a44 --- /dev/null +++ b/.ignore @@ -0,0 +1,11 @@ +/.git +/.vs +/.vscode +/Content +/DerivedDataCache +/Intermediate +/Saved +/Plugins/VisualStudioTools/azure-pipelines +/Plugins/VisualStudioTools/Docs +/Plugins/VisualStudioTools/Intermediate +/Plugins/VisualStudioTools/Scripts diff --git a/ARQUITETURA.md b/ARQUITETURA.md index 9000898..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. @@ -210,7 +214,9 @@ Content/ │ ├── UI/ ← HUD, Login, CharacterSelect, Inventory, │ │ │ Chat, Party, Guild, Trade, Quest, Map, │ │ │ Shared, Icons -│ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_Master…) +│ │ ├── Shared/ ← WBPs reutilizáveis (UI_Button_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) @@ -268,9 +274,16 @@ Source/ZMMO/ │ ├── Network/ ← UZMMOWorldSubsystem │ └── UI/ ← runtime de UI (não é contrato de dados) │ ├── ZMMOThemeSubsystem.* ← resolução de tema -│ ├── Widgets/ ← UUIButton_Base, UUIPanel_Base +│ ├── Widgets/ ← UUIButton_Base, UUIPanel_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), @@ -622,6 +635,24 @@ Fluxo geral: **Boot → Connecting → Login → ServerSelect → Lobby → Ente navegadas por um stack CommonUI **dentro** da tela do Lobby — não são estados de topo do fluxo. +**Rede do pré-login:** o `UUIFrontEndFlowSubsystem` dirige a conexão ao +**`UZeusCharServerSubsystem`** (WebSocket, `CharServerUrl` de Project Settings, +default `ws://127.0.0.1:7100/zeus`) ao entrar em **Boot** — NÃO o +`UZeusNetworkSubsystem` (UDP, world server, usado só no travel +EnteringWorld→InWorld). A tela Boot (`UUIBootScreen_Base`) observa o +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/Config/DefaultGame.ini b/Config/DefaultGame.ini index 2b56716..a93670a 100644 --- a/Config/DefaultGame.ini +++ b/Config/DefaultGame.ini @@ -29,3 +29,8 @@ UIStyleTable=/Game/ZMMO/Data/UI/DT_UI_Styles.DT_UI_Styles ; Mapa estado->tela. O DA já existe (RootLayoutClass = WBP_PrimaryGameLayout); ; StateScreens fica vazio até os WBPs de página virem do Zeus UMG Forge (§4.8). ScreenSetAsset=/Game/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.DA_FrontEndScreenSet + +; Fase 4 — char spawn DB-driven. DT_Maps espelha o `maps_config.json` do +; server (Server/ZeusServerEngine/Config/DataTables/maps_config.json) — o +; cliente resolve `mapId -> ClientLevel/spawns` via FZMMOMapDef rows. +MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps diff --git a/Content/AutoCreated/MMOWidget/Fonts/Font_Cinzel.uasset b/Content/AutoCreated/MMOWidget/Fonts/Font_Cinzel.uasset new file mode 100644 index 0000000..13030b5 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/Font_Cinzel.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/Font_Rajdhani.uasset b/Content/AutoCreated/MMOWidget/Fonts/Font_Rajdhani.uasset new file mode 100644 index 0000000..0b0f203 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/Font_Rajdhani.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_700_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_700_Face.uasset new file mode 100644 index 0000000..7e51d53 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_700_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_800_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_800_Face.uasset new file mode 100644 index 0000000..9aaed6b Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_cinzel_800_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_400_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_400_Face.uasset new file mode 100644 index 0000000..0a98af1 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_400_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_500_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_500_Face.uasset new file mode 100644 index 0000000..5481e25 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_500_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_600_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_600_Face.uasset new file mode 100644 index 0000000..f65fffe Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_600_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_700_Face.uasset b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_700_Face.uasset new file mode 100644 index 0000000..2cafdbe Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Fonts/font_rajdhani_700_Face.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Textures/bake_bg_7.uasset b/Content/AutoCreated/MMOWidget/Textures/bake_bg_7.uasset new file mode 100644 index 0000000..3322847 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Textures/bake_bg_7.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_BootScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_BootScreen.uasset new file mode 100644 index 0000000..cf79a7c Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_BootScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterCreateScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterCreateScreen.uasset new file mode 100644 index 0000000..9867dd3 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterCreateScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterSelectScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterSelectScreen.uasset new file mode 100644 index 0000000..a85bfd8 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_CharacterSelectScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_LoginScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_LoginScreen.uasset new file mode 100644 index 0000000..b41881d Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_LoginScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_ServerSelectScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_ServerSelectScreen.uasset new file mode 100644 index 0000000..568ee3a Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_ServerSelectScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_SettingsScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_SettingsScreen.uasset new file mode 100644 index 0000000..79f383e Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_SettingsScreen.uasset differ diff --git a/Content/AutoCreated/MMOWidget/Widgets/WBP_UIWorkshopScreen.uasset b/Content/AutoCreated/MMOWidget/Widgets/WBP_UIWorkshopScreen.uasset new file mode 100644 index 0000000..9811001 Binary files /dev/null and b/Content/AutoCreated/MMOWidget/Widgets/WBP_UIWorkshopScreen.uasset differ diff --git a/Content/Editor/ZeusUMGForge/FontMap.uasset b/Content/Editor/ZeusUMGForge/FontMap.uasset new file mode 100644 index 0000000..7a0d518 Binary files /dev/null and b/Content/Editor/ZeusUMGForge/FontMap.uasset differ diff --git a/Content/ExternalContent/Fab/.gitkeep b/Content/ExternalContent/Fab/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Content/ExternalContent/Marketplace/.gitkeep b/Content/ExternalContent/Marketplace/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Content/ExternalContent/Megascans/.gitkeep b/Content/ExternalContent/Megascans/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Content/ThirdPerson/Menu.umap b/Content/ThirdPerson/Menu.umap new file mode 100644 index 0000000..6fd7fbb Binary files /dev/null and b/Content/ThirdPerson/Menu.umap differ diff --git a/Content/ThirdPerson/TestWorld.umap b/Content/ThirdPerson/TestWorld.umap deleted file mode 100644 index 3ce30a4..0000000 Binary files a/Content/ThirdPerson/TestWorld.umap and /dev/null differ diff --git a/Content/ZMMO/Data/UI/DT_UI_Styles.uasset b/Content/ZMMO/Data/UI/DT_UI_Styles.uasset index c239beb..2406533 100644 Binary files a/Content/ZMMO/Data/UI/DT_UI_Styles.uasset and b/Content/ZMMO/Data/UI/DT_UI_Styles.uasset differ diff --git a/Content/ZMMO/Data/World/DT_Maps.uasset b/Content/ZMMO/Data/World/DT_Maps.uasset new file mode 100644 index 0000000..fd76096 Binary files /dev/null and b/Content/ZMMO/Data/World/DT_Maps.uasset differ diff --git a/Content/ZMMO/Maps/World/L_TestWorld.umap b/Content/ZMMO/Maps/World/L_TestWorld.umap new file mode 100644 index 0000000..34bf930 Binary files /dev/null and b/Content/ZMMO/Maps/World/L_TestWorld.umap differ diff --git a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Regular.uasset b/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Regular.uasset deleted file mode 100644 index 5996164..0000000 Binary files a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Regular.uasset and /dev/null differ diff --git a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_SemiBold.uasset b/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_SemiBold.uasset deleted file mode 100644 index 8ec0fc7..0000000 Binary files a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_SemiBold.uasset and /dev/null differ diff --git a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Bold.uasset b/Content/ZMMO/UI/Fonts/Cinzel/F_Cinzel_700_Face.uasset similarity index 96% rename from Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Bold.uasset rename to Content/ZMMO/UI/Fonts/Cinzel/F_Cinzel_700_Face.uasset index a2c35d6..d6aca67 100644 Binary files a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_Bold.uasset and b/Content/ZMMO/UI/Fonts/Cinzel/F_Cinzel_700_Face.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_ExtraBold.uasset b/Content/ZMMO/UI/Fonts/Cinzel/F_cinzel_800_Face.uasset similarity index 96% rename from Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_ExtraBold.uasset rename to Content/ZMMO/UI/Fonts/Cinzel/F_cinzel_800_Face.uasset index 15858d3..ed8af53 100644 Binary files a/Content/ZMMO/UI/Fonts/Cinzel/FF_Cinzel_ExtraBold.uasset and b/Content/ZMMO/UI/Fonts/Cinzel/F_cinzel_800_Face.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Cinzel/Font_Cinzel.uasset b/Content/ZMMO/UI/Fonts/Cinzel/Font_Cinzel.uasset new file mode 100644 index 0000000..873fea7 Binary files /dev/null and b/Content/ZMMO/UI/Fonts/Cinzel/Font_Cinzel.uasset differ diff --git a/Content/ZMMO/UI/Fonts/F_Cinzel.uasset b/Content/ZMMO/UI/Fonts/F_Cinzel.uasset new file mode 100644 index 0000000..f2628a6 Binary files /dev/null and b/Content/ZMMO/UI/Fonts/F_Cinzel.uasset differ diff --git a/Content/ZMMO/UI/Fonts/F_Rajdhani.uasset b/Content/ZMMO/UI/Fonts/F_Rajdhani.uasset new file mode 100644 index 0000000..b9e64c9 Binary files /dev/null and b/Content/ZMMO/UI/Fonts/F_Rajdhani.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani.uasset b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani.uasset new file mode 100644 index 0000000..1495c82 Binary files /dev/null and b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Regular.uasset b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_400_Face.uasset similarity index 99% rename from Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Regular.uasset rename to Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_400_Face.uasset index dd5b32e..e4e6301 100644 Binary files a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Regular.uasset and b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_400_Face.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Medium.uasset b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_500_Face.uasset similarity index 99% rename from Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Medium.uasset rename to Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_500_Face.uasset index 3410d15..1a937f8 100644 Binary files a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Medium.uasset and b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_500_Face.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_SemiBold.uasset b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_600_Face.uasset similarity index 99% rename from Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_SemiBold.uasset rename to Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_600_Face.uasset index 5d4acd9..fbee2df 100644 Binary files a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_SemiBold.uasset and b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_600_Face.uasset differ diff --git a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Bold.uasset b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_700_Face.uasset similarity index 99% rename from Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Bold.uasset rename to Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_700_Face.uasset index 880f980..a79a648 100644 Binary files a/Content/ZMMO/UI/Fonts/Rajdhani/FF_Rajdhani_Bold.uasset and b/Content/ZMMO/UI/Fonts/Rajdhani/F_Rajdhani_700_Face.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset index bdf26a0..5cdc89e 100644 Binary files a/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset and b/Content/ZMMO/UI/FrontEnd/DA_FrontEndScreenSet.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharCard.uasset b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharCard.uasset new file mode 100644 index 0000000..564b850 Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharCard.uasset differ 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 0000000..14f2b12 Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_CharacterCreate.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/Lobby/WBP_UserLobby.uasset b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_UserLobby.uasset new file mode 100644 index 0000000..d715bf1 Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/Lobby/WBP_UserLobby.uasset differ 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 0000000..be9ff2a Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/ServerSelect/WBP_ServerCard.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/WBP_Boot.uasset b/Content/ZMMO/UI/FrontEnd/WBP_Boot.uasset new file mode 100644 index 0000000..91d15bb Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/WBP_Boot.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/WBP_Login.uasset b/Content/ZMMO/UI/FrontEnd/WBP_Login.uasset new file mode 100644 index 0000000..3d9681a Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/WBP_Login.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/WBP_PrimaryGameLayout.uasset b/Content/ZMMO/UI/FrontEnd/WBP_PrimaryGameLayout.uasset index 3903827..df073e5 100644 Binary files a/Content/ZMMO/UI/FrontEnd/WBP_PrimaryGameLayout.uasset and b/Content/ZMMO/UI/FrontEnd/WBP_PrimaryGameLayout.uasset differ diff --git a/Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset b/Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset new file mode 100644 index 0000000..3103b01 Binary files /dev/null and b/Content/ZMMO/UI/FrontEnd/WBP_ServerSelect.uasset differ diff --git a/Content/ZMMO/UI/Shared/UI_Button_Master.uasset b/Content/ZMMO/UI/Shared/UI_Button_Master.uasset index 8f4859b..781eb17 100644 Binary files a/Content/ZMMO/UI/Shared/UI_Button_Master.uasset and b/Content/ZMMO/UI/Shared/UI_Button_Master.uasset differ diff --git a/Content/ZMMO/UI/Shared/UI_CheckBox_Master.uasset b/Content/ZMMO/UI/Shared/UI_CheckBox_Master.uasset new file mode 100644 index 0000000..5c98b84 Binary files /dev/null and b/Content/ZMMO/UI/Shared/UI_CheckBox_Master.uasset differ diff --git a/Content/ZMMO/UI/Shared/UI_Input_Master.uasset b/Content/ZMMO/UI/Shared/UI_Input_Master.uasset new file mode 100644 index 0000000..5232bbd Binary files /dev/null and b/Content/ZMMO/UI/Shared/UI_Input_Master.uasset differ diff --git a/Content/ZMMO/UI/Shared/UI_Label_Master.uasset b/Content/ZMMO/UI/Shared/UI_Label_Master.uasset new file mode 100644 index 0000000..9f25a0e Binary files /dev/null and b/Content/ZMMO/UI/Shared/UI_Label_Master.uasset differ diff --git a/Content/ZMMO/UI/Shared/UI_Spinner_Master.uasset b/Content/ZMMO/UI/Shared/UI_Spinner_Master.uasset new file mode 100644 index 0000000..53d0eea Binary files /dev/null and b/Content/ZMMO/UI/Shared/UI_Spinner_Master.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/Bars/T_Bar_Gradient_Background_Long.uasset b/Content/ZMMO/UI/Textures/Hyper/Bars/T_Bar_Gradient_Background_Long.uasset new file mode 100644 index 0000000..397e6d4 Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/Bars/T_Bar_Gradient_Background_Long.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Line-512-10.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Line-512-10.uasset new file mode 100644 index 0000000..a6b2aef Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Line-512-10.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_border_white.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_border_white.uasset new file mode 100644 index 0000000..e6c42de Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_border_white.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_white.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_white.uasset new file mode 100644 index 0000000..26fd61d Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Rounded_Corner_white.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Square_FullWhite.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Square_FullWhite.uasset new file mode 100644 index 0000000..ddf4984 Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_Square_FullWhite.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_2048-512.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_2048-512.uasset new file mode 100644 index 0000000..15f1a3f Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_2048-512.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_4096-512.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_4096-512.uasset new file mode 100644 index 0000000..8af5f78 Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_long_4096-512.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_rectangle_1024-512.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_rectangle_1024-512.uasset new file mode 100644 index 0000000..624c43c Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_rectangle_1024-512.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_square.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_square.uasset new file mode 100644 index 0000000..2a29099 Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_square.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thick_glow_2048-1280.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thick_glow_2048-1280.uasset new file mode 100644 index 0000000..18f8aff Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thick_glow_2048-1280.uasset differ diff --git a/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thin_2048-1280.uasset b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thin_2048-1280.uasset new file mode 100644 index 0000000..44cda40 Binary files /dev/null and b/Content/ZMMO/UI/Textures/Hyper/UI_Borders/T_border_frame_thin_2048-1280.uasset differ diff --git a/Content/ZMMO/UI/Themes/Default/Boot/T_Boot_Background.uasset b/Content/ZMMO/UI/Themes/Default/Boot/T_Boot_Background.uasset new file mode 100644 index 0000000..fa1c77b Binary files /dev/null and b/Content/ZMMO/UI/Themes/Default/Boot/T_Boot_Background.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/9O/FRJGV6530E9MDUAXUUMVT2.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/1/9O/FRJGV6530E9MDUAXUUMVT2.uasset deleted file mode 100644 index b1556bc..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/9O/FRJGV6530E9MDUAXUUMVT2.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/NR/GSOWAY7OVPG4EJ6N53FBT5.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/2/NR/GSOWAY7OVPG4EJ6N53FBT5.uasset deleted file mode 100644 index 6ae956c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/NR/GSOWAY7OVPG4EJ6N53FBT5.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/UV/TIA4QC1RY52JZAK9G2KJAU.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/2/UV/TIA4QC1RY52JZAK9G2KJAU.uasset deleted file mode 100644 index 483ba4f..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/UV/TIA4QC1RY52JZAK9G2KJAU.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/29/VOEXR6TWUNVF3TIA73I71O.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/3/29/VOEXR6TWUNVF3TIA73I71O.uasset deleted file mode 100644 index 7a3b319..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/29/VOEXR6TWUNVF3TIA73I71O.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/HB/IVEC500UBZYR67J9NU0UU2.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/3/HB/IVEC500UBZYR67J9NU0UU2.uasset deleted file mode 100644 index aab550c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/HB/IVEC500UBZYR67J9NU0UU2.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/K7/YSP4L3AFACO2UFE02PWPAZ.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/3/K7/YSP4L3AFACO2UFE02PWPAZ.uasset deleted file mode 100644 index 7f43873..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/K7/YSP4L3AFACO2UFE02PWPAZ.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/WS/JQJDHWS2C0NEV2PQQ24DS9.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/3/WS/JQJDHWS2C0NEV2PQQ24DS9.uasset deleted file mode 100644 index fced162..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/3/WS/JQJDHWS2C0NEV2PQQ24DS9.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/D4/5F0B0BSJ06CAOWTYEMJ9UP.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/4/D4/5F0B0BSJ06CAOWTYEMJ9UP.uasset deleted file mode 100644 index 38fceb0..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/D4/5F0B0BSJ06CAOWTYEMJ9UP.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/FH/5VFMLGY79X7A0RY85CW7GP.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/4/FH/5VFMLGY79X7A0RY85CW7GP.uasset deleted file mode 100644 index 2f32d89..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/FH/5VFMLGY79X7A0RY85CW7GP.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/4E/Z2WSCL5N559UJC5TOP6F1W.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/5/4E/Z2WSCL5N559UJC5TOP6F1W.uasset deleted file mode 100644 index c112f87..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/4E/Z2WSCL5N559UJC5TOP6F1W.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/JW/M23AIJR1F3QFTN7C70JL6N.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/5/JW/M23AIJR1F3QFTN7C70JL6N.uasset deleted file mode 100644 index 7de5f7e..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/JW/M23AIJR1F3QFTN7C70JL6N.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/V8/53KYKU6SU2KEFHAVL4ATJV.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/5/V8/53KYKU6SU2KEFHAVL4ATJV.uasset deleted file mode 100644 index cc406b6..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/V8/53KYKU6SU2KEFHAVL4ATJV.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/X8/4IKXRVK1SD5I5PXETE1VUX.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/5/X8/4IKXRVK1SD5I5PXETE1VUX.uasset deleted file mode 100644 index 29baeb8..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/X8/4IKXRVK1SD5I5PXETE1VUX.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/3Q/9YY85F8AXT6KNDZU0RF2BL.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/6/3Q/9YY85F8AXT6KNDZU0RF2BL.uasset deleted file mode 100644 index ef26341..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/3Q/9YY85F8AXT6KNDZU0RF2BL.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/6B/82CFIGAYT1PTOJ0QQV8X71.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/6/6B/82CFIGAYT1PTOJ0QQV8X71.uasset deleted file mode 100644 index fd8781c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/6B/82CFIGAYT1PTOJ0QQV8X71.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/8F/7YOPYVC7H0KELG7EBXME2G.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/6/8F/7YOPYVC7H0KELG7EBXME2G.uasset deleted file mode 100644 index 21fa11e..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/8F/7YOPYVC7H0KELG7EBXME2G.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/96/YJWLSNJ996N1D5LBQA3H23.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/6/96/YJWLSNJ996N1D5LBQA3H23.uasset deleted file mode 100644 index d43de3c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/96/YJWLSNJ996N1D5LBQA3H23.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/5Q/TVDW5C9545EN7HD9Z8OS1V.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/5Q/TVDW5C9545EN7HD9Z8OS1V.uasset deleted file mode 100644 index 427a3f6..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/5Q/TVDW5C9545EN7HD9Z8OS1V.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/6N/YAW7D6CUC40GV5TSLJD2BG.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/6N/YAW7D6CUC40GV5TSLJD2BG.uasset deleted file mode 100644 index 080373b..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/6N/YAW7D6CUC40GV5TSLJD2BG.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/LW/XI60F8RUM0JYJIT9T17L9I.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/LW/XI60F8RUM0JYJIT9T17L9I.uasset deleted file mode 100644 index f7a03e4..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/LW/XI60F8RUM0JYJIT9T17L9I.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/N6/5APYRECUJRT1SO9LS515IJ.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/N6/5APYRECUJRT1SO9LS515IJ.uasset deleted file mode 100644 index c21efe6..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/N6/5APYRECUJRT1SO9LS515IJ.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RB/Q5T6BQ2QCBTJG0ORINH627.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RB/Q5T6BQ2QCBTJG0ORINH627.uasset deleted file mode 100644 index 8ceea01..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RB/Q5T6BQ2QCBTJG0ORINH627.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RD/69QYQMRG4U4JVJ1CZ5JWVN.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RD/69QYQMRG4U4JVJ1CZ5JWVN.uasset deleted file mode 100644 index c93f100..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/RD/69QYQMRG4U4JVJ1CZ5JWVN.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/0M/ALJ2HD5RZZRC1WYRBVZYZX.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/8/0M/ALJ2HD5RZZRC1WYRBVZYZX.uasset deleted file mode 100644 index 21eefa1..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/0M/ALJ2HD5RZZRC1WYRBVZYZX.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/KK/V3O1E59SOM8L1HNJC4QCG6.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/8/KK/V3O1E59SOM8L1HNJC4QCG6.uasset deleted file mode 100644 index 61f0a63..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/KK/V3O1E59SOM8L1HNJC4QCG6.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/KE/RK629GGHY991OCX4UXBSSA.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/9/KE/RK629GGHY991OCX4UXBSSA.uasset deleted file mode 100644 index fe93e17..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/KE/RK629GGHY991OCX4UXBSSA.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/W4/KMG00ML6Q16EP4R6VH2YFG.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/9/W4/KMG00ML6Q16EP4R6VH2YFG.uasset deleted file mode 100644 index c04b4a1..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/W4/KMG00ML6Q16EP4R6VH2YFG.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/JK/0N0M191D7Z264BZA766ROC.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/A/JK/0N0M191D7Z264BZA766ROC.uasset deleted file mode 100644 index f6c4595..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/JK/0N0M191D7Z264BZA766ROC.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/AI/QJ8PX3QYKSSEO00QVSLYUW.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/AI/QJ8PX3QYKSSEO00QVSLYUW.uasset deleted file mode 100644 index ccde783..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/AI/QJ8PX3QYKSSEO00QVSLYUW.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/HP/1KT74TGZW04W95O7HLODEU.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/HP/1KT74TGZW04W95O7HLODEU.uasset deleted file mode 100644 index 1a1f938..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/HP/1KT74TGZW04W95O7HLODEU.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/QB/0RD2BROGX8XFH78MYAWP86.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/QB/0RD2BROGX8XFH78MYAWP86.uasset deleted file mode 100644 index e23456c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/QB/0RD2BROGX8XFH78MYAWP86.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/SM/5I0T2D0GU71R9GQCIAYR8R.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/SM/5I0T2D0GU71R9GQCIAYR8R.uasset deleted file mode 100644 index c4656de..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/SM/5I0T2D0GU71R9GQCIAYR8R.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/WD/GXMDY3RPBGPRCMC0M3DB6W.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/WD/GXMDY3RPBGPRCMC0M3DB6W.uasset deleted file mode 100644 index 34df7da..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/WD/GXMDY3RPBGPRCMC0M3DB6W.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/YC/S87SXF4J05LAT97WW4RVOY.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/B/YC/S87SXF4J05LAT97WW4RVOY.uasset deleted file mode 100644 index da4016c..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/YC/S87SXF4J05LAT97WW4RVOY.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/5W/QKXQAODBKITNE7N675IXIY.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/C/5W/QKXQAODBKITNE7N675IXIY.uasset deleted file mode 100644 index 61e4370..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/5W/QKXQAODBKITNE7N675IXIY.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/GI/PA6OC25PAKSGPL8O9L6VQN.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/C/GI/PA6OC25PAKSGPL8O9L6VQN.uasset deleted file mode 100644 index dd5762b..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/GI/PA6OC25PAKSGPL8O9L6VQN.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/RC/UR7H1P0OTSXVPAVZXZE82H.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/C/RC/UR7H1P0OTSXVPAVZXZE82H.uasset deleted file mode 100644 index ae85ff6..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/RC/UR7H1P0OTSXVPAVZXZE82H.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/1D/CCU5BL3843RTMTMNRNWRSA.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/D/1D/CCU5BL3843RTMTMNRNWRSA.uasset deleted file mode 100644 index 5eb5e2b..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/1D/CCU5BL3843RTMTMNRNWRSA.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/N3/NGASPVUDH6C9JIQSA6ZE0T.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/D/N3/NGASPVUDH6C9JIQSA6ZE0T.uasset deleted file mode 100644 index 7576614..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/N3/NGASPVUDH6C9JIQSA6ZE0T.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/7E/3C4PXLT28R9WV3DFGC0NZF.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/E/7E/3C4PXLT28R9WV3DFGC0NZF.uasset deleted file mode 100644 index ae1a408..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/7E/3C4PXLT28R9WV3DFGC0NZF.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/AZ/HF62OU7IM25GN5KTW2SHXR.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/E/AZ/HF62OU7IM25GN5KTW2SHXR.uasset deleted file mode 100644 index 3a8e261..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/AZ/HF62OU7IM25GN5KTW2SHXR.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/FT/KQOFMMBGIE9YTZU6IQVXUJ.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/E/FT/KQOFMMBGIE9YTZU6IQVXUJ.uasset deleted file mode 100644 index 01a605a..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/FT/KQOFMMBGIE9YTZU6IQVXUJ.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/KS/GNYW8XCQ974697OMM2VZNN.uasset b/Content/__ExternalActors__/ThirdPerson/TestWorld/E/KS/GNYW8XCQ974697OMM2VZNN.uasset deleted file mode 100644 index 5868be9..0000000 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/KS/GNYW8XCQ974697OMM2VZNN.uasset and /dev/null differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/7H/CIY1WWZN65F5UV5368TWQ2.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/7H/CIY1WWZN65F5UV5368TWQ2.uasset new file mode 100644 index 0000000..1296b82 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/7H/CIY1WWZN65F5UV5368TWQ2.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/2G/02U8G0EXYJ1BTT3AV3Q9WM.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/PB/BIU19AVLEIMM8MZO86XEN6.uasset similarity index 55% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/1/2G/02U8G0EXYJ1BTT3AV3Q9WM.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/PB/BIU19AVLEIMM8MZO86XEN6.uasset index 333a023..b195ee4 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/2G/02U8G0EXYJ1BTT3AV3Q9WM.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/0/PB/BIU19AVLEIMM8MZO86XEN6.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/4N/3AN4MHDH8EWRJ80FUEUF91.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/4N/3AN4MHDH8EWRJ80FUEUF91.uasset new file mode 100644 index 0000000..4bd625d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/4N/3AN4MHDH8EWRJ80FUEUF91.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/5A/1SDSFWUSZGGL4EB3M500A6.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/5A/1SDSFWUSZGGL4EB3M500A6.uasset new file mode 100644 index 0000000..5f8eaae Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/5A/1SDSFWUSZGGL4EB3M500A6.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/D8/Z8ESP1H7I23WVKKEWUMT6Z.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/D8/Z8ESP1H7I23WVKKEWUMT6Z.uasset new file mode 100644 index 0000000..9023fd5 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/D8/Z8ESP1H7I23WVKKEWUMT6Z.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/FW/YYTHDS16UU8FPV4OECR492.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/FW/YYTHDS16UU8FPV4OECR492.uasset new file mode 100644 index 0000000..cfca49c Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/FW/YYTHDS16UU8FPV4OECR492.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/G8/TZL9W6KEL0LHDUK3KZ6NJZ.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/G8/TZL9W6KEL0LHDUK3KZ6NJZ.uasset new file mode 100644 index 0000000..2f9c6ff Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/G8/TZL9W6KEL0LHDUK3KZ6NJZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/JO/XLA1EYZY895MILHDOZ6SDI.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/JO/XLA1EYZY895MILHDOZ6SDI.uasset new file mode 100644 index 0000000..4fda3f0 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/JO/XLA1EYZY895MILHDOZ6SDI.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/YS/5M6ZW56UUSEDQGRAJ95B7F.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/MN/D1Y4F9M9TMKTCXVUZHJEG1.uasset similarity index 74% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/2/YS/5M6ZW56UUSEDQGRAJ95B7F.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/MN/D1Y4F9M9TMKTCXVUZHJEG1.uasset index a11130e..0a338fd 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/YS/5M6ZW56UUSEDQGRAJ95B7F.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/MN/D1Y4F9M9TMKTCXVUZHJEG1.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/O2/JL18U9WI478K3Q0GMF1BZO.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/O2/JL18U9WI478K3Q0GMF1BZO.uasset new file mode 100644 index 0000000..dd7ffe8 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/O2/JL18U9WI478K3Q0GMF1BZO.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/P0/7GPH0OCV0545NTYCWE83Q4.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/P0/7GPH0OCV0545NTYCWE83Q4.uasset new file mode 100644 index 0000000..9e8d1ee Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/P0/7GPH0OCV0545NTYCWE83Q4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/WF/OVUP4ULP0G8PJNAMEYG04T.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/WF/OVUP4ULP0G8PJNAMEYG04T.uasset new file mode 100644 index 0000000..4c422bd Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/1/WF/OVUP4ULP0G8PJNAMEYG04T.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/68/Q971G3T2SO9JLUJ0IUVRU3.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/CX/23734T2F91Y1OGQRO4C0HI.uasset similarity index 53% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/2/68/Q971G3T2SO9JLUJ0IUVRU3.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/CX/23734T2F91Y1OGQRO4C0HI.uasset index b369ceb..ad45663 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/68/Q971G3T2SO9JLUJ0IUVRU3.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/CX/23734T2F91Y1OGQRO4C0HI.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/SZ/VQZO6BRL2LVZVPJXJRTCW8.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/FI/Y72WGZ0VKU1XA4QF86VP72.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/A/SZ/VQZO6BRL2LVZVPJXJRTCW8.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/FI/Y72WGZ0VKU1XA4QF86VP72.uasset index 6a69b1b..1c8b866 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/SZ/VQZO6BRL2LVZVPJXJRTCW8.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/2/FI/Y72WGZ0VKU1XA4QF86VP72.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/7B/YMRPMHSSUMIOPNIGDGN0R2.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/7B/YMRPMHSSUMIOPNIGDGN0R2.uasset new file mode 100644 index 0000000..9bb98fa Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/7B/YMRPMHSSUMIOPNIGDGN0R2.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/QQ/YJQPAKT8LWP1B301MWEDV6.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/8H/56LWCKVP40BT0E2L0B57Z5.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/5/QQ/YJQPAKT8LWP1B301MWEDV6.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/8H/56LWCKVP40BT0E2L0B57Z5.uasset index c3a9030..1d087e9 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/QQ/YJQPAKT8LWP1B301MWEDV6.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/8H/56LWCKVP40BT0E2L0B57Z5.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/F8/Q236QIPYDYORSUR8FO42W3.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/F8/Q236QIPYDYORSUR8FO42W3.uasset new file mode 100644 index 0000000..9c7b097 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/F8/Q236QIPYDYORSUR8FO42W3.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/KX/68HTHPLG2LJALB1RH8SBTO.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/KX/68HTHPLG2LJALB1RH8SBTO.uasset new file mode 100644 index 0000000..f991cd9 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/KX/68HTHPLG2LJALB1RH8SBTO.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/A1/CV60LLIDPR76AC94BI4R36.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/TF/0ZRE0SZ11LFP8LG6PJAOVP.uasset similarity index 58% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/5/A1/CV60LLIDPR76AC94BI4R36.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/TF/0ZRE0SZ11LFP8LG6PJAOVP.uasset index 80ce419..1db28aa 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/5/A1/CV60LLIDPR76AC94BI4R36.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/3/TF/0ZRE0SZ11LFP8LG6PJAOVP.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/20/5EQXGWR1FW9TDGNMMU80O5.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/20/5EQXGWR1FW9TDGNMMU80O5.uasset new file mode 100644 index 0000000..b724349 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/20/5EQXGWR1FW9TDGNMMU80O5.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/3L/A2MGK5FOHKL79MG3GHM98G.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/5F/I9FZP0D4JHSRFW1TTZKA8M.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/1/3L/A2MGK5FOHKL79MG3GHM98G.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/5F/I9FZP0D4JHSRFW1TTZKA8M.uasset index 4269434..00cc3f4 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/3L/A2MGK5FOHKL79MG3GHM98G.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/5F/I9FZP0D4JHSRFW1TTZKA8M.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/8L/NPMW1IYIHA0042JIUT0F58.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/8L/NPMW1IYIHA0042JIUT0F58.uasset new file mode 100644 index 0000000..10ada0c Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/8L/NPMW1IYIHA0042JIUT0F58.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/CC/D326DXUDYJFCAUUQX5SN7O.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/CC/D326DXUDYJFCAUUQX5SN7O.uasset new file mode 100644 index 0000000..c388572 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/CC/D326DXUDYJFCAUUQX5SN7O.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/IR/ZJ0IL1L560VPX36XR02HFX.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/IR/ZJ0IL1L560VPX36XR02HFX.uasset new file mode 100644 index 0000000..c4602b3 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/IR/ZJ0IL1L560VPX36XR02HFX.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MF/JSV7UQVZ3P4BCMIO1H3R2O.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MF/JSV7UQVZ3P4BCMIO1H3R2O.uasset new file mode 100644 index 0000000..1a8a035 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MF/JSV7UQVZ3P4BCMIO1H3R2O.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/6Y/SO0XTY2CO0JIY74NDT8AZE.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MQ/9C7D9BO11HYSC27Q9OGSE3.uasset similarity index 76% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/9/6Y/SO0XTY2CO0JIY74NDT8AZE.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MQ/9C7D9BO11HYSC27Q9OGSE3.uasset index 6f4a3c7..5a85198 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/6Y/SO0XTY2CO0JIY74NDT8AZE.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/MQ/9C7D9BO11HYSC27Q9OGSE3.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/SP/UDKBWLIKT8PEI4SCUTNKB5.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/SP/UDKBWLIKT8PEI4SCUTNKB5.uasset new file mode 100644 index 0000000..079bfd8 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/SP/UDKBWLIKT8PEI4SCUTNKB5.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/SD/S3UTR49GRCI3BL0GB4MTC1.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/X3/KYLD4RA19YSNYKRHD3VFT9.uasset similarity index 51% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/E/SD/S3UTR49GRCI3BL0GB4MTC1.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/X3/KYLD4RA19YSNYKRHD3VFT9.uasset index 2f11935..a74515c 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/SD/S3UTR49GRCI3BL0GB4MTC1.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/4/X3/KYLD4RA19YSNYKRHD3VFT9.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/1M/UYFC7VMV2RSIXPM63JHARO.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/1M/UYFC7VMV2RSIXPM63JHARO.uasset new file mode 100644 index 0000000..2805ec2 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/1M/UYFC7VMV2RSIXPM63JHARO.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/28/AIH2H2MUKIP1Z3NT5X2XXB.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/28/AIH2H2MUKIP1Z3NT5X2XXB.uasset new file mode 100644 index 0000000..60bbde3 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/28/AIH2H2MUKIP1Z3NT5X2XXB.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/B1/UNL95LPUP3TWS87184G67I.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/E9/CHKB08X4F6KP97ZF7HLC3M.uasset similarity index 59% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/6/B1/UNL95LPUP3TWS87184G67I.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/E9/CHKB08X4F6KP97ZF7HLC3M.uasset index 7a09f23..b66623a 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/B1/UNL95LPUP3TWS87184G67I.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/E9/CHKB08X4F6KP97ZF7HLC3M.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/DC/A1TO6K1ZIIQLSIAZ9BYSJD.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/HM/UFTYISBFIY6JLELUUPCZ45.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/4/DC/A1TO6K1ZIIQLSIAZ9BYSJD.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/HM/UFTYISBFIY6JLELUUPCZ45.uasset index 7f79466..f418d12 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/DC/A1TO6K1ZIIQLSIAZ9BYSJD.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/HM/UFTYISBFIY6JLELUUPCZ45.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/60/JYTMT702VL1JGAVDIC5FQ7.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/ZZ/HGEZIQE7OIBARS0IUT46CG.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/A/60/JYTMT702VL1JGAVDIC5FQ7.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/ZZ/HGEZIQE7OIBARS0IUT46CG.uasset index ae30db4..9baa614 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/60/JYTMT702VL1JGAVDIC5FQ7.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/5/ZZ/HGEZIQE7OIBARS0IUT46CG.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/7Y/VTT7CVYIPL1QF7MGWS9FFC.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/7Y/VTT7CVYIPL1QF7MGWS9FFC.uasset new file mode 100644 index 0000000..9b8e425 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/7Y/VTT7CVYIPL1QF7MGWS9FFC.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/1YGUL9B6PNE6IHFXLWG50R.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/1YGUL9B6PNE6IHFXLWG50R.uasset new file mode 100644 index 0000000..8f72bdb Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/1YGUL9B6PNE6IHFXLWG50R.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/R0T1E2QKSG5H7UEPH0VPTX.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/R0T1E2QKSG5H7UEPH0VPTX.uasset new file mode 100644 index 0000000..f0c0f96 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/FX/R0T1E2QKSG5H7UEPH0VPTX.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/JE/LQP12W8GCEWNAX6DFL2114.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/JE/LQP12W8GCEWNAX6DFL2114.uasset new file mode 100644 index 0000000..ef0dfc5 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/JE/LQP12W8GCEWNAX6DFL2114.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/PI/8V2E1D2BN7EIS1G5JHEDK0.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/PI/8V2E1D2BN7EIS1G5JHEDK0.uasset new file mode 100644 index 0000000..8700a6d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/PI/8V2E1D2BN7EIS1G5JHEDK0.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/UZ/5HOHN0CL4BSZ0NA575V74I.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/UZ/5HOHN0CL4BSZ0NA575V74I.uasset new file mode 100644 index 0000000..7c5d8ee Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/UZ/5HOHN0CL4BSZ0NA575V74I.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/V5/7C5QBPO3Q2V5OQXW219MAZ.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/V5/7C5QBPO3Q2V5OQXW219MAZ.uasset new file mode 100644 index 0000000..4edc9f7 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/6/V5/7C5QBPO3Q2V5OQXW219MAZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/2U/715FFEAQOEST7S1ZFKH1EV.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/2U/715FFEAQOEST7S1ZFKH1EV.uasset new file mode 100644 index 0000000..974caec Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/2U/715FFEAQOEST7S1ZFKH1EV.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/AP/TMSOFX5AVGNXHJW2UFE3IF.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/AP/TMSOFX5AVGNXHJW2UFE3IF.uasset new file mode 100644 index 0000000..6c90b28 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/AP/TMSOFX5AVGNXHJW2UFE3IF.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/EK/KL8CKPC49VXM06SLIIWV6N.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/EK/KL8CKPC49VXM06SLIIWV6N.uasset new file mode 100644 index 0000000..a0e192e Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/EK/KL8CKPC49VXM06SLIIWV6N.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/H8/FR3EUZJDXCHUJXWJAFFUUN.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/H8/FR3EUZJDXCHUJXWJAFFUUN.uasset new file mode 100644 index 0000000..6dac9f7 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/H8/FR3EUZJDXCHUJXWJAFFUUN.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/T9/YID9AT8SZJSB8IEEA6F17J.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/SK/T0R37DVTIY2FAFRN21Y0GD.uasset similarity index 55% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/2/T9/YID9AT8SZJSB8IEEA6F17J.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/SK/T0R37DVTIY2FAFRN21Y0GD.uasset index ca5d2a0..6beabf8 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/T9/YID9AT8SZJSB8IEEA6F17J.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/SK/T0R37DVTIY2FAFRN21Y0GD.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/XZ/4B9S1TXRVTRUH65OYK2B2P.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/VN/CGZ904T1TEOEX9JY6UQW6Y.uasset similarity index 59% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/1/XZ/4B9S1TXRVTRUH65OYK2B2P.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/VN/CGZ904T1TEOEX9JY6UQW6Y.uasset index e1bbdf8..7f4a941 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/XZ/4B9S1TXRVTRUH65OYK2B2P.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/7/VN/CGZ904T1TEOEX9JY6UQW6Y.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/OE/W8557RV08ADPOSLGO1LVZ8.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/OE/W8557RV08ADPOSLGO1LVZ8.uasset new file mode 100644 index 0000000..92bf2d0 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/OE/W8557RV08ADPOSLGO1LVZ8.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/XB/J2Q80E0UTYVUSUAU6YU3AP.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/XB/J2Q80E0UTYVUSUAU6YU3AP.uasset new file mode 100644 index 0000000..584f445 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/8/XB/J2Q80E0UTYVUSUAU6YU3AP.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/95/T31Q2J34RS9UKWBQF722VL.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/95/T31Q2J34RS9UKWBQF722VL.uasset new file mode 100644 index 0000000..3a26578 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/95/T31Q2J34RS9UKWBQF722VL.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/FY/NA00ONP3IT1SUJJ4L42ZGU.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/CX/LIL7EQWBCANSAOUH32VBYL.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/9/FY/NA00ONP3IT1SUJJ4L42ZGU.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/CX/LIL7EQWBCANSAOUH32VBYL.uasset index 2e0f066..c9730ef 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/9/FY/NA00ONP3IT1SUJJ4L42ZGU.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/CX/LIL7EQWBCANSAOUH32VBYL.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/EA/7CJAQLB6OD0PZ1D1ROKL70.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/EA/7CJAQLB6OD0PZ1D1ROKL70.uasset new file mode 100644 index 0000000..0895800 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/EA/7CJAQLB6OD0PZ1D1ROKL70.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/N2/26Z3YY64ORUYLCPSW6AYGH.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/N2/26Z3YY64ORUYLCPSW6AYGH.uasset new file mode 100644 index 0000000..9dc1ebe Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/9/N2/26Z3YY64ORUYLCPSW6AYGH.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/ST/GMMJEH1Q6094WUSCJNO7L3.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/FO/B2D90W0ND9JBZBMUWJ3QR4.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/E/ST/GMMJEH1Q6094WUSCJNO7L3.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/FO/B2D90W0ND9JBZBMUWJ3QR4.uasset index f5b498b..0fa95e0 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/ST/GMMJEH1Q6094WUSCJNO7L3.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/FO/B2D90W0ND9JBZBMUWJ3QR4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/GB/BV63XEORHWQ8JWL5UPGP93.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/GB/BV63XEORHWQ8JWL5UPGP93.uasset new file mode 100644 index 0000000..00b825e Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/GB/BV63XEORHWQ8JWL5UPGP93.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/J8/XOA7OSQHOOFO345I37WMZ1.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/J8/XOA7OSQHOOFO345I37WMZ1.uasset new file mode 100644 index 0000000..902d787 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/J8/XOA7OSQHOOFO345I37WMZ1.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/30/9W5D9QWS33B6YHBQ3PJWFY.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/NZ/100PFU1FCZ8TK2V1SEDCCA.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/A/30/9W5D9QWS33B6YHBQ3PJWFY.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/NZ/100PFU1FCZ8TK2V1SEDCCA.uasset index f08fbb1..31fd603 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/A/30/9W5D9QWS33B6YHBQ3PJWFY.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/NZ/100PFU1FCZ8TK2V1SEDCCA.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/PX/GGNBTCN4X0V1GFH7XEFZI4.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/PX/GGNBTCN4X0V1GFH7XEFZI4.uasset new file mode 100644 index 0000000..7734e85 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/PX/GGNBTCN4X0V1GFH7XEFZI4.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/0K/W9V7DZHVD1813TO2W9A5Q0.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/YE/KL79NFHN68219S8PU7LN2Q.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/D/0K/W9V7DZHVD1813TO2W9A5Q0.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/YE/KL79NFHN68219S8PU7LN2Q.uasset index 8c30fef..958c788 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/D/0K/W9V7DZHVD1813TO2W9A5Q0.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/A/YE/KL79NFHN68219S8PU7LN2Q.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/E6/MLANNHEKLRCH3U6O18XOFD.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/P4/68VRY63MJA0NO59JQ7HCXJ.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/2/E6/MLANNHEKLRCH3U6O18XOFD.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/P4/68VRY63MJA0NO59JQ7HCXJ.uasset index 28c9eae..0f30126 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/E6/MLANNHEKLRCH3U6O18XOFD.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/P4/68VRY63MJA0NO59JQ7HCXJ.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/9R/9LHPFE23OYWT3JZ6CBSMDF.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/R4/UMHZAQWOVA9COL27P38XZP.uasset similarity index 69% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/6/9R/9LHPFE23OYWT3JZ6CBSMDF.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/R4/UMHZAQWOVA9COL27P38XZP.uasset index e59fdfa..c07651c 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/6/9R/9LHPFE23OYWT3JZ6CBSMDF.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/B/R4/UMHZAQWOVA9COL27P38XZP.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/5H/UXFO8Z8MJRMWJJBFOKVPIU.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/5H/UXFO8Z8MJRMWJJBFOKVPIU.uasset new file mode 100644 index 0000000..f222064 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/5H/UXFO8Z8MJRMWJJBFOKVPIU.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/60/AACCY8NR64HMRLZNDZNME0.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/60/AACCY8NR64HMRLZNDZNME0.uasset new file mode 100644 index 0000000..f01a612 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/60/AACCY8NR64HMRLZNDZNME0.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/Z0/V7C0Q33RM7DNPVBLZFE8HU.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/J7/5CEJ2NGOEJUC917CC1LRAR.uasset similarity index 59% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/E/Z0/V7C0Q33RM7DNPVBLZFE8HU.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/J7/5CEJ2NGOEJUC917CC1LRAR.uasset index e5dc422..f12e7e7 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/E/Z0/V7C0Q33RM7DNPVBLZFE8HU.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/J7/5CEJ2NGOEJUC917CC1LRAR.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/0B/4BRKVOKK1A3WPRBJJGYD47.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/X9/I5QJBCL1C7QSGSNAN7145N.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/7/0B/4BRKVOKK1A3WPRBJJGYD47.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/X9/I5QJBCL1C7QSGSNAN7145N.uasset index 148ac66..3b07567 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/0B/4BRKVOKK1A3WPRBJJGYD47.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/C/X9/I5QJBCL1C7QSGSNAN7145N.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/1I/VUF6L45VQ8J88OBT8RHLJR.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/1I/VUF6L45VQ8J88OBT8RHLJR.uasset new file mode 100644 index 0000000..c2532ca Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/1I/VUF6L45VQ8J88OBT8RHLJR.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/2B/VNO3XRM05EW214PG5XMX70.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/2B/VNO3XRM05EW214PG5XMX70.uasset new file mode 100644 index 0000000..c36fdfa Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/2B/VNO3XRM05EW214PG5XMX70.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/4X/AWAUEAF6SCBZ2UGBDPYJLH.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/4X/AWAUEAF6SCBZ2UGBDPYJLH.uasset new file mode 100644 index 0000000..c9af90a Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/4X/AWAUEAF6SCBZ2UGBDPYJLH.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/73/34F3WJ37A5IRZZDVVMDCC8.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/73/34F3WJ37A5IRZZDVVMDCC8.uasset new file mode 100644 index 0000000..9bbc881 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/73/34F3WJ37A5IRZZDVVMDCC8.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/DO/JJE7G9US01S7ZR5SVQI6AD.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/DO/JJE7G9US01S7ZR5SVQI6AD.uasset new file mode 100644 index 0000000..76617d8 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/DO/JJE7G9US01S7ZR5SVQI6AD.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/N1/S96KI2IJWDPHU10GB2K3J3.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/IE/YC27K3BQU50I9PC3Q4G9I3.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/2/N1/S96KI2IJWDPHU10GB2K3J3.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/IE/YC27K3BQU50I9PC3Q4G9I3.uasset index 86e2501..c3bccd2 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/2/N1/S96KI2IJWDPHU10GB2K3J3.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/IE/YC27K3BQU50I9PC3Q4G9I3.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/J0/KX0K662MF8YCCY1QRAE6AU.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/NV/KRE0PVMUHFNYHXUJMABTYJ.uasset similarity index 66% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/4/J0/KX0K662MF8YCCY1QRAE6AU.uasset rename to Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/NV/KRE0PVMUHFNYHXUJMABTYJ.uasset index 644caa6..6c580cc 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/4/J0/KX0K662MF8YCCY1QRAE6AU.uasset and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/NV/KRE0PVMUHFNYHXUJMABTYJ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/OD/U51V2OJEPTDKPKQGSMQU8U.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/OD/U51V2OJEPTDKPKQGSMQU8U.uasset new file mode 100644 index 0000000..e6582ba Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/D/OD/U51V2OJEPTDKPKQGSMQU8U.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/1K/H535CDCQFTK00ML5BIWL7J.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/1K/H535CDCQFTK00ML5BIWL7J.uasset new file mode 100644 index 0000000..7d9aeb0 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/1K/H535CDCQFTK00ML5BIWL7J.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/8C/I3YO2VNDDG64OMSWO9E88M.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/8C/I3YO2VNDDG64OMSWO9E88M.uasset new file mode 100644 index 0000000..899ef4f Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/8C/I3YO2VNDDG64OMSWO9E88M.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/IM/UTJE9SQPY2MIHGVRDD2XAD.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/IM/UTJE9SQPY2MIHGVRDD2XAD.uasset new file mode 100644 index 0000000..d4aeeeb Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/IM/UTJE9SQPY2MIHGVRDD2XAD.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/J0/XD2YEQH60M9MKC591B2POJ.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/J0/XD2YEQH60M9MKC591B2POJ.uasset new file mode 100644 index 0000000..8a3c5dd Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/J0/XD2YEQH60M9MKC591B2POJ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/PN/037UG5M4QJJG374YP9USU9.uasset b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/PN/037UG5M4QJJG374YP9USU9.uasset new file mode 100644 index 0000000..a5e906d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Debug/Maps/L_Test_UI/E/PN/037UG5M4QJJG374YP9USU9.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/0H/7N078HJ4DQ71SG6ZXS6JIO.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/0H/7N078HJ4DQ71SG6ZXS6JIO.uasset new file mode 100644 index 0000000..d2d7b2b Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/0H/7N078HJ4DQ71SG6ZXS6JIO.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/HH/AN079M2NET5BQV0XAEGEHZ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/HH/AN079M2NET5BQV0XAEGEHZ.uasset new file mode 100644 index 0000000..6a45818 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/HH/AN079M2NET5BQV0XAEGEHZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/KL/HBTXQTYYAFIH2ZLAXU64JY.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/KL/HBTXQTYYAFIH2ZLAXU64JY.uasset new file mode 100644 index 0000000..56f48c4 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/KL/HBTXQTYYAFIH2ZLAXU64JY.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/Y3/O5X6DA33MAYAO1H1ECZGRM.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/Y3/O5X6DA33MAYAO1H1ECZGRM.uasset new file mode 100644 index 0000000..7fdb55b Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/Y3/O5X6DA33MAYAO1H1ECZGRM.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/YR/WFWAP5XT0SZ8AH7W4ZKDNQ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/YR/WFWAP5XT0SZ8AH7W4ZKDNQ.uasset new file mode 100644 index 0000000..ae847c8 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/0/YR/WFWAP5XT0SZ8AH7W4ZKDNQ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/3H/QMRZ3AA7U7QZWEBRRVG3P7.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/3H/QMRZ3AA7U7QZWEBRRVG3P7.uasset new file mode 100644 index 0000000..e36c3ca Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/3H/QMRZ3AA7U7QZWEBRRVG3P7.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/4Y/OSZFEZHL080NXST70YMLR4.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/4Y/OSZFEZHL080NXST70YMLR4.uasset new file mode 100644 index 0000000..f028ec3 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/4Y/OSZFEZHL080NXST70YMLR4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/6S/XZ3R4UIWXXKIOL28Q1TH1O.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/6S/XZ3R4UIWXXKIOL28Q1TH1O.uasset new file mode 100644 index 0000000..93dbeb4 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/6S/XZ3R4UIWXXKIOL28Q1TH1O.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/96/CKG3F7E1MQUIV4M89B1VLP.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/8R/M1VCKYHIS9SQ0S0REIYO24.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/B/96/CKG3F7E1MQUIV4M89B1VLP.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/8R/M1VCKYHIS9SQ0S0REIYO24.uasset index 309ca85..c290666 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/96/CKG3F7E1MQUIV4M89B1VLP.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/8R/M1VCKYHIS9SQ0S0REIYO24.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/E5/0I8L1PWJ96IAKNJ7D2FZ4I.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/E5/0I8L1PWJ96IAKNJ7D2FZ4I.uasset new file mode 100644 index 0000000..75ef2ac Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/E5/0I8L1PWJ96IAKNJ7D2FZ4I.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/H1/EK6DGO03BXNYJN6KR40BLR.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/H1/EK6DGO03BXNYJN6KR40BLR.uasset new file mode 100644 index 0000000..f963d38 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/H1/EK6DGO03BXNYJN6KR40BLR.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/SW/3JXIC81UYRLIQ907YSQ8AI.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/SW/3JXIC81UYRLIQ907YSQ8AI.uasset new file mode 100644 index 0000000..ad64d6d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/SW/3JXIC81UYRLIQ907YSQ8AI.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/YG/Y7NMB7LPKNNP9SO9F01Y9R.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/YG/Y7NMB7LPKNNP9SO9F01Y9R.uasset new file mode 100644 index 0000000..b414473 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/1/YG/Y7NMB7LPKNNP9SO9F01Y9R.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/5M/F10BTMMKAH9IEC0611G3YA.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/5M/F10BTMMKAH9IEC0611G3YA.uasset new file mode 100644 index 0000000..07d6c82 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/5M/F10BTMMKAH9IEC0611G3YA.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/HE/4ZB024N462RAYZZ1UE089V.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/HE/4ZB024N462RAYZZ1UE089V.uasset new file mode 100644 index 0000000..8ab06ec Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/HE/4ZB024N462RAYZZ1UE089V.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/D5/VT5NTL3GGUG5WSHNAVRAMS.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/IA/7IKZFPIFU46E05JXGU3HIF.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/C/D5/VT5NTL3GGUG5WSHNAVRAMS.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/IA/7IKZFPIFU46E05JXGU3HIF.uasset index a129d87..f8660f5 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/C/D5/VT5NTL3GGUG5WSHNAVRAMS.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/IA/7IKZFPIFU46E05JXGU3HIF.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/V5/S4LEVTIMLMX77XM7T0JGC7.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/V5/S4LEVTIMLMX77XM7T0JGC7.uasset new file mode 100644 index 0000000..964c91e Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/2/V5/S4LEVTIMLMX77XM7T0JGC7.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/IS/GW0R7SK96TS7RPH5KESOIC.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/IS/GW0R7SK96TS7RPH5KESOIC.uasset new file mode 100644 index 0000000..f2248b9 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/IS/GW0R7SK96TS7RPH5KESOIC.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/FE/TIDI0C6ET0K1IM67FLQUB1.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/J0/IUKTCDET8DNCUC5YYBV3W4.uasset similarity index 55% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/8/FE/TIDI0C6ET0K1IM67FLQUB1.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/J0/IUKTCDET8DNCUC5YYBV3W4.uasset index aae5738..58f9c69 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/8/FE/TIDI0C6ET0K1IM67FLQUB1.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/3/J0/IUKTCDET8DNCUC5YYBV3W4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/6J/SFHLERNRQJAM88RL70KY5X.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/6J/SFHLERNRQJAM88RL70KY5X.uasset new file mode 100644 index 0000000..fd73d18 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/6J/SFHLERNRQJAM88RL70KY5X.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DR/0TXT0XMFXLV48UDKZ2DDMB.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DR/0TXT0XMFXLV48UDKZ2DDMB.uasset new file mode 100644 index 0000000..8b8faf0 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DR/0TXT0XMFXLV48UDKZ2DDMB.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DS/A0WNJ17UPRDM2T5OYGX961.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DS/A0WNJ17UPRDM2T5OYGX961.uasset new file mode 100644 index 0000000..82ac79a Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/DS/A0WNJ17UPRDM2T5OYGX961.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/FG/1FJ5ALGXPXT2TB3XX9DI3T.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/FG/1FJ5ALGXPXT2TB3XX9DI3T.uasset new file mode 100644 index 0000000..cb6ca38 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/FG/1FJ5ALGXPXT2TB3XX9DI3T.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/IY/ZYEB41YVJCWPXQANZNM8Y8.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/IY/ZYEB41YVJCWPXQANZNM8Y8.uasset new file mode 100644 index 0000000..5689e0f Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/4/IY/ZYEB41YVJCWPXQANZNM8Y8.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/4O/DK0VAFT34MTL60T9LW67NW.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/4O/DK0VAFT34MTL60T9LW67NW.uasset new file mode 100644 index 0000000..69ea7f8 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/4O/DK0VAFT34MTL60T9LW67NW.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/B7/E0GNZ4LQXEUL026U5ESKXI.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/B7/E0GNZ4LQXEUL026U5ESKXI.uasset new file mode 100644 index 0000000..72edbf2 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/B7/E0GNZ4LQXEUL026U5ESKXI.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/NS/VY0DYS66UMQ259ORSWWNHQ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/NS/VY0DYS66UMQ259ORSWWNHQ.uasset new file mode 100644 index 0000000..da8315f Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/5/NS/VY0DYS66UMQ259ORSWWNHQ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/8X/WNVR13Z6NKLW7EIIBWRSXM.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/8X/WNVR13Z6NKLW7EIIBWRSXM.uasset new file mode 100644 index 0000000..230b82d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/8X/WNVR13Z6NKLW7EIIBWRSXM.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/M3/HY3AJYDT2ZBU027549NEOQ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/M3/HY3AJYDT2ZBU027549NEOQ.uasset new file mode 100644 index 0000000..58ff6be Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/M3/HY3AJYDT2ZBU027549NEOQ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/ND/OT4VEBKI3TK89NUCJFC9ND.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/ND/OT4VEBKI3TK89NUCJFC9ND.uasset new file mode 100644 index 0000000..911fdf4 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/ND/OT4VEBKI3TK89NUCJFC9ND.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/R3/ORKODPHM3HWTSURCWIU6H4.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/R3/ORKODPHM3HWTSURCWIU6H4.uasset new file mode 100644 index 0000000..339b391 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/R3/ORKODPHM3HWTSURCWIU6H4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/U4/XWD4432JDULVOMB5YN7U31.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/U4/XWD4432JDULVOMB5YN7U31.uasset new file mode 100644 index 0000000..0e48497 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/U4/XWD4432JDULVOMB5YN7U31.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/YE/JKI3VN4PL0BT9FQTCKW09R.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/YE/JKI3VN4PL0BT9FQTCKW09R.uasset new file mode 100644 index 0000000..c8ac42f Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/6/YE/JKI3VN4PL0BT9FQTCKW09R.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/14/HE3UU6K6S38DOFBM73KU7C.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/14/HE3UU6K6S38DOFBM73KU7C.uasset new file mode 100644 index 0000000..1457ec7 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/14/HE3UU6K6S38DOFBM73KU7C.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/1Y/2DMSWMI4IFBL4OCZ1X3OER.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/1Y/2DMSWMI4IFBL4OCZ1X3OER.uasset new file mode 100644 index 0000000..1754101 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/1Y/2DMSWMI4IFBL4OCZ1X3OER.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/3J/MP2DR7DVTMPJHWGYRARVJZ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/3J/MP2DR7DVTMPJHWGYRARVJZ.uasset new file mode 100644 index 0000000..b80a4e9 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/3J/MP2DR7DVTMPJHWGYRARVJZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/5R/GBRF5QNPM08EMIKHGIL4P3.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/5R/GBRF5QNPM08EMIKHGIL4P3.uasset new file mode 100644 index 0000000..0a15475 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/5R/GBRF5QNPM08EMIKHGIL4P3.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/J5/ZZVO35N7FQUMD0WOL92VWZ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/J5/ZZVO35N7FQUMD0WOL92VWZ.uasset new file mode 100644 index 0000000..28d3974 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/J5/ZZVO35N7FQUMD0WOL92VWZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/LK/RDM0GUA7R3KFV9EPWBAFJ5.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/LK/RDM0GUA7R3KFV9EPWBAFJ5.uasset new file mode 100644 index 0000000..377a553 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/LK/RDM0GUA7R3KFV9EPWBAFJ5.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/3Q/0N4150SN0K2NYR1N1SJ1LC.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/Z8/JXS3UMJVAEKH0JT5H3Q4ZE.uasset similarity index 56% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/B/3Q/0N4150SN0K2NYR1N1SJ1LC.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/Z8/JXS3UMJVAEKH0JT5H3Q4ZE.uasset index 496f3e7..9502505 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/B/3Q/0N4150SN0K2NYR1N1SJ1LC.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/7/Z8/JXS3UMJVAEKH0JT5H3Q4ZE.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/39/1NRMGJUS59QOB6Q12GR6BH.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/39/1NRMGJUS59QOB6Q12GR6BH.uasset new file mode 100644 index 0000000..47d4c74 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/39/1NRMGJUS59QOB6Q12GR6BH.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/IG/UMDLFRNRL0X2EHVXYK5GHU.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/80/IDP1LPNPJPOASJ5KAP0I96.uasset similarity index 55% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/1/IG/UMDLFRNRL0X2EHVXYK5GHU.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/80/IDP1LPNPJPOASJ5KAP0I96.uasset index 6af849b..0c41ea8 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/1/IG/UMDLFRNRL0X2EHVXYK5GHU.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/80/IDP1LPNPJPOASJ5KAP0I96.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/AQ/TKQBVW8TA23RXB4J1DAPJI.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/AQ/TKQBVW8TA23RXB4J1DAPJI.uasset new file mode 100644 index 0000000..9b415d2 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/AQ/TKQBVW8TA23RXB4J1DAPJI.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/GX/DZDUILROSPPHG228GYL9I5.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/GX/DZDUILROSPPHG228GYL9I5.uasset new file mode 100644 index 0000000..4467e6d Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/GX/DZDUILROSPPHG228GYL9I5.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/RF/SILMUSBK5JLAHROHYXFW3H.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/RF/SILMUSBK5JLAHROHYXFW3H.uasset new file mode 100644 index 0000000..57c05d3 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/RF/SILMUSBK5JLAHROHYXFW3H.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/ZK/M4VHAJEE5CDSA3RBN7DBFI.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/ZK/M4VHAJEE5CDSA3RBN7DBFI.uasset new file mode 100644 index 0000000..891a8d2 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/8/ZK/M4VHAJEE5CDSA3RBN7DBFI.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/8K/TI0FUPQ8OGDZCY64EY4AWB.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/8K/TI0FUPQ8OGDZCY64EY4AWB.uasset new file mode 100644 index 0000000..278cf6e Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/8K/TI0FUPQ8OGDZCY64EY4AWB.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KB/V28W8B92OQI6DBTUIVKU77.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KB/V28W8B92OQI6DBTUIVKU77.uasset new file mode 100644 index 0000000..6543c66 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KB/V28W8B92OQI6DBTUIVKU77.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KZ/UR4O2B5DCUXBEA46Y22UAU.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KZ/UR4O2B5DCUXBEA46Y22UAU.uasset new file mode 100644 index 0000000..94bfe22 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/9/KZ/UR4O2B5DCUXBEA46Y22UAU.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/1B/QFMBVFPTG23UVWQ78SYKKW.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/1B/QFMBVFPTG23UVWQ78SYKKW.uasset new file mode 100644 index 0000000..476fac6 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/1B/QFMBVFPTG23UVWQ78SYKKW.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/9X/FDMHF5AZNHKS7V3QT3QZCS.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/9X/FDMHF5AZNHKS7V3QT3QZCS.uasset new file mode 100644 index 0000000..f26c893 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/9X/FDMHF5AZNHKS7V3QT3QZCS.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/JJ/F2YIJ9M3YY1NPG7GHRYC5W.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/JJ/F2YIJ9M3YY1NPG7GHRYC5W.uasset new file mode 100644 index 0000000..b5d38e7 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/JJ/F2YIJ9M3YY1NPG7GHRYC5W.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/ON/BIU9MAKE5TD6TY9EXWU5KM.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/ON/BIU9MAKE5TD6TY9EXWU5KM.uasset new file mode 100644 index 0000000..f103811 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/ON/BIU9MAKE5TD6TY9EXWU5KM.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/Y3/RJECMXK7NX1A8Z615MQHPZ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/Y3/RJECMXK7NX1A8Z615MQHPZ.uasset new file mode 100644 index 0000000..68ea4d9 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/Y3/RJECMXK7NX1A8Z615MQHPZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/YI/200XK57HWUKX1807BH9BOR.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/YI/200XK57HWUKX1807BH9BOR.uasset new file mode 100644 index 0000000..466352e Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/A/YI/200XK57HWUKX1807BH9BOR.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/0X/EB21KXG9K0F5AB5NWOEFXT.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/0X/EB21KXG9K0F5AB5NWOEFXT.uasset new file mode 100644 index 0000000..f8fea64 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/0X/EB21KXG9K0F5AB5NWOEFXT.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/CX/OH87ZSH20Y1UMGSSCK4VXZ.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/CX/OH87ZSH20Y1UMGSSCK4VXZ.uasset new file mode 100644 index 0000000..f071690 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/CX/OH87ZSH20Y1UMGSSCK4VXZ.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/IW/KU7OE0MI757XVLLWKVS269.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/IW/KU7OE0MI757XVLLWKVS269.uasset new file mode 100644 index 0000000..0be6c98 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/IW/KU7OE0MI757XVLLWKVS269.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/V7/V6BLNFD5HS81EIWBJ7YLPN.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/V7/V6BLNFD5HS81EIWBJ7YLPN.uasset new file mode 100644 index 0000000..ba39137 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/V7/V6BLNFD5HS81EIWBJ7YLPN.uasset differ diff --git a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/G3/1N8ESWJALGDYB5AJQBR1IY.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/YL/LUFMLQUE1X08REDW9NXKL4.uasset similarity index 66% rename from Content/__ExternalActors__/ThirdPerson/TestWorld/7/G3/1N8ESWJALGDYB5AJQBR1IY.uasset rename to Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/YL/LUFMLQUE1X08REDW9NXKL4.uasset index 247cc6d..f09ae27 100644 Binary files a/Content/__ExternalActors__/ThirdPerson/TestWorld/7/G3/1N8ESWJALGDYB5AJQBR1IY.uasset and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/B/YL/LUFMLQUE1X08REDW9NXKL4.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/2M/XTJIVAU1A9TIMKCTJ973I1.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/2M/XTJIVAU1A9TIMKCTJ973I1.uasset new file mode 100644 index 0000000..f9385e1 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/2M/XTJIVAU1A9TIMKCTJ973I1.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/FN/Y9XMBF93MQOGJ6L3VKII8I.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/FN/Y9XMBF93MQOGJ6L3VKII8I.uasset new file mode 100644 index 0000000..152ea0f Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/FN/Y9XMBF93MQOGJ6L3VKII8I.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/TN/HD26ZHTJQ0JBWM8OFO6NZM.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/TN/HD26ZHTJQ0JBWM8OFO6NZM.uasset new file mode 100644 index 0000000..4c7cb9b Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/TN/HD26ZHTJQ0JBWM8OFO6NZM.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/YT/C4OV5DK3RL18I31C64NSK3.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/YT/C4OV5DK3RL18I31C64NSK3.uasset new file mode 100644 index 0000000..9523ec9 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/YT/C4OV5DK3RL18I31C64NSK3.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/Z4/BC1SUTC0CIH2AIHW2TUQZ3.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/Z4/BC1SUTC0CIH2AIHW2TUQZ3.uasset new file mode 100644 index 0000000..9ed66f6 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/C/Z4/BC1SUTC0CIH2AIHW2TUQZ3.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/4D/KOKGCXHGZLHW5B7WWV9DW0.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/4D/KOKGCXHGZLHW5B7WWV9DW0.uasset new file mode 100644 index 0000000..f81f97a Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/4D/KOKGCXHGZLHW5B7WWV9DW0.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/D0/X024ZJPM5QK61X2Q5YI5XS.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/D0/X024ZJPM5QK61X2Q5YI5XS.uasset new file mode 100644 index 0000000..0be4f64 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/D0/X024ZJPM5QK61X2Q5YI5XS.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/E1/AQYH93CS6SO0D48P4KFF6G.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/E1/AQYH93CS6SO0D48P4KFF6G.uasset new file mode 100644 index 0000000..cdc5124 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/E1/AQYH93CS6SO0D48P4KFF6G.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/GI/9SSZW2HKDCP2MQOK5RYCLY.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/GI/9SSZW2HKDCP2MQOK5RYCLY.uasset new file mode 100644 index 0000000..d582640 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/GI/9SSZW2HKDCP2MQOK5RYCLY.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/Q0/Y24ZN3Q81U02ABOL82SK6V.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/Q0/Y24ZN3Q81U02ABOL82SK6V.uasset new file mode 100644 index 0000000..4104a8c Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/D/Q0/Y24ZN3Q81U02ABOL82SK6V.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6H/HJQE42MHJ9ET0G3U3T949P.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6H/HJQE42MHJ9ET0G3U3T949P.uasset new file mode 100644 index 0000000..bf281bc Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6H/HJQE42MHJ9ET0G3U3T949P.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6P/CNMDINWP4ATMLH5VFVIR2D.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6P/CNMDINWP4ATMLH5VFVIR2D.uasset new file mode 100644 index 0000000..e71c68b Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/6P/CNMDINWP4ATMLH5VFVIR2D.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/SE/SCXIH36007ZUQYZRWD4X0J.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/SE/SCXIH36007ZUQYZRWD4X0J.uasset new file mode 100644 index 0000000..264b823 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/SE/SCXIH36007ZUQYZRWD4X0J.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/ZO/U3MA5UB3G7VSOS9W84PK0E.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/ZO/U3MA5UB3G7VSOS9W84PK0E.uasset new file mode 100644 index 0000000..ddc1a68 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/E/ZO/U3MA5UB3G7VSOS9W84PK0E.uasset differ diff --git a/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/F/0N/J8V9XIZ7GO5BO52B23DZBE.uasset b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/F/0N/J8V9XIZ7GO5BO52B23DZBE.uasset new file mode 100644 index 0000000..d1b3029 Binary files /dev/null and b/Content/__ExternalActors__/ZMMO/Maps/World/L_TestWorld/F/0N/J8V9XIZ7GO5BO52B23DZBE.uasset differ diff --git a/Content/__ExternalObjects__/ThirdPerson/TestWorld/6/WC/U2X8JZUEMFR1L6TN101EG7.uasset b/Content/__ExternalObjects__/ThirdPerson/TestWorld/6/WC/U2X8JZUEMFR1L6TN101EG7.uasset deleted file mode 100644 index b99544f..0000000 Binary files a/Content/__ExternalObjects__/ThirdPerson/TestWorld/6/WC/U2X8JZUEMFR1L6TN101EG7.uasset and /dev/null differ diff --git a/Content/__ExternalObjects__/ThirdPerson/TestWorld/D/N2/R4B449IADCGUC7QYBNJEQY.uasset b/Content/__ExternalObjects__/ThirdPerson/TestWorld/D/N2/R4B449IADCGUC7QYBNJEQY.uasset deleted file mode 100644 index dd26c8a..0000000 Binary files a/Content/__ExternalObjects__/ThirdPerson/TestWorld/D/N2/R4B449IADCGUC7QYBNJEQY.uasset and /dev/null differ diff --git a/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/1/JE/4ZZR31GUL9V8W9NHNSYQ3Y.uasset b/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/1/JE/4ZZR31GUL9V8W9NHNSYQ3Y.uasset new file mode 100644 index 0000000..7943dc3 Binary files /dev/null and b/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/1/JE/4ZZR31GUL9V8W9NHNSYQ3Y.uasset differ diff --git a/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/6/BN/R2EWG9RP3CSUA1RY0P1X28.uasset b/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/6/BN/R2EWG9RP3CSUA1RY0P1X28.uasset new file mode 100644 index 0000000..e5fd30e Binary files /dev/null and b/Content/__ExternalObjects__/ZMMO/Debug/Maps/L_Test_UI/6/BN/R2EWG9RP3CSUA1RY0P1X28.uasset differ diff --git a/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/0/R6/PVSVXIP4OD80GPU6L2FQL9.uasset b/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/0/R6/PVSVXIP4OD80GPU6L2FQL9.uasset new file mode 100644 index 0000000..db62d71 Binary files /dev/null and b/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/0/R6/PVSVXIP4OD80GPU6L2FQL9.uasset differ diff --git a/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/C/T2/YADAUYMZ7RSH4MBTVOVWI2.uasset b/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/C/T2/YADAUYMZ7RSH4MBTVOVWI2.uasset new file mode 100644 index 0000000..09cafb0 Binary files /dev/null and b/Content/__ExternalObjects__/ZMMO/Maps/World/L_TestWorld/C/T2/YADAUYMZ7RSH4MBTVOVWI2.uasset differ diff --git a/Source/ZMMO/Data/UI/ThemeKeys.h b/Source/ZMMO/Data/UI/ThemeKeys.h index d51d30e..c7b2eca 100644 --- a/Source/ZMMO/Data/UI/ThemeKeys.h +++ b/Source/ZMMO/Data/UI/ThemeKeys.h @@ -12,6 +12,11 @@ enum class EZMMOThemeKey : uint8 HUD_HealthBar UMETA(DisplayName = "HUD - Health Bar"), HUD_ManaBar UMETA(DisplayName = "HUD - Mana Bar"), + // Boot + Boot_Background UMETA(DisplayName = "Boot - Background"), + Boot_Logo UMETA(DisplayName = "Boot - Logo"), + Boot_Music UMETA(DisplayName = "Boot - Music"), + // Login / Character Select Login_Background UMETA(DisplayName = "Login - Background"), Login_Logo UMETA(DisplayName = "Login - Logo"), diff --git a/Source/ZMMO/Data/UI/UIStyleRow.h b/Source/ZMMO/Data/UI/UIStyleRow.h index c52f8f9..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; @@ -42,6 +51,23 @@ struct ZMMO_API FUIStyle UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") FUIStyleTooltip Tooltip; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FUIStyleSpinner Spinner; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FUIStyleCheckBox CheckBox; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FUIStyleInput Input; + + /** + * Fundo de tela cheia do front-end (ex.: Boot/Login). Brush data-driven: + * a tela aplica no seu Border_Bg via RefreshUIStyle — nunca hard-ref no + * WBP (ARQUITETURA.md §5). Preenchido em DT_UI_Styles. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style") + FSlateBrush ScreenBackground; }; /** diff --git a/Source/ZMMO/Data/UI/UIStyleTokens.h b/Source/ZMMO/Data/UI/UIStyleTokens.h index 17f505c..68ce067 100644 --- a/Source/ZMMO/Data/UI/UIStyleTokens.h +++ b/Source/ZMMO/Data/UI/UIStyleTokens.h @@ -5,6 +5,7 @@ #include "Styling/SlateBrush.h" #include "Layout/Margin.h" #include "Templates/SubclassOf.h" +#include "Engine/Texture2D.h" #include "UIStyleTypes.h" #include "UIStyleTokens.generated.h" @@ -171,7 +172,97 @@ struct ZMMO_API FUIStyleText float TitleLetterSpacing = 0.f; // informativo (RichText/futuro) }; -/** Cores de uma variante de botão (primary/secondary/danger/ghost). */ +/** + * Uma categoria tipográfica nomeada do tema. As categorias NÃO são fixas em + * C++: o autor cria as que quiser ("Title", "Section", "Body", "Label", …) + * no mapa FUIStyle.TextRoles do DT_UI_Styles. O UUICommonText_Base só + * referencia o nome (dropdown vem do DT). + */ +USTRUCT(BlueprintType) +struct ZMMO_API FUITextStyle +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text") + FSlateFontInfo Font; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text") + FLinearColor Color = FLinearColor(FColor(244, 240, 230)); + + /** Caixa-alta (ex.: labels/eyebrows). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Text") + bool bUppercase = false; +}; + +// ===================================================================== +// FUIStyleButton — padrão multi-map (Hyper-style). 3 sub-mapas com a MESMA +// chave (nome da variante: "Primary", "Secondary", "Danger", "Ghost", ou +// customizada). UUIButton_Base.Variant é FName e busca em cada sub-mapa. +// O struct FUIStyleButtonVariant (composto, FLAT) permanece com a forma +// antiga — assim o hook BP_ApplyUIStyle dos WBP_Master continua funcional. +// ===================================================================== +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleButtonBackground +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BgNormal = FLinearColor::Transparent; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BgHover = FLinearColor::Transparent; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BgPressed = FLinearColor::Transparent; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BgDisabled = FLinearColor(0.f, 0.f, 0.f, 0.42f); + + /** Textura opcional como background (Hyper-style). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + TObjectPtr BackgroundTexture = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FMargin BackgroundMargin = FMargin(8.f); +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleButtonStroke +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BorderNormal = FLinearColor::Transparent; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor BorderHover = FLinearColor::Transparent; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0")) + float BorderWidth = 1.f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button", meta = (ClampMin = "0")) + float CornerRadius = 8.f; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleButtonFont +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + FLinearColor TextColor = FLinearColor::White; + + /** CommonTextStyle define fonte/tamanho/peso; TextColor sobrescreve a cor. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") + TSubclassOf TextStyle; +}; + +/** + * View FLAT composta da variante de botão (Background + Stroke + Font). + * Mantida com a forma antiga (BgNormal/BgHover/.../TextStyle no topo) para + * NÃO QUEBRAR o hook BP_ApplyUIStyle nos WBP_Master existentes. É construída + * pelo UUIButton_Base::ResolveVariant a partir dos 3 sub-mapas do tema. + */ USTRUCT(BlueprintType) struct ZMMO_API FUIStyleButtonVariant { @@ -198,14 +289,21 @@ struct ZMMO_API FUIStyleButtonVariant UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") FLinearColor TextColor = FLinearColor::White; - /** - * Estilo de texto desta variante (padrão Hyper/CommonUI: CommonTextStyle). - * Define fonte/tamanho/peso. A COR é sobreposta pelo TextColor acima - * (FUIStyle manda na cor por tema). Permite Primary/Secondary/Danger/Ghost - * com tipografias diferentes. Preenchido em DT_UI_Styles por tema. - */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") TSubclassOf TextStyle; + + // Campos extras vindos das sub-categorias (background texture + stroke): + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button") + TObjectPtr BackgroundTexture = nullptr; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button") + FMargin BackgroundMargin = FMargin(8.f); + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button") + float BorderWidth = 1.f; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Button") + float CornerRadius = 8.f; }; USTRUCT(BlueprintType) @@ -213,74 +311,85 @@ struct ZMMO_API FUIStyleButton { GENERATED_BODY() - /** - * Defaults = paleta Aurora Arcana (styles.css .btn-*). Garante que mesmo - * sem DataTable (preview de designer) o botão já saia estilizado. - */ FUIStyleButton() { - // .btn-primary — dourado, texto quase preto - Primary.BgNormal = FLinearColor(FColor(201, 167, 90)); // gold - Primary.BgHover = FLinearColor(FColor(231, 200, 115)); // gold-hi - Primary.BgPressed = FLinearColor(FColor(201, 167, 90)); - Primary.BgDisabled = FLinearColor(FColor(201, 167, 90, 107)); - Primary.BorderNormal = FLinearColor(FColor(201, 167, 90)); - Primary.BorderHover = FLinearColor(FColor(231, 200, 115)); - Primary.TextColor = FLinearColor(FColor(10, 10, 18)); // ~#0A0A12 + // ---- Backgrounds (Primary/Secondary/Danger/Ghost) ---- + FUIStyleButtonBackground PriBg; + PriBg.BgNormal = FLinearColor(FColor(201, 167, 90)); // gold + PriBg.BgHover = FLinearColor(FColor(231, 200, 115)); // gold-hi + PriBg.BgPressed = FLinearColor(FColor(201, 167, 90)); + PriBg.BgDisabled = FLinearColor(FColor(201, 167, 90, 107)); + Backgrounds.Add(TEXT("Primary"), PriBg); - // .btn-secondary — painel sólido + borda - Secondary.BgNormal = FLinearColor(FColor(17, 24, 42)); // panel-2 - Secondary.BgHover = FLinearColor(FColor(22, 32, 58)); // #16203A - Secondary.BgPressed = FLinearColor(FColor(17, 24, 42)); - Secondary.BorderNormal = FLinearColor(FColor(58, 75, 120)); // border - Secondary.BorderHover = FLinearColor(FColor(78, 163, 255)); // blue - Secondary.TextColor = FLinearColor(FColor(244, 240, 230));// text + FUIStyleButtonBackground SecBg; + SecBg.BgNormal = FLinearColor(FColor(17, 24, 42)); // panel-2 + SecBg.BgHover = FLinearColor(FColor(22, 32, 58)); + SecBg.BgPressed = FLinearColor(FColor(17, 24, 42)); + Backgrounds.Add(TEXT("Secondary"), SecBg); - // .btn-danger - Danger.BgNormal = FLinearColor(FColor(217, 92, 92, 36)); // rgba(.14) - Danger.BgHover = FLinearColor(FColor(217, 92, 92, 66)); // rgba(.26) - Danger.BgPressed = FLinearColor(FColor(217, 92, 92, 36)); - Danger.BorderNormal = FLinearColor(FColor(217, 92, 92)); // danger - Danger.BorderHover = FLinearColor(FColor(217, 92, 92)); - Danger.TextColor = FLinearColor(FColor(240, 182, 182)); // #F0B6B6 + FUIStyleButtonBackground DngBg; + DngBg.BgNormal = FLinearColor(FColor(217, 92, 92, 36)); + DngBg.BgHover = FLinearColor(FColor(217, 92, 92, 66)); + DngBg.BgPressed = FLinearColor(FColor(217, 92, 92, 36)); + Backgrounds.Add(TEXT("Danger"), DngBg); - // .btn-ghost — transparente + borda suave - Ghost.BgNormal = FLinearColor::Transparent; - Ghost.BgHover = FLinearColor(FColor(22, 32, 58, 128)); - Ghost.BgPressed = FLinearColor::Transparent; - Ghost.BorderNormal = FLinearColor(FColor(58, 75, 120, 140)); // border-soft - Ghost.BorderHover = FLinearColor(FColor(78, 163, 255)); - Ghost.TextColor = FLinearColor(FColor(174, 182, 200)); // text-2 + FUIStyleButtonBackground GhoBg; + GhoBg.BgNormal = FLinearColor::Transparent; + GhoBg.BgHover = FLinearColor(FColor(22, 32, 58, 128)); + GhoBg.BgPressed = FLinearColor::Transparent; + Backgrounds.Add(TEXT("Ghost"), GhoBg); + + // ---- Strokes ---- + FUIStyleButtonStroke PriS; + PriS.BorderNormal = FLinearColor(FColor(201, 167, 90)); + PriS.BorderHover = FLinearColor(FColor(231, 200, 115)); + Strokes.Add(TEXT("Primary"), PriS); + + FUIStyleButtonStroke SecS; + SecS.BorderNormal = FLinearColor(FColor(58, 75, 120)); + SecS.BorderHover = FLinearColor(FColor(78, 163, 255)); + Strokes.Add(TEXT("Secondary"), SecS); + + FUIStyleButtonStroke DngS; + DngS.BorderNormal = FLinearColor(FColor(217, 92, 92)); + DngS.BorderHover = FLinearColor(FColor(217, 92, 92)); + Strokes.Add(TEXT("Danger"), DngS); + + FUIStyleButtonStroke GhoS; + GhoS.BorderNormal = FLinearColor(FColor(58, 75, 120, 140)); + GhoS.BorderHover = FLinearColor(FColor(78, 163, 255)); + Strokes.Add(TEXT("Ghost"), GhoS); + + // ---- Fonts ---- + FUIStyleButtonFont PriF; PriF.TextColor = FLinearColor(FColor(10, 10, 18)); + Fonts.Add(TEXT("Primary"), PriF); + FUIStyleButtonFont SecF; SecF.TextColor = FLinearColor(FColor(244, 240, 230)); + Fonts.Add(TEXT("Secondary"), SecF); + FUIStyleButtonFont DngF; DngF.TextColor = FLinearColor(FColor(240, 182, 182)); + Fonts.Add(TEXT("Danger"), DngF); + FUIStyleButtonFont GhoF; GhoF.TextColor = FLinearColor(FColor(174, 182, 200)); + Fonts.Add(TEXT("Ghost"), GhoF); } - // .btn-primary — dourado, texto quase preto + /** Cores/textura de fundo por variante (chave FName). */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - FUIStyleButtonVariant Primary; + TMap Backgrounds; - // .btn-secondary — painel sólido + borda + /** Borda (cor, espessura, raio) por variante. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - FUIStyleButtonVariant Secondary; + TMap Strokes; - // .btn-danger + /** Tipografia (cor de texto, CommonTextStyle) por variante. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - FUIStyleButtonVariant Danger; - - // .btn-ghost — transparente + borda suave - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - FUIStyleButtonVariant Ghost; + TMap Fonts; + // Globais comuns às variantes: UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") FMargin Padding = FMargin(22.f, 12.f); UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") float MinHeight = 48.f; - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - float CornerRadius = 8.f; - - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") - float BorderWidth = 1.f; - UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Button") float HoverScale = 1.02f; @@ -444,3 +553,333 @@ struct ZMMO_API FUIStyleTooltip UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Tooltip") FLinearColor TextColor = FLinearColor(FColor(244, 240, 230)); // Text }; + +// ===================================================================== +// FUIStyleSpinner — padrão multi-map (Hyper-style). Sub-mapas por categoria +// (Colors, Layouts) com a MESMA CHAVE de variante. Permite criar variantes +// como "Default", "Small", "Loading", etc. +// ===================================================================== +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleSpinnerColors +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + FLinearColor Color = FLinearColor(FColor(201, 167, 90)); // Gold + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + FLinearColor TrackColor = FLinearColor(FColor(58, 75, 120, 140)); +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleSpinnerLayout +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + float Radius = 28.f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + float Thickness = 3.f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "1")) + int32 NumberOfPieces = 8; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner", meta = (ClampMin = "0.05")) + float Period = 0.9f; +}; + +/** View FLAT composta da variante de Spinner. */ +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleSpinnerVariant +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Spinner") + FUIStyleSpinnerColors Colors; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Spinner") + FUIStyleSpinnerLayout Layout; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleSpinner +{ + GENERATED_BODY() + + FUIStyleSpinner() + { + Colors.Add(TEXT("Default"), FUIStyleSpinnerColors()); + Layouts.Add(TEXT("Default"), FUIStyleSpinnerLayout()); + } + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + TMap Colors; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Spinner") + TMap Layouts; +}; + +// ===================================================================== +// FUIStyleCheckBox — padrão multi-map (Hyper-style). Sub-mapas por categoria +// visual (Fills, Strokes, Layouts) com a MESMA CHAVE de variante. Cada +// UUICheckBox_Base referencia o nome (FName Variant) e o C++ compõe. +// ===================================================================== +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleCheckBoxFill +{ + GENERATED_BODY() + + /** Preenchimento da caixa quando marcada. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor BoxColor = FLinearColor(FColor(201, 167, 90)); // Gold + + /** Cor do "tique"/glifo de check. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor CheckColor = FLinearColor(FColor(18, 22, 34)); // Bg0 + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor HoveredColor = FLinearColor(FColor(224, 196, 130)); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor PressedColor = FLinearColor(FColor(178, 146, 74)); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor DisabledColor = FLinearColor(FColor(90, 96, 112, 140)); +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleCheckBoxStroke +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + FLinearColor BorderColor = FLinearColor(FColor(58, 75, 120, 140)); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "0")) + float BorderThickness = 1.5f; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "0")) + float CornerRadius = 4.f; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleCheckBoxLayout +{ + GENERATED_BODY() + + /** Lado da caixa (px). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox", meta = (ClampMin = "8")) + float BoxSize = 22.f; +}; + +/** View FLAT composta da variante de CheckBox — usada pelo BP hook. */ +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleCheckBoxVariant +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox") + FUIStyleCheckBoxFill Fill; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox") + FUIStyleCheckBoxStroke Stroke; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|CheckBox") + FUIStyleCheckBoxLayout Layout; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleCheckBox +{ + GENERATED_BODY() + + FUIStyleCheckBox() + { + Fills.Add(TEXT("Default"), FUIStyleCheckBoxFill()); + Strokes.Add(TEXT("Default"), FUIStyleCheckBoxStroke()); + Layouts.Add(TEXT("Default"), FUIStyleCheckBoxLayout()); + } + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + TMap Fills; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + TMap Strokes; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|CheckBox") + TMap Layouts; +}; + +// ===================================================================== +// FUIStyleInput — padrão multi-map (estilo Hyper): sub-mapas independentes +// por categoria visual (Backgrounds, Strokes, Typography). O widget escolhe +// um nome único de variante (ex.: "Box") que existe em CADA sub-mapa; +// reaproveita-se sem dor (ex.: mesma Stroke em vários Backgrounds). +// ===================================================================== +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleInputBackground +{ + GENERATED_BODY() + + /** + * Brush completo do background (Image, Image Size, Matiz, Desenhar como, + * Ladrilhos, Margin/9-slice, UV). Mesma UI de FSlateBrush exposta no UMG. + * Se Brush.ResourceObject for nulo, pinta cor sólida (procedural — DrawAs + * = Image sem textura). + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FSlateBrush Brush; + + /** Tint multiplicado por cima do Brush em estado de hover. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FLinearColor HoveredColor = FLinearColor::White; + + /** Tint multiplicado por cima do Brush em estado de focus. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FLinearColor FocusedColor = FLinearColor::White; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleInputStroke +{ + GENERATED_BODY() + + /** + * Brush completo da borda (Image, Image Size, Matiz, Desenhar como, + * Ladrilhos, Margin). Use textura 9-slice (DrawAs=Box) ou Image para + * stroke decorada estilo Hyper Search. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FSlateBrush Brush; + + /** Espessura procedural (efeito visual; usado por widgets que renderizam stroke vector). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0")) + float BorderThickness = 1.f; + + /** Raio do canto procedural (efeito visual; usado por widgets que renderizam RoundedBox). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0")) + float CornerRadius = 6.f; +}; + +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleInputTypography +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FLinearColor TextColor = FLinearColor(FColor(244, 240, 230)); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FLinearColor HintColor = FLinearColor(FColor(150, 160, 180, 200)); + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "1")) + float Size = 16.f; + + /** Fonte específica desta tipografia. Se vazia, usa FUIStyle.Text.BodyFont. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + FSlateFontInfo FontOverride; +}; + +/** + * View "composta" da variante (Background + Stroke + Typography). Não é + * editada diretamente no DT — é o que o UUIInput_Base monta ao resolver + * a chave Variant nos 3 sub-mapas. Mantida aqui para uso no C++. + */ +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleInputVariant +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input") + FUIStyleInputBackground Background; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input") + FUIStyleInputStroke Stroke; + + UPROPERTY(BlueprintReadOnly, Category = "UI Style|Input") + FUIStyleInputTypography Typography; +}; + +/** + * Campo de texto — padrão multi-map (Hyper-style). 3 sub-mapas independentes + * com a MESMA CHAVE (nome da variante: "Box", "Outline", "Underline", + * "Search", ou customizada). O UUIInput_Base usa Variant=FName e busca o + * pedaço em cada sub-mapa; assim dá pra reaproveitar (ex.: a mesma Stroke + * em variantes com Backgrounds diferentes). + * + * Construtor já popula com defaults bonitos inspirados na Login. + */ +USTRUCT(BlueprintType) +struct ZMMO_API FUIStyleInput +{ + GENERATED_BODY() + + FUIStyleInput() + { + auto MakeBg = [](const FLinearColor& Tint) + { + FUIStyleInputBackground B; + B.Brush.TintColor = FSlateColor(Tint); + B.Brush.DrawAs = ESlateBrushDrawType::Image; + return B; + }; + + // ---- Backgrounds ---- + Backgrounds.Add(TEXT("Box"), MakeBg(FLinearColor(FColor(9, 13, 24, 235)))); + Backgrounds.Add(TEXT("Outline"), MakeBg(FLinearColor(0, 0, 0, 0))); + Backgrounds.Add(TEXT("Underline"), MakeBg(FLinearColor(0, 0, 0, 0))); + Backgrounds.Add(TEXT("Search"), MakeBg(FLinearColor(FColor(17, 24, 42)))); + + auto MakeStroke = [](const FLinearColor& Tint, float Thickness, float Radius) + { + FUIStyleInputStroke S; + S.Brush.TintColor = FSlateColor(Tint); + S.Brush.DrawAs = ESlateBrushDrawType::Box; + S.Brush.Margin = FMargin(4.f); + S.BorderThickness = Thickness; + S.CornerRadius = Radius; + return S; + }; + + // ---- Strokes ---- + Strokes.Add(TEXT("Box"), MakeStroke(FLinearColor(FColor(58, 75, 120, 140)), 1.f, 6.f)); + Strokes.Add(TEXT("Outline"), MakeStroke(FLinearColor(FColor(58, 75, 120)), 1.f, 6.f)); + Strokes.Add(TEXT("Underline"), MakeStroke(FLinearColor(FColor(58, 75, 120, 160)), 2.f, 0.f)); + Strokes.Add(TEXT("Search"), MakeStroke(FLinearColor(FColor(58, 75, 120, 140)), 1.f, 18.f)); + + // ---- Typography ---- + FUIStyleInputTypography DefaultType; + DefaultType.TextColor = FLinearColor(FColor(244, 240, 230)); + DefaultType.HintColor = FLinearColor(FColor(150, 160, 180, 200)); + DefaultType.Size = 16.f; + Typography.Add(TEXT("Box"), DefaultType); + Typography.Add(TEXT("Outline"), DefaultType); + Typography.Add(TEXT("Underline"), DefaultType); + Typography.Add(TEXT("Search"), DefaultType); + } + + /** Cores/textura de fundo por variante (chave FName). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + TMap Backgrounds; + + /** Borda (cor, espessura, arredondamento) por variante. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + TMap Strokes; + + /** Tipografia (cor de texto/hint, tamanho, fonte) por variante. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input") + TMap Typography; + + /** Padding interno do campo (px, uniforme) — comum a todas variantes. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0")) + float Padding = 10.f; + + /** Altura mínima do campo (px). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0")) + float MinHeight = 44.f; + + /** Largura mínima PADRÃO do campo (px). Designer mostra esse tamanho. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "UI Style|Input", meta = (ClampMin = "0")) + float MinWidth = 240.f; +}; 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/Data/World/MapDef.h b/Source/ZMMO/Data/World/MapDef.h new file mode 100644 index 0000000..9309b7c --- /dev/null +++ b/Source/ZMMO/Data/World/MapDef.h @@ -0,0 +1,92 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "Engine/World.h" +#include "ZoneRow.h" +#include "MapDef.generated.h" + +/** + * FZMMOMapSpawn + * + * Ponto de spawn dentro de um mapa, identificado por `Tag`. Usado pelo + * server pra decidir onde materializar um char quando ele entra: + * - Char sem posicao salva: usa spawn com tag="default" + * - Char saindo de raid: pode pedir tag="raid_return" + * - Char morto: pode pedir tag="graveyard" + * + * ZeusEditorTools pode auto-popular o `Spawns[]` do FZMMOMapDef varrendo + * todos APlayerStart (ou ator custom AZMMOMapSpawnPoint) presentes no + * `.umap` do `ClientLevel` — botao "Sync from level". + */ +USTRUCT(BlueprintType) +struct ZMMO_API FZMMOMapSpawn +{ + GENERATED_BODY() + + /** Identificador do spawn (ex.: "default", "raid_return"). "default" e obrigatorio. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn") + FName Tag = TEXT("default"); + + /** Posicao em cm (espaco do mundo). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn") + FVector PositionCm = FVector::ZeroVector; + + /** Yaw em graus. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Spawn") + float YawDeg = 0.f; +}; + +/** + * FZMMOMapDef + * + * Definicao canonica de um mapa do MMO. Linha da DT_Maps no cliente. + * Exportada para `Server/ZeusServerEngine/Config/DataTables/maps_config.json` + * pelo plugin ZeusEditorTools — o JSON e a fonte que o WorldServer consome. + * + * Identidade: + * - `MapId` (uint16) e a chave estavel cross-cliente/server. 0 = invalido + * (detecta bug de leitura de DB / migration esquecida). + * - RowName na DataTable e o nome interno (ex.: "TestWorld") — usado no + * exporter como `name` do JSON. Nao duplicamos em campo `Name`. + * + * Wire economy: + * - `S_CHAR_SELECT_OK` envia `mapId` como uint16 (2 bytes) em vez de + * `mapName` (string), evitando ~10-30 bytes por handoff. + */ +USTRUCT(BlueprintType) +struct ZMMO_API FZMMOMapDef : public FTableRowBase +{ + GENERATED_BODY() + + /** ID estavel uint16. 0 = invalido. int32 aqui pois UE nao expoe uint16 em UPROPERTY. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map", meta = (ClampMin = "1", ClampMax = "65535")) + int32 MapId = 0; + + /** Nome de exibicao localizavel. Aparece em UI/loading screen. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map") + FText DisplayName; + + /** Asset do level no cliente. Exporter converte pra string canonica no JSON do server. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Level") + TSoftObjectPtr ClientLevel; + + /** + * Pontos de spawn taggeados. Editavel inline na DataTable; + * ZeusEditorTools pode auto-popular varrendo PlayerStart do .umap. + * Pelo menos um com Tag="default" e esperado. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Spawns") + TArray Spawns; + + /** Regras PvP/Safe/Dungeon. Reusa enum de FZoneRow. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map") + EZoneRules Rules = EZoneRules::PvE; + + /** Faixa de level recomendada — display only. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display") + int32 RecommendedLevelMin = 1; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Map|Display") + int32 RecommendedLevelMax = 99; +}; diff --git a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp index 5a7be63..b6fb1cb 100644 --- a/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp +++ b/Source/ZMMO/Game/Controller/ZMMOPlayerController.cpp @@ -34,6 +34,20 @@ void AZMMOPlayerController::BeginPlay() { Super::BeginPlay(); + if (IsLocalPlayerController()) + { + // Limpa estado de input herdado da UI do FrontEnd (UIManagerSubsystem + // e LocalPlayerSubsystem — sobrevive ao travel, e o controller anterior + // (AZMMOFrontEndPlayerController) tinha setado FInputModeUIOnly + + // bShowMouseCursor=true). Sem este reset, WASD/Look podem nao chegar + // ao pawn porque o Slate ainda esta com foco no widget antigo. + FInputModeGameOnly InputMode; + InputMode.SetConsumeCaptureMouseDown(true); + SetInputMode(InputMode); + bShowMouseCursor = false; + FlushPressedKeys(); + } + if (ShouldUseTouchControls() && IsLocalPlayerController()) { MobileControlsWidget = CreateWidget(this, MobileControlsWidgetClass); diff --git a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp index a76eaba..fcc9ac7 100644 --- a/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp +++ b/Source/ZMMO/Game/Entity/ZMMOPlayerCharacter.cpp @@ -18,6 +18,7 @@ #include "ZMMO.h" #include "ZMMOWorldSubsystem.h" #include "ZeusNetworkSubsystem.h" +#include "UI/FrontEnd/UIFrontEndFlowSubsystem.h" DEFINE_LOG_CATEGORY(LogZMMOPlayer); @@ -108,6 +109,31 @@ void AZMMOPlayerCharacter::BeginPlay() ResolveZeusNetworkSubsystem(); BindZeusSpawnDelegate(); TryRegisterLocalEntityFromCachedSpawn(); + + // Fase 4: reposiciona o pawn na pos salva no DB (vinda no S_CHAR_SELECT_OK + // e memorizada no Flow). Substitui o PlayerStart default do level. Se nao + // ha pose pendente (primeiro boot / debug PIE direto no level), mantem o + // PlayerStart. + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + FVector PosCm = FVector::ZeroVector; + float YawDeg = 0.0f; + if (Flow->ConsumePendingSpawnPose(PosCm, YawDeg)) + { + const FRotator NewRot(0.0f, YawDeg, 0.0f); + SetActorLocationAndRotation(PosCm, NewRot, /*bSweep=*/false, nullptr, ETeleportType::TeleportPhysics); + if (AController* C = GetController()) + { + C->SetControlRotation(NewRot); + } + UE_LOG(LogZMMOPlayer, Log, + TEXT("AZMMOPlayerCharacter: pawn reposicionado pra pos do DB (%s) yaw=%.1f"), + *PosCm.ToString(), YawDeg); + } + } + } } void AZMMOPlayerCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason) diff --git a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp index f5ce148..cf72d08 100644 --- a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp +++ b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.cpp @@ -19,6 +19,15 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection) RemoteEntityClasses.Add(EZMMOEntityType::NPC, AZMMOEntity::StaticClass()); RemoteEntityClasses.Add(EZMMOEntityType::Object, AZMMOEntity::StaticClass()); } + // Bind + replay vivem em OnWorldBeginPlay (mundo pronto). Initialize roda + // MUITO cedo no pipeline de LoadMap — antes do GameMode ser instanciado + // e antes do WorldPartition terminar de carregar cells. Spawns dinamicos + // feitos aqui podem ser perdidos durante a fase de setup posterior. +} + +void UZMMOWorldSubsystem::OnWorldBeginPlay(UWorld& InWorld) +{ + Super::OnWorldBeginPlay(InWorld); if (UZeusNetworkSubsystem* ZeusNet = ResolveZeusNetworkSubsystem()) { @@ -26,10 +35,26 @@ void UZMMOWorldSubsystem::Initialize(FSubsystemCollectionBase& Collection) ZeusNet->OnPlayerDespawned.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerDespawned); ZeusNet->OnPlayerStateUpdate.AddDynamic(this, &UZMMOWorldSubsystem::HandlePlayerStateUpdate); UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem bound to ZeusNetworkSubsystem delegates.")); + + // Catch-up race fix (Fase 3): broadcasts de proxies remotos chegam + // enquanto o cliente novo ainda esta em OpenLevel. UZeusNetworkSubsystem + // cacheia em PendingRemoteSpawnsByEntity. Aplicamos aqui — mundo ja + // esta pronto (GameMode ativo, WorldPartition cells inicializadas). + int32 ReplayCount = 0; + ZeusNet->ForEachPendingRemoteSpawn( + [this, &ReplayCount](const int32 EntityId, const FVector PosCm, const float YawDeg, const int64 ServerTimeMs) + { + HandlePlayerSpawned(EntityId, /*bIsLocal=*/false, PosCm, YawDeg, ServerTimeMs); + ++ReplayCount; + }); + if (ReplayCount > 0) + { + UE_LOG(LogZMMO, Log, TEXT("ZMMOWorldSubsystem: replay de %d proxy(ies) cacheados."), ReplayCount); + } } else { - UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at Initialize.")); + UE_LOG(LogZMMO, Warning, TEXT("ZMMOWorldSubsystem: ZeusNetworkSubsystem not found at OnWorldBeginPlay.")); } } diff --git a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h index 3359784..44edfcf 100644 --- a/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h +++ b/Source/ZMMO/Game/Network/ZMMOWorldSubsystem.h @@ -44,6 +44,7 @@ class ZMMO_API UZMMOWorldSubsystem : public UWorldSubsystem public: virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void OnWorldBeginPlay(UWorld& InWorld) override; virtual void Deinitialize() override; /** diff --git a/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h b/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h new file mode 100644 index 0000000..3b32cf0 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/CharServerOpcodes.h @@ -0,0 +1,90 @@ +#pragma once + +#include "CoreMinimal.h" + +/** + * Opcodes do CharServer (WebSocket) consumidos pelo cliente. Espelha + * `Server/ZeusCharServer/src/protocol/CharOpcodes.ts` (faixa 2000-2099). + * + * Wire: string = uint16 LE (len) + UTF-8; inteiros LE. O cabeçalho de 32B + * é responsabilidade do UZeusCharServerSubsystem (Send/OnRawMessage operam + * sobre o payload já sem header). + */ +namespace ZMMOCharOp +{ + // Autenticação — payload começa com uint8 method (ver ZMMOCharAuthMethod) + constexpr int32 C_CHAR_AUTH_REQUEST = 2000; + constexpr int32 S_CHAR_AUTH_OK = 2001; // string(accountId)+string(user)+uint64(expiresMs) + constexpr int32 S_CHAR_AUTH_REJECT = 2002; // uint16(reason) + + // Listagem/criação/seleção de personagem + constexpr int32 C_CHAR_LIST_REQUEST = 2010; // uint8 hasWorldFilter [+ uint8[16] worldId] + constexpr int32 S_CHAR_LIST = 2011; + + constexpr int32 C_CHAR_CREATE = 2020; // uint8[16] worldId + slot + ... + constexpr int32 S_CHAR_CREATE_OK = 2021; + constexpr int32 S_CHAR_CREATE_REJECT= 2022; + + // Delete agendado (rathena-style two-step) + constexpr int32 C_CHAR_DELETE_REQUEST = 2030; + constexpr int32 S_CHAR_DELETE_ACK = 2031; + constexpr int32 C_CHAR_DELETE_ACCEPT = 2032; + constexpr int32 S_CHAR_DELETE_ACCEPT_ACK = 2033; + constexpr int32 C_CHAR_DELETE_CANCEL = 2034; + constexpr int32 S_CHAR_DELETE_CANCEL_ACK = 2035; + + constexpr int32 C_CHAR_SELECT = 2040; + constexpr int32 S_CHAR_SELECT_OK = 2041; + constexpr int32 S_CHAR_SELECT_REJECT= 2042; + + // Listagem de mundos (Fase 1 do ARQUITETURA_SERVER_SELECT) + constexpr int32 C_WORLD_LIST_REQUEST = 2060; // payload vazio + constexpr int32 S_WORLD_LIST = 2061; // uint16 count + entries + /** + * Push do CharServer com update de UM mundo (state/pop/queueLen). + * Wire: string worldId + uint16 pop + uint16 queueLen + uint8 state + */ + constexpr int32 S_WORLD_STATUS_UPDATE = 2062; +} + +/** + * Método de autenticação enviado como uint8 prefix no payload do + * C_CHAR_AUTH_REQUEST. + */ +namespace ZMMOCharAuthMethod +{ + constexpr uint8 TOKEN = 0; + constexpr uint8 PASSWORD = 1; +} + +/** Espelha `CharRejectReason` em CharOpcodes.ts. */ +namespace ZMMOCharRejectReason +{ + constexpr uint16 Unknown = 0; + constexpr uint16 InvalidToken = 1; + constexpr uint16 AccountBanned = 2; + constexpr uint16 AlreadyOnline = 3; + constexpr uint16 InvalidCredentials = 4; + constexpr uint16 AccountLocked = 5; + constexpr uint16 AccountSuspended = 6; + constexpr uint16 CredentialsAuthDisabled = 7; + constexpr uint16 WorldOffline = 8; + constexpr uint16 WorldMaintenance = 9; + constexpr uint16 WorldFull = 10; + constexpr uint16 WorldRegionMismatch = 11; + constexpr uint16 WorldNotFound = 12; + constexpr uint16 NameInUse = 20; + constexpr uint16 InvalidName = 21; + constexpr uint16 SlotOccupied = 22; + constexpr uint16 CharNotFound = 23; + constexpr uint16 DeleteScheduled = 24; + constexpr uint16 DeleteNotScheduled = 25; +} + +/** Estado do mundo no wire (uint8). Espelha `WireWorldState` em CharOpcodes.ts. */ +namespace ZMMOWireWorldState +{ + constexpr uint8 Offline = 0; + constexpr uint8 Online = 1; + constexpr uint8 Maintenance = 2; +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp new file mode 100644 index 0000000..a92af2e --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.cpp @@ -0,0 +1,178 @@ +#include "UIBootScreen_Base.h" + +#include "ZMMO.h" +#include "UIButton_Base.h" +#include "UISpinner_Base.h" +#include "UIFrontEndFlowSubsystem.h" +#include "ZMMOThemeSubsystem.h" +#include "ZeusCharServerSubsystem.h" +#include "CommonTextBlock.h" +#include "Components/Border.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "UI/UIStyleRow.h" + +namespace +{ + // Runtime usa o tema ativo; design-time carrega a row "Default" de + // DT_UI_Styles para o Designer ver o fundo (mesmo padrão de UUIPanel_Base). + FUIStyle ResolveBootStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + 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("UIBootDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } +} + +void UUIBootScreen_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + RefreshUIStyle(); // preview do fundo no Designer (DT_UI_Styles "Default") +} + +void UUIBootScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); // base: RefreshUIStyle + BP_OnScreenActivated + + if (Btn_Start) + { + Btn_Start->OnClicked().AddUObject(this, &UUIBootScreen_Base::HandleStartClicked); + } + + UZeusCharServerSubsystem* Char = GetCharServer(); + const bool bConnected = Char && Char->IsConnected(); + SetReady(bConnected); + + if (Char && !bCharBound) + { + Char->OnConnected.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnected); + Char->OnConnectionFailed.AddDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed); + bCharBound = true; + } + // A conexão em si é disparada pelo UUIFrontEndFlowSubsystem (estado Boot). +} + +void UUIBootScreen_Base::NativeOnDeactivated() +{ + if (Btn_Start) + { + Btn_Start->OnClicked().RemoveAll(this); + } + if (bCharBound) + { + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnConnected.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnected); + Char->OnConnectionFailed.RemoveDynamic(this, &UUIBootScreen_Base::HandleCharConnectionFailed); + } + bCharBound = false; + } + Super::NativeOnDeactivated(); +} + +void UUIBootScreen_Base::RefreshUIStyle_Implementation() +{ + Super::RefreshUIStyle_Implementation(); + + const FUIStyle Fallback; + const FUIStyle AS = ResolveBootStyle(this, Fallback); + + if (Border_Bg) + { + // Fundo data-driven do DT_UI_Styles (T_Boot_Background) — sem + // hard-ref de textura no WBP (§5). Fallback p/ cor se vazio. + if (AS.ScreenBackground.GetResourceObject() != nullptr) + { + Border_Bg->SetBrush(AS.ScreenBackground); + } + else + { + Border_Bg->SetBrushColor(AS.Palette.Bg0); + } + } + if (Text_Status) + { + Text_Status->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurfaceMuted)); + } +} + +void UUIBootScreen_Base::SetReady(bool bReady) +{ + if (Btn_Start) + { + // Enquanto conecta: OCULTO (Collapsed, sem ocupar layout). Conectado: + // visível e interagível. + Btn_Start->SetVisibility( + bReady ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + Btn_Start->SetIsInteractionEnabled(bReady); + } + if (Spinner_Connect) + { + Spinner_Connect->SetVisibility( + bReady ? ESlateVisibility::Collapsed : ESlateVisibility::HitTestInvisible); + } + if (Text_Status) + { + Text_Status->SetText(bReady ? ReadyText : ConnectingText); + } +} + +UZeusCharServerSubsystem* UUIBootScreen_Base::GetCharServer() const +{ + const UGameInstance* GI = GetGameInstance(); + return GI ? GI->GetSubsystem() : nullptr; +} + +void UUIBootScreen_Base::HandleStartClicked() +{ + UZeusCharServerSubsystem* Char = GetCharServer(); + if (!Char || !Char->IsConnected()) + { + return; // botão só age quando conectado (defensivo) + } + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->RequestEnterLogin(); + } + } +} + +void UUIBootScreen_Base::HandleCharConnected() +{ + UE_LOG(LogZMMO, Log, TEXT("BootScreen: CharServer conectado — botão liberado.")); + SetReady(true); +} + +void UUIBootScreen_Base::HandleCharConnectionFailed(FString Reason) +{ + UE_LOG(LogZMMO, Warning, TEXT("BootScreen: falha ao conectar ao CharServer: %s"), *Reason); + SetReady(false); + if (Text_Status) + { + Text_Status->SetText(FailedText); + } +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.h new file mode 100644 index 0000000..de1cbc1 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIBootScreen_Base.h @@ -0,0 +1,73 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UIActivatableScreen_Base.h" +#include "UIBootScreen_Base.generated.h" + +class UUIButton_Base; +class UUISpinner_Base; +class UCommonTextBlock; +class UBorder; +class UZeusCharServerSubsystem; + +/** + * Tela de Boot — primeira tela do front-end. Reconstruída a partir do mock + * do Zeus UMG Forge com os widgets compartilhados (UI_Button_Master, + * UI_Spinner_Master). Camada Abstract; o WBP concreto (WBP_Boot) herda + * DIRETO desta classe (ARQUITETURA.md §3.3). + * + * O UUIFrontEndFlowSubsystem dispara a conexão ao ZeusCharServer (WebSocket) + * ao entrar no estado Boot. Esta tela OBSERVA o UZeusCharServerSubsystem: + * enquanto não conecta, mostra o spinner e o botão desabilitado; ao conectar, + * libera o botão "Iniciar" (clique → Flow::RequestEnterLogin → Login). + * Sem hard-ref de asset de tema (§5): cores vêm do FUIStyle. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUIBootScreen_Base : public UUIActivatableScreen_Base +{ + GENERATED_BODY() + +protected: + virtual void NativePreConstruct() override; + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + virtual void RefreshUIStyle_Implementation() override; + + /** Texto exibido enquanto conecta / quando pronto / em erro. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot") + FText ConnectingText = FText::FromString(TEXT("Conectando ao Gateway")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot") + FText ReadyText = FText::FromString(TEXT("Pronto")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Boot") + FText FailedText = FText::FromString(TEXT("Falha ao conectar — tentando novamente")); + + // ---- Widgets do WBP (nomes esperados) ---- + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Btn_Start; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Spinner_Connect; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Status; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Border_Bg; + +private: + void SetReady(bool bReady); + UZeusCharServerSubsystem* GetCharServer() const; + + UFUNCTION() + void HandleStartClicked(); + + UFUNCTION() + void HandleCharConnected(); + + UFUNCTION() + void HandleCharConnectionFailed(FString Reason); + + bool bCharBound = false; +}; 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..e318910 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UICharacterCreatePage_Base.cpp @@ -0,0 +1,97 @@ +#include "UICharacterCreatePage_Base.h" + +#include "ZMMO.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "WireHelpers.h" +#include "ZeusCharServerSubsystem.h" +#include "Components/EditableTextBox.h" +#include "Components/ComboBoxString.h" +#include "CommonTextBlock.h" +#include "Engine/GameInstance.h" +#include "UI/Widgets/UIButton_Base.h" + +using ZMMOWire::WriteUuid16; +using ZMMOWire::WriteUInt8; +using ZMMOWire::WriteUInt16; +using ZMMOWire::WriteUInt32; +using ZMMOWire::WriteStringUtf8; + +void UUICharacterCreatePage_Base::NativeConstruct() +{ + Super::NativeConstruct(); + if (!bBound) + { + if (Btn_Confirm) Btn_Confirm->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleSubmitClicked); + if (Btn_Cancel) Btn_Cancel->OnClicked().AddUObject(this, &UUICharacterCreatePage_Base::HandleCancelClicked); + bBound = true; + } + if (Text_Error) Text_Error->SetText(FText::GetEmpty()); +} + +void UUICharacterCreatePage_Base::NativeDestruct() +{ + if (bBound) + { + if (Btn_Confirm) Btn_Confirm->OnClicked().RemoveAll(this); + if (Btn_Cancel) Btn_Cancel->OnClicked().RemoveAll(this); + bBound = false; + } + Super::NativeDestruct(); +} + +void UUICharacterCreatePage_Base::SubmitCreate() +{ + const FString Name = Input_Name ? Input_Name->GetText().ToString() : FString(); + if (Name.IsEmpty()) + { + if (Text_Error) Text_Error->SetText(FText::FromString(TEXT("Informe um nome."))); + return; + } + + UGameInstance* GI = GetGameInstance(); + if (!GI) return; + UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem(); + 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 0fb443a..cdfd506 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.cpp @@ -1,6 +1,7 @@ #include "UIFrontEndFlowSubsystem.h" #include "ZMMO.h" +#include "Data/World/MapDef.h" #include "UIFrontEndScreenSet.h" #include "UIManagerSubsystem.h" #include "UIPrimaryGameLayout_Base.h" @@ -8,7 +9,9 @@ #include "ZMMOGameInstance.h" #include "ZMMOThemeSubsystem.h" #include "ZeusNetworkSubsystem.h" +#include "ZeusCharServerSubsystem.h" #include "CommonActivatableWidget.h" +#include "Engine/DataTable.h" #include "Engine/GameInstance.h" #include "Engine/LocalPlayer.h" #include "UObject/UObjectGlobals.h" @@ -54,22 +57,15 @@ void UUIFrontEndFlowSubsystem::StartFrontEnd() SetState(EZMMOFrontEndState::Boot); - // Substitui o auto-connect do GameInstance: o fluxo dirige a conexão. - if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) + // Pré-login fala com o ZeusCharServer (WebSocket), NÃO com o world server + // UDP. A tela Boot observa o CharServer e libera o botão ao conectar. + if (UZeusCharServerSubsystem* Char = GetCharServer()) { - FString Host = TEXT("127.0.0.1"); - int32 Port = 27777; - if (const UZMMOGameInstance* GI = Cast(GetGameInstance())) - { - Host = GI->ZeusServerHost; - Port = GI->ZeusServerPort; - } - SetState(EZMMOFrontEndState::Connecting); - Zeus->ConnectToZeusServer(Host, Port); + Char->ConnectToDefaultCharServer(); // lê CharServerUrl de Project Settings } else { - UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusNetworkSubsystem indisponível; sem conexão.")); + UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: ZeusCharServerSubsystem indisponível; sem conexão de Boot.")); } } @@ -160,6 +156,10 @@ bool UUIFrontEndFlowSubsystem::RequestBack() { switch (CurrentState) { + case EZMMOFrontEndState::Login: + // Volta à Boot (CharServer segue conectado; só re-mostra a tela). + SetState(EZMMOFrontEndState::Boot); + return true; case EZMMOFrontEndState::ServerSelect: SetState(EZMMOFrontEndState::Login); return true; @@ -221,6 +221,12 @@ UZeusNetworkSubsystem* UUIFrontEndFlowSubsystem::GetZeusNetwork() const return GI ? GI->GetSubsystem() : nullptr; } +UZeusCharServerSubsystem* UUIFrontEndFlowSubsystem::GetCharServer() const +{ + const UGameInstance* GI = GetGameInstance(); + return GI ? GI->GetSubsystem() : nullptr; +} + UUIManagerSubsystem* UUIFrontEndFlowSubsystem::GetUIManager() const { if (const UGameInstance* GI = GetGameInstance()) @@ -257,14 +263,19 @@ void UUIFrontEndFlowSubsystem::BindNetwork() { return; } + // CharServer (WebSocket) dirige o pré-login: Boot→Login. + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected); + Char->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed); + Char->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected); + } + // UDP (world server) só interessa para o handoff de travel. if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { - Zeus->OnConnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected); - Zeus->OnConnectionFailed.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed); - Zeus->OnDisconnected.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected); Zeus->OnServerTravelRequested.AddDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); - bNetBound = true; } + bNetBound = true; } void UUIFrontEndFlowSubsystem::UnbindNetwork() @@ -273,11 +284,14 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork() { return; } + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected); + Char->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed); + Char->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleCharDisconnected); + } if (UZeusNetworkSubsystem* Zeus = GetZeusNetwork()) { - Zeus->OnConnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnected); - Zeus->OnConnectionFailed.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleConnectionFailed); - Zeus->OnDisconnected.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleDisconnected); Zeus->OnServerTravelRequested.RemoveDynamic(this, &UUIFrontEndFlowSubsystem::HandleServerTravelRequested); } bNetBound = false; @@ -285,25 +299,66 @@ void UUIFrontEndFlowSubsystem::UnbindNetwork() void UUIFrontEndFlowSubsystem::HandleConnected() { - UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: conectado → Login.")); - SetState(EZMMOFrontEndState::Login); + // NÃO auto-avança: a tela Boot libera o botão "Iniciar"; o usuário clica + // (→ RequestEnterLogin). Mantém o estado Boot. + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer conectado (estado %s)."), + *UEnum::GetValueAsString(CurrentState)); } void UUIFrontEndFlowSubsystem::HandleConnectionFailed(FString Reason) { - UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão falhou (%s) → volta a Login."), *Reason); - SetState(EZMMOFrontEndState::Login); + // Permanece em Boot; a tela mostra erro e o CharServer pode re-tentar. + UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: conexão ao CharServer falhou (%s)."), *Reason); } -void UUIFrontEndFlowSubsystem::HandleDisconnected() +void UUIFrontEndFlowSubsystem::HandleCharDisconnected(int32 StatusCode, FString Reason) { - UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: desconectado.")); - if (CurrentState != EZMMOFrontEndState::InWorld) + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: CharServer desconectou (code=%d, %s)."), + StatusCode, *Reason); + // Se caiu antes de entrar no mundo, volta a Boot para reconectar. + if (CurrentState != EZMMOFrontEndState::InWorld && + CurrentState != EZMMOFrontEndState::EnteringWorld && + CurrentState != EZMMOFrontEndState::Boot) + { + SetState(EZMMOFrontEndState::Boot); + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->ConnectToDefaultCharServer(); + } + } +} + +void UUIFrontEndFlowSubsystem::RequestEnterLogin() +{ + if (CurrentState == EZMMOFrontEndState::Boot) { SetState(EZMMOFrontEndState::Login); } } +void UUIFrontEndFlowSubsystem::RequestEnterServerSelect() +{ + if (CurrentState == EZMMOFrontEndState::Login) + { + SetState(EZMMOFrontEndState::ServerSelect); + } +} + +void UUIFrontEndFlowSubsystem::SetSelectedWorldId(const FString& WorldId) +{ + SelectedWorldId = WorldId; + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: mundo selecionado = %s"), *SelectedWorldId); +} + +void UUIFrontEndFlowSubsystem::ClearSelectedWorld() +{ + if (!SelectedWorldId.IsEmpty()) + { + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: limpando mundo selecionado (%s)"), *SelectedWorldId); + SelectedWorldId.Reset(); + } +} + void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapName, const FString& MapPath) { UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: travel pedido pelo servidor → %s (%s)."), *MapName, *MapPath); @@ -312,7 +367,15 @@ void UUIFrontEndFlowSubsystem::HandleServerTravelRequested(const FString& MapNam if (!MapPath.IsEmpty()) { - UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath)); + // Forca ZMMOGameMode em qualquer mapa que venha do WorldServer. + // World Settings do .umap podem ter override herdado de L_FrontEnd + // (GameModeOverride=ZMMOFrontEndGameMode) — sobrescrevemos via URL + // option pra nao depender de cada artista configurar. + // Evolucao natural: quando o WorldServer enviar GameMode no + // S_TRAVEL_TO_MAP (wire estendido), trocar este literal por + // `?game=` + valor recebido do server. + const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode"); + UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options); } } @@ -337,3 +400,70 @@ void UUIFrontEndFlowSubsystem::HandleServerHelloTheme(FName ThemeId) } } } + +const FZMMOMapDef* UUIFrontEndFlowSubsystem::FindMapDef(int32 MapId) const +{ + if (MapId <= 0) return nullptr; + if (MapsTableAsset.IsNull()) + { + UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: MapsTableAsset nao configurado — adicione DT_Maps em DefaultGame.ini")); + return nullptr; + } + const UDataTable* Table = MapsTableAsset.LoadSynchronous(); + if (!Table) return nullptr; + + for (const TPair& Row : Table->GetRowMap()) + { + const FZMMOMapDef* Def = reinterpret_cast(Row.Value); + if (Def && Def->MapId == MapId) + { + return Def; + } + } + return nullptr; +} + +FString UUIFrontEndFlowSubsystem::ResolveLevelPathByMapId(int32 MapId) const +{ + const FZMMOMapDef* Def = FindMapDef(MapId); + if (!Def) return FString(); + const FSoftObjectPath ObjPath = Def->ClientLevel.ToSoftObjectPath(); + if (!ObjPath.IsValid()) return FString(); + return ObjPath.GetLongPackageName(); +} + +void UUIFrontEndFlowSubsystem::SetPendingSpawnPose(FVector PosCm, float YawDeg) +{ + PendingSpawnPosCm = PosCm; + PendingSpawnYawDeg = YawDeg; + bHasPendingSpawnPose = true; + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose set pos=(%.1f,%.1f,%.1f) yaw=%.1f"), + PosCm.X, PosCm.Y, PosCm.Z, YawDeg); +} + +bool UUIFrontEndFlowSubsystem::ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg) +{ + if (!bHasPendingSpawnPose) return false; + OutPosCm = PendingSpawnPosCm; + OutYawDeg = PendingSpawnYawDeg; + bHasPendingSpawnPose = false; + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: PendingSpawnPose consumido pos=(%.1f,%.1f,%.1f) yaw=%.1f"), + OutPosCm.X, OutPosCm.Y, OutPosCm.Z, OutYawDeg); + return true; +} + +bool UUIFrontEndFlowSubsystem::TravelToMapById(int32 MapId) +{ + const FString MapPath = ResolveLevelPathByMapId(MapId); + if (MapPath.IsEmpty()) + { + UE_LOG(LogZMMO, Warning, TEXT("FrontEndFlow: TravelToMapById(%d) falhou — sem entrada no DT_Maps"), MapId); + return false; + } + UE_LOG(LogZMMO, Log, TEXT("FrontEndFlow: TravelToMapById(%d) -> %s"), MapId, *MapPath); + bTravelingToWorld = true; + SetState(EZMMOFrontEndState::EnteringWorld); + const FString Options = TEXT("game=/Script/ZMMO.ZMMOGameMode"); + UGameplayStatics::OpenLevel(GetGameInstance(), FName(*MapPath), /*bAbsolute=*/true, Options); + return true; +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h index 33d00ff..1f3b156 100644 --- a/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h +++ b/Source/ZMMO/Game/UI/FrontEnd/UIFrontEndFlowSubsystem.h @@ -1,6 +1,7 @@ #pragma once #include "CoreMinimal.h" +#include "Engine/DataTable.h" #include "Subsystems/GameInstanceSubsystem.h" #include "UI/FrontEndTypes.h" #include "UIFrontEndFlowSubsystem.generated.h" @@ -8,6 +9,8 @@ class UUIFrontEndScreenSet; class UUIManagerSubsystem; class UZeusNetworkSubsystem; +class UZeusCharServerSubsystem; +struct FZMMOMapDef; DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnZMMOFrontEndStateChanged, EZMMOFrontEndState, NewState); @@ -57,6 +60,64 @@ public: UFUNCTION(BlueprintCallable, Category = "FrontEnd") bool RequestBack(); + /** Chamado pela tela Boot quando o usuário clica "Iniciar" (já conectado). */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd") + void RequestEnterLogin(); + + /** Chamado pela tela Login após autenticação OK no CharServer. */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd") + void RequestEnterServerSelect(); + + /** + * Memoriza o mundo escolhido na ServerSelect (UUID v4 string). Lido pelo + * CharSelect/CharCreate na hora de filtrar/criar personagens. Limpo no + * logout/back para ServerSelect. + */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") + void SetSelectedWorldId(const FString& WorldId); + + UFUNCTION(BlueprintPure, Category = "FrontEnd|Worlds") + FString GetSelectedWorldId() const { return SelectedWorldId; } + + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Worlds") + void ClearSelectedWorld(); + + /** + * Resolve `MapId` (uint16 recebido do CharServer) → row do DT_Maps. + * Carrega o asset sync (PIE/dev OK; em prod considerar StreamableManager). + * Retorna nullptr se MapId=0, DT_Maps nao configurado, ou mapId nao + * encontrado. + */ + const FZMMOMapDef* FindMapDef(int32 MapId) const; + + /** + * Helper: resolve `MapId` no DT_Maps e retorna `ClientLevel.ToSoftObjectPath().GetLongPackageName()` + * (ex.: `/Game/ThirdPerson/TestWorld`). String vazia se nao encontrado. + */ + UFUNCTION(BlueprintPure, Category = "FrontEnd|Maps") + FString ResolveLevelPathByMapId(int32 MapId) const; + + /** + * Inicia transicao pra mundo dado um `MapId`: faz `OpenLevel` com + * `?game=/Script/ZMMO.ZMMOGameMode`. No-op se `MapId` invalido ou + * DT_Maps nao tem entrada. + */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Maps") + bool TravelToMapById(int32 MapId); + + /** + * Memoriza a pose autoritativa do char vinda no `S_CHAR_SELECT_OK` + * (pos salva no DB + yaw). Sobrevive ao travel porque vivemos no + * GameInstance. Lida pelo `AZMMOPlayerCharacter::BeginPlay` pra + * reposicionar o pawn local em vez de spawnar no PlayerStart default + * do level. Limpa apos consumo (a proxima sessao reenvia). + */ + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn") + void SetPendingSpawnPose(FVector PosCm, float YawDeg); + + UFUNCTION(BlueprintCallable, Category = "FrontEnd|Spawn") + bool ConsumePendingSpawnPose(FVector& OutPosCm, float& OutYawDeg); + UPROPERTY(BlueprintAssignable, Category = "FrontEnd") FOnZMMOFrontEndStateChanged OnStateChanged; @@ -65,10 +126,20 @@ protected: UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd") TSoftObjectPtr ScreenSetAsset; + /** + * DataTable com FZMMOMapDef. Source of truth client-side para + * `MapId -> ClientLevel/spawns/displayName`. Configure em DefaultGame.ini: + * [/Script/ZMMO.UIFrontEndFlowSubsystem] + * MapsTableAsset=/Game/ZMMO/Data/World/DT_Maps.DT_Maps + */ + UPROPERTY(Config, EditDefaultsOnly, Category = "FrontEnd|Maps") + TSoftObjectPtr MapsTableAsset; + private: void BindNetwork(); void UnbindNetwork(); UZeusNetworkSubsystem* GetZeusNetwork() const; + UZeusCharServerSubsystem* GetCharServer() const; UUIManagerSubsystem* GetUIManager() const; UUIFrontEndScreenSet* GetScreenSet(); @@ -82,7 +153,7 @@ private: void HandleConnectionFailed(FString Reason); UFUNCTION() - void HandleDisconnected(); + void HandleCharDisconnected(int32 StatusCode, FString Reason); UFUNCTION() void HandleServerTravelRequested(const FString& MapName, const FString& MapPath); @@ -100,6 +171,19 @@ private: UPROPERTY(Transient) EZMMOFrontEndState CurrentState = EZMMOFrontEndState::None; + UPROPERTY(Transient) + FString SelectedWorldId; + + /** Pose autoritativa pendente (do S_CHAR_SELECT_OK). */ + UPROPERTY(Transient) + FVector PendingSpawnPosCm = FVector::ZeroVector; + + UPROPERTY(Transient) + float PendingSpawnYawDeg = 0.f; + + UPROPERTY(Transient) + bool bHasPendingSpawnPose = false; + UPROPERTY(Transient) TObjectPtr ScreenSet; diff --git a/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.cpp b/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.cpp new file mode 100644 index 0000000..83150ed --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.cpp @@ -0,0 +1,340 @@ +#include "UILoginScreen_Base.h" + +#include "ZMMO.h" +#include "CharServerOpcodes.h" +#include "UIButton_Base.h" +#include "UICheckBox_Base.h" +#include "UILabel_Base.h" +#include "ZMMOLoginSaveGame.h" +#include "Kismet/GameplayStatics.h" +#include "UIFrontEndFlowSubsystem.h" +#include "ZMMOThemeSubsystem.h" +#include "ZeusCharServerSubsystem.h" +#include "ZeusPacketWriter.h" +#include "ZeusPacketReader.h" +#include "CommonTextBlock.h" +#include "Components/Border.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "UI/UIStyleRow.h" + +namespace +{ + // Mesmo helper da Boot: runtime usa o tema ativo; design-time carrega a + // row "Default" de DT_UI_Styles para o Designer ver o fundo. + FUIStyle ResolveLoginStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + 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("UILoginDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } + +} + +void UUILoginScreen_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + RefreshUIStyle(); // preview do fundo no Designer (DT_UI_Styles "Default") +} + +void UUILoginScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); // base: RefreshUIStyle + BP_OnScreenActivated + + if (Btn_Login) + { + Btn_Login->OnClicked().AddUObject(this, &UUILoginScreen_Base::HandleLoginClicked); + } + if (Btn_Back) + { + Btn_Back->OnClicked().AddUObject(this, &UUILoginScreen_Base::HandleBackClicked); + } + + UZeusCharServerSubsystem* Char = GetCharServer(); + if (Char && !bRawBound) + { + Char->OnRawMessage.AddDynamic(this, &UUILoginScreen_Base::HandleCharRawMessage); + bRawBound = true; + } + + bAwaitingAuth = false; + const bool bConnected = Char && Char->IsConnected(); + SetBusy(false); + if (Btn_Login) + { + Btn_Login->SetIsInteractionEnabled(bConnected); + } + SetStatus(bConnected ? IdleText : NotConnectedText); + + // "Lembrar Acesso": tenta restaurar usuário salvo (async, não trava UI). + LoadRememberedAccess(); +} + +void UUILoginScreen_Base::NativeOnDeactivated() +{ + if (Btn_Login) + { + Btn_Login->OnClicked().RemoveAll(this); + } + if (Btn_Back) + { + Btn_Back->OnClicked().RemoveAll(this); + } + if (bRawBound) + { + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnRawMessage.RemoveDynamic(this, &UUILoginScreen_Base::HandleCharRawMessage); + } + bRawBound = false; + } + Super::NativeOnDeactivated(); +} + +void UUILoginScreen_Base::RefreshUIStyle_Implementation() +{ + Super::RefreshUIStyle_Implementation(); + + const FUIStyle Fallback; + const FUIStyle AS = ResolveLoginStyle(this, Fallback); + + if (Border_Bg) + { + // Fundo data-driven (DT_UI_Styles) — sem hard-ref de textura no WBP + // (§5). Fallback p/ cor se vazio. + if (AS.ScreenBackground.GetResourceObject() != nullptr) + { + Border_Bg->SetBrush(AS.ScreenBackground); + } + else + { + Border_Bg->SetBrushColor(AS.Palette.Bg0); + } + } + if (Text_Status) + { + Text_Status->SetColorAndOpacity(FSlateColor(AS.Semantic.OnSurfaceMuted)); + } +} + +UZeusCharServerSubsystem* UUILoginScreen_Base::GetCharServer() const +{ + const UGameInstance* GI = GetGameInstance(); + return GI ? GI->GetSubsystem() : nullptr; +} + +void UUILoginScreen_Base::SetBusy(bool bBusy) +{ + if (Btn_Login) + { + Btn_Login->SetIsInteractionEnabled(!bBusy); + } + if (Input_User) + { + Input_User->SetIsEnabled(!bBusy); + } + if (Input_Password) + { + Input_Password->SetIsEnabled(!bBusy); + } +} + +void UUILoginScreen_Base::SetStatus(const FText& InText) +{ + if (Text_Status) + { + Text_Status->SetText(InText); + } +} + +void UUILoginScreen_Base::HandleLoginClicked() +{ + if (bAwaitingAuth) + { + return; + } + + UZeusCharServerSubsystem* Char = GetCharServer(); + if (!Char || !Char->IsConnected()) + { + SetStatus(NotConnectedText); + return; + } + + const FString User = Input_User ? Input_User->GetText().ToString() : FString(); + const FString Pass = Input_Password ? Input_Password->GetText().ToString() : FString(); + if (User.TrimStartAndEnd().IsEmpty() || Pass.IsEmpty()) + { + SetStatus(EmptyFieldsText); + return; + } + + // "Lembrar Acesso": salva/apaga ANTES do envio do auth — atende a + // expectativa "marquei → vai lembrar próxima vez" independente do + // resultado da autenticação (senha errada ainda lembra do usuário, + // padrão dos launchers comerciais). + SaveOrClearRememberedAccess(User); + + FZeusPacketWriter W; + W.WriteUInt8(ZMMOCharAuthMethod::PASSWORD); + W.WriteStringUtf8(User.TrimStartAndEnd()); + W.WriteStringUtf8(Pass); + + const bool bSent = Char->SendCharRequest( + ZMMOCharOp::C_CHAR_AUTH_REQUEST, W.GetBuffer()); + if (!bSent) + { + UE_LOG(LogZMMO, Warning, TEXT("Login: falha ao enviar C_CHAR_AUTH_REQUEST.")); + SetStatus(NotConnectedText); + return; + } + + bAwaitingAuth = true; + SetBusy(true); + SetStatus(AuthenticatingText); + UE_LOG(LogZMMO, Log, TEXT("Login: C_CHAR_AUTH_REQUEST enviado (PASSWORD).")); +} + +void UUILoginScreen_Base::HandleBackClicked() +{ + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->RequestBack(); + } + } +} + +void UUILoginScreen_Base::HandleCharRawMessage(int32 Opcode, const TArray& Payload) +{ + if (!bAwaitingAuth) + { + return; // só nos interessa a resposta do auth que disparamos + } + + if (Opcode == ZMMOCharOp::S_CHAR_AUTH_OK) + { + bAwaitingAuth = false; + FZeusPacketReader R(Payload.GetData(), Payload.Num()); + FString AccountId, Username; + R.ReadStringUtf8(AccountId); + R.ReadStringUtf8(Username); + uint64 ExpiresMs = 0; + R.ReadUInt64(ExpiresMs); + UE_LOG(LogZMMO, Log, TEXT("Login: S_CHAR_AUTH_OK (account=%s user=%s) → ServerSelect."), + *AccountId, *Username); + + if (const UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + Flow->RequestEnterServerSelect(); + } + } + } + else if (Opcode == ZMMOCharOp::S_CHAR_AUTH_REJECT) + { + bAwaitingAuth = false; + FZeusPacketReader R(Payload.GetData(), Payload.Num()); + uint16 Reason = 0; + R.ReadUInt16(Reason); + UE_LOG(LogZMMO, Warning, TEXT("Login: S_CHAR_AUTH_REJECT (reason=%u)."), Reason); + SetBusy(false); + + FText Message; + switch (Reason) + { + case ZMMOCharRejectReason::InvalidCredentials: Message = InvalidCredentialsText; break; + case ZMMOCharRejectReason::AccountLocked: Message = AccountLockedText; break; + case ZMMOCharRejectReason::AccountSuspended: Message = AccountSuspendedText; break; + case ZMMOCharRejectReason::AccountBanned: Message = AccountSuspendedText; break; + case ZMMOCharRejectReason::CredentialsAuthDisabled: Message = CredentialsDisabledText; break; + default: + Message = FText::Format( + NSLOCTEXT("ZMMO", "LoginRejectFmt", "{0} (cód. {1})"), + RejectedText, FText::AsNumber(Reason)); + break; + } + SetStatus(Message); + } +} + +void UUILoginScreen_Base::LoadRememberedAccess() +{ + if (!UGameplayStatics::DoesSaveGameExist( + UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex)) + { + return; + } + + FAsyncLoadGameFromSlotDelegate Delegate; + Delegate.BindUFunction(this, + FName(TEXT("HandleLoadLoginCacheCompleted"))); + UGameplayStatics::AsyncLoadGameFromSlot( + UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex, Delegate); +} + +void UUILoginScreen_Base::HandleLoadLoginCacheCompleted(const FString& /*InSlotName*/, + int32 /*InUserIndex*/, USaveGame* LoadedGame) +{ + const UZMMOLoginSaveGame* Save = Cast(LoadedGame); + if (!Save || !Save->bRememberAccess || Save->SavedUsername.IsEmpty()) + { + return; + } + if (Input_User) + { + Input_User->SetText(FText::FromString(Save->SavedUsername)); + } + if (RememberAccess) + { + RememberAccess->SetIsChecked(true); + } +} + +void UUILoginScreen_Base::SaveOrClearRememberedAccess(const FString& User) +{ + const bool bRemember = RememberAccess ? RememberAccess->IsChecked() : false; + if (bRemember) + { + UZMMOLoginSaveGame* Save = Cast( + UGameplayStatics::CreateSaveGameObject(UZMMOLoginSaveGame::StaticClass())); + if (Save) + { + Save->SavedUsername = User.TrimStartAndEnd(); + Save->bRememberAccess = true; + UGameplayStatics::AsyncSaveGameToSlot( + Save, UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex); + } + } + else if (UGameplayStatics::DoesSaveGameExist( + UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex)) + { + // Checkbox desmarcada: apaga save antigo (não vamos lembrar mais). + UGameplayStatics::DeleteGameInSlot( + UZMMOLoginSaveGame::SlotName, UZMMOLoginSaveGame::UserIndex); + } +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.h b/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.h new file mode 100644 index 0000000..4424096 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UILoginScreen_Base.h @@ -0,0 +1,119 @@ +#pragma once + +#include "CoreMinimal.h" +#include "UIActivatableScreen_Base.h" +#include "UILoginScreen_Base.generated.h" + +class UUIButton_Base; +class UUICheckBox_Base; +class UUILabel_Base; +class UCommonTextBlock; +class UBorder; +class USaveGame; +class UZeusCharServerSubsystem; + +/** + * Tela de Login — reconstruída a partir do mock do Zeus UMG Forge + * (WBP_LoginScreen) com os widgets compartilhados (UI_Button_Master, + * UI_CheckBox_Master). Camada Abstract; o WBP concreto (WBP_Login) herda + * DIRETO desta classe (ARQUITETURA.md §3.3). + * + * Auth: ao clicar "Entrar", envia C_CHAR_AUTH_REQUEST (opcode 2000) pelo + * WebSocket do UZeusCharServerSubsystem como `uint8(PASSWORD) + + * string(user) + string(password)`. CharServer chama AccountService.login + * (Argon2id + lockout). S_CHAR_AUTH_OK → Flow::RequestEnterServerSelect; + * reject (InvalidCredentials/AccountLocked/Suspended/etc.) → status + * mapeado. Sem hard-ref de asset de tema (§5). Contas são criadas no + * servidor via `npm run account`. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUILoginScreen_Base : public UUIActivatableScreen_Base +{ + GENERATED_BODY() + +protected: + virtual void NativePreConstruct() override; + virtual void NativeOnActivated() override; + virtual void NativeOnDeactivated() override; + virtual void RefreshUIStyle_Implementation() override; + + /** Textos de status/feedback da autenticação. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText IdleText = FText::FromString(TEXT("Acesso protegido pelo Farol da Manhã.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText AuthenticatingText = FText::FromString(TEXT("Autenticando…")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText NotConnectedText = FText::FromString(TEXT("Sem conexão com o Gateway.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText EmptyFieldsText = FText::FromString(TEXT("Informe usuário e senha.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText RejectedText = FText::FromString(TEXT("Acesso negado pelo Gateway.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText InvalidCredentialsText = FText::FromString(TEXT("Usuário ou senha inválidos.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText AccountLockedText = FText::FromString(TEXT("Conta bloqueada por excesso de tentativas. Aguarde e tente novamente.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText AccountSuspendedText = FText::FromString(TEXT("Conta suspensa. Contate o suporte.")); + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Login") + FText CredentialsDisabledText = FText::FromString(TEXT("Autenticação por credenciais indisponível no servidor.")); + + // ---- Widgets do WBP (nomes esperados) ---- + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Border_Bg; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Input_User; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Input_Password; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr RememberAccess; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Btn_Login; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Btn_Back; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Status; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Text_Version; + +private: + UZeusCharServerSubsystem* GetCharServer() const; + void SetBusy(bool bBusy); + void SetStatus(const FText& InText); + + void HandleLoginClicked(); + void HandleBackClicked(); + + UFUNCTION() + void HandleCharRawMessage(int32 Opcode, const TArray& Payload); + + /** "Lembrar Acesso" — async load do cache local (LoginCache slot) e + popula Input_User + Check_Remember se houver entrada salva. */ + void LoadRememberedAccess(); + + UFUNCTION() + void HandleLoadLoginCacheCompleted(const FString& InSlotName, int32 InUserIndex, USaveGame* LoadedGame); + + /** Salva (se checkbox marcada) ou apaga (se desmarcada) o cache de Login. + Chamado em HandleLoginClicked — antes do envio do auth, atende a + expectativa "marquei → vai lembrar próxima vez" independente do + resultado da autenticação. */ + void SaveOrClearRememberedAccess(const FString& User); + + bool bRawBound = false; + bool bAwaitingAuth = false; +}; 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..671717b --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIServerSelectScreen_Base.cpp @@ -0,0 +1,331 @@ +#include "UIServerSelectScreen_Base.h" + +#include "ZMMO.h" +#include "ZMMOThemeSubsystem.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "UIServerCard_Base.h" +#include "WireHelpers.h" +#include "ZeusCharServerSubsystem.h" +#include "CommonTextBlock.h" +#include "Components/Border.h" +#include "Components/GridPanel.h" +#include "Components/GridSlot.h" +#include "Components/PanelWidget.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "UI/UIStyleRow.h" + +using ZMMOWire::ReadU8; +using ZMMOWire::ReadU16; +using ZMMOWire::ReadStringUtf8; + +namespace +{ + FUIStyle ResolveServerSelectStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + 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; + } +} + +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..47f1127 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/UIUserLobbyScreen_Base.cpp @@ -0,0 +1,419 @@ +#include "UIUserLobbyScreen_Base.h" + +#include "ZMMO.h" +#include "CharServerOpcodes.h" +#include "UIFrontEndFlowSubsystem.h" +#include "UICharCard_Base.h" +#include "UICharacterCreatePage_Base.h" +#include "WireHelpers.h" +#include "ZeusCharServerSubsystem.h" +#include "ZeusNetworkSubsystem.h" +#include "CommonTextBlock.h" +#include "Components/PanelWidget.h" +#include "Components/WidgetSwitcher.h" +#include "Engine/GameInstance.h" +#include "UI/Widgets/UIButton_Base.h" + +using ZMMOWire::ReadU8; +using ZMMOWire::ReadU16; +using ZMMOWire::ReadU32; +using ZMMOWire::ReadU64; +using ZMMOWire::ReadFloat; +using ZMMOWire::ReadStringUtf8; +using ZMMOWire::ReadUuid16; +using ZMMOWire::WriteUuid16; + +void UUIUserLobbyScreen_Base::NativeOnActivated() +{ + Super::NativeOnActivated(); + UE_LOG(LogZMMO, Log, TEXT("Lobby: ativada.")); + + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + if (!bSubscribed) + { + Char->OnRawMessage.AddDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage); + bSubscribed = true; + } + } + + if (!bButtonsBound) + { + if (Btn_Back) Btn_Back->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::BackToServerSelect); + if (Btn_Create) Btn_Create->OnClicked().AddUObject(this, &UUIUserLobbyScreen_Base::ShowCharCreatePage); + bButtonsBound = true; + } + + ShowCharSelectPage(); + RequestCharList(); +} + +void UUIUserLobbyScreen_Base::NativeOnDeactivated() +{ + if (bSubscribed) + { + if (UZeusCharServerSubsystem* Char = GetCharServer()) + { + Char->OnRawMessage.RemoveDynamic(this, &UUIUserLobbyScreen_Base::HandleCharRawMessage); + } + bSubscribed = false; + } + Super::NativeOnDeactivated(); +} + +void UUIUserLobbyScreen_Base::RequestCharList() +{ + UZeusCharServerSubsystem* Char = GetCharServer(); + if (!Char || !Char->IsConnected()) return; + + FString WorldId; + if (UGameInstance* GI = GetGameInstance()) + { + if (UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem()) + { + 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; + uint16 mapId16 = 0; + float px = 0, py = 0, pz = 0, yaw = 0; + uint32 hair = 0, hairColor = 0, skinColor = 0; + uint8 bodyType = 0; + uint64 deleteAt = 0; + + if (!ReadU64(Payload, Pos, charId64) + || !ReadUuid16(Payload, Pos, E.WorldId) + || !ReadU8(Payload, Pos, slot) + || !ReadStringUtf8(Payload, Pos, E.Name) + || !ReadU16(Payload, Pos, classId) + || !ReadU32(Payload, Pos, baseLvl) + || !ReadU32(Payload, Pos, baseExp) + || !ReadU32(Payload, Pos, jobLvl) + || !ReadU32(Payload, Pos, jobExp) + || !ReadU16(Payload, Pos, s) || !ReadU16(Payload, Pos, a) || !ReadU16(Payload, Pos, v) + || !ReadU16(Payload, Pos, in_) || !ReadU16(Payload, Pos, d) || !ReadU16(Payload, Pos, l) + || !ReadU32(Payload, Pos, hp) || !ReadU32(Payload, Pos, maxHp) + || !ReadU32(Payload, Pos, sp) || !ReadU32(Payload, Pos, maxSp) + || !ReadU32(Payload, Pos, money) + || !ReadU16(Payload, Pos, mapId16) + || !ReadFloat(Payload, Pos, px) || !ReadFloat(Payload, Pos, py) || !ReadFloat(Payload, Pos, pz) + || !ReadFloat(Payload, Pos, yaw) + || !ReadU32(Payload, Pos, hair) || !ReadU32(Payload, Pos, hairColor) || !ReadU32(Payload, Pos, skinColor) + || !ReadU8(Payload, Pos, bodyType) + || !ReadU64(Payload, Pos, deleteAt)) + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_LIST malformado (entry %d)"), i); + return; + } + E.CharId = FString::Printf(TEXT("%llu"), static_cast(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.MapId = static_cast(mapId16); + E.Position = FVector(px, py, pz); E.YawDeg = yaw; + E.Appearance.Hair = hair; E.Appearance.HairColor = hairColor; + E.Appearance.SkinColor = skinColor; E.Appearance.BodyType = bodyType; + E.DeleteScheduledAtMs = static_cast(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 + uint16 mapId + float x/y/z + float yaw + + // string worldHost + uint16 worldPort + string handoffToken + string region + int32 Pos = 0; + uint64 CharId64 = 0; FString WorldHost, HandoffToken, Region; + uint16 MapId16 = 0; + float Px=0, Py=0, Pz=0, Yaw=0; uint16 WorldPort = 0; + if (!ReadU64(Payload, Pos, CharId64) + || !ReadU16(Payload, Pos, MapId16) + || !ReadFloat(Payload, Pos, Px) || !ReadFloat(Payload, Pos, Py) || !ReadFloat(Payload, Pos, Pz) + || !ReadFloat(Payload, Pos, Yaw) + || !ReadStringUtf8(Payload, Pos, WorldHost) + || !ReadU16(Payload, Pos, WorldPort) + || !ReadStringUtf8(Payload, Pos, HandoffToken) + || !ReadStringUtf8(Payload, Pos, Region)) + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: S_CHAR_SELECT_OK malformado")); + return; + } + UE_LOG(LogZMMO, Log, TEXT("Lobby: handoff -> world=%s:%d mapId=%u token=%s..."), + *WorldHost, WorldPort, static_cast(MapId16), *HandoffToken.Left(8)); + + UGameInstance* GI = GetGameInstance(); + if (!GI) return; + UUIFrontEndFlowSubsystem* Flow = GI->GetSubsystem(); + if (Flow) + { + Flow->SetState(EZMMOFrontEndState::EnteringWorld); + // Memoriza pose pra `AZMMOPlayerCharacter::BeginPlay` reposicionar + // o pawn local na pos salva no DB (em vez do PlayerStart default). + Flow->SetPendingSpawnPose(FVector(Px, Py, Pz), Yaw); + } + + // Fase 3: handoff UDP — apresenta o ticket no `C_CONNECT_REQUEST`. O + // WorldServer valida via GETDEL no Valkey regional. WorldServer envia + // `S_CONNECT_OK` (sucesso) ou `S_CONNECT_REJECT` (ticket invalido/expirado). + // Disparamos PRIMEIRO o connect (nao-bloqueante), depois o OpenLevel — + // ZeusNetworkSubsystem e per-GameInstance e persiste cross-travel. + if (UZeusNetworkSubsystem* ZeusNet = GI->GetSubsystem()) + { + ZeusNet->ConnectToZeusServerWithTicket(WorldHost, static_cast(WorldPort), HandoffToken); + } + else + { + UE_LOG(LogZMMO, Error, TEXT("Lobby: ZeusNetworkSubsystem ausente — handoff abortado")); + } + + // Fase 4: lookup mapId no DT_Maps e dispara OpenLevel pro level certo. + // Se DT_Maps nao tem entrada (ou ainda nao foi criada no editor), + // caimos no fallback do S_TRAVEL_TO_MAP do server (mapName/mapPath string). + if (Flow && MapId16 != 0) + { + if (!Flow->TravelToMapById(static_cast(MapId16))) + { + UE_LOG(LogZMMO, Warning, TEXT("Lobby: mapId=%u nao resolvido no DT_Maps — esperando S_TRAVEL_TO_MAP do server."), + static_cast(MapId16)); + } + } +} + +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/WireHelpers.h b/Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h new file mode 100644 index 0000000..c383bcc --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/WireHelpers.h @@ -0,0 +1,136 @@ +#pragma once + +// Helpers de leitura/escrita binarios LE (UTF-8) compartilhados pelas telas +// do FrontEnd (Boot/Login/ServerSelect/Lobby/CharCreate). Funcoes `inline` +// pra evitar ODR violation quando dois .cpp caem no mesmo unity build do +// UBT — definicoes em anonymous namespace cada um colidiam apos a +// reorganizacao automatica de unities. + +#include "CoreMinimal.h" + +namespace ZMMOWire +{ + inline bool ReadU8(const TArray& Buf, int32& Pos, uint8& Out) + { + if (Pos + 1 > Buf.Num()) return false; + Out = Buf[Pos]; + Pos += 1; + return true; + } + + inline 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; + } + + inline 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; + } + + inline bool ReadU64(const TArray& Buf, int32& Pos, uint64& Out) + { + if (Pos + 8 > Buf.Num()) return false; + Out = 0; + for (int32 i = 0; i < 8; ++i) + { + Out |= (static_cast(Buf[Pos + i]) << (8 * i)); + } + Pos += 8; + return true; + } + + inline bool ReadFloat(const TArray& Buf, int32& Pos, float& Out) + { + uint32 Raw = 0; + if (!ReadU32(Buf, Pos, Raw)) return false; + FMemory::Memcpy(&Out, &Raw, sizeof(float)); + return true; + } + + inline bool ReadStringUtf8(const TArray& 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; + } + + /** Le 16 bytes raw e devolve UUID canonico "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx". */ + inline bool ReadUuid16(const TArray& Buf, int32& Pos, FString& Out) + { + if (Pos + 16 > Buf.Num()) return false; + auto HexNibble = [](uint8 B, ANSICHAR* Dst) + { + static const ANSICHAR* H = "0123456789abcdef"; + Dst[0] = H[(B >> 4) & 0xF]; + Dst[1] = H[B & 0xF]; + }; + ANSICHAR Raw[37]; Raw[36] = 0; + int32 J = 0; + for (int32 I = 0; I < 16; ++I) + { + if (I == 4 || I == 6 || I == 8 || I == 10) Raw[J++] = '-'; + HexNibble(Buf[Pos + I], &Raw[J]); + J += 2; + } + Out = ANSI_TO_TCHAR(Raw); + Pos += 16; + return true; + } + + inline void WriteUInt8(TArray& Buf, uint8 V) { Buf.Add(V); } + + inline void WriteUInt16(TArray& Buf, uint16 V) + { + Buf.Add(static_cast(V & 0xFF)); + Buf.Add(static_cast((V >> 8) & 0xFF)); + } + + inline void WriteUInt32(TArray& Buf, uint32 V) + { + for (int32 I = 0; I < 4; ++I) Buf.Add(static_cast((V >> (8 * I)) & 0xFF)); + } + + inline 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); + } + + /** Aceita UUID canonico (com hifens) e escreve 16 bytes binarios. */ + inline bool WriteUuid16(TArray& Buf, const FString& Uuid) + { + FString Hex = Uuid.Replace(TEXT("-"), TEXT("")); + if (Hex.Len() != 32) return false; + auto HexVal = [](TCHAR C) -> int32 + { + if (C >= TEXT('0') && C <= TEXT('9')) return C - TEXT('0'); + if (C >= TEXT('a') && C <= TEXT('f')) return C - TEXT('a') + 10; + if (C >= TEXT('A') && C <= TEXT('F')) return C - TEXT('A') + 10; + return -1; + }; + for (int32 I = 0; I < 16; ++I) + { + const int32 H = HexVal(Hex[I * 2]); + const int32 L = HexVal(Hex[I * 2 + 1]); + if (H < 0 || L < 0) return false; + Buf.Add(static_cast((H << 4) | L)); + } + return true; + } +} diff --git a/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h b/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h new file mode 100644 index 0000000..c4c6cb6 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/ZMMOCharSummary.h @@ -0,0 +1,103 @@ +#pragma once + +#include "CoreMinimal.h" +#include "ZMMOCharSummary.generated.h" + +/** Stats primarios do personagem (espelha `CharStats` em char.types.ts). */ +USTRUCT(BlueprintType) +struct FZMMOCharStats +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Str = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Agi = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Vit = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Int = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Dex = 1; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Luk = 1; +}; + +/** Aparencia (espelha `CharAppearance` em char.types.ts). */ +USTRUCT(BlueprintType) +struct FZMMOCharAppearance +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 Hair = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 HairColor = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 SkinColor = 0; + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") int32 BodyType = 0; +}; + +/** + * Resumo de personagem recebido do CharServer via S_CHAR_LIST. + * Espelha `CharSummary` em `Server/ZeusCharServer/src/types/char.types.ts`. + */ +USTRUCT(BlueprintType) +struct FZMMOCharSummary +{ + GENERATED_BODY() + + /** charId — BIGINT UNSIGNED no banco; armazenado como FString aqui pra preservar 64 bits. */ + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString CharId; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString WorldId; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Slot = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FString Name; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 ClassId = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 BaseLevel = 1; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 BaseExp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 JobLevel = 1; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 JobExp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FZMMOCharStats Stats; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Hp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 MaxHp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Sp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 MaxSp = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 Money = 0; + + /** ID do mapa (uint16). Resolvido via DT_Maps no cliente. 0 = invalido. */ + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int32 MapId = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FVector Position = FVector::ZeroVector; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + float YawDeg = 0.f; + + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + FZMMOCharAppearance Appearance; + + /** Epoch ms em que o delete vai efetivar. 0 = nao agendado. */ + UPROPERTY(BlueprintReadOnly, Category = "Zeus|Char") + int64 DeleteScheduledAtMs = 0; +}; diff --git a/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.cpp b/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.cpp new file mode 100644 index 0000000..cad1be3 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.cpp @@ -0,0 +1,3 @@ +#include "ZMMOLoginSaveGame.h" + +const FString UZMMOLoginSaveGame::SlotName = TEXT("LoginCache"); diff --git a/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.h b/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.h new file mode 100644 index 0000000..9cdab81 --- /dev/null +++ b/Source/ZMMO/Game/UI/FrontEnd/ZMMOLoginSaveGame.h @@ -0,0 +1,37 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/SaveGame.h" +#include "ZMMOLoginSaveGame.generated.h" + +/** + * Cache local da tela de Login. Persiste APENAS o nome do usuário (texto + * plano — risco zero, é só rótulo) e o estado da checkbox "Lembrar Acesso". + * + * NÃO persistir senha aqui (CWE-256). Quando o auth real (HTTP/JWT) entrar, + * adicionar `FString EncryptedRefreshToken` cifrado via DPAPI + * (CryptProtectData no Windows / equivalente em outras plataformas) — o token + * é opaco e revogável, padrão dominante em jogos comerciais + * (Battle.net/Riot/Epic). Senha em si nunca toca o disco do cliente. + * + * Arquivo: [Project]/Saved/SaveGames/LoginCache_0.sav (binary blob das + * UPROPERTY não-Transient via UE serializer). + */ +UCLASS() +class ZMMO_API UZMMOLoginSaveGame : public USaveGame +{ + GENERATED_BODY() + +public: + UPROPERTY() + FString SavedUsername; + + UPROPERTY() + bool bRememberAccess = false; + + /** Slot do arquivo .sav (sem extensão). */ + static const FString SlotName; + + /** Index de usuário do SO — 0 para single-player local. */ + static constexpr int32 UserIndex = 0; +}; 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/UIInput_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UIInput_Base.cpp new file mode 100644 index 0000000..4a3a4a6 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UIInput_Base.cpp @@ -0,0 +1,325 @@ +#include "UIInput_Base.h" + +#include "ZMMOThemeSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "Components/EditableTextBox.h" +#include "Components/SizeBox.h" +#include "Components/Image.h" +#include "Styling/CoreStyle.h" +#include "Styling/SlateBrush.h" +#include "UI/UIStyleRow.h" + +namespace +{ + // Mesmo helper das outras bases: runtime usa o tema ativo; design-time + // carrega a row "Default" de DT_UI_Styles para o Designer ver igual. + FUIStyle ResolveInputStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + 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("UIInputDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } + + // Pinta um UImage com o FSlateBrush vindo do DT (já tem Image, ImageSize, + // TintColor, DrawAs, Tiling, Margin/9-slice, UV — tudo). Brush vazio + // (sem ResourceObject e DrawAs!=NoDrawType) força Image procedural com + // Tint da própria struct — evita brush nulo que vira magenta. + void PaintImage(UImage* Img, const FSlateBrush& Brush) + { + if (!IsValid(Img)) { return; } + FSlateBrush B = Brush; + if (!B.GetResourceObject() && B.DrawAs != ESlateBrushDrawType::NoDrawType) + { + // Sem textura: pintura procedural (cor sólida). + B.DrawAs = ESlateBrushDrawType::Image; + } + Img->SetBrush(B); + // SetColorAndOpacity = identidade; cor já vem do Brush.TintColor. + Img->SetColorAndOpacity(FLinearColor::White); + } +} + +TArray UUIInput_Base::CollectVariantOptions() +{ + TArray Options; + for (const TCHAR* N : { TEXT("Box"), TEXT("Outline"), TEXT("Underline"), TEXT("Search") }) + { + 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("UIInputVariantOptions"), false)) + { + for (const TPair& P : Row->Style.Input.Backgrounds) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& P : Row->Style.Input.Strokes) { Options.AddUnique(P.Key.ToString()); } + for (const TPair& P : Row->Style.Input.Typography) { Options.AddUnique(P.Key.ToString()); } + } + } + return Options; +} + +TArray UUIInput_Base::GetVariantOptions() const +{ + return CollectVariantOptions(); +} + +FUIStyleInputVariant UUIInput_Base::ResolveVariant(const FUIStyleInput& I) const +{ + // Padrão multi-map (Hyper-style): compõe a variante buscando o nome + // (Variant FName) em CADA sub-mapa independente. Se um sub-mapa não + // tiver a chave, cai nos Defaults inline (sempre GC-safe — só + // cores/floats no constructor de FUIStyleInput). + static const FUIStyleInput Defaults; + FUIStyleInputVariant V; + + if (const FUIStyleInputBackground* E = I.Backgrounds.Find(Variant)) { V.Background = *E; } + else if (const FUIStyleInputBackground* E2 = Defaults.Backgrounds.Find(Variant)) { V.Background = *E2; } + else { V.Background = Defaults.Backgrounds.FindRef(TEXT("Box")); } + + if (const FUIStyleInputStroke* E = I.Strokes.Find(Variant)) { V.Stroke = *E; } + else if (const FUIStyleInputStroke* E2 = Defaults.Strokes.Find(Variant)) { V.Stroke = *E2; } + else { V.Stroke = Defaults.Strokes.FindRef(TEXT("Box")); } + + if (const FUIStyleInputTypography* E = I.Typography.Find(Variant)) { V.Typography = *E; } + else if (const FUIStyleInputTypography* E2 = Defaults.Typography.Find(Variant)) { V.Typography = *E2; } + else { V.Typography = Defaults.Typography.FindRef(TEXT("Box")); } + + return V; +} + +void UUIInput_Base::ApplyStyle(const FUIStyleInputVariant& V, float InPadding, + const FSlateFontInfo& ThemeFont, float FontSizePx) +{ + // Background e Stroke (UImages) — seguro em design-time e runtime. + if (IsValid(Background)) + { + PaintImage(Background, V.Background.Brush); + } + if (IsValid(Stroke)) + { + PaintImage(Stroke, V.Stroke.Brush); + } + + if (!IsValid(Input)) + { + return; + } + + // IMPORTANTE: NÃO mexer no UEditableTextBox em design-time. Qualquer + // chamada (SetForegroundColor / SetIsPassword / SetHintText / SetWidgetStyle) + // dispara SEditableText::SynchronizeTextStyle → DetermineFont via FAttribute + // delegate dangling → crash de "FSlateFontInfo::FSlateFontInfo() reading + // 0xff..." nesta build da engine. Em design-time só pintamos Background/ + // Stroke (UImages, path estável). Preview do HintText/Password só em PIE. + if (IsDesignTime()) + { + return; + } + + // SÓ chamadas pontuais — NÃO chamar SetWidgetStyle (copia FEditableTextBoxStyle + // inteiro incluindo TextStyle.Font). DetermineFont callback durante Prepass + // do Slate dereferencia FSlateFontInfo dangling se a Font veio do DT com + // FontObject UObject que ficou em estado intermediário → crash em + // FSlateFontInfo::FSlateFontInfo() durante SEditableText::SynchronizeTextStyle. + // Background brushes do EditableTextBox: configurar transparente direto no + // asset UI_Input_Master no Designer (one-time setup) — o Background/Stroke + // (UImages) atrás do EditableTextBox é quem pinta o visual via DT. + Input->SetForegroundColor(V.Typography.TextColor); + Input->SetIsPassword(bIsPassword); + if (!HintText.IsEmpty()) + { + Input->SetHintText(HintText); + } + + // Suprime warnings de parâmetros não usados (assinatura mantida pra + // compatibilidade — InPadding/ThemeFont/FontSizePx podem ser usados + // futuramente quando o caminho de SetWidgetStyle for re-habilitado de + // forma segura). + (void)InPadding; + (void)ThemeFont; + (void)FontSizePx; +} + +void UUIInput_Base::RefreshUIStyle() +{ + const FUIStyle Fallback; + const FUIStyle AS = ResolveInputStyle(this, Fallback); + const FUIStyleInput& I = AS.Input; + const FUIStyleInputVariant V = ResolveVariant(I); + + // Tamanho via Width/Height Override (mais agressivo que MinDesired — força + // tamanho exato). Resolução em 3 camadas: + // 1. PreferredWidth/Height UPROPERTY > 0 → override explícito (designer) + // 2. User mexeu DIRETO no Size_Root.WidthOverride (Designer) → preserva + // 3. Senão → aplica do DT (I.MinWidth/MinHeight) + if (IsValid(Size_Root)) + { + auto ApplyDimension = [](USizeBox* Box, bool bIsWidth, float PreferredVal, + float ThemeVal, float& LastThemeApplied) + { + const bool bHasManualFlag = bIsWidth ? Box->IsWidthOverride() : Box->IsHeightOverride(); + const float CurrentVal = bIsWidth ? Box->GetWidthOverride() : Box->GetHeightOverride(); + auto Setter = [Box, bIsWidth](float V) + { + if (bIsWidth) { Box->SetWidthOverride(V); } + else { Box->SetHeightOverride(V); } + }; + + if (PreferredVal > 0.f) + { + // (1) Override explícito via UPROPERTY. + Setter(PreferredVal); + LastThemeApplied = -1.f; // user controla via UPROPERTY a partir daqui + } + else if (bHasManualFlag && LastThemeApplied >= 0.f && CurrentVal != LastThemeApplied) + { + // (2) User editou direto no SizeBox no Designer — preserva. + } + else if (bHasManualFlag && LastThemeApplied < 0.f) + { + // (2b) Flag manual ativo mas C++ nunca aplicou nesta instância + // (fresh load): assume que é override do user no asset — preserva. + } + else + { + // (3) Aplica do DT e marca como "C++ aplicou". + Setter(ThemeVal); + LastThemeApplied = ThemeVal; + } + }; + + ApplyDimension(Size_Root, /*bIsWidth=*/true, PreferredWidth, I.MinWidth, LastThemeWidthApplied); + ApplyDimension(Size_Root, /*bIsWidth=*/false, PreferredHeight, I.MinHeight, LastThemeHeightApplied); + } + + // Size: 0 na instância → usa V.Typography.Size (do DT da variante). + const float ChosenSize = (FontSize > 0.f) ? FontSize : V.Typography.Size; + // Font: V.Typography.FontOverride se válida; senão FUIStyle.Text.BodyFont. + const FSlateFontInfo& ThemeFont = V.Typography.FontOverride.HasValidFont() + ? V.Typography.FontOverride : AS.Text.BodyFont; + ApplyStyle(V, I.Padding, ThemeFont, ChosenSize); + + BP_ApplyInputStyle(I); +} + +FText UUIInput_Base::GetText() const +{ + return IsValid(Input) ? Input->GetText() : FText::GetEmpty(); +} + +void UUIInput_Base::SetText(FText InText) +{ + if (IsValid(Input)) + { + Input->SetText(InText); + } +} + +void UUIInput_Base::SetHintTextRuntime(FText InHint) +{ + HintText = InHint; + if (IsValid(Input)) { Input->SetHintText(InHint); } +} + +void UUIInput_Base::SetIsPasswordRuntime(bool bInIsPassword) +{ + bIsPassword = bInIsPassword; + if (IsValid(Input)) { Input->SetIsPassword(bInIsPassword); } +} + +void UUIInput_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + RefreshUIStyle(); +} + +void UUIInput_Base::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + RefreshUIStyle(); +} + +void UUIInput_Base::NativeConstruct() +{ + Super::NativeConstruct(); + + if (!bInputBound && IsValid(Input)) + { + Input->OnTextChanged.AddDynamic(this, &UUIInput_Base::HandleTextChanged); + Input->OnTextCommitted.AddDynamic(this, &UUIInput_Base::HandleTextCommitted); + bInputBound = true; + } + + if (!bThemeBound) + { + if (const UGameInstance* GI = GetGameInstance()) + { + if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + Theme->OnThemeChanged.AddDynamic(this, &UUIInput_Base::HandleThemeChanged); + bThemeBound = true; + } + } + } + RefreshUIStyle(); +} + +void UUIInput_Base::NativeDestruct() +{ + if (bInputBound && IsValid(Input)) + { + Input->OnTextChanged.RemoveDynamic(this, &UUIInput_Base::HandleTextChanged); + Input->OnTextCommitted.RemoveDynamic(this, &UUIInput_Base::HandleTextCommitted); + bInputBound = false; + } + if (bThemeBound) + { + if (const UGameInstance* GI = GetGameInstance()) + { + if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + Theme->OnThemeChanged.RemoveDynamic(this, &UUIInput_Base::HandleThemeChanged); + } + } + bThemeBound = false; + } + Super::NativeDestruct(); +} + +void UUIInput_Base::HandleThemeChanged(FName /*NewThemeId*/) +{ + RefreshUIStyle(); +} + +void UUIInput_Base::HandleTextChanged(const FText& Text) +{ + OnTextChanged.Broadcast(Text); +} + +void UUIInput_Base::HandleTextCommitted(const FText& Text, ETextCommit::Type CommitMethod) +{ + OnTextCommitted.Broadcast(Text, CommitMethod); +} diff --git a/Source/ZMMO/Game/UI/Widgets/UIInput_Base.h b/Source/ZMMO/Game/UI/Widgets/UIInput_Base.h new file mode 100644 index 0000000..cdd51a4 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UIInput_Base.h @@ -0,0 +1,156 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "UI/UIStyleTokens.h" +#include "UI/UIStyleTypes.h" +#include "UIInput_Base.generated.h" + +class UEditableTextBox; +class USizeBox; +class UImage; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUIInputTextChanged, const FText&, Text); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FUIInputTextCommitted, const FText&, Text, ETextCommit::Type, CommitMethod); + +/** + * Campo de texto SÓ-INPUT (sem label). Padrão visual inspirado no UI_Search do + * Hyper: SizeBox raiz → Overlay → Background (UImage) + Stroke (UImage) + + * Input (UEditableTextBox). Os UImages recebem a aparência (cor ou textura + * 9-slice via DT_UI_Styles); o EditableTextBox fica transparente e só + * recebe fonte/cor de texto — assim a cor magenta default (brush vazio) é + * eliminada. + * + * Variantes visuais são data-driven via FUIStyle.Input (Backgrounds/Strokes/ + * Typography) — chave FName (Box/Outline/Underline/Search/custom). Para + * compor com rótulo + layout (Stacked/Inline), use UUILabel_Base. + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUIInput_Base : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + /** Re-resolve os tokens do tema ativo e reaplica. */ + UFUNCTION(BlueprintCallable, Category = "UI Style") + void RefreshUIStyle(); + + UFUNCTION(BlueprintPure, Category = "Input") + FText GetText() const; + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetText(FText InText); + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetHintTextRuntime(FText InHint); + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetIsPasswordRuntime(bool bInIsPassword); + + UPROPERTY(BlueprintAssignable, Category = "Input") + FUIInputTextChanged OnTextChanged; + + UPROPERTY(BlueprintAssignable, Category = "Input") + FUIInputTextCommitted OnTextCommitted; + + /** + * Tamanho da fonte do campo. 0 = usa o padrão do DT_UI_Styles + * (FUIStyleInputTypography.Size). > 0 = sobrescreve aqui no Designer. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", meta = (ClampMin = "0")) + float FontSize = 0.f; + + /** Placeholder exibido quando vazio. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") + FText HintText; + + /** + * Variante visual do input (dropdown vem de FUIStyle.Input.Variants no + * DT_UI_Styles + as 4 clássicas como fallback). Data-driven, sem enum. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", + meta = (GetOptions = "GetVariantOptions")) + FName Variant = TEXT("Box"); + + /** Opções do dropdown de Variant (data-driven do DT_UI_Styles). */ + UFUNCTION() + TArray GetVariantOptions() const; + + /** Helper compartilhado p/ UUILabel_Base (mesma lista de variantes). */ + static TArray CollectVariantOptions(); + + /** + * Largura preferida do campo (px). 0 = usa o padrão do DT + * (FUIStyle.Input.MinWidth). > 0 = sobrescreve aqui no Designer. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0")) + float PreferredWidth = 0.f; + + /** + * Altura preferida do campo (px). 0 = usa o padrão do DT + * (FUIStyle.Input.MinHeight). > 0 = sobrescreve aqui no Designer. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0")) + float PreferredHeight = 0.f; + + /** Campo de senha (oculta os caracteres). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") + bool bIsPassword = false; + +protected: + virtual void NativePreConstruct() override; + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual void SynchronizeProperties() override; + + /** Hook opcional: o WBP pode refinar além do que o C++ aplica. */ + UFUNCTION(BlueprintImplementableEvent, Category = "UI Style", + meta = (DisplayName = "Apply Input Style")) + void BP_ApplyInputStyle(const FUIStyleInput& InputStyle); + + // SizeBox raiz (controla PreferredWidth/MinHeight). Opcional: se o WBP + // não tiver, fallback fica ileso (sem set de tamanho). + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Size_Root; + + // Background pintável (cor + textura 9-slice opcional). + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Background; + + // Stroke / borda (cor + textura 9-slice opcional via FUIStyleInputStroke). + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Stroke; + + // Campo de texto cru. Fica transparente — Background/Stroke pintam atrás. + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Input; + +private: + FUIStyleInputVariant ResolveVariant(const FUIStyleInput& I) const; + void ApplyStyle(const FUIStyleInputVariant& V, float InPadding, + const FSlateFontInfo& ThemeFont, float FontSizePx); + + UFUNCTION() + void HandleThemeChanged(FName NewThemeId); + + UFUNCTION() + void HandleTextChanged(const FText& Text); + + UFUNCTION() + void HandleTextCommitted(const FText& Text, ETextCommit::Type CommitMethod); + + bool bThemeBound = false; + bool bInputBound = false; + + /** + * Último valor que o C++ aplicou no Size_Root->WidthOverride a partir do + * tema. Se o valor atual do SizeBox for diferente, presume-se que o user + * editou manualmente (no Designer) — C++ deixa intacto. -1 = não aplicado + * (próxima chamada aplica do tema). + */ + UPROPERTY(Transient) + float LastThemeWidthApplied = -1.f; + + UPROPERTY(Transient) + float LastThemeHeightApplied = -1.f; +}; diff --git a/Source/ZMMO/Game/UI/Widgets/UILabel_Base.cpp b/Source/ZMMO/Game/UI/Widgets/UILabel_Base.cpp new file mode 100644 index 0000000..077f30f --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UILabel_Base.cpp @@ -0,0 +1,181 @@ +#include "UILabel_Base.h" + +#include "UIInput_Base.h" +#include "UICommonText_Base.h" +#include "Components/VerticalBox.h" +#include "Components/HorizontalBox.h" +#include "CommonTextBlock.h" + +UUIInput_Base* UUILabel_Base::ActiveInput() const +{ + if (LabelLayout == EUIInputLabelLayout::Inline) + { + return IsValid(Input_Inline) ? Input_Inline : Input_Stacked; + } + return IsValid(Input_Stacked) ? Input_Stacked : Input_Inline; +} + +void UUILabel_Base::ApplyLayout() +{ + // Só alterna VISIBILIDADE (nunca muta a árvore — isso crasha em + // construção/thumbnail do editor). + const bool bInline = (LabelLayout == EUIInputLabelLayout::Inline); + if (IsValid(Box_Stacked)) + { + Box_Stacked->SetVisibility(bInline + ? ESlateVisibility::Collapsed : ESlateVisibility::SelfHitTestInvisible); + } + if (IsValid(Row_Inline)) + { + Row_Inline->SetVisibility(bInline + ? ESlateVisibility::SelfHitTestInvisible : ESlateVisibility::Collapsed); + } +} + +void UUILabel_Base::PropagateInputProps() +{ + auto Apply = [this](UUIInput_Base* In) + { + if (!IsValid(In)) { return; } + // Variant: controle de aparência do UILabel_Base — sempre propaga. + In->Variant = Variant; + // bIsPassword: sempre propaga (controle explícito). + In->bIsPassword = bIsPassword; + // Overrides "0/vazio = passthrough": só sobrescreve se o UILabel_Base + // definiu valor explícito; assim o user pode setar PreferredWidth/Height, + // FontSize, HintText DIRETO no UI_Input_Master filho e o pai não zera. + if (FontSize > 0.f) { In->FontSize = FontSize; } + if (!HintText.IsEmpty()) { In->HintText = HintText; } + if (PreferredWidth > 0.f) { In->PreferredWidth = PreferredWidth; } + if (PreferredHeight > 0.f) { In->PreferredHeight = PreferredHeight; } + In->RefreshUIStyle(); + }; + Apply(Input_Stacked); + Apply(Input_Inline); +} + +TArray UUILabel_Base::GetVariantOptions() const +{ + return UUIInput_Base::CollectVariantOptions(); +} + +TArray UUILabel_Base::GetLabelTextRoleOptions() const +{ + return UUICommonText_Base::GetThemeTextRoleOptions(); +} + +void UUILabel_Base::RefreshUIStyle() +{ + auto SetupLabel = [this](UCommonTextBlock* L) + { + if (!L) { return; } + if (!LabelText.IsEmpty()) { L->SetText(LabelText); } + if (UUICommonText_Base* CT = Cast(L)) + { + if (CT->TextRole != LabelTextRole) { CT->TextRole = LabelTextRole; } + CT->RefreshUIStyle(); + } + }; + SetupLabel(Label_Stacked); + SetupLabel(Label_Inline); + + PropagateInputProps(); +} + +FText UUILabel_Base::GetText() const +{ + const UUIInput_Base* In = ActiveInput(); + return In ? In->GetText() : FText::GetEmpty(); +} + +void UUILabel_Base::SetText(FText InText) +{ + if (UUIInput_Base* In = ActiveInput()) + { + In->SetText(InText); + } +} + +void UUILabel_Base::SetHintTextRuntime(FText InHint) +{ + HintText = InHint; + if (IsValid(Input_Stacked)) { Input_Stacked->SetHintTextRuntime(InHint); } + if (IsValid(Input_Inline)) { Input_Inline->SetHintTextRuntime(InHint); } +} + +void UUILabel_Base::SetIsPasswordRuntime(bool bInIsPassword) +{ + bIsPassword = bInIsPassword; + if (IsValid(Input_Stacked)) { Input_Stacked->SetIsPasswordRuntime(bInIsPassword); } + if (IsValid(Input_Inline)) { Input_Inline->SetIsPasswordRuntime(bInIsPassword); } +} + +void UUILabel_Base::BindInputDelegates() +{ + if (bInputBound) { return; } + if (IsValid(Input_Stacked)) + { + Input_Stacked->OnTextChanged.AddDynamic(this, &UUILabel_Base::HandleChildTextChanged); + Input_Stacked->OnTextCommitted.AddDynamic(this, &UUILabel_Base::HandleChildTextCommitted); + } + if (IsValid(Input_Inline)) + { + Input_Inline->OnTextChanged.AddDynamic(this, &UUILabel_Base::HandleChildTextChanged); + Input_Inline->OnTextCommitted.AddDynamic(this, &UUILabel_Base::HandleChildTextCommitted); + } + bInputBound = true; +} + +void UUILabel_Base::UnbindInputDelegates() +{ + if (!bInputBound) { return; } + if (IsValid(Input_Stacked)) + { + Input_Stacked->OnTextChanged.RemoveDynamic(this, &UUILabel_Base::HandleChildTextChanged); + Input_Stacked->OnTextCommitted.RemoveDynamic(this, &UUILabel_Base::HandleChildTextCommitted); + } + if (IsValid(Input_Inline)) + { + Input_Inline->OnTextChanged.RemoveDynamic(this, &UUILabel_Base::HandleChildTextChanged); + Input_Inline->OnTextCommitted.RemoveDynamic(this, &UUILabel_Base::HandleChildTextCommitted); + } + bInputBound = false; +} + +void UUILabel_Base::HandleChildTextChanged(const FText& Text) +{ + OnTextChanged.Broadcast(Text); +} + +void UUILabel_Base::HandleChildTextCommitted(const FText& Text, ETextCommit::Type CommitMethod) +{ + OnTextCommitted.Broadcast(Text, CommitMethod); +} + +void UUILabel_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + ApplyLayout(); + RefreshUIStyle(); +} + +void UUILabel_Base::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + ApplyLayout(); + RefreshUIStyle(); +} + +void UUILabel_Base::NativeConstruct() +{ + Super::NativeConstruct(); + BindInputDelegates(); + ApplyLayout(); + RefreshUIStyle(); +} + +void UUILabel_Base::NativeDestruct() +{ + UnbindInputDelegates(); + Super::NativeDestruct(); +} diff --git a/Source/ZMMO/Game/UI/Widgets/UILabel_Base.h b/Source/ZMMO/Game/UI/Widgets/UILabel_Base.h new file mode 100644 index 0000000..1373036 --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UILabel_Base.h @@ -0,0 +1,131 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "UI/UIStyleTokens.h" +#include "UI/UIStyleTypes.h" +#include "UILabel_Base.generated.h" + +class UUIInput_Base; +class UCommonTextBlock; +class UVerticalBox; +class UHorizontalBox; + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUILabelTextChanged, const FText&, Text); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FUILabelTextCommitted, const FText&, Text, ETextCommit::Type, CommitMethod); + +/** + * Composição "label + input" — dois arranjos pré-montados (Box_Stacked, + * Row_Inline) onde cada Input_* é uma instância concreta de UI_Input_Master + * (UUIInput_Base). Alterna VISIBILIDADE entre layouts (Stacked vs Inline); + * nunca muta a árvore (mutar em construção/thumbnail crasha o editor). + * + * Encaminha a API de texto pro input ativo. Variant/HintText/FontSize/ + * PreferredWidth/bIsPassword são propagados pros DOIS filhos (preview + * correto em qualquer LabelLayout). + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUILabel_Base : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + UFUNCTION(BlueprintCallable, Category = "UI Style") + void RefreshUIStyle(); + + UFUNCTION(BlueprintPure, Category = "Input") + FText GetText() const; + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetText(FText InText); + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetHintTextRuntime(FText InHint); + + UFUNCTION(BlueprintCallable, Category = "Input") + void SetIsPasswordRuntime(bool bInIsPassword); + + UPROPERTY(BlueprintAssignable, Category = "Input") + FUILabelTextChanged OnTextChanged; + + UPROPERTY(BlueprintAssignable, Category = "Input") + FUILabelTextCommitted OnTextCommitted; + + // ---- Rótulo ---- + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label") + FText LabelText; + + /** Categoria tipográfica do rótulo (dropdown vem do DT_UI_Styles). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label", + meta = (GetOptions = "GetLabelTextRoleOptions")) + FName LabelTextRole = TEXT("Label"); + + UFUNCTION() + TArray GetLabelTextRoleOptions() const; + + /** Posição do rótulo (label acima x label à esquerda). */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Label") + EUIInputLabelLayout LabelLayout = EUIInputLabelLayout::Stacked; + + // ---- Encaminhado pro input ---- + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", + meta = (GetOptions = "GetVariantOptions")) + FName Variant = TEXT("Box"); + + UFUNCTION() + TArray GetVariantOptions() const; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") + FText HintText; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input", meta = (ClampMin = "0")) + float FontSize = 0.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0")) + float PreferredWidth = 0.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input|Size", meta = (ClampMin = "0")) + float PreferredHeight = 0.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input") + bool bIsPassword = false; + +protected: + virtual void NativePreConstruct() override; + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual void SynchronizeProperties() override; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Box_Stacked; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Row_Inline; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Label_Stacked; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Input_Stacked; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Label_Inline; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Input_Inline; + +private: + UUIInput_Base* ActiveInput() const; + void ApplyLayout(); + void PropagateInputProps(); + void BindInputDelegates(); + void UnbindInputDelegates(); + + UFUNCTION() + void HandleChildTextChanged(const FText& Text); + + UFUNCTION() + void HandleChildTextCommitted(const FText& Text, ETextCommit::Type CommitMethod); + + bool bInputBound = false; +}; 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 new file mode 100644 index 0000000..4357bea --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.cpp @@ -0,0 +1,135 @@ +#include "UISpinner_Base.h" + +#include "ZMMOThemeSubsystem.h" +#include "Engine/GameInstance.h" +#include "Engine/DataTable.h" +#include "Components/CircularThrobber.h" +#include "UI/UIStyleRow.h" + +namespace +{ + // Mesmo padrão de UUIPanel_Base: runtime usa o tema ativo; design-time + // carrega a row "Default" de DT_UI_Styles para o Designer ver igual. + FUIStyle ResolveSpinnerStyle(const UUserWidget* Widget, const FUIStyle& Fallback) + { + if (Widget) + { + if (const UGameInstance* GI = Widget->GetGameInstance()) + { + if (const UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + 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("UISpinnerDesign"), false)) + { + DesignStyle = Row->Style; + bHas = true; + } + } + return bHas ? DesignStyle : Fallback; + } +} + +static FUIStyleSpinnerVariant ResolveSpinnerVariant(const FUIStyleSpinner& Sp, FName VariantName) +{ + static const FUIStyleSpinner Defaults; + FUIStyleSpinnerVariant V; + + if (const FUIStyleSpinnerColors* E = Sp.Colors.Find(VariantName)) { V.Colors = *E; } + else if (const FUIStyleSpinnerColors* E2 = Defaults.Colors.Find(VariantName)) { V.Colors = *E2; } + else { V.Colors = Defaults.Colors.FindRef(TEXT("Default")); } + + if (const FUIStyleSpinnerLayout* E = Sp.Layouts.Find(VariantName)) { V.Layout = *E; } + else if (const FUIStyleSpinnerLayout* E2 = Defaults.Layouts.Find(VariantName)) { V.Layout = *E2; } + else { V.Layout = Defaults.Layouts.FindRef(TEXT("Default")); } + + return V; +} + +TArray 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 FUIStyleSpinnerVariant V = ResolveSpinnerVariant(AS.Spinner, Variant); + + if (Throbber) + { + Throbber->SetNumberOfPieces(FMath::Max(1, V.Layout.NumberOfPieces)); + Throbber->SetPeriod(FMath::Max(0.05f, V.Layout.Period)); + Throbber->SetRadius(V.Layout.Radius); + } + + BP_ApplySpinnerStyle(V); +} + +void UUISpinner_Base::NativePreConstruct() +{ + Super::NativePreConstruct(); + RefreshUIStyle(); +} + +void UUISpinner_Base::NativeConstruct() +{ + Super::NativeConstruct(); + + if (!bThemeBound) + { + if (const UGameInstance* GI = GetGameInstance()) + { + if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + Theme->OnThemeChanged.AddDynamic(this, &UUISpinner_Base::HandleThemeChanged); + bThemeBound = true; + } + } + } + RefreshUIStyle(); +} + +void UUISpinner_Base::NativeDestruct() +{ + if (bThemeBound) + { + if (const UGameInstance* GI = GetGameInstance()) + { + if (UZMMOThemeSubsystem* Theme = GI->GetSubsystem()) + { + Theme->OnThemeChanged.RemoveDynamic(this, &UUISpinner_Base::HandleThemeChanged); + } + } + bThemeBound = false; + } + Super::NativeDestruct(); +} + +void UUISpinner_Base::HandleThemeChanged(FName /*NewThemeId*/) +{ + RefreshUIStyle(); +} diff --git a/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h new file mode 100644 index 0000000..ed13dbb --- /dev/null +++ b/Source/ZMMO/Game/UI/Widgets/UISpinner_Base.h @@ -0,0 +1,60 @@ +#pragma once + +#include "CoreMinimal.h" +#include "CommonUserWidget.h" +#include "UI/UIStyleTokens.h" +#include "UISpinner_Base.generated.h" + +class UCircularThrobber; + +/** + * Spinner/throbber indeterminado do ZMMO — mesmo padrão de UUIPanel_Base. + * Fundação CommonUI (UCommonUserWidget). Camada Abstract; o WBP concreto + * (UI_Spinner_Master) herda DIRETO desta classe C++ (UMG não encadeia + * árvores — ARQUITETURA.md §3.3). + * + * Visual data-driven por FUIStyle.Spinner (UZMMOThemeSubsystem). C++ aplica + * NumberOfPieces/Period/Radius no UCircularThrobber; cor/brush vão pelo hook + * BP_ApplySpinnerStyle (o WBP tinge a imagem do throbber). + */ +UCLASS(Abstract, Blueprintable) +class ZMMO_API UUISpinner_Base : public UCommonUserWidget +{ + GENERATED_BODY() + +public: + /** Re-resolve os tokens do tema ativo e reaplica no throbber. */ + UFUNCTION(BlueprintCallable, Category = "UI Style") + void RefreshUIStyle(); + + /** + * Variante visual do spinner (dropdown vem de FUIStyle.Spinner.Colors/ + * Layouts no DT_UI_Styles). Data-driven, sem enum fixo. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Spinner", + meta = (GetOptions = "GetVariantOptions")) + FName Variant = TEXT("Default"); + + UFUNCTION() + TArray GetVariantOptions() const; + +protected: + virtual void NativePreConstruct() override; + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + + /** Hook opcional: o WBP aplica cor/brush do spinner (variante composta). */ + UFUNCTION(BlueprintImplementableEvent, Category = "UI Style", + meta = (DisplayName = "Apply Spinner Style")) + void BP_ApplySpinnerStyle(const FUIStyleSpinnerVariant& VariantStyle); + + /** Throbber circular (nome esperado no WBP: "Throbber"). */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr Throbber; + +private: + UFUNCTION() + void HandleThemeChanged(FName NewThemeId); + + bool bThemeBound = false; +}; diff --git a/Source/ZMMO/Game/UI/ZMMOThemeSubsystem.cpp b/Source/ZMMO/Game/UI/ZMMOThemeSubsystem.cpp index 6803d72..de29915 100644 --- a/Source/ZMMO/Game/UI/ZMMOThemeSubsystem.cpp +++ b/Source/ZMMO/Game/UI/ZMMOThemeSubsystem.cpp @@ -27,6 +27,13 @@ void UZMMOThemeSubsystem::Initialize(FSubsystemCollectionBase& Collection) } ResolveActiveTheme(); + + // Garantia: se ResolveActiveTheme não disparou ResolveActiveUIStyle (porque + // ActiveTheme não mudou — caso comum quando DefaultTheme é nullptr e nada + // resolveu), força a carga do DT_UI_Styles agora para não deixar + // ActiveUIStyle como FUIStyle() default (brushes vazios → nada renderiza + // em runtime). O fallback interno acha a linha "Default" do DT. + ResolveActiveUIStyle(ActiveThemeId); } void UZMMOThemeSubsystem::Deinitialize() diff --git a/ZMMO.code-workspace b/ZMMO.code-workspace new file mode 100644 index 0000000..faf2973 --- /dev/null +++ b/ZMMO.code-workspace @@ -0,0 +1,627 @@ +{ + "folders": [ + { + "name": "ZMMO", + "path": "." + }, + { + "name": "UE5", + "path": "D:\\Program Files\\Epic Games\\UE_5.7" + } + ], + "settings": { + "typescript.tsc.autoDetect": "off", + "npm.autoDetect": "off", + "terminal.integrated.env.windows": { + "PATH": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64;${env:PATH}", + "DOTNET_ROOT": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64", + "DOTNET_HOST_PATH": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64\\dotnet.exe", + "DOTNET_MULTILEVEL_LOOKUP": "0", + "DOTNET_ROLL_FORWARD": "LatestMajor" + } + }, + "extensions": { + "recommendations": [ + "ms-vscode.cpptools", + "ms-dotnettools.csharp" + ] + }, + "tasks": { + "version": "2.0.0", + "tasks": [ + { + "label": "ZMMO Win64 Debug Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Debug Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMO Win64 Debug Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Debug Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMO", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 DebugGame Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 DebugGame Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMO Win64 DebugGame Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 DebugGame Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMO", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Development Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Development Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMO Win64 Development Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Development Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMO", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Test Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Test", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Test Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Test", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMO Win64 Test Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Test Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMO", + "Win64", + "Test", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Shipping Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Shipping", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Shipping Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMO", + "Win64", + "Shipping", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMO Win64 Shipping Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMO Win64 Shipping Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMO", + "Win64", + "Shipping", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Debug Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Debug Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMOEditor Win64 Debug Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Debug Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Debug", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 DebugGame Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 DebugGame Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMOEditor Win64 DebugGame Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 DebugGame Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMOEditor", + "Win64", + "DebugGame", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Development Build", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Development Rebuild", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Build.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "dependsOn": [ + "ZMMOEditor Win64 Development Clean" + ], + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "label": "ZMMOEditor Win64 Development Clean", + "group": "build", + "command": "Engine\\Build\\BatchFiles\\Clean.bat", + "args": [ + "ZMMOEditor", + "Win64", + "Development", + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-waitmutex" + ], + "problemMatcher": "$msCompile", + "type": "shell", + "options": { + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + } + ] + }, + "launch": { + "version": "0.2.0", + "configurations": [ + { + "name": "Launch ZMMO (Debug)", + "request": "launch", + "program": "F:\\ZeusProject\\Clients\\ZMMO\\Binaries\\Win64\\UnrealGame-Win64-Debug.exe", + "preLaunchTask": "ZMMO Win64 Debug Build", + "args": [ + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMO (DebugGame)", + "request": "launch", + "program": "F:\\ZeusProject\\Clients\\ZMMO\\Binaries\\Win64\\UnrealGame-Win64-DebugGame.exe", + "preLaunchTask": "ZMMO Win64 DebugGame Build", + "args": [ + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMO (Development)", + "request": "launch", + "program": "F:\\ZeusProject\\Clients\\ZMMO\\Binaries\\Win64\\UnrealGame.exe", + "preLaunchTask": "ZMMO Win64 Development Build", + "args": [ + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMO (Test)", + "request": "launch", + "program": "F:\\ZeusProject\\Clients\\ZMMO\\Binaries\\Win64\\UnrealGame-Win64-Test.exe", + "preLaunchTask": "ZMMO Win64 Test Build", + "args": [ + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMO (Shipping)", + "request": "launch", + "program": "F:\\ZeusProject\\Clients\\ZMMO\\Binaries\\Win64\\UnrealGame-Win64-Shipping.exe", + "preLaunchTask": "ZMMO Win64 Shipping Build", + "args": [ + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMOEditor (Debug)", + "request": "launch", + "program": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\Win64\\UnrealEditor-Win64-Debug.exe", + "preLaunchTask": "ZMMOEditor Win64 Debug Build", + "args": [ + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject" + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMOEditor (DebugGame)", + "request": "launch", + "program": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\Win64\\UnrealEditor-Win64-DebugGame.exe", + "preLaunchTask": "ZMMOEditor Win64 DebugGame Build", + "args": [ + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject" + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Launch ZMMOEditor (Development)", + "request": "launch", + "program": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\Win64\\UnrealEditor.exe", + "preLaunchTask": "ZMMOEditor Win64 Development Build", + "args": [ + "F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject" + ], + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7", + "stopAtEntry": false, + "console": "integratedTerminal", + "type": "cppvsdbg", + "visualizerFile": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Extras\\VisualStudioDebugging\\Unreal.natvis", + "sourceFileMap": { + "D:\\build\\++UE5\\Sync": "D:\\Program Files\\Epic Games\\UE_5.7" + } + }, + { + "name": "Generate Project Files", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "UnrealBuildTool Win64 Development Build", + "program": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Build\\BatchFiles\\RunUBT.bat", + "args": [ + "-projectfiles", + "-vscode", + "-project=F:\\ZeusProject\\Clients\\ZMMO\\ZMMO.uproject", + "-game", + "-engine", + "-dotnet" + ], + "env": { + "PATH": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64;${env:PATH}", + "DOTNET_ROOT": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64", + "DOTNET_HOST_PATH": "D:\\Program Files\\Epic Games\\UE_5.7\\Engine\\Binaries\\ThirdParty\\DotNet\\8.0.412\\win-x64\\dotnet.exe", + "DOTNET_MULTILEVEL_LOOKUP": "0", + "DOTNET_ROLL_FORWARD": "LatestMajor" + }, + "console": "integratedTerminal", + "stopAtEntry": false, + "cwd": "D:\\Program Files\\Epic Games\\UE_5.7" + } + ] + } +} 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",