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
- Herramientas
- GIT y una rama separada para la migración.
- MariaDB o MySQL 8 con copias de seguridad completas habilitadas.
- Un servidor de staging que refleje la producción.
- Artefactos del servidor
- Servidor FX actualizado a la misma versión que producción.
- QBCore framework base y recursos predeterminados.
- Bibliotecas que usarás
oxmysqlpara la base de datos.ox_libpara callbacks, helpers UI y envoltorios de utilidad.
Paso 1. Haz un plan y un punto de restauración
- Congelar cambios en producción. Detener nuevas instalaciones de scripts y escrituras en la BD no necesarias para pruebas.
- Realiza una copia de seguridad completa de tu base de datos como una instantánea con nombre.
- Crea una rama de tu repositorio del servidor y una rama dedicada
migrate-esx-to-qbcore. - 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
- Despliega una base QBCore nueva en staging.
- Mantén solo lo esencial habilitado. Desactiva trabajos, inventarios y scripts personalizados hasta después de la migración de la base de datos.
- Instala e inicia estos recursos primero
qb-coreqb-vehicleso tus reemplazos preferidosoxmysqlox_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
- Preferir
query.await,scalar.await, yprepare.awaitpara un flujo limpio. - 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 ESX | Columna clave | Tabla QBCore | Columna clave | Notas |
|---|---|---|---|---|
users | identifier | players | citizenid | Convierte identificadores y crea citizenid para cada fila |
owned_vehicles | owner | player_vehicles | citizenid | Convertir formato de matrícula y cargas JSON |
datastore_data | owner | player_metadata | citizenid | Si almacenas JSON, combínalo cuidadosamente |
addon_account_data | owner | player_accounts | citizenid | Mapear nombres de cuentas a QBCore bancario o efectivo |
addon_inventory_items | owner | player_inventories | citizenid | Si 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
- Crear una
citizenidpara cada jugador. - Vincular identificadores heredados al nuevo registro.
- 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.
- Congelar las adiciones de objetos.
- Exportar la lista maestra de objetos.
- Mapear nombres de objetos uno a uno.
- 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
- Pruebas unitarias
- Probar búsquedas de identificadores para un conjunto aleatorio de jugadores.
- Probar transferencias de dinero, cambios de trabajo y propiedad de vehículos.
- Pruebas de juego
- Spawnear jugadores con antiguos identificadores ESX y confirmar el mapeo automático.
- Ejecutar un flujo de servicio policial, un robo a tienda y una compra de vehículo.
- Pruebas de rendimiento
- Usar
resmonpara monitorear CPU y memoria. - Confirmar que los conteos de consultas DB disminuyeron tras la conversión a oxmysql.
- Usar
- Plan de despliegue
- Mover la base de datos de staging a producción durante una ventana de mantenimiento.
- Anunciar un tiempo de inactividad de 60 minutos.
- Monitorear los registros en busca de identificadores faltantes y errores de clave foránea.
Solución de problemas
- Ciudadanos duplicados
- Causa. Ejecutar la migración dos veces.
- Solución. Aplicar claves únicas en
citizenidy usoINSERT IGNOREdurante la siembra.
- Vehículos faltantes
- Causa. Discrepancia de clave de propietario entre
owned_vehicles.ownerylegacy_identifier_map.license. - Solución. Normalizar los valores del propietario y volver a ejecutar la inserción de vehículos para las placas afectadas.
- Causa. Discrepancia de clave de propietario entre
- Los jugadores aparecen sin inventario
- Causa. Migración de inventario omitida.
- Solución. Reconstruir el mapeo de inventario y reimportar.
- Scripts falla con
MySQL.Asyncno encontrado- Causa. El script aún depende de mysql-async.
- Solución. Reemplazar las llamadas con oxmysql y eliminar mysql-async del servidor.
Lista de verificación de migración
- Realizar una copia de seguridad de la base de datos de producción con una marca de tiempo.
- Detener el servidor y bloquear las uniones de jugadores.
- Restaurar el volcado final de staging a producción.
- Implementar la compilación QBCore con
qb-core,oxmysql,ox_libprimero en el orden de aseguramiento. - Ejecutar el script de siembra de identificadores una vez.
- Habilitar los scripts convertidos solo cuando sus consultas estén en oxmysql.
- Reabre el servidor y observa los registros durante 30 minutos.
- 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
- Registros de jugadores estables claveados por
citizenid. - Una capa limpia de oxmysql con declaraciones preparadas y awaits.
- Código ESX portado a QBCore usando callbacks y utilidades de ox_lib.
- Un plan versionado que puedes repetir para servidores futuros.
Enlaces útiles dentro de tu sitio
- Centro de conversión de frameworks. https://fivemx.com/framework-conversion/
- Guía de MySQL Async a oxmysql. https://fivemx.com/mysql-async-to-oxmysql/
- Patrones de adaptador para puertos de scripts. https://fivemx.com/adapter-patterns/
- Inicio rápido de instalación QBCore. https://fivemx.com/qbcore/
- Lista de verificación de conversión de scripts. https://fivemx.com/converting-fivem-scripts/
- Resumen de QBOX con ox stack. https://fivemx.com/qbox-ox-stack/
- Resmon y rendimiento. https://fivemx.com/how-to-use-resmon-in-fivem-optimize-resources/
