Cómo crear cambio de nick y clase vía web en MU Online
Implementa el cambio de nick y de clase vía web en MU Online con validación de nombre, actualización segura de claves foráneas y cobro opcional en WCoin.
Cambiar el nombre (nick) o la clase de un personaje son dos de los servicios de pago más solicitados en servidores privados de MU Online. Un jugador quiere renombrar el personaje que creó con prisas; otro se cansó del Dark Wizard y quiere convertirse en Dark Knight sin empezar de cero. Ofrecer estos
Cambiar el nombre (nick) o la clase de un personaje son dos de los servicios de pago más solicitados en servidores privados de MU Online. Un jugador quiere renombrar el personaje que creó con prisas; otro se cansó del Dark Wizard y quiere convertirse en Dark Knight sin empezar de cero. Ofrecer estos cambios desde la web es cómodo y rentable, pero también es una de las operaciones más peligrosas de implementar. A diferencia del reset y del PK Clear, que tocan pocas columnas de una fila, el cambio de nick toca relaciones: guild, lista de amigos, mensajes, rankings. Hacerlo mal genera personajes huérfanos de guild, ítems perdidos e inconsistencias difíciles de rastrear.
Este tutorial muestra cómo construir el cambio de nick y de clase vía web de forma segura y atómica, usando stored procedures en SQL Server y una capa PHP escueta. Los nombres de campos y tablas son un EJEMPLO de Season 6; la estructura exacta varía según la versión y el emulador. Mapea siempre las dependencias del nombre en tu base de datos antes de aplicar. Si aún no tienes servidor y web funcionando, empieza por cómo crear servidor de MU Online.
Requisitos previos
- Servidor de MU Online funcional con base de datos
MuOnlineaccesible. - SQL Server (2008/2014/2017/2019) con SSMS y permiso
db_owner. - Web en PHP 7.4+ conectada vía PDO (
sqlsrvodblib). - Panel de cuenta con login/sesión funcionando.
- Copia de seguridad reciente y entorno de homologación para pruebas.
Paso 1 — Mapear todas las dependencias del nombre del personaje
Antes de cambiar un solo carácter, descubre dónde está referenciado el nombre del personaje. El nombre suele ser la clave lógica usada en varias tablas. Ejecuta:
SELECT t.name AS Tabela, c.name AS Coluna
FROM sys.columns c
JOIN sys.tables t ON t.object_id = c.object_id
WHERE c.name IN ('Name', 'Char', 'CharName', 'Member', 'GameID')
ORDER BY t.name;
En un EJEMPLO común de Season 6, el nombre aparece en:
| Tabla | Columna | Rol |
|---|---|---|
Character | Name | Registro principal del personaje |
Guild | G_Master | Nombre del máster de la guild |
GuildMember | Name | Miembros de la guild |
Friend | Name / FriendName | Lista de amigos |
MEMB_INFO (vía AccountCharacter) | — | Slots de personaje por cuenta |
warehouse/Ranking | dependiente | Referencias diversas |
Ignorar cualquiera de ellas genera inconsistencia: por ejemplo, renombrar en Character pero no en GuildMember hace que el personaje "desaparezca" de la guild. La lista varía según la versión — mapea la tuya con la query de arriba.
Paso 2 — Reglas de validación del nuevo nombre
Un nombre inválido puede colgar el cliente del juego o abrir espacio para exploits. Aplica validación rigurosa tanto en PHP como en el procedure:
- Solo letras y números; sin espacios, acentos ni símbolos.
- Longitud típica de 1 a 10 caracteres (varía según la versión).
- Verificación de duplicidad case-insensitive (evita que coexistan "Player" y "player").
- Lista de palabras prohibidas (nombres reservados, ofensas, "GM", "Admin").
<?php
function nomeValido(string $nome): bool {
if (!preg_match('/^[A-Za-z0-9]{1,10}$/', $nome)) return false;
$proibidos = ['gm', 'admin', 'gamemaster', 'moderador'];
return !in_array(strtolower($nome), $proibidos, true);
}
Paso 3 — Stored procedure de cambio de nick
El cambio de nombre debe ser atómico: o se actualizan todas las tablas, o ninguna. Una transacción con XACT_ABORT ON lo garantiza. El procedure también valida propiedad, estado online, duplicidad y cobro.
USE MuOnline;
GO
IF OBJECT_ID('dbo.WZ_TrocaNickViaSite', 'P') IS NOT NULL
DROP PROCEDURE dbo.WZ_TrocaNickViaSite;
GO
CREATE PROCEDURE dbo.WZ_TrocaNickViaSite
@AccountID VARCHAR(10),
@OldName VARCHAR(10),
@NewName VARCHAR(10),
@WCoinCost INT = 0
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @Owner VARCHAR(10), @Online TINYINT, @WCoin INT;
BEGIN TRANSACTION;
SELECT @Owner = AccountID, @Online = ConnectStat
FROM dbo.Character WITH (UPDLOCK, ROWLOCK)
WHERE Name = @OldName;
IF @Owner IS NULL OR @Owner <> @AccountID
BEGIN
ROLLBACK TRANSACTION;
SELECT -10 AS Result, 'El personaje no pertenece a esta cuenta.' AS Message;
RETURN;
END
IF @Online <> 0
BEGIN
ROLLBACK TRANSACTION;
SELECT -1 AS Result, 'Sal del juego antes de cambiar el nick.' AS Message;
RETURN;
END
-- Duplicidad case-insensitive
IF EXISTS (SELECT 1 FROM dbo.Character WHERE Name = @NewName)
BEGIN
ROLLBACK TRANSACTION;
SELECT -2 AS Result, 'Este nombre ya esta en uso.' AS Message;
RETURN;
END
-- Cobro en WCoin (EJEMPLO: campo en MEMB_INFO)
IF @WCoinCost > 0
BEGIN
SELECT @WCoin = WCoin FROM dbo.MEMB_INFO WHERE memb___id = @AccountID;
IF @WCoin < @WCoinCost
BEGIN
ROLLBACK TRANSACTION;
SELECT -3 AS Result, 'WCoin insuficiente.' AS Message;
RETURN;
END
UPDATE dbo.MEMB_INFO SET WCoin = WCoin - @WCoinCost WHERE memb___id = @AccountID;
END
-- Actualiza TODAS las referencias (ajusta segun tu version)
UPDATE dbo.Character SET Name = @NewName WHERE Name = @OldName;
UPDATE dbo.GuildMember SET Name = @NewName WHERE Name = @OldName;
UPDATE dbo.Guild SET G_Master = @NewName WHERE G_Master = @OldName;
UPDATE dbo.Friend SET Name = @NewName WHERE Name = @OldName;
UPDATE dbo.Friend SET FriendName = @NewName WHERE FriendName = @OldName;
INSERT INTO dbo.NickChangeLog (AccountID, OldName, NewName, ChangeDate)
VALUES (@AccountID, @OldName, @NewName, GETDATE());
COMMIT TRANSACTION;
SELECT 1 AS Result, 'Nick cambiado con exito.' AS Message;
END
GO
Crea la tabla de log:
CREATE TABLE dbo.NickChangeLog (
ID INT IDENTITY(1,1) PRIMARY KEY,
AccountID VARCHAR(10),
OldName VARCHAR(10),
NewName VARCHAR(10),
ChangeDate DATETIME
);
> Algunas bases de datos definen las claves foráneas con ON UPDATE CASCADE. Si es el caso de tu versión, actualizar solo Character.Name propaga automáticamente. Confirma con sp_fkeys 'Character' antes de decidir si necesitas los UPDATEs manuales.
Paso 4 — Stored procedure de cambio de clase
El cambio de clase es conceptualmente más simple (toca una fila) pero tiene efectos colaterales: los atributos e ítems equipados pueden quedar incompatibles con la nueva clase. El enfoque seguro es cambiar el Class, resetear los atributos base y devolver los puntos para redistribución.
USE MuOnline;
GO
IF OBJECT_ID('dbo.WZ_TrocaClasseViaSite', 'P') IS NOT NULL
DROP PROCEDURE dbo.WZ_TrocaClasseViaSite;
GO
CREATE PROCEDURE dbo.WZ_TrocaClasseViaSite
@AccountID VARCHAR(10),
@CharName VARCHAR(10),
@NewClass TINYINT, -- EJEMPLO: 0=DW, 16=DK, 32=Elf, 48=MG, 64=DL
@WCoinCost INT = 0
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
DECLARE @Owner VARCHAR(10), @Online TINYINT, @WCoin INT, @Points INT, @Level INT;
BEGIN TRANSACTION;
SELECT @Owner = AccountID, @Online = ConnectStat,
@Level = cLevel
FROM dbo.Character WITH (UPDLOCK, ROWLOCK)
WHERE Name = @CharName;
IF @Owner IS NULL OR @Owner <> @AccountID
BEGIN
ROLLBACK TRANSACTION;
SELECT -10 AS Result, 'El personaje no pertenece a esta cuenta.' AS Message;
RETURN;
END
IF @Online <> 0
BEGIN
ROLLBACK TRANSACTION;
SELECT -1 AS Result, 'Sal del juego antes de cambiar de clase.' AS Message;
RETURN;
END
IF @WCoinCost > 0
BEGIN
SELECT @WCoin = WCoin FROM dbo.MEMB_INFO WHERE memb___id = @AccountID;
IF @WCoin < @WCoinCost
BEGIN
ROLLBACK TRANSACTION;
SELECT -3 AS Result, 'WCoin insuficiente.' AS Message;
RETURN;
END
UPDATE dbo.MEMB_INFO SET WCoin = WCoin - @WCoinCost WHERE memb___id = @AccountID;
END
-- Puntos totales aproximados para redistribuir (EJEMPLO)
SET @Points = 300 + (@Level * 4);
UPDATE dbo.Character
SET Class = @NewClass,
Strength = 20,
Dexterity = 20,
Vitality = 20,
Energy = 20,
Leadership = CASE WHEN @NewClass = 64 THEN 25 ELSE 0 END,
LevelUpPoint = @Points
WHERE Name = @CharName;
INSERT INTO dbo.ClassChangeLog (AccountID, CharName, NewClass, ChangeDate)
VALUES (@AccountID, @CharName, @NewClass, GETDATE());
COMMIT TRANSACTION;
SELECT 1 AS Result, 'Clase cambiada con exito. Redistribuye tus puntos en el juego.' AS Message;
END
GO
Paso 5 — Procesador PHP unificado
Un único procesador atiende ambos cambios, siempre con CSRF, validación y parámetros vinculados.
<?php
session_start();
require 'db.php';
require 'validacao.php'; // funcion nomeValido()
if (empty($_SESSION['account_id'])) { http_response_code(403); exit('No autenticado.'); }
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) {
http_response_code(400); exit('Token invalido.');
}
unset($_SESSION['csrf']);
$acc = $_SESSION['account_id'];
$char = trim($_POST['char'] ?? '');
$acao = $_POST['acao'] ?? '';
$pdo = getDB();
if ($acao === 'nick') {
$novo = trim($_POST['novo_nome'] ?? '');
if (!nomeValido($novo)) { exit('Nombre invalido.'); }
$stmt = $pdo->prepare('EXEC dbo.WZ_TrocaNickViaSite
@AccountID=:acc, @OldName=:old, @NewName=:new, @WCoinCost=:wc');
$stmt->execute([':acc'=>$acc, ':old'=>$char, ':new'=>$novo, ':wc'=>200]);
} elseif ($acao === 'classe') {
$classe = (int)($_POST['nova_classe'] ?? -1);
$permitidas = [0, 16, 32, 48, 64]; // EJEMPLO
if (!in_array($classe, $permitidas, true)) { exit('Clase invalida.'); }
$stmt = $pdo->prepare('EXEC dbo.WZ_TrocaClasseViaSite
@AccountID=:acc, @CharName=:char, @NewClass=:cls, @WCoinCost=:wc');
$stmt->execute([':acc'=>$acc, ':char'=>$char, ':cls'=>$classe, ':wc'=>300]);
} else {
exit('Accion desconocida.');
}
$res = $stmt->fetch();
echo (($res['Result'] ?? -99) == 1)
? 'Exito: ' . htmlspecialchars($res['Message'])
: 'Fallo: ' . htmlspecialchars($res['Message'] ?? 'Error.');
Paso 6 — Interfaz en el panel de cuenta
Muestra los personajes de la cuenta y ofrece las dos acciones. Un pequeño JS ajusta el formulario según la acción elegida.
document.querySelectorAll('input[name="acao"]').forEach(radio => {
radio.addEventListener('change', e => {
document.getElementById('campo-nick').hidden = e.target.value !== 'nick';
document.getElementById('campo-classe').hidden = e.target.value !== 'classe';
});
});
Paso 7 — Bloquear la operación con el personaje online
Como en el reset y el PK Clear, el personaje online es el mayor riesgo: el GameServer sobrescribe la base de datos al guardar, revirtiendo el cambio y pudiendo cobrar sin entregar. Los procedures ya bloquean con ConnectStat <> 0. Confirma dónde reside ese campo en tu versión y orienta al jugador a salir del juego antes de cualquier cambio.
Errores comunes y soluciones
| Error | Causa probable | Solución |
|---|---|---|
| El personaje desaparece de la guild tras cambiar el nick | Solo se actualizó Character.Name | Actualizar GuildMember/Guild en la misma transacción |
| Nombre duplicado aceptado | Comprobación case-sensitive | Validar la duplicidad sin diferenciar mayúsculas |
| Crash del cliente tras cambiar de clase | Ítems equipados incompatibles | Desequipar/mover los ítems de la nueva clase |
| Cambio revertido al entrar al juego | El personaje estaba online | Bloquear con ConnectStat <> 0 |
| WCoin debitado sin cambio completado | Débito fuera de la transacción | Débito y cambio en la misma transacción |
| Inyección por el nombre nuevo | Sin validación/regex | Regex estricto + prepared statements |
Lista de verificación de lanzamiento
- Copia de seguridad de la base de datos
MuOnlinehecha antes de las pruebas. - Todas las tablas que referencian el nombre mapeadas en tu versión.
- Claves foráneas verificadas (
sp_fkeys 'Character'). - Códigos numéricos de clase confirmados en tu base de datos.
- Procedures
WZ_TrocaNickViaSiteyWZ_TrocaClasseViaSiteprobados en SSMS. - Tablas
NickChangeLogyClassChangeLogcreadas. - Validación de nombre (regex + palabras prohibidas + duplicidad) activa.
- Token CSRF de un solo uso en el formulario y en el procesador.
- Bloqueo de cambio para personaje online validado.
- Tratamiento de ítems incompatibles en el cambio de clase probado.
- Costo en WCoin/Zen definido y validado dentro de la transacción.
- Prueba de punta a punta en homologación antes de abrir al público.
El cambio de nick y de clase son servicios de alto valor percibido, pero exigen un cuidado redoblado por tocar relaciones entre tablas. Manteniendo toda la lógica dentro de transacciones atómicas en la base de datos, mapeando correctamente las dependencias del nombre y tratando los ítems incompatibles en el cambio de clase, entregas un servicio confiable y monetizable sin generar personajes rotos. Prueba siempre en homologación y ajusta los códigos de clase y las tablas a las particularidades de tu versión.
Preguntas frecuentes
¿Cambiar el nombre del personaje rompe la guild, los amigos y los ítems?
Puede romperse si actualizas solo la tabla Character. El nombre del personaje suele estar referenciado en GuildMember, Friend y otras tablas. Hay que actualizarlas todas dentro de la misma transacción, o usar cascada.
¿El jugador necesita estar offline para cambiar el nick o la clase?
Sí. Con el personaje online, el GameServer sobrescribe la base de datos al guardar. Bloquea la operación cuando ConnectStat sea distinto de 0 y pide el logout antes.
¿Cómo validar un nuevo nombre de personaje?
Aplica un regex que acepte solo letras y números (longitud típica hasta 10), rechaza nombres ya existentes con verificación case-insensitive y bloquea palabras prohibidas. Haz la comprobación de duplicidad dentro de la transacción.
¿Cambiar de clase modifica los atributos y las habilidades?
El cambio de Class en la tabla Character altera la clase base, pero los atributos, ítems equipados y skills pueden quedar incompatibles. Lo más seguro es resetear stats y desequipar los ítems que la nueva clase no puede usar durante el cambio.
¿Cómo saber el código numérico de cada clase?
El campo Class es un entero. En Season 6, los valores base comunes son 0 (Dark Wizard), 16 (Dark Knight), 32 (Elf), 48 (Magic Gladiator), 64 (Dark Lord). Esto varía según la versión, confírmalo en tu base de datos.