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.
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_ownerpermission on theMuOnlinedatabase. - A PHP 7.4+ website hosted and connected to the database, ideally via PDO with the
sqlsrvordblibdriver. - 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:
| Field | Purpose | Note |
|---|---|---|
cLevel | Character's current level | Reset to 1 on reset |
Resets or ResetCount | Accumulated reset counter | Varies by version |
LevelUpPoint | Available attribute points | Some emus use AddPoint |
Strength/Dexterity/Vitality/Energy | Attributes | Restored to the class base value |
Leadership | Command (Dark Lord only) | Zero or preserve as per your rule |
Experience | Current EXP | Zeroed on reset |
MapNumber/MapPosX/MapPosY | Position | Move to Lorencia/spawn |
ConnectStat | Online status | 0 = 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
LastResetSitecolumn. - Double confirmation: an "are you sure?" step reduces accidental resets and automated clicks.
- Auditing: periodically query the
ResetLoglooking for accounts with many resets in a short interval.
Common errors and fixes
| Error | Likely cause | Fix |
|---|---|---|
| Reset disappears after the player enters the game | The character was online at the moment of the reset | Block the reset with ConnectStat <> 0; ask for logout |
| Points do not appear on the character | Wrong field (LevelUpPoint vs AddPoint) | Confirm the column with INFORMATION_SCHEMA |
| Doubled reset on fast clicks | Missing transaction/lock and single-use token | UPDLOCK in the procedure + single-use CSRF |
| "conversion failed" error in PHP | Incompatible parameter type (BIGINT vs INT) | Align the types between PDO and the procedure |
| A character from another account is reset | Ownership not validated | Compare AccountID in the procedure and in PHP |
| SQL Injection in the name | String concatenation in the query | Always use prepared statements/parameters |
Launch checklist
- Backup of the
MuOnlinedatabase taken before any test. - Column names confirmed via INFORMATION_SCHEMA on your version.
WZ_ResetViaSitestored procedure created and tested in SSMS.ResetLogtable 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.