Cómo integrar el Cash Shop (WCoin) al sitio del servidor de MU
Conecta el Cash Shop del juego al sitio de tu servidor de MU Online gestionando el saldo de WCoin, WCoinP y Goblin Point con transacciones atómicas en SQL, panel de recarga y entrega segura de créditos.
El Cash Shop es donde el servidor de MU Online transforma el engagement en ingresos, y el sitio es la puerta de entrada de ese flujo. Cuando un jugador recarga créditos en el panel web, espera abrir la tienda dentro del juego y ver el saldo ahí. Hacer que ese puente parezca instantáneo y, sobre todo
El Cash Shop es donde el servidor de MU Online transforma el engagement en ingresos, y el sitio es la puerta de entrada de ese flujo. Cuando un jugador recarga créditos en el panel web, espera abrir la tienda dentro del juego y ver el saldo ahí. Hacer que ese puente parezca instantáneo y, sobre todo, a prueba de fallas es lo que separa a un sistema de cash confiable de uno que genera quejas y perjuicios. En este tutorial vas a construir la integración entre el sitio y el Cash Shop cuidando lo que realmente importa: dónde vive el saldo en la base, cómo acreditarlo con seguridad transaccional y cómo evitar créditos duplicados o perdidos. Los nombres de columnas y tablas varían según la versión del emulador, así que trata cada fragmento de código como un ejemplo a adaptar a tu schema.
Antes de tocar cualquier cosa, es importante entender que el Cash Shop del cliente MU no habla con el sitio directamente. Lee un saldo numérico que está en la tabla de cuentas de la base. El sitio, por lo tanto, no "conversa" con la tienda: solo altera ese número. Toda la integración se resume en manipular ese saldo de forma correcta y en registrar cada movimiento. Esta claridad mental evita que busques APIs que no existen y te enfoques en la parte que de verdad importa, que es la base de datos.
Requisitos previos
Si el servidor todavía no está en línea, sigue primero la guía de cómo crear un servidor de MU Online. Con el servidor listo, necesitas:
- SQL Server con la base
MuOnliney acceso a la tabla de cuentas (MEMB_INFOo equivalente de tu versión). - PHP 7.4 o superior con la extensión
sqlsrv/pdo_sqlsrvhabilitada. - Sistema de login del sitio ya funcionando, con la sesión del jugador autenticada.
- Usuario SQL específico del sitio con permiso de lectura y escritura solo en las columnas de saldo y en las tablas de log, nunca con
db_owner. - Conocimiento de las columnas de moneda de tu versión: WCoinC, WCoinP, GoblinPoint o los nombres equivalentes.
- Un gateway de pago ya elegido (el crédito solo debe liberarse tras la confirmación del pago).
Confirma antes que nada qué columnas de moneda existen en tu base. Una consulta rápida evita dolores de cabeza:
SELECT COLUMN_NAME, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MEMB_INFO'
AND COLUMN_NAME IN ('WCoinC', 'WCoinP', 'GoblinPoint', 'GameIDC');
Si las columnas no aparecen con esos nombres, búscalas en la documentación de tu emulador. Alterar la columna equivocada hace que el saldo se sume en un lugar que la tienda del juego no lee.
Dónde vive el saldo de moneda
Las tres monedas más comunes del Cash Shop están, en la mayoría de las versiones, en la propia tabla de cuentas. Entender el papel de cada una orienta lo que el sitio debe acreditar.
| Moneda | Columna típica | Uso común | Origen en el sitio |
|---|---|---|---|
| WCoin / WCoinC | WCoinC | Moneda principal comprable | Recarga pagada |
| WCoinP | WCoinP | Moneda premium o de bonus | Bonus, promociones |
| Goblin Point | GoblinPoint | Moneda de eventos y tiendas alternativas | Recompensas, eventos |
El Cash Shop dentro del juego lee esos campos cuando el jugador abre la tienda. Esto significa que el trabajo del sitio es, esencialmente, ejecutar un UPDATE seguro en esas columnas y registrar la operación. Toda la complejidad está en hacer ese UPDATE de forma que nunca acredite de más ni de menos, incluso frente a fallas de red, clics dobles y webhooks repetidos.
Modelado de las tablas de control
Nunca acredites directo sin registrar. Crea dos tablas de apoyo: una para el historial de transacciones y otra que sirve de traba de idempotencia. En muchos casos una única tabla bien modelada cumple los dos papeles.
CREATE TABLE CashShop_Transacao (
Id BIGINT IDENTITY(1,1) PRIMARY KEY,
ContaId VARCHAR(10) NOT NULL,
Moeda VARCHAR(20) NOT NULL, -- 'WCoinC', 'WCoinP', 'GoblinPoint'
Quantidade INT NOT NULL,
SaldoAntes INT NOT NULL,
SaldoDepois INT NOT NULL,
Origem VARCHAR(40) NOT NULL, -- 'mercadopago', 'admin', 'bonus'
GatewayRef VARCHAR(80) NULL, -- ID del pago (idempotencia)
CriadoEm DATETIME NOT NULL DEFAULT GETDATE()
);
GO
-- El índice único garantiza que el mismo pago nunca acredite dos veces
CREATE UNIQUE INDEX UX_CashShop_GatewayRef
ON CashShop_Transacao (GatewayRef)
WHERE GatewayRef IS NOT NULL;
GO
El índice único filtrado es la pieza central de la seguridad. Vuelve físicamente imposible grabar dos transacciones con el mismo GatewayRef. Si un webhook llega duplicado, la segunda inserción falla en la base, y tu código trata ese error como "ya procesado". No necesitas confiar solo en la lógica de la aplicación; la base impone la regla.
Guardar SaldoAntes y SaldoDepois parece redundante, pero es oro para la auditoría. Cuando un jugador abre una disputa diciendo que no recibió, tienes el valor exacto antes y después de cada operación, con sello de tiempo. Esto resuelve quejas en segundos.
El procedimiento de crédito con transacción
El corazón del sistema es la operación de acreditar. Necesita ser atómica: leer el saldo, sumar, grabar el nuevo saldo y registrar la transacción, todo dentro de una única transacción SQL. Una stored procedure encapsula esta lógica y la vuelve reutilizable.
CREATE PROCEDURE dbo.CreditarWCoin
@ContaId VARCHAR(10),
@Quantidade INT,
@Origem VARCHAR(40),
@GatewayRef VARCHAR(80) = NULL
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- cualquier error revierte la transacción
BEGIN TRAN;
-- Traba la fila de la cuenta para una lectura consistente
DECLARE @saldoAntes INT;
SELECT @saldoAntes = WCoinC
FROM MEMB_INFO WITH (UPDLOCK, ROWLOCK)
WHERE memb___id = @ContaId;
IF @saldoAntes IS NULL
BEGIN
ROLLBACK TRAN;
THROW 50001, 'Conta inexistente', 1;
END
-- Idempotencia: si ya fue procesado, aborta silenciosamente
IF @GatewayRef IS NOT NULL AND EXISTS (
SELECT 1 FROM CashShop_Transacao WHERE GatewayRef = @GatewayRef)
BEGIN
ROLLBACK TRAN;
RETURN; -- ya acreditado antes
END
UPDATE MEMB_INFO
SET WCoinC = WCoinC + @Quantidade
WHERE memb___id = @ContaId;
INSERT INTO CashShop_Transacao
(ContaId, Moeda, Quantidade, SaldoAntes, SaldoDepois, Origem, GatewayRef)
VALUES
(@ContaId, 'WCoinC', @Quantidade, @saldoAntes,
@saldoAntes + @Quantidade, @Origem, @GatewayRef);
COMMIT TRAN;
END
GO
Tres detalles hacen que esta procedure sea segura. El XACT_ABORT ON garantiza que cualquier error revierte todo automáticamente. El WITH (UPDLOCK, ROWLOCK) en la lectura del saldo traba la fila de la cuenta para que dos ejecuciones simultáneas no lean el mismo saldo y se sobrescriban una a la otra, lo que causaría pérdida de crédito. Y el chequeo de GatewayRef da la segunda capa de idempotencia, complementando el índice único. Con esto, incluso dos webhooks llegando en el mismo milisegundo resultan en un único crédito.
Capa PHP que llama al crédito
En PHP, la llamada queda limpia porque toda la complejidad está en la procedure. El papel de PHP es validar quién está pidiendo y pasar los parámetros con binding.
<?php
// lib/cashshop.php
require_once __DIR__ . '/../config/db.php';
function creditarWCoin(string $contaId, int $qtd, string $origem, ?string $ref = null): bool {
if ($qtd <= 0) return false;
$conn = getDB();
$sql = "{CALL dbo.CreditarWCoin(?, ?, ?, ?)}";
$params = [
[$contaId, SQLSRV_PARAM_IN],
[$qtd, SQLSRV_PARAM_IN],
[$origem, SQLSRV_PARAM_IN],
[$ref, SQLSRV_PARAM_IN],
];
$stmt = sqlsrv_query($conn, $sql, $params);
if ($stmt === false) {
error_log('Erro ao creditar WCoin: ' . print_r(sqlsrv_errors(), true));
return false;
}
return true;
}
El uso de parámetros vinculados (?) es innegociable. Nunca armes la llamada concatenando el $contaId o la cantidad directamente en la cadena, porque eso abre la puerta a la inyección de SQL. Con el binding, el driver trata cada valor como dato, no como comando, y el ataque simplemente no funciona.
Panel de recarga en el sitio
El panel que ve el jugador organiza los paquetes de créditos y lleva al pago. El crédito en sí solo se libera después de que el gateway confirma, nunca en el clic del botón.
<?php
// recarga.php
session_start();
require_once __DIR__ . '/../lib/cashshop.php';
if (empty($_SESSION['conta_mu'])) {
header('Location: /login');
exit;
}
$pacotes = [
'p1' => ['wcoin' => 1000, 'preco' => 'R$ 10,00'],
'p2' => ['wcoin' => 2500, 'preco' => 'R$ 20,00', 'bonus' => 300],
'p3' => ['wcoin' => 6000, 'preco' => 'R$ 40,00', 'bonus' => 1000],
];
// Consultar el saldo actual para mostrarlo
$conn = getDB();
$stmt = sqlsrv_query($conn,
"SELECT WCoinC, WCoinP FROM MEMB_INFO WHERE memb___id = ?",
[[$_SESSION['conta_mu'], SQLSRV_PARAM_IN]]);
$saldo = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);
?>
<h1>Recarga de WCoin</h1>
<p>Saldo actual: <strong><?= (int)$saldo['WCoinC'] ?> WCoin</strong></p>
<div class="pacotes">
<?php foreach ($pacotes as $id => $p): ?>
<div class="pacote">
<h3><?= number_format($p['wcoin'], 0, ',', '.') ?> WCoin</h3>
<?php if (!empty($p['bonus'])): ?>
<span class="bonus">+<?= $p['bonus'] ?> de bonus</span>
<?php endif; ?>
<p class="preco"><?= $p['preco'] ?></p>
<a href="/pagamento?pacote=<?= $id ?>" class="btn">Comprar</a>
</div>
<?php endforeach; ?>
</div>
El botón lleva al flujo de pago, no al crédito. Esta separación es una regla de seguridad fundamental: el crédito sucede exclusivamente en el callback del gateway, cuando tienes certeza de que el dinero entró. Acreditar en el clic permitiría a cualquiera ganar WCoin gratis con solo llamar la URL.
Entrega del crédito tras la confirmación del pago
Cuando el gateway confirma el pago, llama a tu endpoint de callback. Es ahí donde sucede el crédito, usando el ID del pago como clave de idempotencia.
<?php
// callback-pagamento.php (llamado por el gateway, no por el jugador)
require_once __DIR__ . '/../lib/cashshop.php';
// 1) Validar la autenticidad de la notificación (firma/token del gateway)
// La forma varía según el gateway; NUNCA acredites sin validar.
// 2) Recuperar los datos confirmados
$pagamentoId = $notificacao['id']; // ID único del gateway
$statusPago = $notificacao['status'] === 'approved';
$contaId = $notificacao['metadata']['conta'];
$wcoin = (int)$notificacao['metadata']['wcoin'];
// 3) Acreditar solo si fue aprobado, usando el ID como referencia
if ($statusPago) {
$ok = creditarWCoin($contaId, $wcoin, 'gateway', $pagamentoId);
// Si el webhook se repite, el índice único bloquea el segundo crédito
http_response_code($ok ? 200 : 500);
} else {
http_response_code(200); // reconoce la notificación aun sin acreditar
}
Nota que el $pagamentoId se vuelve el GatewayRef de la transacción. Si el gateway reenvía la notificación, algo común y esperado, el segundo intento choca con el índice único y no acredita de nuevo. Esa es la protección que impide que un reintento de red se vuelva WCoin duplicado.
Cuándo ve el jugador el saldo en el juego
Un punto que genera muchas dudas de soporte: acredité en la base, ¿por qué el jugador no lo ve? La respuesta depende de cómo el Cash Shop lee el saldo, y eso varía según la versión. En general hay dos comportamientos:
- Lectura directa de la base cada vez que se abre la tienda: el jugador solo necesita cerrar y reabrir el Cash Shop. Es el caso más común y el más cómodo.
- Saldo en cache en el GameServer: el servidor mantiene el valor en memoria y solo lo recarga al loguear. Aquí el jugador necesita reloguear para ver el crédito.
Deja esto claro en la pantalla de éxito de la recarga, algo como "tu WCoin fue acreditado; reabre la tienda o reloguea para verlo". Un aviso simple elimina la mitad de los tickets de soporte. Si tu emulador ofrece un comando o puerto de recarga en tiempo real en el GameServer, integrar por ahí entrega la mejor experiencia, pero no es obligatorio para un sistema funcional.
Seguridad y buenas prácticas
El sistema de cash maneja dinero real, así que merece un rigor extra. Además del binding de parámetros y la idempotencia ya cubiertos, aplica estos principios:
- Menor privilegio en la base: el usuario SQL del sitio solo necesita
EXECUTEen la procedure de crédito ySELECTen las columnas de saldo, nada más. - Validación de la notificación del gateway: valida la firma o el token de toda notificación de pago; sin eso, cualquiera puede forjar un callback y ganar créditos.
- Log completo e inmutable: nunca permitas
DELETEniUPDATEen la tabla de transacciones al usuario del sitio; es un libro mayor. - Límites de sanidad: rechaza cantidades absurdas de WCoin por transacción; un valor muy por encima del paquete mayor es señal de manipulación.
- HTTPS obligatorio: todo el flujo de recarga y callback tiene que viajar bajo TLS.
Errores comunes y soluciones
| Síntoma | Causa probable | Solución |
|---|---|---|
| El crédito no aparece en el juego | El Cash Shop lee el saldo en cache | Pide al jugador que reloguee; documéntalo en la pantalla |
| El jugador recibió WCoin duplicado | Webhook duplicado sin idempotencia | Agrega el índice único en GatewayRef y el chequeo en la procedure |
| El saldo se sumó en la columna equivocada | Columna de moneda incorrecta | Confirma el nombre real de la columna en el schema antes de acreditar |
| Crédito liberado sin pago | Crédito en el clic en vez del callback | Mueve el crédito al callback validado del gateway |
| Error de deadlock bajo concurrencia | Orden de trabas inconsistente | Usa UPDLOCK, ROWLOCK y mantén la transacción corta |
sqlsrv_query devuelve false al acreditar | Permiso insuficiente | Concede EXECUTE en la procedure al usuario del sitio |
| Alguien forjó una notificación | Callback sin validación de firma | Valida el token/firma de toda notificación antes de acreditar |
Lista de verificación de lanzamiento
- Columnas de moneda (
WCoinC,WCoinP,GoblinPoint) confirmadas en el schema real - Tabla
CashShop_Transacaocreada conSaldoAntesySaldoDepois - Índice único en
GatewayReffuncionando - Stored procedure
CreditarWCoinconXACT_ABORTyUPDLOCK - Usuario SQL del sitio con solo el
EXECUTEySELECTnecesarios - PHP usando parámetros vinculados en toda llamada
- Crédito sucediendo solo en el callback validado del gateway
- Validación de firma/token de la notificación de pago implementada
- Pantalla de éxito avisando sobre reabrir la tienda o reloguear
- Límites de sanidad por transacción configurados
- Todo el flujo bajo HTTPS
- Prueba de webhook duplicado confirmando un crédito único
- Prueba de cuenta inexistente devolviendo un error tratado
Con esta base, el Cash Shop de tu sitio acredita moneda de forma confiable, audita cada centavo y resiste las fallas de red que inevitablemente suceden. La lección que atraviesa todo es la misma: el crédito es dinero, así que trata cada operación como una transacción bancaria, con atomicidad, idempotencia y registro completo.
Preguntas frecuentes
¿Cuál es la diferencia entre WCoin, WCoinP y Goblin Point?
WCoin (también llamado WCoinC) es la moneda de créditos comprable, WCoinP es la moneda premium o de bonus, y Goblin Point es una moneda alternativa usada en algunos eventos y tiendas. El nombre y la columna exacta varían según la versión, pero las tres están en la tabla de cuentas y las lee el Cash Shop dentro del juego.
¿El crédito agregado en el sitio aparece al instante en el juego?
Depende de dónde el Cash Shop lea el saldo. Si lo lee directo de la tabla de cuentas cada vez que se abre la tienda, el jugador solo necesita reabrir el Cash Shop. Algunos servidores mantienen el saldo en cache en el GameServer, y en ese caso el jugador necesita reloguear para actualizar.
¿Por qué necesito una transacción SQL para agregar créditos?
Porque agregar crédito implica leer el saldo, sumar y grabar, además de registrar el log de la compra. Si el proceso falla a la mitad sin transacción, el jugador puede recibir crédito sin registro o pagar sin recibir. La transacción garantiza que o todo sucede, o nada sucede.
¿Cómo evito que alguien reciba crédito dos veces por el mismo pago?
Usa una clave de idempotencia única por transacción, normalmente el ID del pago del gateway, grabada en una tabla de control con índice único. Antes de acreditar, verifica si ese ID ya fue procesado; si ya lo fue, ignóralo. Esto neutraliza los webhooks duplicados y las recargas de página.
¿Necesito una columna nueva en la tabla de cuentas para el WCoin?
En la mayoría de las versiones la columna ya existe, con un nombre como WCoinC, WCoinP o GameIDC según el emulador. Confirma el schema antes de crear cualquier cosa; crear una columna duplicada puede hacer que el Cash Shop del juego lea el saldo equivocado.