O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Servidor

Como criar um servidor de MU Origin (mobile) privado

Guia técnico para criar um servidor privado de MU Origin mobile — arquitetura REST API + WebSocket, banco MySQL, servidor Node.js/PHP, modificação do APK Android e hospedagem no Linux.

EQ Equipe ViciadosMU · Atualizado em 1 jul 2026 · ⏱ 24 min de leitura
Resposta rápida

O MU Origin é o MU Online para dispositivos móveis — mas sua arquitetura é completamente diferente do MU PC. Enquanto o MU PC usa uma arquitetura de software proprietário (DataServer/GameServer/ConnectServer), o MU Origin usa uma arquitetura web moderna baseada em REST API e WebSockets.

O MU Origin é o MU Online para dispositivos móveis — mas sua arquitetura é completamente diferente do MU PC. Enquanto o MU PC usa uma arquitetura de software proprietário (DataServer/GameServer/ConnectServer), o MU Origin usa uma arquitetura web moderna baseada em REST API e WebSockets.

Diferenças fundamentais: MU PC vs MU Origin

AspectoMU Online (PC)MU Origin (Mobile)
ProtocoloTCP binário proprietárioREST API + WebSocket
Banco de dadosSQL Server (Microsoft)MySQL / PostgreSQL
ServidorDataServer, GameServer (.exe)Node.js / PHP / Java
Linguagem server-sideC++ (compilado)JavaScript, PHP, Java, Go
Sistema de autenticaçãoVerificação bináriaJWT / OAuth2
Sistema de arquivos.ini, .txt, .bmdJSON, MySQL, YAML
Configuração de mundoMonsterSetBase.txtBanco de dados
ComunicaçãoPacotes binários TCPJSON sobre WebSocket
Atenção: Este tutorial é para fins educacionais e de estudo de arquitetura de servidores de jogos mobile. Criar um servidor privado de MU Origin pode violar os termos de serviço da Webzen. Estude a arquitetura para aprendizado, mas respeite os termos do produto oficial.

Arquitetura de um servidor mobile tipo MU Origin

CLIENTE ANDROID (APK modificado)
        │ HTTPS (REST API)
        │ WSS (WebSocket)
        ▼
SERVIDOR DE APLICAÇÃO (Node.js / PHP)
├── /auth        → login, registro, JWT
├── /player      → dados do personagem
├── /world       → estado do mundo (monstros, outros players)
├── /inventory   → inventário, equipamentos
├── /shop        → loja virtual
└── WebSocket    → eventos em tempo real
        │
        ▼
BANCO DE DADOS (MySQL)
├── accounts     → contas de jogadores
├── characters   → personagens
├── inventory    → inventários
├── monsters     → definição de monstros (substituindo Monster.txt)
└── maps         → dados dos mapas (substituindo MonsterSetBase.txt)

Banco de dados MySQL para servidor mobile

-- Schema básico para servidor mobile tipo MU Origin

CREATE DATABASE mu_origin_pvt CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE mu_origin_pvt;

-- Contas
CREATE TABLE accounts (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    username    VARCHAR(50) NOT NULL UNIQUE,
    password    VARCHAR(255) NOT NULL,         -- bcrypt hash
    email       VARCHAR(150) UNIQUE,
    is_banned   TINYINT(1) NOT NULL DEFAULT 0,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    last_login  DATETIME NULL,
    INDEX idx_username (username)
) ENGINE=InnoDB;

-- Personagens
CREATE TABLE characters (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    account_id  INT NOT NULL,
    name        VARCHAR(20) NOT NULL UNIQUE,
    class       TINYINT NOT NULL,              -- 0=DK, 1=DW, 2=Elf, etc.
    level       INT NOT NULL DEFAULT 1,
    exp         BIGINT NOT NULL DEFAULT 0,
    str         INT NOT NULL DEFAULT 18,
    agi         INT NOT NULL DEFAULT 18,
    vit         INT NOT NULL DEFAULT 18,
    ene         INT NOT NULL DEFAULT 18,
    hp          INT NOT NULL DEFAULT 110,
    mp          INT NOT NULL DEFAULT 60,
    max_hp      INT NOT NULL DEFAULT 110,
    max_mp      INT NOT NULL DEFAULT 60,
    zen         BIGINT NOT NULL DEFAULT 0,
    map_id      INT NOT NULL DEFAULT 0,
    pos_x       SMALLINT NOT NULL DEFAULT 130,
    pos_y       SMALLINT NOT NULL DEFAULT 130,
    is_online   TINYINT(1) NOT NULL DEFAULT 0,
    created_at  DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
    INDEX idx_level (level DESC),
    INDEX idx_online (is_online)
) ENGINE=InnoDB;

-- Inventário
CREATE TABLE inventory (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    char_id     INT NOT NULL,
    slot        TINYINT NOT NULL,              -- slot 0-127
    item_id     INT NOT NULL,
    item_level  TINYINT NOT NULL DEFAULT 0,
    item_opt    TINYINT NOT NULL DEFAULT 0,
    quantity    INT NOT NULL DEFAULT 1,
    is_equipped TINYINT(1) NOT NULL DEFAULT 0,
    FOREIGN KEY (char_id) REFERENCES characters(id) ON DELETE CASCADE,
    UNIQUE KEY uq_char_slot (char_id, slot)
) ENGINE=InnoDB;

-- Definição de monstros (substitui Monster.txt)
CREATE TABLE monsters (
    id          INT PRIMARY KEY,
    name        VARCHAR(50) NOT NULL,
    level       INT NOT NULL,
    hp          INT NOT NULL,
    mp          INT NOT NULL DEFAULT 0,
    atk_min     INT NOT NULL,
    atk_max     INT NOT NULL,
    def         INT NOT NULL,
    exp_reward  INT NOT NULL,
    zen_reward  INT NOT NULL DEFAULT 0,
    move_type   TINYINT NOT NULL DEFAULT 1,
    atk_type    TINYINT NOT NULL DEFAULT 0,
    respawn_sec INT NOT NULL DEFAULT 30
) ENGINE=InnoDB;

-- Spawn de monstros por mapa (substitui MonsterSetBase.txt)
CREATE TABLE map_spawns (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    map_id      INT NOT NULL,
    monster_id  INT NOT NULL,
    x1          SMALLINT NOT NULL,
    y1          SMALLINT NOT NULL,
    x2          SMALLINT NOT NULL,
    y2          SMALLINT NOT NULL,
    quantity    INT NOT NULL DEFAULT 1,
    respawn_sec INT NOT NULL DEFAULT 30,
    FOREIGN KEY (monster_id) REFERENCES monsters(id) ON DELETE CASCADE
) ENGINE=InnoDB;

Servidor Node.js + Express (API)

// server.js — servidor principal

const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const mysql = require('mysql2/promise');

const app = express();
app.use(express.json());

// Pool de conexões MySQL
const pool = mysql.createPool({
    host:     'localhost',
    user:     'mu_app',
    password: 'SenhaMuApp@2026!',
    database: 'mu_origin_pvt',
    connectionLimit: 20
});

const JWT_SECRET = 'seu_secret_jwt_aqui_muito_secreto_2026';

// === ENDPOINTS REST ===

// Registro de conta
app.post('/auth/register', async (req, res) => {
    const { username, password, email } = req.body;
    
    if (!username || !password || username.length < 4) {
        return res.status(400).json({ error: 'Dados inválidos' });
    }
    
    try {
        const hash = await bcrypt.hash(password, 12);
        await pool.execute(
            'INSERT INTO accounts (username, password, email) VALUES (?, ?, ?)',
            [username, hash, email]
        );
        res.json({ success: true, message: 'Conta criada' });
    } catch (err) {
        if (err.code === 'ER_DUP_ENTRY') {
            return res.status(409).json({ error: 'Username já em uso' });
        }
        res.status(500).json({ error: 'Erro interno' });
    }
});

// Login
app.post('/auth/login', async (req, res) => {
    const { username, password } = req.body;
    
    const [rows] = await pool.execute(
        'SELECT id, password, is_banned FROM accounts WHERE username = ?',
        [username]
    );
    
    if (!rows.length) return res.status(401).json({ error: 'Credenciais inválidas' });
    if (rows[0].is_banned) return res.status(403).json({ error: 'Conta banida' });
    
    const valid = await bcrypt.compare(password, rows[0].password);
    if (!valid) return res.status(401).json({ error: 'Credenciais inválidas' });
    
    // Atualizar last_login
    await pool.execute('UPDATE accounts SET last_login = NOW() WHERE id = ?', [rows[0].id]);
    
    const token = jwt.sign({ accountId: rows[0].id, username }, JWT_SECRET, { expiresIn: '24h' });
    res.json({ token, accountId: rows[0].id });
});

// Middleware de autenticação JWT
function authMiddleware(req, res, next) {
    const token = req.headers.authorization?.replace('Bearer ', '');
    if (!token) return res.status(401).json({ error: 'Token necessário' });
    
    try {
        req.user = jwt.verify(token, JWT_SECRET);
        next();
    } catch {
        res.status(401).json({ error: 'Token inválido' });
    }
}

// Listar personagens da conta
app.get('/player/characters', authMiddleware, async (req, res) => {
    const [chars] = await pool.execute(
        'SELECT id, name, class, level, is_online FROM characters WHERE account_id = ?',
        [req.user.accountId]
    );
    res.json({ characters: chars });
});

// Dados completos do personagem
app.get('/player/character/:id', authMiddleware, async (req, res) => {
    const [chars] = await pool.execute(
        'SELECT * FROM characters WHERE id = ? AND account_id = ?',
        [req.params.id, req.user.accountId]
    );
    
    if (!chars.length) return res.status(404).json({ error: 'Personagem não encontrado' });
    
    const char = chars[0];
    
    // Buscar inventário
    const [items] = await pool.execute(
        'SELECT slot, item_id, item_level, item_opt, quantity, is_equipped FROM inventory WHERE char_id = ?',
        [char.id]
    );
    
    res.json({ character: char, inventory: items });
});

// Ranking
app.get('/ranking', async (req, res) => {
    const [ranking] = await pool.execute(
        `SELECT c.name, c.class, c.level, c.is_online,
                ROW_NUMBER() OVER (ORDER BY c.level DESC) AS pos
         FROM characters c
         JOIN accounts a ON a.id = c.account_id
         WHERE a.is_banned = 0
         ORDER BY c.level DESC
         LIMIT 50`
    );
    res.json({ ranking });
});

// === WEBSOCKET PARA TEMPO REAL ===

const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

const onlinePlayers = new Map();  // charId → { ws, character, position }

wss.on('connection', (ws) => {
    let playerId = null;
    
    ws.on('message', async (data) => {
        let msg;
        try { msg = JSON.parse(data); } catch { return; }
        
        switch (msg.type) {
            case 'AUTH':
                // Autenticar WebSocket com o mesmo JWT
                try {
                    const user = jwt.verify(msg.token, JWT_SECRET);
                    const [chars] = await pool.execute(
                        'SELECT * FROM characters WHERE id = ? AND account_id = ?',
                        [msg.charId, user.accountId]
                    );
                    if (!chars.length) { ws.close(); return; }
                    
                    playerId = msg.charId;
                    onlinePlayers.set(playerId, { ws, character: chars[0], pos: { x: chars[0].pos_x, y: chars[0].pos_y } });
                    
                    await pool.execute('UPDATE characters SET is_online = 1 WHERE id = ?', [playerId]);
                    
                    ws.send(JSON.stringify({ type: 'AUTH_OK', character: chars[0] }));
                } catch {
                    ws.close();
                }
                break;
                
            case 'MOVE':
                // Movimento do personagem
                if (!playerId) return;
                const player = onlinePlayers.get(playerId);
                if (!player) return;
                
                player.pos = { x: msg.x, y: msg.y };
                player.character.map_id = msg.mapId;
                
                // Notificar jogadores próximos
                broadcastToNearby(playerId, {
                    type: 'PLAYER_MOVE',
                    charId: playerId,
                    name: player.character.name,
                    x: msg.x, y: msg.y
                });
                break;
                
            case 'CHAT':
                // Mensagem de chat
                if (!playerId) return;
                const sender = onlinePlayers.get(playerId);
                if (!sender) return;
                
                broadcastToMap(sender.character.map_id, {
                    type: 'CHAT',
                    name: sender.character.name,
                    message: msg.message.substring(0, 100)  // limitar tamanho
                });
                break;
        }
    });
    
    ws.on('close', async () => {
        if (playerId) {
            const player = onlinePlayers.get(playerId);
            if (player) {
                // Salvar posição e marcar offline
                await pool.execute(
                    'UPDATE characters SET is_online = 0, pos_x = ?, pos_y = ? WHERE id = ?',
                    [player.pos.x, player.pos.y, playerId]
                );
            }
            onlinePlayers.delete(playerId);
        }
    });
});

// Broadcast para jogadores no mesmo mapa
function broadcastToMap(mapId, message) {
    const json = JSON.stringify(message);
    onlinePlayers.forEach(({ ws, character }) => {
        if (character.map_id === mapId && ws.readyState === WebSocket.OPEN) {
            ws.send(json);
        }
    });
}

// Broadcast para jogadores próximos (raio de 15 tiles)
function broadcastToNearby(fromCharId, message, radius = 15) {
    const from = onlinePlayers.get(fromCharId);
    if (!from) return;
    
    const json = JSON.stringify(message);
    onlinePlayers.forEach(({ ws, character, pos }, charId) => {
        if (charId === fromCharId || character.map_id !== from.character.map_id) return;
        const dist = Math.abs(pos.x - from.pos.x) + Math.abs(pos.y - from.pos.y);
        if (dist <= radius && ws.readyState === WebSocket.OPEN) {
            ws.send(json);
        }
    });
}

server.listen(3000, () => console.log('Servidor MU Origin rodando na porta 3000'));

Modificar o APK para apontar para o servidor privado

Para o app Android se conectar ao servidor privado:

Passo 1 — Extrair o APK

# Instalar ferramentas necessárias
sudo apt install apktool jadx default-jdk -y

# Descompilar APK
apktool d MuOrigin.apk -o mu-origin-src/

Passo 2 — Encontrar a URL do servidor

# Buscar strings com URL do servidor oficial
grep -r "webzen" mu-origin-src/ --include="*.smali"
grep -r "muorigin" mu-origin-src/ --include="*.smali"
grep -r "https://" mu-origin-src/ --include="*.xml"

# Também no código Java descompilado
jadx -d mu-origin-decompiled/ MuOrigin.apk
grep -r "api.muorigin" mu-origin-decompiled/ --include="*.java"

Passo 3 — Substituir URLs

# Substituir URL oficial pela do servidor privado
find mu-origin-src/ -name "*.smali" -exec \
    sed -i 's|api.muorigin.webzen.com|api.meuservidor.com|g' {} \;

find mu-origin-src/ -name "*.xml" -exec \
    sed -i 's|api.muorigin.webzen.com|api.meuservidor.com|g' {} \;

Passo 4 — Recompilar e assinar o APK

# Recompilar
apktool b mu-origin-src/ -o MuOrigin-privado-unsigned.apk

# Criar keystore para assinar (uma vez)
keytool -genkey -v -keystore meu-servidor.keystore \
    -alias mu-privado -keyalg RSA -keysize 2048 -validity 10000 \
    -storepass "SenhaKeystore@2026" -keypass "SenhaChave@2026" \
    -dname "CN=Servidor Privado,O=MU,C=BR"

# Assinar o APK
apksigner sign --ks meu-servidor.keystore \
    --ks-key-alias mu-privado \
    --ks-pass pass:SenhaKeystore@2026 \
    --key-pass pass:SenhaChave@2026 \
    --out MuOrigin-privado.apk \
    MuOrigin-privado-unsigned.apk

# Verificar assinatura
apksigner verify MuOrigin-privado.apk

Nginx como proxy reverso

# /etc/nginx/sites-available/mu-origin
server {
    listen 443 ssl;
    server_name api.meuservidor.com;
    
    ssl_certificate     /etc/letsencrypt/live/api.meuservidor.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.meuservidor.com/privkey.pem;
    
    # API REST
    location /auth/ {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    location /player/ {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    
    location /ranking {
        proxy_pass http://localhost:3000;
    }
    
    # WebSocket (necessário headers específicos)
    location /ws {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

Checklist servidor mobile

INFRAESTRUTURA
[ ] VPS Linux (Ubuntu 22.04) com 2+ GB RAM
[ ] Node.js 18+ instalado
[ ] MySQL 8.0 instalado e configurado
[ ] Nginx configurado como proxy reverso
[ ] Certificado SSL (Let's Encrypt via certbot)
[ ] PM2 para manter o Node.js rodando

BANCO DE DADOS
[ ] Schema criado (accounts, characters, inventory, monsters, map_spawns)
[ ] Monstros populados (Monster.txt convertido para SQL)
[ ] Spawns configurados (MonsterSetBase.txt convertido para SQL)

SERVIDOR
[ ] Endpoints REST funcionando (register, login, characters)
[ ] WebSocket funcionando (AUTH, MOVE, CHAT)
[ ] JWT validação funcionando
[ ] Rate limiting nas rotas de auth

APK
[ ] APK extraído e URLs substituídas
[ ] APK recompilado e assinado
[ ] Instalação testada no Android
[ ] Conexão ao servidor privado funcionando

SEGURANÇA
[ ] Senhas em bcrypt
[ ] JWT com expiração curta (24h)
[ ] HTTPS obrigatório (sem HTTP)
[ ] Rate limiting nas rotas

Perguntas frequentes

MU Origin mobile usa os mesmos arquivos do MU Online PC?

Não — MU Origin é um jogo completamente diferente, desenvolvido separadamente. Não usa MuServer, SQL Server ou GameServer.exe. A arquitetura é REST API + WebSocket, com banco MySQL e servidor Node.js ou PHP.

É possível criar um servidor privado de MU Origin?

Tecnicamente sim, mas é muito mais complexo que o MU PC. Requer reverter engenharia do APK, implementar servidor compatível com o protocolo do jogo, e manter a infraestrutura de API.

O MU Origin original e o MU Online PC se conectam entre si?

Não — são jogos completamente separados. MU Origin é baseado no universo de MU Online mas é um produto mobile independente da Webzen, desenvolvido pela WebzenMobile.

Há distribuições prontas de servidor MU Origin como há para MU PC?

Distribuições de MU Origin são muito mais raras e menos estáveis que as de MU PC. A maioria das tentativas de servidor privado mobile tem duração limitada por falta de manutenção.

EQ

Equipe editorial do ViciadosMU — portal de MU Online no ar desde 2003.

Continue lendo

Artigos relacionados