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.
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_ownerno bancoMuOnline. - Site em PHP 7.4+ hospedado e conectado ao banco, idealmente via PDO com o driver
sqlsrvoudblib. - 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:
| Campo | Função | Observação |
|---|---|---|
cLevel | Nível atual do personagem | Zerado para 1 no reset |
Resets ou ResetCount | Contador acumulado de resets | Varia por versão |
LevelUpPoint | Pontos de atributo disponíveis | Alguns emus usam AddPoint |
Strength/Dexterity/Vitality/Energy | Atributos | Restaurados ao valor base da classe |
Leadership | Comando (apenas Dark Lord) | Zerar ou preservar conforme regra |
Experience | EXP atual | Zerado no reset |
MapNumber/MapPosX/MapPosY | Posição | Mover para Lorencia/spawn |
ConnectStat | Status online | 0 = 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
ResetLogprocurando contas com muitos resets em curto intervalo.
Erros comuns e soluções
| Erro | Causa provável | Solução |
|---|---|---|
| Reset some após o jogador entrar no jogo | Personagem estava online no momento do reset | Bloquear reset com ConnectStat <> 0; pedir logout |
| Pontos não aparecem no personagem | Campo errado (LevelUpPoint x AddPoint) | Confirmar coluna com INFORMATION_SCHEMA |
| Reset dobrado em cliques rápidos | Falta de transação/lock e token de uso único | UPDLOCK na procedure + CSRF de uso único |
| Erro "conversion failed" no PHP | Tipo de parâmetro incompatível (BIGINT x INT) | Alinhar tipos entre PDO e a procedure |
| Personagem de outra conta é resetado | Não valida propriedade | Comparar AccountID na procedure e no PHP |
| SQL Injection no nome | Concatenação de string na query | Usar sempre prepared statements/parâmetros |
Checklist de lançamento
- Backup do banco
MuOnlinerealizado antes de qualquer teste. - Nomes de colunas confirmados via INFORMATION_SCHEMA na sua versão.
- Stored procedure
WZ_ResetViaSitecriada e testada no SSMS. - Tabela
ResetLogcriada 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.