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

Como criar um sistema de reset via site no MU Online

Implemente um sistema de reset via site no MU Online com validação segura em PHP, stored procedures no SQL Server e proteção contra fraudes e resets duplicados.

BR Bruno · Atualizado em 10 jul 2025 · ⏱ 18 min de leitura
Resposta rápida

Oferecer o reset direto pelo site é uma das funcionalidades mais requisitadas por jogadores de servidores privados de MU Online. Em vez de digitar /reset dentro do jogo ou depender de um NPC, o jogador acessa o painel de conta, escolhe o personagem e confirma o reset em poucos cliques. Parece simple

Oferecer o reset direto pelo site é uma das funcionalidades mais requisitadas por jogadores de servidores privados de MU Online. Em vez de digitar /reset dentro do jogo ou depender de um NPC, o jogador acessa o painel de conta, escolhe o personagem e confirma o reset em poucos cliques. Parece simples, mas por trás dessa comodidade existe uma cadeia de validações críticas: nível mínimo, saldo, status online, limites de reset e — acima de tudo — segurança contra manipulação. Um sistema de reset mal feito é a porta de entrada mais comum para exploits que arruínam a economia de um servidor.

Este tutorial mostra como construir um sistema de reset via site robusto, com a lógica pesada dentro de uma stored procedure no SQL Server e uma camada PHP fina, segura e auditável. Os nomes de campos e tabelas apresentados são um EXEMPLO comum de Season 6; a estrutura exata varia por versão (Season 6, Season 9, seasons modernas e emuladores como IGCN, MuEmu, X-Team têm diferenças). Sempre confirme o esquema do seu banco antes de aplicar. Se você ainda não tem a base do servidor e do site rodando, comece pelo guia de como criar servidor de MU Online e volte aqui depois.

Pré-requisitos

Antes de começar, garanta que você tem o ambiente pronto:

  • Servidor de MU Online funcional (GameServer + ConnectServer + banco de dados) rodando.
  • SQL Server (2008, 2014, 2017 ou 2019) com acesso via SQL Server Management Studio (SSMS) e permissão db_owner no banco MuOnline.
  • Site em PHP 7.4+ hospedado e conectado ao banco, idealmente via PDO com o driver sqlsrv ou dblib.
  • Sistema de login/sessão do painel de conta já funcionando (o jogador precisa estar autenticado para resetar).
  • Backup recente do banco de dados. Nunca teste stored procedures de UPDATE em massa em produção sem backup.

Passo 1 — Entender a estrutura de reset no banco

O reset é, em essência, um UPDATE na tabela Character que zera o nível, restaura os atributos base, incrementa o contador de resets e concede pontos. O primeiro passo é descobrir exatamente quais colunas sua versão usa. Rode no SSMS:

SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Character'
  AND (COLUMN_NAME LIKE '%Reset%'
    OR COLUMN_NAME LIKE '%Level%'
    OR COLUMN_NAME LIKE '%Point%'
    OR COLUMN_NAME IN ('Strength','Dexterity','Vitality','Energy','Leadership'));

Os campos que interessam num EXEMPLO típico de Season 6:

CampoFunçãoObservação
cLevelNível atual do personagemZerado para 1 no reset
Resets ou ResetCountContador acumulado de resetsVaria por versão
LevelUpPointPontos de atributo disponíveisAlguns emus usam AddPoint
Strength/Dexterity/Vitality/EnergyAtributosRestaurados ao valor base da classe
LeadershipComando (apenas Dark Lord)Zerar ou preservar conforme regra
ExperienceEXP atualZerado no reset
MapNumber/MapPosX/MapPosYPosiçãoMover para Lorencia/spawn
ConnectStatStatus online0 = offline; usado para bloquear reset online

Anote os nomes corretos — todo o resto do tutorial se apoia neles.

Passo 2 — Criar a stored procedure de reset

A regra de ouro: a lógica de reset mora no banco, não no PHP. Colocar tudo dentro de uma stored procedure com transação garante atomicidade (ou tudo acontece, ou nada acontece) e reduz drasticamente a superfície de ataque. O PHP apenas chama a procedure e interpreta o retorno.

USE MuOnline;
GO

IF OBJECT_ID('dbo.WZ_ResetViaSite', 'P') IS NOT NULL
    DROP PROCEDURE dbo.WZ_ResetViaSite;
GO

CREATE PROCEDURE dbo.WZ_ResetViaSite
    @AccountID   VARCHAR(10),
    @CharName    VARCHAR(10),
    @MinLevel    INT = 400,
    @MaxReset    INT = 0,      -- 0 = sem limite
    @ZenCost     BIGINT = 0
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    DECLARE @Level INT, @Reset INT, @Money BIGINT, @Online TINYINT, @Owner VARCHAR(10);

    BEGIN TRANSACTION;

    -- Bloqueia a linha do personagem durante a leitura para evitar corrida
    SELECT @Level = cLevel, @Reset = Resets, @Money = Money, @Online = ConnectStat
    FROM dbo.Character WITH (UPDLOCK, ROWLOCK)
    WHERE Name = @CharName;

    -- Confirma que o personagem pertence à conta autenticada
    SELECT @Owner = AccountID
    FROM dbo.Character
    WHERE Name = @CharName;

    IF @Owner IS NULL OR @Owner <> @AccountID
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -10 AS Result, 'Personagem nao pertence a esta conta.' AS Message;
        RETURN;
    END

    IF @Online <> 0
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -1 AS Result, 'Saia do jogo antes de resetar.' AS Message;
        RETURN;
    END

    IF @Level < @MinLevel
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -2 AS Result, 'Nivel insuficiente para reset.' AS Message;
        RETURN;
    END

    IF @MaxReset > 0 AND @Reset >= @MaxReset
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -3 AS Result, 'Limite de resets atingido.' AS Message;
        RETURN;
    END

    IF @ZenCost > 0 AND @Money < @ZenCost
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -4 AS Result, 'Zen insuficiente.' AS Message;
        RETURN;
    END

    -- Executa o reset
    UPDATE dbo.Character
    SET cLevel       = 1,
        Resets       = Resets + 1,
        LevelUpPoint = LevelUpPoint + 500,   -- EXEMPLO: pontos por reset
        Experience   = 0,
        Strength     = 20,
        Dexterity    = 20,
        Vitality     = 20,
        Energy       = 20,
        Money        = Money - @ZenCost,
        MapNumber    = 0,     -- Lorencia
        MapPosX      = 125,
        MapPosY      = 125
    WHERE Name = @CharName;

    -- Log de auditoria
    INSERT INTO dbo.ResetLog (AccountID, CharName, ResetNumber, ResetDate, Origem)
    VALUES (@AccountID, @CharName, @Reset + 1, GETDATE(), 'SITE');

    COMMIT TRANSACTION;

    SELECT 1 AS Result, 'Reset realizado com sucesso.' AS Message;
END
GO

Crie também a tabela de log, essencial para suporte e detecção de abuso:

CREATE TABLE dbo.ResetLog (
    ID          INT IDENTITY(1,1) PRIMARY KEY,
    AccountID   VARCHAR(10),
    CharName    VARCHAR(10),
    ResetNumber INT,
    ResetDate   DATETIME,
    Origem      VARCHAR(10)
);

> Dica de balanceamento: a fórmula de pontos por reset e o custo em Zen devem ser calibrados junto com as rates de EXP. Servidores de alta rate costumam dar de 300 a 700 pontos por reset; low rate raramente passa de 100.

Passo 3 — Configurar a conexão PDO no PHP

Centralize a conexão em um único arquivo para reaproveitar em todo o site. Nunca deixe credenciais no repositório público — use variáveis de ambiente ou um arquivo config.php fora da raiz web.

<?php
// db.php
function getDB(): PDO {
    $host = getenv('MU_DB_HOST') ?: '127.0.0.1';
    $db   = 'MuOnline';
    $user = getenv('MU_DB_USER');
    $pass = getenv('MU_DB_PASS');

    $dsn = "sqlsrv:Server={$host};Database={$db}";
    $options = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ];
    return new PDO($dsn, $user, $pass, $options);
}

Passo 4 — Construir o formulário de reset

O formulário deve listar apenas os personagens da conta logada e incluir um token CSRF. Nunca confie em campos ocultos com o nome do personagem sem revalidar a propriedade no servidor.

<?php
session_start();
require 'db.php';

if (empty($_SESSION['account_id'])) {
    header('Location: /login.php');
    exit;
}

// Gera token CSRF de uso único
if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}

$pdo = getDB();
$stmt = $pdo->prepare(
    'SELECT Name, cLevel, Resets FROM Character WHERE AccountID = ? ORDER BY Name'
);
$stmt->execute([$_SESSION['account_id']]);
$chars = $stmt->fetchAll();
?>
<form method="post" action="/reset_processar.php">
  <input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf']) ?>">
  <select name="char" required>
    <?php foreach ($chars as $c): ?>
      <option value="<?= htmlspecialchars($c['Name']) ?>">
        <?= htmlspecialchars($c['Name']) ?> — Nível <?= (int)$c['cLevel'] ?> — Resets: <?= (int)$c['Resets'] ?>
      </option>
    <?php endforeach; ?>
  </select>
  <button type="submit">Resetar personagem</button>
</form>

Passo 5 — Processar o reset com segurança

Este é o coração do sistema. O script valida o token CSRF, chama a stored procedure com parâmetros vinculados e interpreta o código de retorno. Toda a validação de regra de negócio já aconteceu no banco — aqui apenas orquestramos e mostramos o resultado.

<?php
session_start();
require 'db.php';

if (empty($_SESSION['account_id'])) { http_response_code(403); exit('Nao autenticado.'); }

// Valida CSRF
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(400); exit('Token invalido. Recarregue a pagina.');
}
unset($_SESSION['csrf']); // token de uso unico

$char = trim($_POST['char'] ?? '');
if ($char === '' || strlen($char) > 10) { exit('Personagem invalido.'); }

$pdo = getDB();
$sql = 'EXEC dbo.WZ_ResetViaSite
            @AccountID = :acc,
            @CharName  = :char,
            @MinLevel  = :minlv,
            @MaxReset  = :maxrst,
            @ZenCost   = :zen';
$stmt = $pdo->prepare($sql);
$stmt->execute([
    ':acc'    => $_SESSION['account_id'],
    ':char'   => $char,
    ':minlv'  => 400,
    ':maxrst' => 0,
    ':zen'    => 5000000,   // EXEMPLO de custo
]);
$res = $stmt->fetch();

if (($res['Result'] ?? -99) == 1) {
    echo 'Sucesso: ' . htmlspecialchars($res['Message']);
} else {
    echo 'Falha: ' . htmlspecialchars($res['Message'] ?? 'Erro desconhecido.');
}

Passo 6 — Bloquear o reset de personagem online

O ponto mais delicado do reset via site é o personagem estar dentro do jogo. Enquanto online, o GameServer mantém os dados em memória e, ao salvar (auto-save periódico ou logout), sobrescreve o que o site alterou — o jogador perderia o reset ou, pior, ganharia pontos e voltaria ao nível antigo. Por isso a procedure valida ConnectStat <> 0. Em algumas versões o campo é ConnectStat na tabela MEMB_STAT (associada à conta) e não em Character; ajuste conforme sua build. A abordagem mais segura é combinar as duas verificações e, se possível, integrar com uma flag de "kick" para forçar a saída antes do reset.

Passo 7 — Adicionar limites e proteção contra abuso

Além do token CSRF, implemente:

  • Rate limiting: no máximo 1 reset a cada X segundos por conta, controlado por um timestamp em sessão ou numa coluna LastResetSite.
  • Confirmação dupla: uma etapa de "tem certeza?" reduz resets acidentais e cliques automatizados.
  • Auditoria: consulte periodicamente a ResetLog procurando contas com muitos resets em curto intervalo.

Erros comuns e soluções

ErroCausa provávelSolução
Reset some após o jogador entrar no jogoPersonagem estava online no momento do resetBloquear reset com ConnectStat <> 0; pedir logout
Pontos não aparecem no personagemCampo errado (LevelUpPoint x AddPoint)Confirmar coluna com INFORMATION_SCHEMA
Reset dobrado em cliques rápidosFalta de transação/lock e token de uso únicoUPDLOCK na procedure + CSRF de uso único
Erro "conversion failed" no PHPTipo de parâmetro incompatível (BIGINT x INT)Alinhar tipos entre PDO e a procedure
Personagem de outra conta é resetadoNão valida propriedadeComparar AccountID na procedure e no PHP
SQL Injection no nomeConcatenação de string na queryUsar sempre prepared statements/parâmetros

Checklist de lançamento

  • Backup do banco MuOnline realizado antes de qualquer teste.
  • Nomes de colunas confirmados via INFORMATION_SCHEMA na sua versão.
  • Stored procedure WZ_ResetViaSite criada e testada no SSMS.
  • Tabela ResetLog criada e recebendo registros.
  • Conexão PDO usando prepared statements, sem concatenação.
  • Token CSRF de uso único no formulário e no processador.
  • Bloqueio de reset para personagem online validado.
  • Validação de propriedade do personagem (AccountID) em duas camadas.
  • Rate limiting por conta configurado.
  • Custo em Zen/WCoin e pontos por reset calibrados com as rates.
  • Teste ponta a ponta com conta de homologação antes de abrir ao público.

Com esse desenho, o reset via site fica rápido para o jogador e tranquilo para você: a lógica sensível vive protegida no banco, cada operação é atômica e auditável, e a camada PHP permanece pequena e difícil de explorar. Ajuste as fórmulas de pontos, os limites e os custos conforme o perfil do seu servidor e teste sempre em homologação antes de subir para produção.

Perguntas frequentes

O jogador precisa deslogar para o reset via site funcionar?

Sim, na maioria das versões. Enquanto o personagem está online, o GameServer mantém os dados em memória e sobrescreve o banco ao salvar. Bloqueie o reset se o personagem estiver com ConnectStat diferente de 0 ou peça que o jogador saia do jogo antes.

Como impedir que o jogador dê reset duas vezes clicando rápido?

Use uma transação SQL com bloqueio de linha e valide o nível novamente dentro da stored procedure. No PHP, adicione um token CSRF de uso único e um lock por sessão para evitar cliques duplicados.

Onde fica o contador de resets no banco de dados?

Varia por versão. Em muitas builds de Season 6 o campo é Character.Resets ou Character.ResetCount. Em outras fica em uma tabela separada. Confira com a query INFORMATION_SCHEMA antes de codar.

Posso cobrar Zen ou WCoin pelo reset no site?

Sim. Adicione a validação e o decremento do saldo dentro da mesma stored procedure, na mesma transação do reset, para garantir atomicidade. Nunca faça o débito em uma query separada da que zera o nível.

O reset via site é seguro contra SQL Injection?

Somente se você usar PDO com prepared statements ou parâmetros nomeados em stored procedures. Nunca concatene o nome do personagem direto na query. Trate todo dado vindo do formulário como hostil.

BR
Editor de eventos, mapas e itens

Bruno é especialista em eventos, mapas, bosses e economia de itens do MU Online. Documenta cada detalhe com base em jogo real.

Continue lendo

Artigos relacionados