O maior portal de MU Online do Brasil — desde 2003
Tutorial Intermediário Servidor

Como configurar ranking e VIP no site do servidor de MU

Configure ranking de personagens, guilds e o sistema de VIP no site do servidor de MU Online — queries SQL completas, integração com PHP, páginas de ranking com paginação e painel de conta com VIP.

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

O ranking é o coração do engajamento em servidores de MU Online — jogadores verificam sua posição diariamente. O sistema de VIP é a principal forma de monetização. Ambos são implementados no site PHP integrado com o banco SQL Server.

O ranking é o coração do engajamento em servidores de MU Online — jogadores verificam sua posição diariamente. O sistema de VIP é a principal forma de monetização. Ambos são implementados no site PHP integrado com o banco SQL Server.

Ranking de personagens — query completa

-- Ranking por Resets (mais comum em servidores Season 6+)
SELECT TOP 100
    ROW_NUMBER() OVER (ORDER BY c.Resets DESC, c.cLevel DESC) AS Posicao,
    c.Name            AS Personagem,
    c.Class           AS ClasseNum,
    c.cLevel          AS Level,
    c.Resets,
    c.ConnectStat     AS Online,
    c.AccountID
FROM Character c
INNER JOIN MEMB_INFO m ON m.memb_id = c.AccountID
WHERE c.CtlCode = 0              -- excluir GMs
  AND m.bloc_code = 0            -- excluir contas banidas
ORDER BY c.Resets DESC, c.cLevel DESC;

-- Ranking por Level (para servidores sem reset)
SELECT TOP 100
    ROW_NUMBER() OVER (ORDER BY c.cLevel DESC) AS Posicao,
    c.Name, c.Class, c.cLevel, c.ConnectStat
FROM Character c
WHERE c.CtlCode = 0
ORDER BY c.cLevel DESC;

-- Ranking de Guilds por Score
SELECT TOP 20
    ROW_NUMBER() OVER (ORDER BY g.Score DESC) AS Posicao,
    g.Name       AS GuildName,
    g.Score,
    g.Mark,
    COUNT(u.Id)  AS Membros
FROM Guild g
LEFT JOIN G_UserList u ON u.G_Name = g.Name
WHERE g.Name != ''
  AND g.Score > 0
GROUP BY g.Name, g.Score, g.Mark
ORDER BY g.Score DESC;

PHP — página de ranking completa

<?php
// ranking.php

require_once 'config/db-muserver.php';

// Mapeamento de classes
function getClassName(int $classCode): string {
    // No MU Online, a classe evolui somando ao base:
    // Base 0 = DW, +1 = SM (Soul Master), +2 = GM (Grand Master)
    // Base 16 = DK, +1 = BK, +2 = BM
    // etc.
    $nomes = [
        0  => 'Dark Wizard',     1  => 'Soul Master',       2 => 'Grand Master',
        16 => 'Dark Knight',    17  => 'Blade Knight',      18 => 'Blade Master',
        32 => 'Elf',            33  => 'Muse Elf',          34 => 'High Elf',
        48 => 'Magic Gladiator', 49 => 'Duel Master',
        64 => 'Dark Lord',      65  => 'Lord Emperor',
        80 => 'Summoner',       81  => 'Bloody Summoner',   82 => 'Dimension Master',
        96 => 'Rage Fighter',   97  => 'Fist Master',
       112 => 'Grow Lancer',   113  => 'Mirage Lancer',
       128 => 'Rune Wizard',   129  => 'Rune Spell Master',
       144 => 'Slayer',        145  => 'Royal Slayer',
       160 => 'Gun Crusher',   161  => 'Gun Breaker',
    ];
    
    // Tentar código exato primeiro
    if (isset($nomes[$classCode])) return $nomes[$classCode];
    
    // Fallback: procurar base
    $base = intdiv($classCode, 16) * 16 + ($classCode % 16 <= 2 ? $classCode % 16 : 0);
    return $nomes[$base] ?? "Classe $classCode";
}

// Ícones SVG por classe (opcional — pode usar imagens)
function getClassIcon(int $classCode): string {
    $baseClass = intdiv($classCode, 16) * 16;
    $icons = [
        0  => '🔮', // DW/SM/GM
        16 => '⚔️', // DK/BK/BM
        32 => '🏹', // Elf
        48 => '🗡️', // MG
        64 => '👑', // DL
        80 => '🌀', // Summoner
        96 => '👊', // RF
    ];
    return $icons[$baseClass] ?? '⚔️';
}

$conn = getMuDB();

// Paginação
$page = max(1, (int)($_GET['p'] ?? 1));
$perPage = 50;
$offset = ($page - 1) * $perPage;

// Tipo de ranking
$tipo = $_GET['tipo'] ?? 'resets';

$sql = $tipo === 'level' ? "
    SELECT TOP $perPage
        ROW_NUMBER() OVER (ORDER BY c.cLevel DESC) + $offset AS Pos,
        c.Name, c.Class, c.cLevel, c.Resets, c.ConnectStat
    FROM Character c
    JOIN MEMB_INFO m ON m.memb_id = c.AccountID
    WHERE c.CtlCode = 0 AND m.bloc_code = 0
    ORDER BY c.cLevel DESC
    OFFSET $offset ROWS
" : "
    SELECT
        ROW_NUMBER() OVER (ORDER BY c.Resets DESC, c.cLevel DESC) AS Pos,
        c.Name, c.Class, c.cLevel, c.Resets, c.ConnectStat
    FROM Character c
    JOIN MEMB_INFO m ON m.memb_id = c.AccountID
    WHERE c.CtlCode = 0 AND m.bloc_code = 0
    ORDER BY c.Resets DESC, c.cLevel DESC
    OFFSET $offset ROWS FETCH NEXT $perPage ROWS ONLY
";

$stmt = sqlsrv_query($conn, $sql);

$jogadores = [];
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
    $row['ClasseNome'] = getClassName((int)$row['Class']);
    $row['ClasseIcon'] = getClassIcon((int)$row['Class']);
    $jogadores[] = $row;
}
?>

<div class="ranking-page">
    <div class="ranking-tabs">
        <a href="?tipo=resets" class="<?= $tipo==='resets' ? 'active' : '' ?>">Por Resets</a>
        <a href="?tipo=level"  class="<?= $tipo==='level'  ? 'active' : '' ?>">Por Level</a>
        <a href="?tipo=guild"  class="<?= $tipo==='guild'  ? 'active' : '' ?>">Guilds</a>
    </div>

    <table class="ranking-table">
        <thead>
            <tr>
                <th>#</th>
                <th>Personagem</th>
                <th>Classe</th>
                <th>Level</th>
                <th>Resets</th>
                <th>Status</th>
            </tr>
        </thead>
        <tbody>
        <?php foreach ($jogadores as $j): ?>
            <tr class="<?= (int)$j['ConnectStat'] ? 'row-online' : '' ?>">
                <td class="rank-pos"><?= $j['Pos'] ?></td>
                <td class="rank-name">
                    <?= $j['ClasseIcon'] ?> <?= htmlspecialchars($j['Name']) ?>
                </td>
                <td><?= htmlspecialchars($j['ClasseNome']) ?></td>
                <td class="rank-level"><?= $j['cLevel'] ?></td>
                <td class="rank-resets"><?= $j['Resets'] ?></td>
                <td><?= (int)$j['ConnectStat'] ? '<span class="online">Online</span>' : '' ?></td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>

    <!-- Paginação -->
    <div class="pagination">
        <?php if ($page > 1): ?>
            <a href="?tipo=<?= $tipo ?>&p=<?= $page-1 ?>">&laquo; Anterior</a>
        <?php endif; ?>
        <?php for ($i = max(1, $page-2); $i <= $page+2; $i++): ?>
            <a href="?tipo=<?= $tipo ?>&p=<?= $i ?>" 
               class="<?= $i === $page ? 'current' : '' ?>"><?= $i ?></a>
        <?php endfor; ?>
        <?php if (count($jogadores) === $perPage): ?>
            <a href="?tipo=<?= $tipo ?>&p=<?= $page+1 ?>">Próxima &raquo;</a>
        <?php endif; ?>
    </div>
</div>

Sistema de VIP no site

Tabela de VIP no banco

-- Verificar se MEMB_VIP existe (Season 8+)
SELECT * FROM MEMB_VIP LIMIT 1;

-- Se não existir, criar:
CREATE TABLE MEMB_VIP (
    memb_id    VARCHAR(10)  NOT NULL PRIMARY KEY,
    vip_type   TINYINT      NOT NULL DEFAULT 1,    -- 1=Bronze, 2=Prata, 3=Ouro
    start_date DATETIME     NOT NULL DEFAULT GETDATE(),
    end_date   DATETIME     NOT NULL,
    CONSTRAINT FK_VIP_MEMB FOREIGN KEY (memb_id) REFERENCES MEMB_INFO(memb_id)
);
GO

Benefícios por tipo de VIP no GameServer.ini

; GameServer.ini — configuração de benefícios VIP

[VIP1]                          ; VIP Bronze
VIP1ExpBonus=50                 ; +50% EXP
VIP1DropBonus=20                ; +20% chance de drop
VIP1CommandPoints=5             ; +5 pontos de CMD por level
VIP1MaxSlots=160                ; mais slots no warehouse

[VIP2]                          ; VIP Prata
VIP2ExpBonus=100                ; +100% EXP (2x)
VIP2DropBonus=30
VIP2CommandPoints=10
VIP2MaxSlots=180

[VIP3]                          ; VIP Ouro
VIP3ExpBonus=150                ; +150% EXP (2.5x)
VIP3DropBonus=50
VIP3CommandPoints=15
VIP3MaxSlots=200
VIP3RuudBonus=100               ; +100% Ruud (Season 14+)

VipCheckInterval=60             ; verificar VIP a cada 60 segundos
VipExpiryNotify=1               ; avisar jogador quando VIP vai expirar
VipExpiryNotifyHours=24         ; avisar 24h antes

PHP — ativar VIP no site

<?php
// vip.php — ativar VIP para a conta do jogador logado

require_once 'config/db-muserver.php';
session_start();

if (!isset($_SESSION['conta_mu'])) {
    header('Location: /login');
    exit;
}

$conta = $_SESSION['conta_mu'];

// Tipos de VIP disponíveis
$planos = [
    1 => ['nome' => 'VIP Bronze', 'dias' => 30,  'preco' => 'R$ 15,00', 'cor' => '#cd7f32'],
    2 => ['nome' => 'VIP Prata',  'dias' => 30,  'preco' => 'R$ 25,00', 'cor' => '#c0c0c0'],
    3 => ['nome' => 'VIP Ouro',   'dias' => 30,  'preco' => 'R$ 45,00', 'cor' => '#ffd700'],
];

// Buscar VIP atual
$conn = getMuDB();
$vipStmt = sqlsrv_query($conn,
    "SELECT vip_type, start_date, end_date FROM MEMB_VIP WHERE memb_id = ?",
    [[$conta, SQLSRV_PARAM_IN]]
);
$vipAtual = sqlsrv_fetch_array($vipStmt, SQLSRV_FETCH_ASSOC);

// Função de ativação
function ativarVIP(string $conta, int $tipo, int $dias): void {
    global $conn;
    
    $sql = "
        MERGE MEMB_VIP AS target
        USING (SELECT ? AS memb_id) AS source ON target.memb_id = source.memb_id
        WHEN MATCHED THEN
            UPDATE SET
                vip_type  = ?,
                end_date  = CASE 
                    WHEN end_date > GETDATE() THEN DATEADD(DAY, ?, end_date)
                    ELSE DATEADD(DAY, ?, GETDATE())
                END
        WHEN NOT MATCHED THEN
            INSERT (memb_id, vip_type, start_date, end_date)
            VALUES (?, ?, GETDATE(), DATEADD(DAY, ?, GETDATE()));
    ";
    
    sqlsrv_query($conn, $sql, [
        [$conta, SQLSRV_PARAM_IN],
        [$tipo,  SQLSRV_PARAM_IN],
        [$dias,  SQLSRV_PARAM_IN],
        [$dias,  SQLSRV_PARAM_IN],
        [$conta, SQLSRV_PARAM_IN],
        [$tipo,  SQLSRV_PARAM_IN],
        [$dias,  SQLSRV_PARAM_IN],
    ]);
}

// Processar ativação (após confirmação de pagamento)
// Na prática, isso é chamado pelo webhook do gateway de pagamento
if ($_GET['action'] === 'ativar' && isset($_GET['tipo'])) {
    $tipo = (int)$_GET['tipo'];
    if (isset($planos[$tipo])) {
        ativarVIP($conta, $tipo, $planos[$tipo]['dias']);
        $mensagem = "VIP {$planos[$tipo]['nome']} ativado com sucesso!";
    }
}
?>
<!DOCTYPE html>
<html>
<body>
<h1>VIP - <?= SERVER_NAME ?></h1>

<?php if ($vipAtual): ?>
<div class="vip-atual">
    <h2>Seu VIP Atual</h2>
    <p>Tipo: <strong><?= $planos[$vipAtual['vip_type']]['nome'] ?? 'VIP' ?></strong></p>
    <p>Expira em: <strong><?= $vipAtual['end_date'] ?></strong></p>
</div>
<?php endif; ?>

<div class="planos">
<?php foreach ($planos as $tipo => $plano): ?>
<div class="plano" style="border-color: <?= $plano['cor'] ?>">
    <h3 style="color: <?= $plano['cor'] ?>"><?= $plano['nome'] ?></h3>
    <p><strong><?= $plano['preco'] ?></strong> / <?= $plano['dias'] ?> dias</p>
    <ul>
        <?php if ($tipo >= 1): ?><li>+50% EXP<?= $tipo >= 2 ? ($tipo >= 3 ? '/+150%' : '/+100%') : '' ?></li><?php endif; ?>
        <?php if ($tipo >= 1): ?><li>+20% Drop<?= $tipo >= 2 ? ($tipo >= 3 ? '/+50%' : '/+30%') : '' ?></li><?php endif; ?>
        <?php if ($tipo >= 3): ?><li>+100% Ruud</li><?php endif; ?>
    </ul>
    <a href="/pagamento?plano=<?= $tipo ?>" class="btn-comprar">Ativar</a>
</div>
<?php endforeach; ?>
</div>
</body>
</html>

Cache do ranking para performance

<?php
// Para servidores com muitos jogadores, use cache para o ranking
// O ranking não precisa ser atualizado a cada segundo

function getRankingCached(string $tipo, int $page, int $ttl = 300): array {
    $cacheFile = sys_get_temp_dir() . "/mu_ranking_{$tipo}_{$page}.cache";
    
    // Verificar cache
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
        return unserialize(file_get_contents($cacheFile));
    }
    
    // Cache expirado ou inexistente — buscar do banco
    $conn = getMuDB();
    $offset = ($page - 1) * 50;
    
    $stmt = sqlsrv_query($conn, "
        SELECT Name, Class, cLevel, Resets, ConnectStat
        FROM Character
        WHERE CtlCode = 0
        ORDER BY Resets DESC, cLevel DESC
        OFFSET ? ROWS FETCH NEXT 50 ROWS ONLY
    ", [[$offset, SQLSRV_PARAM_IN]]);
    
    $resultado = [];
    while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
        $resultado[] = $row;
    }
    
    // Salvar cache
    file_put_contents($cacheFile, serialize($resultado));
    
    return $resultado;
}

// Usar:
$ranking = getRankingCached('resets', 1, 300);  // cache de 5 minutos

Perguntas frequentes

O ranking do site atualiza em tempo real?

O ranking é lido do banco a cada requisição — é sempre atualizado. Para performance, adicione cache de 5-10 minutos nos rankings (file cache ou Redis).

Como saber a classe do personagem pelo número na tabela?

O campo Class na tabela Character é um número inteiro. Cada classe tem um código: 0=Dark Wizard, 16=Dark Knight, 32=Elf, 48=Magic Gladiator, 64=Dark Lord, 80=Summoner, 96=Rage Fighter. Evoluções somam +1 ou +2 ao código base.

VIP ativado no site entra imediamente no jogo?

Sim — a tabela MEMB_VIP é lida pelo GameServer a cada verificação de VIP (configurada por VipCheckInterval). O jogador pode precisar relogar para ver os bônus.

Como criar diferentes tipos de VIP (Bronze, Prata, Ouro)?

Configure os tipos no GameServer.ini (VIP1, VIP2, VIP3) e na tabela MEMB_VIP use o campo vip_type (1=Bronze, 2=Prata, 3=Ouro). Cada tipo tem seus parâmetros de bônus.

EQ

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

Continue lendo

Artigos relacionados