O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Website

Como criar uma loja de itens web (webshop) para MU Online

Guia completo para construir uma webshop segura para seu servidor de MU Online, integrando saldo de créditos, entrega automática de itens no baú e proteção contra fraudes.

GA Gabriel · Atualizado em 28 jun 2026 · ⏱ 16 min de leitura
Resposta rápida

Uma webshop (loja de itens web) é um dos recursos que mais diferencia um servidor de MU Online amador de um projeto profissional. Ela permite que o jogador troque créditos — comprados via doação ou ganhos em eventos — por itens, asas, kits e serviços diretamente pelo navegador, sem depender de um GM

Uma webshop (loja de itens web) é um dos recursos que mais diferencia um servidor de MU Online amador de um projeto profissional. Ela permite que o jogador troque créditos — comprados via doação ou ganhos em eventos — por itens, asas, kits e serviços diretamente pelo navegador, sem depender de um GM online. Bem construída, ela roda 24 horas, reduz o trabalho manual da equipe e vira uma das principais fontes de receita e retenção do servidor. Mal construída, ela vira um buraco de segurança que dá itens infinitos, corrompe personagens e permite fraude de saldo.

Este tutorial é avançado porque uma webshop toca nas três partes mais sensíveis do seu ecossistema ao mesmo tempo: o banco de dados de contas/personagens, o formato binário dos itens da sua Season e a camada web exposta à internet. Vamos montar uma arquitetura correta do zero, com entrega segura, transações atômicas e proteção contra as fraudes clássicas. Se você ainda não tem o servidor no ar, comece pelo guia base em como criar um servidor de MU Online e volte aqui depois.

Pré-requisitos

Antes de escrever uma linha de código da loja, garanta que você tem o ambiente e o conhecimento mínimos:

  • Servidor de MU Online funcional (GameServer, ConnectServer e DataServer no ar).
  • Banco de dados acessível pela aplicação web — SQL Server (MuOnline/MuOnline DB) na maioria das Seasons, ou MySQL/MariaDB em distros modernas.
  • Website já publicado com PHP 8.1+ (ou a stack que você usa) e conexão ao banco funcionando.
  • Sistema de contas com login funcional no site e um campo de saldo/créditos (WCoin, Credits, Cash ou coluna equivalente).
  • Conhecimento do formato de item da sua Season — índice, tipo, nível, opções, excellent, ancient, luck e skill.
  • Ambiente de testes (uma conta e um personagem descartáveis) para validar entregas antes de liberar para os jogadores.

> Aviso de segurança: nunca desenvolva a webshop direto no banco de produção. Um erro num INSERT de item pode corromper personagens reais. Trabalhe sempre com um dump de teste primeiro.

Como uma webshop realmente entrega itens

Existe uma confusão comum: muita gente acha que a webshop "fala com o servidor". Na prática, a esmagadora maioria das lojas web de MU não conversa diretamente com o GameServer — ela escreve o item na tabela de baú (warehouse) do banco de dados, e o GameServer lê essa tabela quando o jogador abre o baú.

Isso tem uma consequência crítica: o item só aparece de forma confiável quando o personagem está offline. Enquanto o jogador está logado, o servidor mantém uma cópia do inventário e do baú em memória. Se você escrever no banco nesse momento, o servidor sobrescreve tudo ao salvar no logout, e o item comprado some — gerando ticket, revolta e pedido de reembolso.

Por isso, a arquitetura correta é assíncrona:

  1. O jogador compra na web; o saldo é debitado e um pedido é registrado.
  2. A entrega vai para uma fila (tabela webshop_delivery_queue).
  3. Um processo verifica se o personagem está offline.
  4. Quando offline, o item é escrito no baú dentro de uma transação e o pedido é marcado como entregue.
Modelo de entregaComo funcionaPrósContras
Escrita direta no baú (offline)Loja insere item na tabela warehouseSimples, sem mexer no sourceSó funciona com jogador deslogado
Fila + workerPedido enfileirado e processado por serviçoSeguro, auditável, escalávelExige um processo agendado rodando
Socket em tempo realWeb envia comando ao GameServer via source customizadoEntrega instantânea onlineRequer editar e recompilar o source
Cupom/resgate in-gameWeb gera código, jogador resgata via NPC/comandoZero escrita direta no bancoFricção para o jogador

Para 90% dos servidores, o modelo fila + worker é o certo. É o que vamos montar.

Passo 1 — Modelar as tabelas da loja

Crie tabelas próprias da webshop, separadas das tabelas do jogo. Nunca reutilize tabelas do MU para controle da loja. Exemplo em SQL Server (ajuste tipos para MySQL se for o seu caso):

-- Catálogo de produtos da loja
CREATE TABLE WebshopProducts (
    ProductId      INT IDENTITY(1,1) PRIMARY KEY,
    Name           NVARCHAR(120)  NOT NULL,
    Category       NVARCHAR(40)   NOT NULL,
    PriceCredits   INT            NOT NULL,
    ItemHex        VARCHAR(64)    NOT NULL,  -- hex do item para a Season
    DurabilityQty  INT            NOT NULL DEFAULT 1,
    Active         BIT            NOT NULL DEFAULT 1,
    CreatedAt      DATETIME       NOT NULL DEFAULT GETDATE()
);

-- Pedidos (auditoria e idempotência)
CREATE TABLE WebshopOrders (
    OrderId        BIGINT IDENTITY(1,1) PRIMARY KEY,
    AccountId      VARCHAR(20)  NOT NULL,
    CharName       VARCHAR(20)  NOT NULL,
    ProductId      INT          NOT NULL,
    PriceCredits   INT          NOT NULL,
    IdemToken      CHAR(36)     NOT NULL UNIQUE, -- evita compra duplicada
    Status         VARCHAR(20)  NOT NULL DEFAULT 'PAID', -- PAID/QUEUED/DELIVERED/FAILED
    IpAddress      VARCHAR(45)  NULL,
    CreatedAt      DATETIME     NOT NULL DEFAULT GETDATE()
);

-- Fila de entrega
CREATE TABLE WebshopDeliveryQueue (
    QueueId    BIGINT IDENTITY(1,1) PRIMARY KEY,
    OrderId    BIGINT     NOT NULL,
    CharName   VARCHAR(20) NOT NULL,
    ItemHex    VARCHAR(64) NOT NULL,
    Attempts   INT        NOT NULL DEFAULT 0,
    Delivered  BIT        NOT NULL DEFAULT 0,
    CreatedAt  DATETIME   NOT NULL DEFAULT GETDATE()
);

A coluna IdemToken é o coração da proteção contra duplicidade: cada tentativa de compra gera um UUID; o índice UNIQUE garante que a mesma tentativa nunca vire dois pedidos, mesmo que o jogador dê duplo clique ou a conexão caia e ele reenvie.

Passo 2 — Entender o formato do item da sua Season

Este é o ponto que mais quebra webshop. Cada item no MU é representado por uma sequência binária com campos em offsets fixos. O formato varia por versão — abaixo vai um EXEMPLO didático baseado no layout clássico de 32 bytes (estilo Season 6); confirme os offsets na documentação da sua distro antes de usar em produção.

Campo (exemplo)Bytes aprox.Significado
Index/Section0-1Qual item (ex.: 0x0007 = espada X)
Level1Nível de refino +0 a +15
Options (dur)2Opção adicional / durabilidade
Excellent flags1Bitmask das opções excellent
Ancient/Set1Item ancient e set bônus
Luck/SkillbitsFlags de luck e skill
Serial4-8Serial único do item

Um builder em PHP que monta o hex a partir de parâmetros legíveis torna o cadastro de produtos muito menos propenso a erro:

<?php
// EXEMPLO simplificado — os offsets REAIS variam por versão/Season.
function buildItemHex(array $it): string {
    $index   = $it['index']   ?? 0;   // código do item
    $level   = $it['level']   ?? 0;   // 0..15
    $skill   = !empty($it['skill'])  ? 1 : 0;
    $luck    = !empty($it['luck'])   ? 1 : 0;
    $option  = $it['option']  ?? 0;   // 0..3 (opção +4/+8/...)
    $exc     = $it['exc']     ?? 0;   // bitmask excellent (0..63)

    // Byte de nível: normalmente level << 3 combinado com skill bit
    $lvlByte = (($level & 0x0F) << 3) | ($skill << 7);
    // Byte de opção: option nos bits baixos + luck
    $optByte = ($option & 0x03) | ($luck ? 0x04 : 0x00);

    $bytes = [
        $index & 0xFF,
        $lvlByte & 0xFF,
        $optByte & 0xFF,
        $exc & 0xFF,
    ];
    return strtoupper(bin2hex(pack('C*', ...$bytes)));
}

echo buildItemHex(['index'=>7,'level'=>11,'exc'=>0x3F,'luck'=>true,'skill'=>true]);

> Importante: o código acima é ilustrativo. Não copie os offsets cegamente — pegue o layout correto da sua Season (a documentação da distro ou o próprio source ItemManager/ItemConvert). Um offset errado gera item inválido que trava o baú do jogador.

Passo 3 — O fluxo de compra com transação atômica

A compra precisa ser atômica: debitar o saldo e registrar o pedido têm que acontecer juntos ou não acontecer. Nunca debite crédito num comando e insira o pedido em outro sem transação — uma falha no meio deixa o jogador sem crédito e sem item.

<?php
function comprarItem(PDO $pdo, string $accountId, string $charName, int $productId): array {
    $idem = bin2hex(random_bytes(16)); // token de idempotência
    $idem = substr(preg_replace('/(.{8})(.{4})(.{4})(.{4})(.{12})/','$1-$2-$3-$4-$5', $idem),0,36);

    $pdo->beginTransaction();
    try {
        // 1) Trava a linha do produto e valida
        $prod = $pdo->prepare("SELECT PriceCredits, ItemHex, Active FROM WebshopProducts WHERE ProductId = ?");
        $prod->execute([$productId]);
        $p = $prod->fetch(PDO::FETCH_ASSOC);
        if (!$p || !$p['Active']) throw new RuntimeException('Produto indisponível');

        // 2) Debita o saldo SOMENTE se houver crédito suficiente (checagem na cláusula WHERE)
        $upd = $pdo->prepare(
          "UPDATE MEMB_CREDITS SET Credits = Credits - ?
           WHERE AccountId = ? AND Credits >= ?");
        $upd->execute([$p['PriceCredits'], $accountId, $p['PriceCredits']]);
        if ($upd->rowCount() === 0) throw new RuntimeException('Saldo insuficiente');

        // 3) Registra o pedido (UNIQUE(IdemToken) impede duplicidade)
        $ins = $pdo->prepare(
          "INSERT INTO WebshopOrders (AccountId, CharName, ProductId, PriceCredits, IdemToken, Status, IpAddress)
           VALUES (?,?,?,?,?, 'QUEUED', ?)");
        $ins->execute([$accountId,$charName,$productId,$p['PriceCredits'],$idem,$_SERVER['REMOTE_ADDR'] ?? null]);
        $orderId = (int)$pdo->lastInsertId();

        // 4) Enfileira a entrega
        $q = $pdo->prepare(
          "INSERT INTO WebshopDeliveryQueue (OrderId, CharName, ItemHex) VALUES (?,?,?)");
        $q->execute([$orderId, $charName, $p['ItemHex']]);

        $pdo->commit();
        return ['ok'=>true, 'orderId'=>$orderId];
    } catch (\Throwable $e) {
        $pdo->rollBack();
        return ['ok'=>false, 'error'=>$e->getMessage()];
    }
}

Repare em dois detalhes de segurança fundamentais:

  • O débito é feito com WHERE ... AND Credits >= ?. Assim, mesmo com duas requisições simultâneas (race condition), o banco garante que só uma consegue debitar quando o saldo é apertado — a segunda retorna rowCount() === 0.
  • Todas as queries usam prepared statements. Nunca concatene $accountId ou $productId direto no SQL.

Passo 4 — O worker de entrega (com checagem de offline)

O worker é um script executado periodicamente (Task Scheduler no Windows, cron no Linux) que processa a fila. Ele só entrega se o personagem estiver offline.

<?php
// worker_entrega.php — rode a cada 1 minuto
require 'db.php';

$pendentes = $pdo->query(
  "SELECT TOP 50 QueueId, OrderId, CharName, ItemHex
   FROM WebshopDeliveryQueue WHERE Delivered = 0 AND Attempts < 5")->fetchAll(PDO::FETCH_ASSOC);

foreach ($pendentes as $row) {
    // 1) Está online? (tabela MEMB_STAT/ConnectStat varia por versão)
    $st = $pdo->prepare("SELECT ConnectStat FROM MEMB_STAT ms
        JOIN Character c ON c.AccountID = ms.memb___id WHERE c.Name = ?");
    $st->execute([$row['CharName']]);
    $online = (int)($st->fetchColumn() ?: 0);
    if ($online === 1) { continue; } // deixa na fila até deslogar

    $pdo->beginTransaction();
    try {
        // 2) Escreve o item num slot livre do baú (warehouse)
        //    A tabela e os offsets do baú variam por versão.
        $ok = inserirItemNoBau($pdo, $row['CharName'], $row['ItemHex']);
        if (!$ok) throw new RuntimeException('Baú cheio ou slot inválido');

        // 3) Marca como entregue
        $pdo->prepare("UPDATE WebshopDeliveryQueue SET Delivered=1 WHERE QueueId=?")
            ->execute([$row['QueueId']]);
        $pdo->prepare("UPDATE WebshopOrders SET Status='DELIVERED' WHERE OrderId=?")
            ->execute([$row['OrderId']]);
        $pdo->commit();
    } catch (\Throwable $e) {
        $pdo->rollBack();
        $pdo->prepare("UPDATE WebshopDeliveryQueue SET Attempts=Attempts+1 WHERE QueueId=?")
            ->execute([$row['QueueId']]);
    }
}

A função inserirItemNoBau localiza um slot vazio no baú do personagem e grava o blob do item na posição correta. Como o layout do baú (tabela warehouse, coluna Items, tamanho de cada slot) varia por versão, essa parte precisa ser adaptada à sua Season. O princípio, porém, é universal: achar espaço livre, gravar o hex no offset do slot e não estourar a capacidade.

Passo 5 — Front-end e experiência do jogador

No front-end, exiba o catálogo por categorias, o saldo atual e um botão de compra com confirmação. Um ponto de segurança que muitos esquecem: desabilite o botão após o clique para evitar reenvio, e valide tudo de novo no servidor — o front-end nunca é a fonte de verdade.

// Bloqueia duplo clique no botão de compra
const btn = document.querySelector('#comprar');
btn.addEventListener('click', async (e) => {
  btn.disabled = true;
  btn.textContent = 'Processando...';
  try {
    const r = await fetch('/loja/comprar', {
      method: 'POST',
      headers: {'Content-Type':'application/json','X-CSRF-Token': window.csrf},
      body: JSON.stringify({ productId: btn.dataset.pid, char: window.charName })
    });
    const data = await r.json();
    alert(data.ok ? 'Item na fila! Deslogue para receber no baú.' : 'Erro: ' + data.error);
  } finally {
    btn.disabled = false;
    btn.textContent = 'Comprar';
  }
});

Sempre avise o jogador claramente: "Deslogue o personagem para receber o item no baú". Essa única frase corta a maioria dos tickets de "comprei e não veio".

Passo 6 — Precificação e balanceamento

A parte técnica é metade do trabalho; a outra metade é o que você vende. Vender poder puro (itens full excellent, sets +15) transforma o servidor em pay-to-win e afasta a base gratuita. Estratégias saudáveis:

  • Cosméticos e conveniência: mudança de nome, mudança de classe, reset de stats, asas visuais, transformações.
  • Itens de progressão moderada: kits iniciantes, joias em pacote, buffs temporários de XP.
  • Serviços: slot extra de baú, expansão de guild, aluguel de VIP.

Defina o preço em créditos com base em quanto tempo de jogo aquele item representa. Um item que leva 10 horas de farm não pode custar o equivalente a R$ 1 em créditos, ou o farm perde o sentido.

Erros comuns e soluções

SintomaCausa provávelSolução
Item comprado não aparece no baúPersonagem estava online na entregaConfirme a checagem de offline no worker; entregue só com ConnectStat=0
Baú corrompido / trava ao abrirHex do item com offset errado da SeasonRefaça o hex com a tabela correta da sua versão em ambiente de teste
Jogador comprou 2x com 1 cliqueFalta de idempotênciaImplemente o IdemToken UNIQUE e desabilite o botão no front
Saldo ficou negativoDébito sem checagem no WHEREUse UPDATE ... WHERE Credits >= preco e cheque rowCount
Item duplicado após falha de redeRetry sem tokenReenvie sempre o mesmo IdemToken da tentativa original
Loja lenta / trava sob cargaQueries sem índice e sem transaçãoIndexe CharName/Status e envolva compra numa transação
Preço alterado pelo clienteConfiança no valor vindo do frontSempre leia o preço do banco pelo ProductId, nunca do request

Checklist de lançamento

  • Tabelas da loja criadas e separadas das tabelas do jogo
  • Formato de item validado na sua Season com conta de teste
  • Compra roda dentro de transação atômica (débito + pedido + fila)
  • IdemToken UNIQUE ativo contra compras duplicadas
  • Débito de crédito com WHERE Credits >= preco
  • Worker de entrega só processa personagens offline
  • Todas as queries usando prepared statements
  • Botão de compra desabilita após clique e valida CSRF
  • Preços sempre lidos do banco, nunca do front-end
  • Índices em CharName, Status e Delivered para performance
  • Aviso claro "deslogue para receber" visível na loja
  • Logs de auditoria (quem comprou o quê, quando e de qual IP)
  • Testado o cenário de baú cheio (retry sem perder item nem crédito)

Com essa arquitetura você tem uma webshop confiável, auditável e resistente às fraudes clássicas do MU Online. Comece pequeno, com poucos produtos de conveniência, valide a entrega exaustivamente em teste e só então abra para os jogadores.

Perguntas frequentes

A webshop entrega itens com o jogador online?

O ideal é entregar apenas com o personagem offline. Se o jogador estiver logado, o servidor mantém os dados do inventário em memória e a escrita direta no banco será sobrescrita ao deslogar. Enfileire a entrega e processe quando o status estiver offline.

Preciso mexer no código-fonte do GameServer para ter webshop?

Não necessariamente. A maioria das webshops entrega itens escrevendo diretamente na tabela de baú/warehouse do banco. Só é preciso mexer no source se você quiser uma entrega em tempo real via socket, algo mais avançado.

Como gerar o hexadecimal de um item corretamente?

O formato do item varia por versão (Season 6 usa 32 bytes; seasons modernas usam blobs maiores). Use uma tabela de referência da sua Season e um builder que monte índice, nível, opções, luck, skill, excellent e ancient nos offsets corretos.

Webshop dá vantagem pay-to-win e afasta jogadores?

Depende do que você vende. Cosméticos, asas visuais, mudança de nome e serviços de conveniência costumam ser bem aceitos. Vender itens com opções full excellent quebra o balanceamento e esvazia o servidor a médio prazo.

Como evito que um jogador compre o mesmo item duas vezes por duplo clique?

Use uma trava de idempotência: gere um token único por tentativa de compra, debite o saldo dentro de uma transação SQL e registre o pedido antes de enfileirar a entrega. Bloqueie cliques repetidos no front-end também.

GA
Editor de guias e builds

Gabriel cobre gameplay, builds de classes, PvP e progressão. Testa cada estratégia em servidor antes de publicar.

Continue lendo

Artigos relacionados