# Registro de decisões técnicas (ADR resumido) | # | Data | Decisão | Motivo | Alternativas descartadas | |---|---|---|---|---| | D1 | 2026-10-02 | Repositório próprio com upstreams em **git submodules** (`server/openmu`, `clients/web`, `clients/desktop`) fixados por SHA | Atualização upstream controlada (`scripts/bump-upstream.sh`), histórico próprio limpo, não redistribuímos os assets embutidos nos repositórios dos clientes dentro do nosso Git | subtree (mistura histórico e traria ~2 GB de assets Webzen para o nosso repo); fork direto (perde a separação "nosso × upstream") | | D2 | 2026-10-02 | Deploy **all-in-one** (1 processo OpenMU) + Traefik | Recomendado pelo upstream; o distribuído está "currently broken and unsupported" | distributed/Dapr; Kubernetes | | D3 | 2026-10-02 | Plugins próprios carregados por **`DOTNET_STARTUP_HOOKS`** (`MuCustom.StartupHook`) | O PlugInManager só descobre assemblies carregados; o carregamento externo do upstream está desativado/quebrado no Linux. O hook é recurso oficial do .NET e não exige mudar nenhum arquivo do core. **Validado** | ProjectReference no Startup.csproj (patch no core); compilar no fonte do upstream; reflection/patch de `PlugInManager` | | D4 | 2026-10-02 | Imagem do OpenMU **construída do fonte** (submodule) em vez de `munique/openmu:latest` | Reprodutibilidade (SHA conhecido) e necessidade de compilar a camada custom contra o mesmo fonte | imagem do Docker Hub (só `latest`/versão, sem mapear SHA com garantia) | | D5 | 2026-10-02 | Cliente web servido como **build de produção** em nginx | O compose upstream roda `vite dev` com bind mount (dev only), imagem ~4 GB com `.git` | usar o compose upstream | | D6 | 2026-10-02 | Proxy WS com **`ALLOW_TARGETS` obrigatório** (compose falha sem ele) e `LOG_PACKETS=off` | Sem allowlist o proxy é relay TCP aberto para qualquer host; o dump de pacotes vaza dados e enche disco | — | | D7 | 2026-10-02 | Web → CS **44405** (clássico) / Desktop → CS **44406** (estendido) | É o que cada cliente implementa; ambos chegam à mesma instância de GameServer (medido) | forçar um só CS (quebraria um dos clientes) | | D8 | 2026-10-02 | Traefik com **provider de arquivo** + templates (`env "BASE_DOMAIN"`) | Evita montar `docker.sock` (equivale a root no host) | labels Docker (padrão do upstream) | | D9 | 2026-10-02 | Subdomínios `play/ws/admin/docs.BASE_DOMAIN`; local `*.localhost` | O Babylon monta `${WS_HOST}:${WS_PORT}` sem path → o proxy precisa de host próprio; `*.localhost` resolve para loopback nos browsers | path `/ws` (não suportado pelo cliente) | | D10 | 2026-10-02 | Healthcheck do OpenMU via `/proc/net/tcp*` + HTTP | Não há endpoint de health no all-in-one; `nc -z` não é garantido no busybox | `/api/status` (exige API key depois do 1º usuário) | | D11 | 2026-10-02 | Cliente desktop **não** dockerizado; scripts PowerShell com MSVC | A DLL de rede Native AOT só é gerada com MSVC no Windows; o build MinGW do upstream compila mas **não conecta** | cross-compile em container (geraria um cliente que não conecta) | | D12 | 2026-10-02 | Contas de teste do OpenMU **bloqueadas** automaticamente pelo `install-vps.sh` | O auto-init sempre cria `test*`/`testgm` com senha = login (alguns GM) | reinstalar via Setup sem contas de teste (manual) | | D15 | 2026-10-03 | **Só cliente web.** O cliente desktop (openmu-main) deixa de ser suportado; a regra "três clientes" vira **Servidor + Web**. O OpenMU não publica mais nenhuma porta TCP do jogo (o web chega pelo ws-proxy na rede interna); na VPS só SSH, 80 e 443 ficam abertas | Decisão do dono do projeto. Menos trabalho (toda feature uma vez, em TypeScript, entregue no reload), superfície de ataque menor (sem 44405-55980 públicas, sem `RESOLVE_IP`) e menor risco jurídico (o desktop deriva de fonte vazada) | manter os dois clientes (dobra o custo de toda feature de interface) | | D16 | 2026-10-03 | Cliente web no **protocolo estendido do OpenMU** (versão `season6x`, tag `S6EP3X`, cliente 106.3, CS 44406, GS 55902/04/06) via série de patches `services/web-client/patches/000N-*.patch` aplicada no build | Formato clássico limitava: wings/itens custom invisíveis para outros jogadores (aparência por tabela fixa), dano/HP 16-bit, senha ≤10, IDs de item ≤511. O estendido carrega grupo+número reais (12 bits), valores 32/64-bit e senha até 20 | patch de 1 linha (insuficiente: ~25 pacotes mudam); manter clássico (bloqueia o roadmap de conteúdo custom) | | D17 | 2026-10-03 | Patches do web gerados com `git format-patch` a partir do branch local `mu-custom/extended-protocol` do submodule; o checkout de `clients/web` usa `core.autocrlf=false` (`scripts/lib.sh`) | O upstream mistura blobs CRLF e LF; os patches os reproduzem byte a byte e só aplicam sobre um checkout idêntico aos blobs (o padrão do Git no Windows quebrava a aplicação) | normalizar finais de linha no build (corrompe arquivos com CR legítimo) | | D18 | 2026-10-03 | Remoção do cliente desktop do repositório (complementa a D15): saem o submodule `clients/desktop` (openmu-main), `scripts/windows/*`, `docker-compose.desktop.yml`, `tools/DesktopPresenceProbe`, `tools/compose-lint` e `assets-local/`; `RESOLVE_IP` vira valor fixo no `docker-compose.yml` e `GAME_BIND_IP` sai do `.env.example` | Código sem uso desde a D15 só gerava manutenção e confusão na documentação; tudo continua recuperável pelo histórico do Git | manter os arquivos como referência "sem suporte" | | D19 | 2026-10-03 | **Patch mínimo no core do OpenMU** para o B13: `Player.RemoveFromGameAsync` limpa a lista de objetos observados (`services/openmu/patches/0001-*.patch`, branch local `mu-custom/server-fixes` do submodule), aplicado no build da imagem | O jogador sempre observa a si mesmo, então sair do mapa não o tirava da própria lista; ao reentrar no mundo ("Trocar de personagem") o servidor não reenviava `NewPlayersInScope` para ele e o cliente web — que cria o herói a partir dessa mensagem — ficava sem personagem. Não há ponto de plugin para isso (o método é `internal`). Teste `ReEnterWorldAfterLogoutTest` falha sem o patch; suíte `MUnique.OpenMU.Tests` 1339/1339. Deve ser contribuído upstream e removido daqui quando o upstream tiver a correção | contornar no cliente (criar o herói a partir da lista de personagens): esconderia o bug do servidor para outros clientes | | D20 | 2026-10-03 | Limite de conexões por jogador **no Traefik** (`ws-per-ip`, `inFlightReq`, `WS_MAX_CONN_PER_IP=20`) e o `MaxConnectionsPerAddress` do ConnectServer 44406 vira teto global (`CS_MAX_CONN_PER_ADDRESS=1000`, `scripts/set-connection-limit.sh`, chamado pelo `install-vps.sh`) | Todo jogador web chega ao OpenMU com o IP do ws-proxy; com o padrão (30) o 31º jogador simultâneo era recusado. Só o Traefik vê o IP real | passar o IP real ao OpenMU (PROXY protocol): exige mudar o OpenMU e o proxy | | D21 | 2026-10-03 | Ranking público como **arquivo estático**: o serviço `ranking` (imagem do PostgreSQL, só rede `backend`) gera `ranking.json` a cada 5 min num volume que o `web-client` serve read-only em `/ranking.json`; página `/ranking` no site do jogo. Ficam de fora GMs e contas bloqueadas | Zero superfície nova: nenhuma API pública, nenhuma consulta disparada por visitante, nenhum acesso do Traefik ao banco. Risco aceito: o job usa o superusuário do Postgres (mesmo do OpenMU) dentro da rede interna | API do Admin Panel (exige login); serviço HTTP próprio consultando o banco a cada visita | | D22 | 2026-10-03 | Regras do jogo (rates e reset) versionadas no `.env` e aplicadas por `scripts/apply-game-config.sh` (SQL idempotente numa transação; reinicia o openmu se algo mudou). `GAME_DROP_RATE` multiplica só a chance dos grupos de **item** (não o zen), limitada a 99% por grupo, a partir de uma cópia original em `mucustom.drop_chance_baseline` | Reproduzível em outra máquina/VPS e revisável no Git; o Admin continua mostrando os valores. O OpenMU não tem multiplicador global de drop, e escalar o grupo de zen junto tiraria espaço dos itens (acima de 100% a soma é normalizada) | plugin `IConfigurationUpdatePlugIn` (mais código para o mesmo resultado); só pelo Admin (não reproduzível) | | D23 | 2026-10-03 | **Um canal só** (`GAME_CHANNELS=1`): `apply-game-config.sh` remove as `GameServerDefinition` de `ServerID` >= N (endpoints em cascata); allowlist do proxy sem 55904/55906 | Pedido do dono do projeto: com poucos jogadores, vários canais só dividem a população e confundem o jogador. O OpenMU não tem um campo "desligado" por canal | deixar os 3 canais; esconder canais só no cliente (o ConnectServer continuaria anunciando) | | D24 | 2026-10-03 | **Itens custom**: definição no servidor como update de configuração do OpenMU (`custom/server/src/MuCustom.Items`, instalado por `/customupdates install` de GM ou Admin → Updates; reiniciar o servidor depois); no cliente, alias para um item conhecido (`customItems.ts`, patch 0009): 12/300 "Asas do Guardião" = stats e modelo da Wing of Storm | Mesmo caminho do upstream para dados novos em banco existente; o alias evita duplicar o item em dezenas de tabelas do cliente e não exige asset novo (nada proprietário no nosso Git) | SQL direto nas tabelas de item (grafo grande, frágil); modelo 3D próprio (fica para quando houver arte) | | D25 | 2026-10-03 | **Sistemas das seasons novas como plugins próprios** (`custom/server/src/MuCustom.Systems`): Gremory Case, diárias + registro de caça, banco de joias e Ruud + loja, falando com a janela do cliente web (tecla B, patch 0011) pelo código **0xFC** (pedido `C1 FC sub`, resposta `C2 FC sub|0x80` + JSON). Dados no schema `mucustom` do mesmo PostgreSQL (Npgsql do próprio OpenMU); Ruud e banco de joias por **conta**, Gremory e diárias por **personagem**. Toda operação que mexe em saldo é atômica no SQL (débito com `amount >= preço`, saque reservado com `FOR UPDATE`); item sem espaço vai para a Gremory | Nenhuma mudança no core: o OpenMU tem um handler por código de pacote e 0xFC está livre (0xFA/0xFB são do ws-proxy). JSON evita gerar pacotes binários dos dois lados para telas que só listam dados | tabelas no modelo do OpenMU (exige migração EF no core); um código por sistema (gastaria códigos livres) | | D26 | 2026-10-03 | **Tema MU INFERNUS desenhado em código** (patch 0012): fundo com 6 camadas SVG geradas por semente (céu, cordilheiras, fortaleza, lava, rochas) com parallax pelo ponteiro, brasas em canvas, logotipo em CSS no lugar do logo MU; nome do cliente "MU INFERNUS" | Arte 100% nossa, sem arquivo binário no Git, leve e nítida em qualquer resolução; respeita `prefers-reduced-motion`. A arte pintada definitiva fica descrita em `pending_arts.md` para substituir as camadas | gerar imagens no Google Flow agora (depende de sessão no navegador do dono e de revisão de licença das imagens); manter o logo original (é marca da Webzen) | | D27 | 2026-10-03 | Patch 0002 no core do OpenMU: o spawn gate usa o mapa do personagem quando `CurrentMap` ainda é nulo (troca de mapa sem o F3 12 do cliente) | Desconexão ou fim de evento nesse instante lançava "CurrentMap is not set" e deixava o jogador no mapa do evento (achado na auditoria dos eventos). Teste `SafezoneWarpDuringMapChangeDoesNotThrowAsync` falha sem a correção. Candidato a PR upstream | ignorar o aviso no log (o jogador ficava preso no mapa do evento ao relogar) | | D28 | 2026-10-03 | **Fotos (perfil e guilda) pela própria conexão do jogo** (0xFC 50-55), sem serviço HTTP nem moderação (decisão do dono). O navegador recorta 64×64 e regrava em WebP/PNG (some o EXIF/GPS); o servidor só aceita WebP/PNG de até 24 KB, uma troca a cada 10 s; foto da guilda só pelo mestre | A conexão já é autenticada: nada de token, rota pública ou contêiner novo. ~3 KB por foto, pedida uma vez por sessão | serviço `avatars` com token de upload (a análise original, `docs/analise-foto-de-perfil.md`) | | D29 | 2026-10-03 | **Slots de brinco próprios** (2, fora do inventário, `mucustom.earrings`): o bônus do brinco entra nos atributos do personagem ao entrar no mundo e a cada troca; brincos deixam de caber no slot de anel | Um slot real renumeraria o inventário (slot 12 em diante é a mochila) no core, no protocolo e em todo cliente | slot no core do OpenMU; manter no slot de anel | | D30 | 2026-10-03 | Selos com os números de buff do cliente original que o OpenMU declara e não usa (40 Seal of Ascension, 41 Seal of Wealth) | O cliente mostra o ícone e o tempo restante sem mudar o core (efeitos ≥ 200 nunca vão ao cliente) | ícone próprio (exigiria patch no core) | | D31 | 2026-10-03 | **Textos de quest em pt-BR próprios** para os 342 que a tabela portuguesa da Webzen traz em espanhol (`questWordsPortuguese.ts`, aplicado sobre `QuestWords` como os reparos de página do upstream); a oferta de quest já abre com Aceitar/Recusar | A tabela `Por` é da Webzen e só lemos; a correção é texto nosso por índice. Nomes próprios (mapas, itens, monstros, NPCs) ficam como no inglês | editar o `.bmd` (asset de terceiros, fora do Git) | | D32 | 2026-10-03 | Nome **MU INFERNUS**; primeira tela só com Entrar / Download / Opções; carregamentos com fogo, pentagrama e LOADING; texturas Upscaled 1024 por padrão; andar com as setas; botão de ataque automático (só monstros: `isAttackableEntity`) | Pedidos do dono do projeto | — | | D33 | 2026-10-03 | **Dungeons** (`docs/dungeons.md`): 5 mini games genéricos do OpenMU (tipo `IllusionTemple`, sem contexto próprio nesta versão) nos mapas 45–49 do Illusion Temple, criados por update; sala, convites, ondas, tempo, recompensa e limite de 2/dia são nossos (`MuCustom.Systems/Dungeons.cs`). **Bots do OpenMU ligados** (`GAME_BOTS`, 6 contas × 2) para completar as salas: entram na party do líder e na instância pela fila de ações do próprio bot (lida por reflexão, é `internal` no upstream) | Instância por grupo, entrada/saída e tempo já existem no core; nada muda nele. Os mapas do Illusion Temple estão no servidor e nos assets do cliente e não eram usados | mapa compartilhado (grupos se misturariam); contexto de mini game próprio (exigiria mudar o `MiniGameManager` do core) | | D34 | 2026-10-03 | **Bots do OpenMU mais humanos** (patch 0003 no `BotNavigator`): tick de avaliação variável (0,65–1,55 s, com hesitações de 1–3,5 s) e pausas AFK por bot (chance e duração sorteadas por bot; 12% longas, 3–8 min; nunca durante compra/buff). Sem chat (pedido do dono) | O OpenMU já tinha rotação de presença, atraso humano em convites de party e início com jitter; o que entregava os bots era o batimento fixo de 1 s e a ausência de pausas. O combate segue em timer próprio, então o bot parado se defende | Reescrever a navegação (caminhos imperfeitos); chat falso; rotação de sessão própria (já existe `PresenceRotation`) | | D35 | 2026-10-03 | **Quests de NPC próprias com o texto no servidor** (`MuCustom.Systems/NpcQuests.cs`, patch 0025, `docs/quests-npc.md`): NPCs que o OpenMU deixa sem função (`IPlayerTalkToNpcPlugIn`) abrem um diálogo (`C2 FC 84`); `C1 FC 04 ação id` aceita/conclui; matar N monstros ou visitar outro NPC; recompensa em Ruud, Zen e item (Gremory). Estado em `mucustom.npc_quests`; ids só crescem | O cliente lê as quests originais de tabelas binárias (até 200, 15 idiomas); uma quest nova do OpenMU exigiria texto nelas | Quests do OpenMU + `questWords` no cliente (texto só pt-BR por quest, tabelas binárias); NPCs com loja (o OpenMU não chama o plug-in neles) | | D36 | 2026-10-03 | **Chefe mundial com fases** (`MuCustom.Systems/WorldBoss.cs`, `docs/boss-fases.md`): Red Dragon/Great Drakan/Dark Phoenix (vida ×5–6) às 13h/19h/22h de Brasília ou `/worldboss`; fases a 70/40/15% da vida (servos, +dano, fúria, cura única); prêmio por dano (Ruud + joias na Gremory, bônus aos 3 primeiros); some após 25 min | Sem mudar o core: a vida é lida a cada segundo e o dano vem do `IAttackableGotHitPlugIn`; avisos globais | Hook de HP no core; modelo/habilidades novos (sem arte nova ainda); fuso por `TimeZoneInfo` (a imagem não tem tzdata: UTC-3 fixo, Brasília sem horário de verão) | | D37 | 2026-10-03 | **Mapa detalhado em tela cheia** (patch 0026, `docs/mapa-detalhado.md`): `C1 FC 05` -> `C2 FC 85` com NPCs, tipos de monstro com nível e áreas de nascimento do mapa (cache por mapa); ícone sob o minimapa abre a tela com lista por nível, filtros, zoom e arrasto | O cliente só conhece os objetos da tela; o mapa TAB original só tem NPC e portal do `Minimap.bmd` | Ler os spawns no cliente (não existem no modo online) | | D38 | 2026-10-03 | **`/botparty` (GM)**: forma uma party do GM com `n` bots livres online, traz os bots ao mapa e entrega o item/ingresso, pela fila de ações do bot | Testar conteúdo só de party (Imperial Guardian: entrada com party = Success) | Duas contas reais em party | | D39 | 2026-10-04 | **CI/CD no GitHub Actions** (`.github/workflows/deploy.yml`): `validate` em todo push (shell, compose, plugins C#, tsc do cliente com os patches); push no `main` publica na VPS por `git push` via SSH + `scripts/deploy.sh` (update.sh com rollback automático de código). Autenticação por usuário/senha (`secrets.VPS_PASS`, decisão do dono); sem domínio próprio, `BASE_DOMAIN=82-38-28-175.sslip.io` | O repositório é privado: enviar o commit pelo SSH evita deploy key na VPS. O build acontece com os containers antigos no ar, então um build quebrado não derruba o servidor. sslip.io dá HTTPS real sem DNS | build das imagens no runner + registry (mais peças e segredos); chave SSH dedicada (preferível, fica como próximo passo); deploy só por HTTP sem TLS | | D40 | 2026-10-04 | **Economia transacional** (`MuCustom.Systems/Economy.cs`, `docs/audit/economy-concurrency.md`): Ruud só por `UPDATE` condicional + ledger na mesma transação, CHECKs de não-negativo, recompensas "uma vez" por chave (`reward_grants`), itens entre o mucustom e o inventário por reservar -> entregar -> salvar o personagem -> confirmar (`item_deliveries`; pendência de antes do start vira `orphaned`) | O mucustom e o OpenMU são transações diferentes: fingir atomicidade escondia dupes (depósito no banco/brinco antes de salvar o inventário) e perdas (pagamentos em vários statements). Perder com registro é melhor que duplicar | transação distribuída (o OpenMU não expõe a dele); reentregar pendências automaticamente (dupe se o save já tinha acontecido) | | D41 | 2026-10-04 | **Guarda do 0xFC** (`RequestGuard.cs`): tamanho declarado = buffer, sub-código conhecido, tamanho exato dos dados (a lista de quests aceita 1 byte de preenchimento do cliente), baldes por jogador (leitura 40/15 por s, escrita 6/1,5 por s); fotos validadas pela estrutura PNG/WebP e lado <= 256 px | Leituras em offset fixo (4) erravam em C2; nenhum limite de spam em operações que gravam no banco | validar dentro de cada sistema (repetitivo e fácil de esquecer); decodificar a imagem (sem biblioteca no servidor) | | D42 | 2026-10-04 | **Papéis de menor privilégio** (`scripts/db-roles.sh`): `mucustom_runtime` dono do schema mucustom + SELECT em colunas do OpenMU; `ranking_reader` só leitura; migrations do mucustom versionadas e aplicadas no start (`SchemaPlugIn`) | O OpenMU core aplica as próprias migrations e precisa do superusuário; o que é nosso não | `Database:AssumeExternallyProvisioned` (só evita drop/create; o core ainda migra o schema) | | D43 | 2026-10-04 | **Borda**: BasicAuth gerado no admin (`scripts/admin-edge-auth.sh`, hash em base64 no .env), rate limit no admin e em WebSockets novos, HSTS; ws-proxy com `init` (SIGTERM) e frame de 64 KB (P2); containers com no-new-privileges, cap_drop e raiz read-only onde testado | Admin só tinha o login do OpenMU; o proxy aceitava 16 MB por frame e morria por SIGKILL | VPN/IP allowlist para o admin (o dono acessa de IPs variáveis) | | D44 | 2026-10-04 | **OpenMU em `27b51b3`** (merge do nosso #1062, contém o #1063): os patches antigos 0001/0002 saíram; o dos bots virou 0001. O topo do `master` (#1065, golpes de skill recusados sem animação registrada) ficou de fora | Os PRs foram aceitos upstream; o #1065 muda o combate de um jeito que precisa ser testado com o cliente web e os bots antes | ir direto ao topo do `master` | | D45 | 2026-10-04 | **Limite de 65535 por stat** (`GAME_MAX_STAT`, `apply-game-config.sh`, aplicado a cada deploy) | Pedido do dono ("65k"); 65535 é o máximo dos campos de 16 bits do protocolo | sem limite (o OpenMU vem sem) | | D46 | 2026-10-04 | **Item girando em 3D no hover** (patch 0030, `muCustom/itemHoverPreview.ts`): só o item sob o mouse é renderizado ao vivo (um RenderTargetTexture na cena principal, camada própria, câmera ortográfica pelo bounding box), os demais continuam com o PNG do pacote; opção de ângulo da câmera; avatar quadrado no chat (0029) | O cliente nunca renderizou itens na UI (só PNGs); o original gira o item sob o mouse. Um alvo só = custo zero com o inventário fechado e nada por slot | uma cena/engine por slot (caro); render ao vivo de todos os slots (dezenas de draws por frame sem ganho de fidelidade); `engine.registerView` (sem fundo transparente) | | D47 | 2026-10-04 | **Backup diário por systemd timer** com retenção local e off-site opcional por rclone (container por digest; `.rclone.conf` fora do Git) | Cron não registra resultado nem recupera execuções perdidas; nenhum segredo de storage existe ainda (`OFFSITE_BACKUP_CONFIGURED=false`) | cron; inventar um destino | | D14 | 2026-10-03 | Patch de build no cliente web (`services/web-client/patches/apply.sh`, P1): `VITE_SEED_DEFAULT_PROFILE=off` impede o perfil automático "Local (OpenMU)" | O cliente sempre cria esse perfil quando o ConnectServer do build não é loopback; ele duplicava o mundo "MU Custom" da serverlist e confundia o jogador. O patch tem 1 linha, é opcional, roda só no build da imagem (submodule intocado) e falha o build se o upstream mudar o trecho | lista vazia (sobrava "Local (OpenMU)", nome fixo no código); `VITE_CS_HOST=127.0.0.1` (só esconde fora de `*.localhost` e quebra o perfil local); PR upstream (fica como contribuição sugerida) | | D13 | 2026-10-02 | PostgreSQL 18 com o superusuário usado pelo OpenMU | O OpenMU cria banco e roles (`config`, `account`, `friend`, `guild`) com o usuário admin na 1ª inicialização; as senhas desses roles são fixas no `ConnectionSettings.xml` upstream | `Database__AssumeExternallyProvisioned=true` + roles pré-criados (futuro, M12) |