Brazil's biggest MU Online portal — since 2003
Tutorial Intermediate Website

How to build a website reset system in MU Online

Implement a website reset system in MU Online with secure PHP validation, stored procedures in SQL Server and protection against fraud and duplicate resets.

BR Bruno · Updated on Jul 10, 2025 · ⏱ 18 min read
Quick answer

Offering the reset directly through the website is one of the most requested features by players on MU Online private servers. Instead of typing /reset inside the game or relying on an NPC, the player opens the account panel, picks the character and confirms the reset in a few clicks. It sounds simp

Offering the reset directly through the website is one of the most requested features by players on MU Online private servers. Instead of typing /reset inside the game or relying on an NPC, the player opens the account panel, picks the character and confirms the reset in a few clicks. It sounds simple, but behind that convenience there is a chain of critical validations: minimum level, balance, online status, reset limits and — above all — security against tampering. A poorly built reset system is the most common entry point for exploits that ruin a server's economy.

This tutorial shows how to build a robust website reset system, with the heavy logic inside a stored procedure in SQL Server and a thin, secure and auditable PHP layer. The field and table names presented are a common Season 6 EXAMPLE; the exact structure varies by version (Season 6, Season 9, modern seasons and emulators like IGCN, MuEmu, X-Team have differences). Always confirm your database schema before applying. If you do not yet have the server and website base running, start with the guide on how to create a MU Online server and come back here afterwards.

Prerequisites

Before starting, make sure you have the environment ready:

  • A functional MU Online server (GameServer + ConnectServer + database) running.
  • SQL Server (2008, 2014, 2017 or 2019) with access via SQL Server Management Studio (SSMS) and db_owner permission on the MuOnline database.
  • A PHP 7.4+ website hosted and connected to the database, ideally via PDO with the sqlsrv or dblib driver.
  • The account panel's login/session system already working (the player must be authenticated to reset).
  • A recent database backup. Never test mass UPDATE stored procedures in production without a backup.

Step 1 — Understand the reset structure in the database

The reset is, in essence, an UPDATE on the Character table that zeroes the level, restores the base attributes, increments the reset counter and grants points. The first step is to find out exactly which columns your version uses. Run this in 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'));

The fields that matter in a typical Season 6 EXAMPLE:

FieldPurposeNote
cLevelCharacter's current levelReset to 1 on reset
Resets or ResetCountAccumulated reset counterVaries by version
LevelUpPointAvailable attribute pointsSome emus use AddPoint
Strength/Dexterity/Vitality/EnergyAttributesRestored to the class base value
LeadershipCommand (Dark Lord only)Zero or preserve as per your rule
ExperienceCurrent EXPZeroed on reset
MapNumber/MapPosX/MapPosYPositionMove to Lorencia/spawn
ConnectStatOnline status0 = offline; used to block reset while online

Write down the correct names — the rest of the tutorial relies on them.

Step 2 — Create the reset stored procedure

The golden rule: the reset logic lives in the database, not in PHP. Putting everything inside a stored procedure with a transaction guarantees atomicity (either everything happens, or nothing does) and drastically reduces the attack surface. PHP only calls the procedure and interprets the return.

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 = no limit
    @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;

    -- Lock the character's row during the read to avoid a race
    SELECT @Level = cLevel, @Reset = Resets, @Money = Money, @Online = ConnectStat
    FROM dbo.Character WITH (UPDLOCK, ROWLOCK)
    WHERE Name = @CharName;

    -- Confirm that the character belongs to the authenticated account
    SELECT @Owner = AccountID
    FROM dbo.Character
    WHERE Name = @CharName;

    IF @Owner IS NULL OR @Owner <> @AccountID
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -10 AS Result, 'Character does not belong to this account.' AS Message;
        RETURN;
    END

    IF @Online <> 0
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -1 AS Result, 'Log out of the game before resetting.' AS Message;
        RETURN;
    END

    IF @Level < @MinLevel
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -2 AS Result, 'Insufficient level for reset.' AS Message;
        RETURN;
    END

    IF @MaxReset > 0 AND @Reset >= @MaxReset
    BEGIN
        ROLLBACK TRANSACTION;
        SELECT -3 AS Result, 'Reset limit reached.' AS Message;
        RETURN;
    END

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

    -- Perform the reset
    UPDATE dbo.Character
    SET cLevel       = 1,
        Resets       = Resets + 1,
        LevelUpPoint = LevelUpPoint + 500,   -- EXAMPLE: points per reset
        Experience   = 0,
        Strength     = 20,
        Dexterity    = 20,
        Vitality     = 20,
        Energy       = 20,
        Money        = Money - @ZenCost,
        MapNumber    = 0,     -- Lorencia
        MapPosX      = 125,
        MapPosY      = 125
    WHERE Name = @CharName;

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

    COMMIT TRANSACTION;

    SELECT 1 AS Result, 'Reset completed successfully.' AS Message;
END
GO

Also create the log table, essential for support and abuse detection:

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

> Balancing tip: the points-per-reset formula and the Zen cost should be calibrated together with the EXP rates. High-rate servers usually give 300 to 700 points per reset; low rate rarely goes above 100.

Step 3 — Set up the PDO connection in PHP

Centralize the connection in a single file to reuse across the whole website. Never leave credentials in the public repository — use environment variables or a config.php file outside the web root.

<?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);
}

Step 4 — Build the reset form

The form should list only the characters of the logged-in account and include a CSRF token. Never trust hidden fields with the character name without revalidating ownership on the server.

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

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

// Generate a single-use CSRF token
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']) ?> — Level <?= (int)$c['cLevel'] ?> — Resets: <?= (int)$c['Resets'] ?>
      </option>
    <?php endforeach; ?>
  </select>
  <button type="submit">Reset character</button>
</form>

Step 5 — Process the reset securely

This is the heart of the system. The script validates the CSRF token, calls the stored procedure with bound parameters and interprets the return code. All the business-rule validation has already happened in the database — here we only orchestrate and display the result.

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

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

// Validate CSRF
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
    http_response_code(400); exit('Invalid token. Reload the page.');
}
unset($_SESSION['csrf']); // single-use token

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

$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,   // EXAMPLE cost
]);
$res = $stmt->fetch();

if (($res['Result'] ?? -99) == 1) {
    echo 'Success: ' . htmlspecialchars($res['Message']);
} else {
    echo 'Failure: ' . htmlspecialchars($res['Message'] ?? 'Unknown error.');
}

Step 6 — Block the reset of an online character

The trickiest point of the website reset is the character being inside the game. While online, the GameServer keeps the data in memory and, when it saves (periodic auto-save or logout), overwrites what the website changed — the player would lose the reset or, worse, gain points and go back to the old level. That is why the procedure validates ConnectStat <> 0. In some versions the field is ConnectStat in the MEMB_STAT table (associated with the account) and not in Character; adjust it to your build. The safest approach is to combine both checks and, if possible, integrate a "kick" flag to force the character out before the reset.

Step 7 — Add limits and abuse protection

Besides the CSRF token, implement:

  • Rate limiting: at most 1 reset every X seconds per account, controlled by a timestamp in the session or in a LastResetSite column.
  • Double confirmation: an "are you sure?" step reduces accidental resets and automated clicks.
  • Auditing: periodically query the ResetLog looking for accounts with many resets in a short interval.

Common errors and fixes

ErrorLikely causeFix
Reset disappears after the player enters the gameThe character was online at the moment of the resetBlock the reset with ConnectStat <> 0; ask for logout
Points do not appear on the characterWrong field (LevelUpPoint vs AddPoint)Confirm the column with INFORMATION_SCHEMA
Doubled reset on fast clicksMissing transaction/lock and single-use tokenUPDLOCK in the procedure + single-use CSRF
"conversion failed" error in PHPIncompatible parameter type (BIGINT vs INT)Align the types between PDO and the procedure
A character from another account is resetOwnership not validatedCompare AccountID in the procedure and in PHP
SQL Injection in the nameString concatenation in the queryAlways use prepared statements/parameters

Launch checklist

  • Backup of the MuOnline database taken before any test.
  • Column names confirmed via INFORMATION_SCHEMA on your version.
  • WZ_ResetViaSite stored procedure created and tested in SSMS.
  • ResetLog table created and receiving records.
  • PDO connection using prepared statements, no concatenation.
  • Single-use CSRF token in the form and in the processor.
  • Reset block for an online character validated.
  • Character ownership validation (AccountID) in two layers.
  • Per-account rate limiting configured.
  • Zen/WCoin cost and points per reset calibrated with the rates.
  • End-to-end test with a staging account before opening to the public.

With this design, the website reset is fast for the player and worry-free for you: the sensitive logic lives protected in the database, each operation is atomic and auditable, and the PHP layer stays small and hard to exploit. Adjust the point formulas, the limits and the costs to your server's profile and always test in staging before pushing to production.

Frequently asked questions

Does the player need to log out for the website reset to work?

Yes, in most versions. While the character is online, the GameServer keeps the data in memory and overwrites the database when it saves. Block the reset if the character has a ConnectStat other than 0, or ask the player to leave the game first.

How do I stop a player from resetting twice by clicking fast?

Use a SQL transaction with a row lock and revalidate the level inside the stored procedure. In PHP, add a single-use CSRF token and a per-session lock to prevent double clicks.

Where is the reset counter stored in the database?

It varies by version. In many Season 6 builds the field is Character.Resets or Character.ResetCount. In others it is in a separate table. Check with an INFORMATION_SCHEMA query before coding.

Can I charge Zen or WCoin for the reset on the website?

Yes. Add the validation and the balance decrement inside the same stored procedure, in the same transaction as the reset, to guarantee atomicity. Never do the debit in a query separate from the one that zeroes the level.

Is the website reset safe against SQL Injection?

Only if you use PDO with prepared statements or named parameters in stored procedures. Never concatenate the character name directly into the query. Treat all data coming from the form as hostile.

BR
Events, maps & items editor

Bruno specializes in MU Online events, maps, bosses and item economy. He documents every detail based on real gameplay.

Keep reading

Related articles