Cómo Migrar ESX → QBCore de la Manera Correcta

Cómo Migrar ESX → QBCore de la Manera Correcta

Desea un cambio limpio de ESX a QBCore sin perder datos ni romper sistemas centrales. Siga este plan. Terminará con identificadores estables, consultas oxmysql y código impulsado por ox_lib.

Objetivo: mover su servidor de ESX a QBCore con un tiempo de inactividad mínimo.


Prerrequisitos

  1. Herramientas
    1. GIT y una rama separada para la migración.
    2. MariaDB o MySQL 8 con copias de seguridad completas habilitadas.
    3. Un servidor de staging que refleje la producción.
  2. Artefactos del servidor
    1. Servidor FX actualizado a la misma versión que producción.
    2. QBCore framework base y recursos predeterminados.
  3. Bibliotecas que usarás
    1. oxmysql para la base de datos.
    2. ox_lib para callbacks, helpers UI y envoltorios de utilidad.

Paso 1. Haz un plan y un punto de restauración

  1. Congelar cambios en producción. Detener nuevas instalaciones de scripts y escrituras en la BD no necesarias para pruebas.
  2. Realiza una copia de seguridad completa de tu base de datos como una instantánea con nombre.
  3. Crea una rama de tu repositorio del servidor y una rama dedicada migrate-esx-to-qbcore .
  4. Escribe un runbook. Incluye comandos para iniciar y detener el servidor de staging, restaurar la base de datos y ejecutar verificaciones de estado.

Paso 2. Construye una base limpia QBCore

  1. Despliega una base QBCore nueva en staging.
  2. Mantén solo lo esencial habilitado. Desactiva trabajos, inventarios y scripts personalizados hasta después de la migración de la base de datos.
  3. Instala e inicia estos recursos primero
    1. qb-core
    2. qb-vehicles o tus reemplazos preferidos
    3. oxmysql
    4. ox_lib

Paso 3. Reemplazar mysql-async con oxmysql

Si algún script ESX restante aún usa MySQL.Async, convierte las llamadas a oxmysql. Usa búsqueda y reemplazo simple con verificación.

Conversiones comunes

-- ESX mysql-async
MySQL.Async.fetchAll('SELECT * FROM users WHERE identifier = @id', {['@id'] = identifier}, function(rows)
 -- ...
end)
-- QBCore oxmysql
local rows = MySQL.query.await('SELECT * FROM players WHERE citizenid = ?', { citizenid })
-- rows is a Lua table; handle nil and length checks directly
-- ESX scalar example
MySQL.Async.fetchScalar('SELECT COUNT(1) FROM owned_vehicles', {}, function(count)
 -- ...
end)
-- oxmysql scalar
local count = MySQL.scalar.await('SELECT COUNT(1) FROM player_vehicles')
-- ESX insert
MySQL.Async.execute('INSERT INTO addon_account VALUES (@owner, @name, @money)', {
 ['@owner'] = identifier, ['@name'] = name, ['@money'] = money
})
-- oxmysql insert
MySQL.prepare.await('INSERT INTO player_accounts (citizenid, name, amount) VALUES (?, ?, ?)', { citizenid, name, amount })

Notas

  1. Preferir query.await, scalar.await, y prepare.await para un flujo limpio.
  2. Usa sentencias preparadas para operaciones de escritura.

Paso 4. Asignar estructuras de datos ESX a QBCore

Moverás identidades de jugadores y entidades propiedad. Usa esta referencia para mapear tablas.

Tabla ESXColumna claveTabla QBCoreColumna claveNotas
usersidentifierplayerscitizenidConvierte identificadores y crea citizenid para cada fila
owned_vehiclesownerplayer_vehiclescitizenidConvertir formato de matrícula y cargas JSON
datastore_dataownerplayer_metadatacitizenidSi almacenas JSON, combínalo cuidadosamente
addon_account_dataownerplayer_accountscitizenidMapear nombres de cuentas a QBCore bancario o efectivo
addon_inventory_itemsownerplayer_inventoriescitizenidSi te mudas a ox_inventory, migra por separado

Puedes mantener tablas personalizadas. Ajusta solo las claves foráneas que referencian identificadores ESX.


Paso 5. Estabilizar Identificadores

ESX a menudo almacena un identificador CFX como license:xxxx o histórico steam:xxxx. QBCore utiliza citizenid como clave de jugador estable y mantiene los identificadores de ejecución solo para autenticación.

Deberás

  1. Crear una citizenid para cada jugador.
  2. Vincular identificadores heredados al nuevo registro.
  3. Mantén una tabla de referencia para soporte y auditorías.

Inicialización de SQL

Ejecuta esto en una copia de tu base de datos ESX para preparar las tablas QBCore.

-- 1) Create players table if missing. Adjust to your QBCore schema.
CREATE TABLE IF NOT EXISTS players (
 citizenid VARCHAR(11) PRIMARY KEY,
 license VARCHAR(64) UNIQUE,
 identifiers JSON NOT NULL,
 name VARCHAR(64),
 charinfo JSON NOT NULL,
 metadata JSON NOT NULL,
 money JSON NOT NULL,
 job JSON NOT NULL,
 position VARCHAR(128) DEFAULT NULL,
 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 2) Helper function equivalent in SQL using a deterministic generator would be complex.
-- Instead, stage the mapping in a separate table and generate citizenid in Lua.
CREATE TABLE IF NOT EXISTS legacy_identifier_map (
 license VARCHAR(64) PRIMARY KEY,
 steam VARCHAR(64) NULL,
 fivem VARCHAR(64) NULL,
 discord VARCHAR(64) NULL,
 xbl VARCHAR(64) NULL,
 liveid VARCHAR(64) NULL,
 citizenid VARCHAR(11) UNIQUE
);

-- 3) Seed the mapping from ESX users
INSERT INTO legacy_identifier_map (license)
SELECT DISTINCT REPLACE(identifier, 'identifier:', '')
FROM users
WHERE identifier LIKE 'license:%' OR identifier LIKE 'steam:%';

Generar citizenid e insertar jugadores en Lua

Ejecuta una vez en staging. Haz respaldo primero.

-- server/migrate_identifiers.lua
local QBCore = exports['qb-core']:GetCoreObject()

local function generateCitizenId()
 local charset = {}
 for c = 65, 90 do table.insert(charset, string.char(c)) end
 for n = 48, 57 do table.insert(charset, string.char(n)) end
 math.randomseed(GetGameTimer())
 local id = {}
 for i = 1, 11 do id[i] = charset[math.random(1, #charset)] end
 return table.concat(id)
end

local rows = MySQL.query.await('SELECT license FROM legacy_identifier_map WHERE citizenid IS NULL')
for _, r in ipairs(rows) do
 local citizenid = generateCitizenId()
 MySQL.prepare.await('UPDATE legacy_identifier_map SET citizenid = ? WHERE license = ?', { citizenid, r.license })
end

-- Build players from ESX users
local users = MySQL.query.await([[SELECT u.identifier, u.firstname, u.lastname, u.dateofbirth, u.sex, u.height
 FROM users u]])
for _, u in ipairs(users) do
 local license = u.identifier
 local map = MySQL.single.await('SELECT citizenid FROM legacy_identifier_map WHERE license = ?', { license })
 if map and map.citizenid then
 local name = string.format('%s %s', u.firstname or 'John', u.lastname or 'Doe')
 local charinfo = json.encode({ firstname = u.firstname, lastname = u.lastname, birthdate = u.dateofbirth, gender = u.sex, height = u.height })
 local metadata = json.encode({ hunger = 100, thirst = 100 })
 local money = json.encode({ cash = 0, bank = 0, crypto = 0 })
 local job = json.encode({ name = 'unemployed', label = 'Unemployed', grade = { name = '0', level = 0 }})

 MySQL.prepare.await('INSERT IGNORE INTO players (citizenid, license, identifiers, name, charinfo, metadata, money, job) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
 map.citizenid,
 license,
 json.encode({ license = license }),
 name,
 charinfo,
 metadata,
 money,
 job
 })
 end
end
print('Identifier migration finished')

Mover vehículos propios

INSERT IGNORE INTO player_vehicles (citizenid, plate, vehicle, garage, state)
SELECT m.citizenid,
 UPPER(JSON_UNQUOTE(JSON_EXTRACT(v.vehicle, '$.plate'))),
 v.vehicle,
 'legion',
 1
FROM owned_vehicles v
JOIN legacy_identifier_map m ON m.license = v.owner;

Valida muestras aleatorias en el juego. Verifica formatos de placas y garajes.


Paso 6. Portar código de ESX a QBCore con ox_lib

Reemplaza la API de runtime ESX con equivalentes QBCore. Usa ox_lib para callbacks y notificaciones.

Objeto del jugador

-- ESX
local xPlayer = ESX.GetPlayerFromId(src)
xPlayer.addMoney(100)
-- QBCore
local Player = QBCore.Functions.GetPlayer(src)
Player.Functions.AddMoney('cash', 100)

Empleos

-- ESX job check
if xPlayer.getJob().name == 'police' then
 -- ...
end
-- QBCore job check
local job = Player.PlayerData.job
if job and job.name == 'police' then
 -- ...
end

Callbacks y UI

-- ESX server callback
ESX.RegisterServerCallback('resource:getData', function(source, cb)
 cb({ ok = true })
end)
-- ox_lib callback
lib.callback.register('resource:getData', function(source)
 return { ok = true }
end)
-- Notification
lib.notify(source, { title = 'Job', description = 'Promotion granted', type = 'success' })

Comandos

-- ESX
RegisterCommand('pay', function(src, args)
 local amount = tonumber(args[1]) or 0
 xPlayer.removeMoney(amount)
end)
-- QBCore with permissions
QBCore.Commands.Add('pay', 'Pay cash', {{name = 'amount', help = 'Amount'}}, false, function(src, args)
 local amount = tonumber(args[1]) or 0
 local Player = QBCore.Functions.GetPlayer(src)
 Player.Functions.RemoveMoney('cash', amount)
end)

Paso 7. Inventario y objetos

Si pasas de es_extended inventarios a qb-inventory o ox_inventory, trata esto como una sub‑migración separada.

  1. Congelar las adiciones de objetos.
  2. Exportar la lista maestra de objetos.
  3. Mapear nombres de objetos uno a uno.
  4. Migrar inventarios de jugadores por lotes. Validar tamaños de pila y pesos.

Ejemplo de CSV de mapeo de objetos

esx_name,qb_name,notes
bread,bread,
water,water,
lockpick,lockpick,

Paso 8. Prueba y despliegue

  1. Pruebas unitarias
    1. Probar búsquedas de identificadores para un conjunto aleatorio de jugadores.
    2. Probar transferencias de dinero, cambios de trabajo y propiedad de vehículos.
  2. Pruebas de juego
    1. Spawnear jugadores con antiguos identificadores ESX y confirmar el mapeo automático.
    2. Ejecutar un flujo de servicio policial, un robo a tienda y una compra de vehículo.
  3. Pruebas de rendimiento
    1. Usar resmon para monitorear CPU y memoria.
    2. Confirmar que los conteos de consultas DB disminuyeron tras la conversión a oxmysql.
  4. Plan de despliegue
    1. Mover la base de datos de staging a producción durante una ventana de mantenimiento.
    2. Anunciar un tiempo de inactividad de 60 minutos.
    3. Monitorear los registros en busca de identificadores faltantes y errores de clave foránea.

Solución de problemas

  1. Ciudadanos duplicados
    1. Causa. Ejecutar la migración dos veces.
    2. Solución. Aplicar claves únicas en citizenid y uso INSERT IGNORE durante la siembra.
  2. Vehículos faltantes
    1. Causa. Discrepancia de clave de propietario entre owned_vehicles.owner y legacy_identifier_map.license.
    2. Solución. Normalizar los valores del propietario y volver a ejecutar la inserción de vehículos para las placas afectadas.
  3. Los jugadores aparecen sin inventario
    1. Causa. Migración de inventario omitida.
    2. Solución. Reconstruir el mapeo de inventario y reimportar.
  4. Scripts falla con MySQL.Async no encontrado
    1. Causa. El script aún depende de mysql-async.
    2. Solución. Reemplazar las llamadas con oxmysql y eliminar mysql-async del servidor.

Lista de verificación de migración

  1. Realizar una copia de seguridad de la base de datos de producción con una marca de tiempo.
  2. Detener el servidor y bloquear las uniones de jugadores.
  3. Restaurar el volcado final de staging a producción.
  4. Implementar la compilación QBCore con qb-core, oxmysql, ox_lib primero en el orden de aseguramiento.
  5. Ejecutar el script de siembra de identificadores una vez.
  6. Habilitar los scripts convertidos solo cuando sus consultas estén en oxmysql.
  7. Reabre el servidor y observa los registros durante 30 minutos.
  8. Publica un plan de reversión si aparecen errores críticos.

Apéndice A. Ejemplo de fxmanifest para el asistente de migración

fx_version 'cerulean'
game 'gta5'

lua54 'yes'

server_scripts {
 '@oxmysql/lib/MySQL.lua',
 '@ox_lib/init.lua',
 'server/migrate_identifiers.lua'
}

Apéndice B. Ayudantes JSON seguros

local function safeDecode(jsonStr, fallback)
 if type(jsonStr) ~= 'string' or jsonStr == '' then return fallback end
 local ok, result = pcall(json.decode, jsonStr)
 if not ok then return fallback end
 return result
end

Lo que lograste

  1. Registros de jugadores estables claveados por citizenid.
  2. Una capa limpia de oxmysql con declaraciones preparadas y awaits.
  3. Código ESX portado a QBCore usando callbacks y utilidades de ox_lib.
  4. Un plan versionado que puedes repetir para servidores futuros.

  1. Centro de conversión de frameworks. https://fivemx.com/framework-conversion/
  2. Guía de MySQL Async a oxmysql. https://fivemx.com/mysql-async-to-oxmysql/
  3. Patrones de adaptador para puertos de scripts. https://fivemx.com/adapter-patterns/
  4. Inicio rápido de instalación QBCore. https://fivemx.com/qbcore/
  5. Lista de verificación de conversión de scripts. https://fivemx.com/converting-fivem-scripts/
  6. Resumen de QBOX con ox stack. https://fivemx.com/qbox-ox-stack/
  7. Resmon y rendimiento. https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/

Referencias externas

  1. Framework QBCore
  2. Documentación de oxmysql.
  3. Documentación de ox_lib.
  4. Referencia de identificadores CFX.re.