El mayor portal de MU Online de Brasil — desde 2003
Tutorial Avanzado Servidor

Tienda In-Game Avanzada para Servidor de MU Online

Configura una tienda in-game completa en tu servidor MU Online S6: tablas SQL, NPCs personalizados, múltiples monedas, control de stock y auditoría de transacciones.

EQ Equipo ViciadosMU · Actualizado el 4 jul 2026 · ⏱ 18 min de lectura

Qué es la Tienda In-Game y por qué importa

La tienda in-game es un NPC personalizado que permite a los jugadores intercambiar monedas internas — Zen, Créditos, Ruud o cualquier token definido por el administrador — por ítems, consumibles, buffs temporales y servicios de personaje. En Season 6, esta funcionalidad no existe de forma nativa: se construye sobre tres capas independientes que deben funcionar de forma coordinada.

  • Capa de datos: tablas SQL que definen el catálogo, precios, restricciones y stock
  • Capa de servidor: procedimientos almacenados que validan, debitan y entregan cada compra
  • Capa de mundo: entradas NPC en los archivos de configuración del GameServer que hacen visible al vendedor en el mapa

Una tienda bien calibrada mejora la retención en el early-game, reduce la frustración de los jugadores nuevos y ofrece un sumidero controlado para el Zen excedente que se acumula en el late-game. Mal configurada, acelera la inflación, vacía los mapas de farmeo y mata la progresión en pocas semanas.

Nota: Este tutorial es exclusivamente educativo. Todos los procedimientos descritos aplican a entornos de aprendizaje y desarrollo local. No se aborda la operación comercial de servidores ni la vinculación de monedas internas a pagos reales.

Requisitos Técnicos Previos

Antes de comenzar, verifica que tienes:

  • Servidor MU Online S6 funcional con GameServer, ConnectServer y DataServer activos
  • SQL Server 2008 o superior con acceso db_owner a la base de datos MuOnline
  • SQL Server Management Studio (SSMS) para ejecutar y depurar los scripts
  • Acceso de lectura y escritura al directorio del GameServer (C:\MuServer\GameServer\ o equivalente)
  • Editor de texto plano (Notepad++) para modificar los archivos de configuración

MU Online S6 tiene seis clases jugables con estructuras de equipamiento distintas. La tienda debe respetar estas restricciones mediante bitmasks de clase:

Dark Knight     → Blade Knight    → Blade Master     (bitmask: 1)
Dark Wizard     → Soul Master     → Grand Master     (bitmask: 2)
Fairy Elf       → Muse Elf        → High Elf         (bitmask: 4)
Magic Gladiator → Duel Master     (sin quest 1 ni 2)  (bitmask: 16)
Dark Lord       → Lord Emperor    (stat CMD exclusiva) (bitmask: 32)
Summoner        → Bloody Summoner → Dimension Master  (bitmask: 64)

El valor 0 en RequireClass significa que cualquier clase puede comprar el ítem.


Estructura SQL de la Tienda

La tienda opera sobre una tabla personalizada independiente de las tablas nativas de MU. Crearla por separado facilita el mantenimiento sin riesgo de corromper datos del juego base:

USE MuOnline
GO

CREATE TABLE [dbo].[CustomShop] (
    [ShopID]        INT           NOT NULL IDENTITY(1,1),
    [ShopName]      NVARCHAR(50)  NOT NULL DEFAULT 'General',     -- Categoría visible
    [ItemCategory]  TINYINT       NOT NULL,  -- 0=Armas 1=Armaduras 2=Joyas 3=Consumibles 4=Servicios
    [ItemIndex]     SMALLINT      NOT NULL,
    [ItemLevel]     TINYINT       NOT NULL DEFAULT 0,
    [ItemSkill]     BIT           NOT NULL DEFAULT 0,  -- 1 = ítem con opción Habilidad
    [ItemLuck]      BIT           NOT NULL DEFAULT 0,  -- 1 = ítem con opción Suerte
    [ItemOption]    TINYINT       NOT NULL DEFAULT 0,  -- Valor de opción adicional (0-7)
    [ItemExc]       TINYINT       NOT NULL DEFAULT 0,  -- Bitmask de opciones excelentes
    [PriceZen]      BIGINT        NOT NULL DEFAULT 0,
    [PriceCredit]   INT           NOT NULL DEFAULT 0,
    [PriceRuud]     INT           NOT NULL DEFAULT 0,
    [RequireClass]  TINYINT       NOT NULL DEFAULT 0,
    [RequireLevel]  SMALLINT      NOT NULL DEFAULT 1,
    [StockLimit]    INT           NOT NULL DEFAULT -1,  -- -1 = ilimitado
    [StockCurrent]  INT           NOT NULL DEFAULT -1,  -- -1 = ilimitado
    [Active]        BIT           NOT NULL DEFAULT 1,
    [SortOrder]     INT           NOT NULL DEFAULT 0,
    CONSTRAINT [PK_CustomShop] PRIMARY KEY CLUSTERED ([ShopID])
);

Poblar el catálogo inicial

Un catálogo de inicio orientado a jugadores nuevos en S6. Nota el uso de comentarios con la flecha para trazar el flujo lógico de cada entrada:

-- Consumibles básicos → cualquier clase → desde nivel 1
INSERT INTO [dbo].[CustomShop]
    (ShopName, ItemCategory, ItemIndex, ItemLevel, PriceZen, RequireClass, RequireLevel, StockLimit, StockCurrent)
VALUES
    ('Consumibles', 3, 0,  0,  8000,   0, 1,   -1, -1),  -- Poción de Vida Grande    → Zen 8.000
    ('Consumibles', 3, 1,  0,  8000,   0, 1,   -1, -1),  -- Poción de Mana Grande    → Zen 8.000
    ('Consumibles', 3, 4,  0,  15000,  0, 1,   -1, -1);  -- Poción de Stamina Grande → Zen 15.000

-- Joyas → cualquier clase → desde nivel 50
INSERT INTO [dbo].[CustomShop]
    (ShopName, ItemCategory, ItemIndex, ItemLevel, PriceCredit, RequireClass, RequireLevel, StockLimit, StockCurrent)
VALUES
    ('Joyas',  2, 14, 0, 10, 0, 50,  -1, -1),  -- Joya de Bendición → 10 Créditos
    ('Joyas',  2, 15, 0, 10, 0, 50,  -1, -1),  -- Joya del Alma     → 10 Créditos
    ('Joyas',  2, 16, 0, 50, 0, 100, 20, 20);  -- Joya de Caos      → 50 Créditos → stock: 20 ud/día

-- Armadura de segundo tier → Dark Knight → desde nivel 200
INSERT INTO [dbo].[CustomShop]
    (ShopName, ItemCategory, ItemIndex, ItemLevel, PriceCredit, RequireClass, RequireLevel, StockLimit, StockCurrent)
VALUES
    ('Armaduras DK', 1, 6,  3, 80,  1, 200, -1, -1),  -- Armadura Escarlata +3 → DK
    ('Armaduras DK', 1, 7,  3, 60,  1, 200, -1, -1),  -- Calzas Escarlata +3  → DK
    ('Armaduras DK', 1, 8,  3, 60,  1, 200, -1, -1);  -- Guantes Escarlata +3 → DK

> [!ATENCION] > No incluyas Alas de Nivel 3 en el catálogo. Las Alas L3 (Ala del Dragón, Ala del Alma, etc.) requieren Ala L2 + 3× Loch's Feather + Joya de Creación. La Loch's Feather solo cae de Balgass cuando el evento Crywolf falla. Extraer este ítem de la progresión natural destruye el late-game del servidor: los jugadores dejan de participar en Crywolf y el ciclo de eventos colapsa.


Procedimientos Almacenados de Compra

Validación y entrega

El procedimiento central debe validar saldo, verificar stock, debitar la moneda y registrar el ítem como pendiente — todo dentro de una transacción única:

CREATE PROCEDURE [dbo].[usp_ShopPurchase]
    @AccountID  NVARCHAR(10),
    @CharName   NVARCHAR(10),
    @ShopID     INT,
    @Currency   NVARCHAR(10)  -- 'ZEN', 'CREDIT' o 'RUUD'
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    DECLARE @Price      BIGINT = 0
    DECLARE @Stock      INT
    DECLARE @ZenActual  BIGINT
    DECLARE @CreditAct  INT
    DECLARE @RuudAct    INT

    -- Leer catálogo con bloqueo exclusivo → evita condición de carrera en stock limitado
    SELECT
        @Price  = CASE @Currency
                    WHEN 'ZEN'    THEN PriceZen
                    WHEN 'CREDIT' THEN PriceCredit
                    WHEN 'RUUD'   THEN PriceRuud
                  END,
        @Stock  = StockCurrent
    FROM [dbo].[CustomShop] WITH (UPDLOCK, ROWLOCK)
    WHERE ShopID = @ShopID AND Active = 1

    IF @Price IS NULL
    BEGIN
        SELECT -1 AS Result, 'Ítem no disponible.' AS Message; RETURN
    END

    IF @Stock = 0
    BEGIN
        SELECT -2 AS Result, 'Sin stock disponible.' AS Message; RETURN
    END

    BEGIN TRANSACTION

        -- Validar y debitar según moneda
        IF @Currency = 'ZEN'
        BEGIN
            SELECT @ZenActual = Money FROM [dbo].[Character] WHERE Name = @CharName
            IF @ZenActual < @Price
            BEGIN
                ROLLBACK; SELECT -3 AS Result, 'Zen insuficiente.' AS Message; RETURN
            END
            UPDATE [dbo].[Character] SET Money = Money - @Price WHERE Name = @CharName
        END

        ELSE IF @Currency = 'CREDIT'
        BEGIN
            SELECT @CreditAct = WCoinP FROM [dbo].[AccountCharacter]
            WHERE GameIDC = @AccountID
            IF @CreditAct < @Price
            BEGIN
                ROLLBACK; SELECT -3 AS Result, 'Créditos insuficientes.' AS Message; RETURN
            END
            UPDATE [dbo].[AccountCharacter] SET WCoinP = WCoinP - @Price
            WHERE GameIDC = @AccountID
        END

        ELSE IF @Currency = 'RUUD'
        BEGIN
            SELECT @RuudAct = Ruud FROM [dbo].[AccountCharacter]
            WHERE GameIDC = @AccountID
            IF @RuudAct < @Price
            BEGIN
                ROLLBACK; SELECT -3 AS Result, 'Ruud insuficiente.' AS Message; RETURN
            END
            UPDATE [dbo].[AccountCharacter] SET Ruud = Ruud - @Price
            WHERE GameIDC = @AccountID
        END

        -- Decrementar stock si tiene límite
        UPDATE [dbo].[CustomShop]
        SET StockCurrent = CASE WHEN StockCurrent > 0 THEN StockCurrent - 1 ELSE -1 END
        WHERE ShopID = @ShopID

        -- Encolar entrega en tabla de pendientes → el GameServer procesa al login
        INSERT INTO [dbo].[PendingShopItems]
            (AccountID, CharName, ShopID, Currency, PricePaid, RequestTime, Delivered)
        VALUES
            (@AccountID, @CharName, @ShopID, @Currency, @Price, GETDATE(), 0)

    COMMIT TRANSACTION

    SELECT 1 AS Result, 'Compra exitosa. El ítem será entregado al iniciar sesión.' AS Message
END
GO

> [!CONSEJO] > Usa siempre una tabla PendingShopItems como cola de entrega en lugar de insertar el ítem directamente en el inventario del personaje desde SQL. El GameServer lee esta tabla al iniciar sesión y entrega el ítem de forma segura dentro del ciclo normal de carga del personaje, evitando pérdidas por desconexión durante la transacción.


Tabla de Auditoría y Log de Transacciones

Todo movimiento de moneda y de ítem debe quedar registrado. Un log completo permite al GM identificar abusos, validar reclamos de jugadores y auditar el balance económico del servidor:

CREATE TABLE [dbo].[ShopLog] (
    [LogID]        BIGINT        IDENTITY(1,1) PRIMARY KEY,
    [LogDate]      DATETIME      NOT NULL DEFAULT GETDATE(),
    [AccountID]    NVARCHAR(10)  NOT NULL,
    [CharName]     NVARCHAR(10)  NOT NULL,
    [ShopID]       INT           NOT NULL,
    [ShopName]     NVARCHAR(50)  NOT NULL,
    [ItemCategory] TINYINT       NOT NULL,
    [ItemIndex]    SMALLINT      NOT NULL,
    [ItemLevel]    TINYINT       NOT NULL,
    [PricePaid]    BIGINT        NOT NULL,
    [Currency]     NVARCHAR(10)  NOT NULL,
    [Result]       TINYINT       NOT NULL,  -- 1=éxito 0=fallo
    [IPAddress]    NVARCHAR(20)  NULL
);

-- Índice para consultas por personaje y fecha (las más frecuentes en soporte)
CREATE NONCLUSTERED INDEX [IX_ShopLog_CharDate]
ON [dbo].[ShopLog] ([CharName], [LogDate] DESC);

Agrega el INSERT de log al final del procedimiento de compra, tanto en el camino de éxito como en el de fallo, para tener visibilidad completa de intentos rechazados.


Configuración del NPC en el Mundo

El NPC de tienda se define en GameServer\Data\MonsterSetBase.txt. El formato de cada línea es:

// MapaID   NPC_ID   PosX   PosY   Dir   Respawn   Quest
// → Lorencia (0) → posición central junto a los NPCs de quest nativas
0            237      130    135    3     0         0
// → Noria (33) → tienda secundaria para clases de rango mágico
33           237      175    109    1     0         0

El índice 237 corresponde al Wandering Merchant en la mayoría de los paquetes S6. Verifica en tu versión qué índice está disponible revisando la tabla de NPC del cliente (NPCList.txt o equivalente en Data\).

Los campos críticos son Respawn = 0 para NPCs fijos — si usas 1, el NPC respawneará como monstruo tras un reinicio de mapa — y Quest = 0 para evitar que el NPC active cadenas de quest involuntariamente.

Nota: En paquetes que incluyen panel de administración web (WebEngine, MuEmu AdminPanel), la colocación del NPC puede hacerse desde la interfaz sin editar archivos de texto. El formulario expone los mismos campos: MapID, NPC_ID, PosX, PosY. El efecto en el servidor es idéntico; el archivo de texto se regenera automáticamente al guardar.

Equilibrio de Precios por Fase de Progresión

La tienda debe actuar como red de seguridad del early-game y sumidero de moneda en el late-game, sin cortocircuitar la progresión orgánica. Usa este esquema como punto de partida:

Nivel 1 – 150 (Lorencia, Noria, Devias) Solo consumibles y pergaminos. Precios en Zen accesibles para un jugador sin acumular. El farmeo en estas zonas debe seguir siendo más eficiente para conseguir equipo que comprar en la tienda.

Nivel 151 – 300 (Dungeon, Lost Tower, Atlans) Joyas de Bendición, del Alma y del Caos. Precios en Créditos. El equipo de este rango nunca debe aparecer en la tienda: debe provenir de drops en Lost Tower 4-7, Atlans 3 y Tarkan.

Nivel 301 – 400 (Tarkan, Icarus, Aida, Karutan) Sets de segundo tier sin opciones excelentes. Créditos como moneda principal. Stock limitado por ítem (10-20 unidades por día) para no suplantar el farmeo.

Nivel 400+ (Raklion, Vulcanus, Acheron) Solo consumibles de alta gama y servicios (cambio de nombre, transferencia de stats). Los sets excelentes, Alas L3 y Pentagramas deben provenir exclusivamente de drops y del sistema de creación del Chaos Goblin.


Permisos SQL del GameServer

El proceso del GameServer se conecta con un login de servicio de privilegios reducidos. Otorga exactamente los permisos necesarios, sin más:

-- Reemplaza 'mu_gameserver' con el login real de tu instalación
GRANT EXECUTE ON [dbo].[usp_ShopPurchase]       TO [mu_gameserver]
GRANT SELECT   ON [dbo].[CustomShop]             TO [mu_gameserver]
GRANT SELECT, INSERT ON [dbo].[PendingShopItems] TO [mu_gameserver]
GRANT INSERT   ON [dbo].[ShopLog]                TO [mu_gameserver]
GRANT SELECT, UPDATE ON [dbo].[Character]        TO [mu_gameserver]
GRANT SELECT, UPDATE ON [dbo].[AccountCharacter] TO [mu_gameserver]

Nunca uses db_owner ni sa como cuenta de servicio del GameServer. Si el proceso es comprometido, el daño queda limitado a los objetos con permisos explícitos.


Errores Comunes y Diagnóstico

El NPC no aparece tras reiniciar el GameServer. Verifica que la línea en MonsterSetBase.txt no tenga espacios adicionales o tabulaciones mixtas — el parser del GameServer es estricto. Abre el archivo en Notepad++ con la vista de caracteres no imprimibles activada (Ver → Mostrar símbolo).

El ítem se entrega con opciones incorrectas. Revisa los campos ItemSkill, ItemLuck, ItemOption e ItemExc en la fila de CustomShop. Un ItemExc = 63 activa todas las opciones excelentes simultáneamente y puede romper el equilibrio del servidor completo.

Stock limitado se agota instantáneamente y no se repone. El StockCurrent no se repone solo. Crea un SQL Server Agent Job que ejecute diariamente:

-- Job diario → reponer stock a las 00:00 horas del servidor
UPDATE [dbo].[CustomShop]
SET StockCurrent = StockLimit
WHERE StockLimit > 0 AND Active = 1

Compra duplicada en conexión lenta. Asegúrate de que el procedimiento use SET XACT_ABORT ON y el WITH (UPDLOCK, ROWLOCK) en el SELECT del catálogo. Sin bloqueo explícito a nivel de fila, dos conexiones pueden superar la verificación de stock simultáneamente.

Magic Gladiator no puede comprar ítems de DK o DW. El MG usa bitmask propio. Para que acceda a ítems de ambas clases, combina los bitmasks con OR: RequireClass = 1 | 2 | 16 = 19. Consulta la documentación específica de tu paquete para los valores exactos de cada clase.

> [!ATENCION] > Siempre respalda la tabla CustomShop y Character antes de cualquier modificación masiva de precios o de stock: SELECT * INTO CustomShop_BKP_20260704 FROM [dbo].[CustomShop]. Un error en un UPDATE sin WHERE puede vaciar precios o fijar stock en cero para todo el catálogo.


Conclusión

Una tienda in-game avanzada en MU Online S6 es un sistema de tres capas — datos, lógica de servidor y presencia en el mundo — que deben diseñarse de forma coordinada. La tabla SQL define el catálogo; el procedimiento almacenado garantiza la integridad transaccional y el registro; el NPC traduce ese sistema a la experiencia del jugador.

El equilibrio de precios es el componente más sensible: revísalo periódicamente consultando el log de transacciones y comparando el volumen de compras por ítem con el comportamiento esperado de farmeo en cada zona del mapa. Un ítem que se compra masivamente en la tienda es un ítem que nadie está farmando — y esa es la señal de que el precio necesita ajuste.

Perguntas frequentes

¿Puedo vender ítems excelentes directamente en la tienda?

Técnicamente sí, pero no es recomendable. Los ítems excelentes son el principal incentivo para el farmeo en mapas de alto nivel como Raklion, Vulcanus y Acheron. Venderlos en la tienda vacía esos mapas y destruye la economía del servidor. Si decides incluirlos, limítalos a opciones de bajo impacto, fija un stock muy reducido (StockLimit = 5 o menos) y usa Créditos como moneda — nunca Zen — para mantener la barrera de acceso significativa.

¿Cómo evito que dos jugadores compren el mismo ítem de stock limitado al mismo tiempo?

Usa transacciones SQL explícitas con BEGIN TRAN / COMMIT / ROLLBACK y agrega la cláusula WITH (UPDLOCK, ROWLOCK) en el SELECT que verifica el stock antes de decrementarlo. Esto serializa el acceso a la fila y evita la condición de carrera. Sin este bloqueo, dos solicitudes simultáneas pueden pasar la verificación de stock y entregar dos ítems cuando solo quedaba uno.

¿La tienda in-game funciona igual en Season 9 y versiones posteriores?

Los conceptos de tablas SQL y NPCs son los mismos, pero los nombres de las tablas, los índices de ítems y los campos de moneda cambian según el paquete. En Season 9 el sistema Ruud está más integrado y hay clases adicionales (Rage Fighter, Grow Lancer) con sus propios bitmasks de RequireClass. Siempre consulta la documentación específica de tu paquete y verifica la estructura de tablas con sp_help antes de adaptar los scripts de este tutorial.

¿Es posible que la tienda envíe una notificación al jugador cuando el ítem esté disponible de nuevo en stock?

Sí, con trabajo adicional. Crea una tabla StockAlert donde los jugadores registren su interés en un ShopID con stock agotado. Un job de SQL Server Agent puede consultar periódicamente si el stock fue repuesto — vía UPDATE desde el panel de admin — e insertar un mensaje en la tabla de mensajes del servidor (MessageToCharacter o equivalente). El GameServer entrega estos mensajes al iniciar sesión, sin necesidad de modificar el cliente.

EQ

Equipo ViciadosMU

Equipe editorial do ViciadosMU — portal de MU Online no ar desde 2003.

Sigue leyendo

Artículos relacionados