# Desenvolvimento
## Estrutura
```
server/openmu/ submodule OpenMU (NÃO editar — ver "upstream")
clients/web/ submodule OpenMu-Client-Babylon
custom/server/ nossos plugins C# (MuCustom.*) + startup hook
services/ Dockerfiles nossos (openmu, web-client, game-proxy)
infra/ Traefik, nginx
tools/ ferramentas (ws-login-probe.ts)
scripts/ operação (bash)
docs/ documentação (servida pelo container docs)
```
## Servidor (OpenMU + camada custom)
### Rebuild
```bash
docker compose build openmu && docker compose up -d openmu
docker compose logs -f openmu | grep -E "MuCustom|Error"
```
O log deve conter `[MuCustom.StartupHook] loaded MuCustom.Gameplay …`.
### Testes do upstream (sem Docker, se houver .NET 10 SDK)
```bash
cd server/openmu
dotnet build src/Startup/MUnique.OpenMU.Startup.csproj -c Release -p:ci=true
dotnet test tests/MUnique.OpenMU.Network.Packets.Tests -c Release -p:ci=true
# demais: tests/*Tests (Web.Tests usa bunit)
```
`-p:ci=true` é obrigatório (sem ele o build tenta rodar `npx`/TypeScript para gerar docs).
Resultado em 2026-10-02 (`204c11b5`): Packets 616 ✅, Network 73 ✅ (4 skip), AttributeSystem 44 ✅,
Pathfinding 10 ✅, PlugIns 41 ✅, ChatServer 26 ✅, MUnique.OpenMU.Tests 1338 ✅, Persistence.Initialization 75 ✅ (6 skip).
### Rodar o servidor sem Docker (debug rápido, banco em memória)
```bash
cd server/openmu/src/Startup/bin/Release
ASPNETCORE_URLS=http://127.0.0.1:8090 dotnet MUnique.OpenMU.Startup.dll -demo -autostart -resolveIP:loopback
# com nossos plugins:
DOTNET_STARTUP_HOOKS=/caminho/MuCustom.StartupHook.dll MUCUSTOM_PLUGIN_DIR=/caminho/plugins dotnet ...
```
`-demo` não persiste nada. Útil para testar plugins em segundos.
### Criando um plugin
1. Crie o projeto em `custom/server/src/MuCustom./` (copie o `.csproj` de `MuCustom.Gameplay`).
2. Implemente uma interface de ponto de extensão do OpenMU (exemplos no upstream):
- chat command: `IChatCommandPlugIn` (`GameLogic/PlugIns/ChatCommands`)
- tarefa periódica: `IPeriodicTaskPlugIn` / `PeriodicTaskBasePlugIn`
- eventos de jogo: `GameLogic/PlugIns/I*PlugIn.cs` (morte de monstro, level up, entrar no mapa…)
- handlers de pacote: `IPacketHandlerPlugIn` (`GameServer/MessageHandler`)
- views por cliente: `IViewPlugIn` + `[MinimumClient]`
- configuração editável no Admin: `ISupportCustomConfiguration` + `ISupportDefaultCustomConfiguration`
3. `[Guid("...novo...")]`, `[PlugIn]`, `[Display(Name = "MuCustom: ...")]`.
4. `docker compose build openmu && docker compose up -d openmu` → Admin Panel → *Plugins* (filtrar "MuCustom").
5. Registre na [matriz de compatibilidade](compatibility-matrix.md).
### Boas práticas de merge com o upstream
- **Não edite `server/openmu`.** Se precisar mudar o core: abra issue/PR no MUnique/OpenMU, ou, se urgente,
mantenha um patch mínimo em `services/openmu/patches/000N-*.patch` (aplicado no build por `apply.sh`), com
teste e justificativa em `docs/decisions.md` (exemplo: D19, B13).
### Como alteramos o core do OpenMU (patches)
O submodule `server/openmu` fica no SHA fixado; a correção vive no branch local `mu-custom/server-fixes`
(não é enviado a lugar nenhum) e vira patch:
```bash
cd server/openmu
git checkout mu-custom/server-fixes # ou: git checkout -b mu-custom/server-fixes
# ... corrigir + escrever o teste em tests/MUnique.OpenMU.Tests, commitar no branch ...
git format-patch ..HEAD -o ../../services/openmu/patches/ # apague os .patch antigos antes
git checkout
```
Testes em container (o SDK é o mesmo do Dockerfile; no Git Bash use `MSYS_NO_PATHCONV=1`):
```bash
docker run --rm -v "$PWD/server/openmu:/src" -v mu-nuget:/root/.nuget/packages -w /src mcr.microsoft.com/dotnet/sdk@sha256: dotnet test tests/MUnique.OpenMU.Tests/MUnique.OpenMU.Tests.csproj -c Release -p:ci=true
```
Só `src/` entra na imagem; o `apply.sh` normaliza para LF os arquivos tocados (o checkout no Windows é CRLF).
- Pacotes novos: o upstream exige XML em `src/Network/Packets` e variantes `…Extended` — contribua lá.
- Atualizar upstream: `scripts/bump-upstream.sh openmu` → build → `smoke-test.sh` → `VERSIONS.md` → commit.
- Siga `server/openmu/docs/CODING_RULES.md` em código que possa voltar ao upstream.
## Cliente web (Babylon)
### Como alteramos o cliente web (patches)
O submodule `clients/web` fica **sempre no SHA fixado**. Nossas mudanças vivem em
`services/web-client/patches/000N-*.patch` e são aplicadas no build da imagem (`patches/apply.sh`).
```bash
cd clients/web
git config core.autocrlf false # obrigatório: o upstream mistura CRLF/LF (scripts/lib.sh já faz)
git checkout mu-custom/extended-protocol # branch local com a série (não é enviado a lugar nenhum)
# ... editar, rodar typecheck e testes em container (abaixo), commitar no branch ...
git format-patch ..HEAD -o ../../services/web-client/patches/ # apague os .patch antigos antes
git checkout # volta o submodule ao estado do upstream
```
Ao atualizar o upstream (`scripts/bump-upstream.sh web`): `git rebase` do branch sobre o novo SHA,
regerar os patches, rebuild e smoke test. Um patch que não aplica **falha o build**, de propósito.
Pacote novo do servidor: copie o `` de `server/openmu/src/Network/Packets/ServerToClient/ServerToClientPackets.xml`
para `src/common/packets/packetsDefinitions/` e regere com `bun run generate` (em container, com `node_modules`);
desfaça a mudança que o gerador faz em `src/common/packets/index.ts` (o upstream editou esse arquivo à mão).
Antes de `docker compose build web-client`, volte o submodule ao SHA fixado: o Dockerfile copia a cópia de
trabalho e aplica os patches por cima (no branch eles já estariam aplicados e o build falha).
### Rebuild
```bash
docker compose build web-client && docker compose up -d web-client # bundle
docker compose build ws-proxy && docker compose up -d ws-proxy # proxy
```
Os `VITE_*` são **fixados no build** (`WEB_*` no `.env`). Mudou domínio/porta → rebuild.
Overrides em runtime pela URL (do próprio cliente): `?cs=host:porta`, `?ws=wss://host:porta`, `?gs=cs`, `?list=off`.
### Lint / typecheck / testes (em container, sem instalar Bun no host)
```bash
docker run --rm -v "$PWD/clients/web:/app" -w /app oven/bun:1.4.2-slim sh -c \
"bun install --frozen-lockfile && bun run tsc && bun run lint && bun run test && bun test proxy/"
```
### Proxy WS↔TCP — notas de segurança (`clients/web/proxy/main.ts`)
| Aspecto | Upstream | Nossa configuração |
|---|---|---|
| Alvo da conexão | escolhido pelo cliente (`?host&port`) | `ALLOW_TARGETS` obrigatório (compose falha sem) |
| TLS/WSS | não (ws puro) | Traefik termina TLS |
| Limite de conexões por IP | não existe | 🚧 backlog: middleware `inFlightConn`/`rateLimit` no Traefik |
| Tamanho máx. de mensagem / idle | defaults do Bun (16 MB / 120 s) | 🚧 backlog: contribuição upstream |
| Graceful shutdown | não trata SIGTERM | `stop_grace_period` do compose; 🚧 upstream |
| Log de pacotes | **ligado por padrão** | `LOG_PACKETS=off` |
| IP real do jogador | não lê `X-Forwarded-For` | IP real só nos access logs do Traefik |
| Tracker/journal | SQLite em `~/.mu-proxy` | volume `wsproxy-data`, whispers desligados, retenção 7 dias |
## Debug e logs
```bash
./scripts/logs.sh openmu # segue
./scripts/logs.sh openmu --errors # só erros
docker compose exec openmu ls /app/logs # arquivos do Serilog (volume openmu-logs)
MU_ENV=dev → dashboard do Traefik em http://127.0.0.1:8088
WS_LOG_PACKETS=on (só em dev!) → hexdump de todos os pacotes no ws-proxy
```
## Testes de ponta a ponta
```bash
./scripts/smoke-test.sh # infra + protocolo + HTTP + WS + login TCP estendido + login web
docker compose --profile tools run --rm login-probe --account test0 --host 127.0.0.1 --connect-port 44406
docker compose exec ws-proxy bun tools/ws-login-probe.ts --account test0
./scripts/game-test.sh # bot entra no jogo, comandos, quests, combate/EXP e sistemas 0xFC (contas de teste)
./scripts/audit-events.sh [bc] [ds] [cc] # auditoria dos eventos pelo bot (reinicia o openmu; relatório em docs/audit/eventos.md)
```
### Bot de protocolo (`tools/ws-game-bot.ts`)
Faz o caminho do navegador (WebSocket → ws-proxy → OpenMU, versão 20404) sem interface. Útil para testar
comandos, regras do servidor e combate sem clicar no jogo:
```bash
docker compose exec ws-proxy sh -c "cd /app && bun tools/ws-game-bot.ts --account testgm --character testgmDk --chat '/resetinfo;/online' --expect 'Reset' --teleport '200 150' --spawn 3 --trace"
```
`--chat` envia comandos e imprime as respostas; `--expect` falha se nenhuma resposta tiver o texto;
`--teleport`/`--spawn` (GM) criam um monstro fora da safe zone e o bot ataca até matar; `--trace` lista os
pacotes recebidos. Saída: uma linha JSON por etapa; exit 0 = tudo certo. Monstro não nasce em safe zone.