v1.14.5 - v1.14.6
Summary
This release adds full vRP 2 framework support across player, inventory, job, garage, and offline admin actions, along with MrNewbVehicleKeys integration, improved transaction handling with web panel integration, a Discord screenshot fix, and a draggable stream name fix.
What's New
Features
Official vRP 2 framework support with a dedicated bridge adapter for players, inventory, jobs, gangs, garages, and offline character management
MrNewbVehicleKeys support in vehicle key management
Refactored vehicle spawning logic and enhanced transaction handling with web panel integration
Bug Fixes
Fixed Discord screenshot delivery
Fixed draggable stream name display
File Changes
⚙️ Configuration & Bridge Files
These files are not encrypted. Review the changes below — you may need to manually apply updates to your server's config files.
bridge/framework.lua
object = Ox,
})
elseif GetResourceState('vrp') == 'started' then -- VRP
- require "@vrp.lib.utils"
- ---TODO: Add vRP support, check db structure
+ if type(module) ~= "function" then
+ local utils = LoadResourceFile("vrp", "lib/utils.lua")
+ if not utils then
+ Luxu.print.error("vRP detected, but vrp/lib/utils.lua could not be loaded")
+ Luxu.print.error("LuxU Admin vRP support targets official vRP 2 only")
+ prom:reject()
+ return Citizen.Await(prom)
+ end
+ local chunk, err = load(utils, "@vrp/lib/utils.lua")
+ if not chunk then
+ Luxu.print.error(("Failed to parse vRP utils: %s"):format(err))
+ prom:reject()
+ return Citizen.Await(prom)
+ end
+
+ chunk()
+ end
---@diagnostic disable-next-line
- Tunnel = module("vrp", "lib/Tunnel")
+ local Tunnel = module("vrp", "lib/Tunnel")
---@diagnostic disable-next-line
- Proxy = module("vrp", "lib/Proxy")
+ local Proxy = module("vrp", "lib/Proxy")
---@diagnostic disable-next-line
- vRP = Proxy.getInterface("vRP")
+ local vRP = Proxy.getInterface("vRP")
+ if type(vRP.loadScript) ~= "function" then
+ Luxu.print.error("Legacy vRP detected. LuxU Admin vRP support targets official vRP 2 only.")
+ prom:reject()
+ return Citizen.Await(prom)
+ end
- --[[ ---@diagnostic disable-next-line
- vRPclient = Tunnel.getInterface("vRP", "Luxu Admin") ]]
+ vRP.loadScript(GetCurrentResourceName(), "bridge/vrp2/server")
+ local vRP2 = Proxy.getInterface("luxu_admin_vrp2", GetCurrentResourceName())
+
+ if vRP2.ping() ~= true then
+ Luxu.print.error("Failed to initialize the LuxU Admin vRP 2 bridge")
+ prom:reject()
+ return Citizen.Await(prom)
+ end
prom:resolve({
name = 'vrp',
- object = vRP,
+ object = {
+ proxy = vRP,
+ adapter = vRP2,
+ Tunnel = Tunnel,
+ Proxy = Proxy,
+ },
})
else
Luxu.print.error("Framework not detected")bridge/server/garage.lua
---@class GarageUtils
local garage = {}
+local VRP = Framework.name == "vrp" and Framework.object.adapter or nil
--- Returns a table, with model and plate, of all owned vehicles for a character
---@param charId number
elseif Framework.name == "ox" then
local result = MySQL.query.await("SELECT model, plate, FROM vehicles WHERE owner = ?", { charId })
return result
+ elseif Framework.name == "vrp" then
+ return VRP.getOwnedVehicles(charId)
else
return {}
end
1
elseif Framework.name == "ox" then
return false
+ elseif Framework.name == "vrp" then
+ return false
end
end
{ license, charId, plate, model, joaat(model), json.encode(mods) }
)
elseif Framework.name == "ox" then
+ elseif Framework.name == "vrp" then
+ return VRP.giveVehicle(charId, model)
end
end
+function garage.getVehicleModels()
+ if Framework.name == "vrp" then
+ return VRP.getGarageVehicleModels()
+ end
+
+ return {}
+end
+
--- Deletes a vehicle from the database
---@param plate string
-function garage.deleteVehicle(plate)
+---@param charId? string
+function garage.deleteVehicle(plate, charId)
if Framework.name == "esx" then
return MySQL.update.await("DELETE FROM owned_vehicles WHERE plate = ?", { plate }) == 1
elseif Framework.name == "qb" or Framework.name == "qbx" then
return MySQL.update.await("DELETE FROM player_vehicles WHERE plate = ?", { plate }) == 1
elseif Framework.name == "ox" then
return MySQL.update.await("DELETE FROM vehicles WHERE plate = ?", { plate }) == 1
+ elseif Framework.name == "vrp" then
+ if not charId then return false end
+ return VRP.deleteVehicle(charId, plate)
end
end
-function garage.setVehicleStored(plate)
+function garage.setVehicleStored(plate, charId)
if Framework.name == "esx" then
if GetResourceState("jg-advancedgarages") == "started" then
exports["jg-advancedgarages"]:deleteOutsideVehicle(plate)
return MySQL.update.await("UPDATE player_vehicles SET state = ? WHERE plate = ?", { 1, plate }) == 1
elseif Framework.name == "ox" then
+ elseif Framework.name == "vrp" then
+ if not charId then return false end
+ return VRP.setVehicleStored(charId, plate)
end
end
--- Changes the owner of a vehicle
---@param plate string
---@param charId number
-function garage.changeVehicleOwner(plate, charId)
+---@param oldCharId? number
+function garage.changeVehicleOwner(plate, charId, oldCharId)
if Framework.name == "esx" then
return MySQL.update.await("UPDATE owned_vehicles SET owner = ? WHERE plate = ?", { charId, plate }) == 1
elseif Framework.name == "qb" or Framework.name == "qbx" then
return MySQL.update.await("UPDATE player_vehicles SET citizenid = ? WHERE plate = ?", { charId, plate }) == 1
elseif Framework.name == "ox" then
return MySQL.update.await("UPDATE vehicles SET owner = ? WHERE plate = ?", { charId, plate }) == 1
+ elseif Framework.name == "vrp" then
+ if not oldCharId then return false end
+ return VRP.changeVehicleOwner(oldCharId, charId, plate)
end
endbridge/server/inventory.lua
---@class InventoryUtils : SharedInventoryUtils
local inventory = Luxu.require("bridge.shared.inventory")
+local VRP = Framework.name == "vrp" and Framework.object.adapter or nil
local function isResourceRunning(resource)
return GetResourceState(resource) == "started"
result = playerObj.PlayerData.items
elseif Framework.name == 'esx' then
result = playerObj.getInventory()
+ elseif Framework.name == 'vrp' then
+ result = VRP.getInventory(source)
else
result = {} --[[ CUSTOM FRAMEWORK ]]
end
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addInventoryItem(item, amount)
+ elseif Framework.name == "vrp" then
+ return VRP.giveItem(source, item, amount)
end
end
return true
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeInventoryItem(item, amount)
+ elseif Framework.name == "vrp" then
+ return VRP.takeItem(source, item, amount)
end
end
end
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
return xPlayer.canCarryItem(item, amount)
+ elseif Framework.name == "vrp" then
+ return VRP.canCarryItem(source, item, amount)
end
end
return true
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
return xPlayer.getInventoryItem(item).count >= amount
+ elseif Framework.name == "vrp" then
+ return VRP.getItemCount(source, item) >= amount
end
end
return false
elseif Framework.name == "esx" then
local xPlayer = ESX.GetPlayerFromId(source)
return xPlayer.getInventoryItem(item).count
+ elseif Framework.name == "vrp" then
+ return VRP.getItemCount(source, item)
end
end
return 0
end
end
end
+ elseif Framework.name == "vrp" then
+ return VRP.getAllPlayersInventory()
end
return result
endbridge/server/job.lua
---@class JobManager
local JobManager = {}
+local VRP = Framework.name == "vrp" and Framework.object.adapter or nil
--- Add a job
function JobManager.addJob(job)
exports["qb-core"]:AddJob(job)
elseif Framework.name == "ox" then
--TODO: Add ox_core job
+ elseif Framework.name == "vrp" then
+ -- vRP jobs are configured groups; runtime creation is not supported.
end
end
exports["qb-core"]:RemoveJob(jobName)
elseif Framework.name == "ox" then
--TODO: Add ox_core job
+ elseif Framework.name == "vrp" then
+ -- vRP jobs are configured groups; runtime removal is not supported.
end
end
exports["qb-core"]:AddJobs(jobs)
elseif Framework.name == "ox" then
--TODO: Add ox_core job
+ elseif Framework.name == "vrp" then
+ -- vRP jobs are configured groups; runtime creation is not supported.
end
end
return formatedGangs
elseif Framework.name == "ox" then
return {}
+ elseif Framework.name == "vrp" then
+ return VRP.getGroupsByType("gang") or {}
end
end
return formattedJobs
elseif Framework.name == "ox" then
--TODO: Add ox_core job
+ elseif Framework.name == "vrp" then
+ return VRP.getGroupsByType("job") or {}
end
endbridge/server/player.lua
local QBCore = Framework.object
local QBX = Framework.object
local Ox = Framework.object --[[ @as OxServer ]]
+local VRP = Framework.name == "vrp" and Framework.object.adapter or nil
---@class PlayerServer
local player = {}
elseif Framework.name == 'ox' then
return Ox.GetPlayer(source)
elseif Framework.name == 'vrp' then
- return vRP.users_by_source[source]
+ return VRP.getUser(source)
end
end
elseif Framework.name == 'qb' or Framework.name == 'qbx' then
return playerObj.PlayerData.charinfo.firstname .. " " .. playerObj.PlayerData.charinfo.lastname
elseif Framework.name == 'vrp' then
- local name = vRP.Identity(playerObj.user_id)
- return name
+ if playerObj.identity then
+ local firstname = playerObj.identity.firstname or ""
+ local lastname = playerObj.identity.name or ""
+ local name = (firstname .. " " .. lastname):gsub("^%s+", ""):gsub("%s+$", "")
+ return name ~= "" and name or playerObj.name or "Unknown"
+ end
+
+ return playerObj.name or "Unknown"
end
return "NOT FOUND"
end
return playerObj.PlayerData.charinfo.birthdate
elseif Framework.name == 'ox' then
return playerObj.get("birthdate")
+ elseif Framework.name == 'vrp' then
+ return playerObj.identity and playerObj.identity.age and tostring(playerObj.identity.age) or nil
end
end
elseif Framework.name == 'esx' then
TriggerClientEvent('esx_ambulancejob:revive', source)
Player(source).state:set('isDead', false, true)
+ elseif Framework.name == 'vrp' then
+ VRP.revive(source)
end
end
return Player(source).state.phoneNumber
elseif Framework.name == 'qb' or Framework.name == 'qbx' then
return playerObj.PlayerData.charinfo.phone
+ elseif Framework.name == 'vrp' then
+ return playerObj.identity and playerObj.identity.phone or nil
end
end
local account = playerObj.getAccount()
return { bank = account.get("balance") } --[[ @as table {[string]:number} ]]
elseif Framework.name == 'vrp' then
- return { bank = vRP.getBank({ playerObj }), money = vRP.getMoney({ playerObj }) }
+ return VRP.getMoneyAccounts(playerObj.source)
else
return {} --[[ CUSTOM FRAMEWORK ]]
end
playerObj.Functions.SetMoney(account, amount)
elseif Framework.name == 'ox' then
--[[ playerObj.setAccountMoney(account, amount) ]]
+ elseif Framework.name == 'vrp' then
+ return VRP.setAccountMoney(source, account == "cash" and "wallet" or account, amount)
end
end
playerObj.Functions.RemoveItem(item, count)
elseif Framework.name == 'ox' then
exports.ox_inventory:RemoveItem(source, item, count, nil, slot)
+ elseif Framework.name == 'vrp' then
+ return VRP.takeItem(source, item, count)
else
--[[ CUSTOM FRAMEWORK ]]
end
elseif GetResourceState("core_inventory") == "started" then
local inventory = 'content-' .. (self.getCharId(playerObj)):gsub(":", "")
return exports['core_inventory']:canCarry(inventory, item, count)
+ elseif Framework.name == "vrp" then
+ return VRP.canCarryItem(source, item, count)
else
return true
end
return playerObj.addInventoryItem(item, count)
elseif Framework.name == 'qb' or Framework.name == 'qbx' then
return playerObj.addInventoryItem(item, count)
+ elseif Framework.name == 'vrp' then
+ return VRP.giveItem(playerObj.source, item, count)
end
end
playerObj.setJob(job, grade)
elseif Framework.name == 'qb' or Framework.name == 'qbx' then
playerObj.Functions.SetJob(job, grade)
+ elseif Framework.name == 'vrp' then
+ return VRP.setGroupByType(playerObj.source, "job", job)
end
end
gradeName = playerObj.PlayerData.job.grade.name,
duty = playerObj.PlayerData.job.duty,
}
+ elseif Framework.name == 'vrp' then
+ return VRP.getJob(playerObj.source)
end
end
.gang --[[ @as {name:string, label:string, grade: {name:string, level:number, isboss:boolean}} ]]
elseif Framework.name == 'ox' then
return nil
+ elseif Framework.name == 'vrp' then
+ return VRP.getGang(playerObj.source)
else
return nil --[[ CUSTOM FRAMEWORK ]]
end
---@param source number
---@return {id:string?, name:string, birthday:string?, phoneNumber:string?, job:{name:string, label:string, grade:number, gradeName:string, duty:boolean}?, gang:{name:string, label:string, grade:{name:string, level:number, isboss:boolean}}?}?
function player.getCharacterData(source)
+ if Framework.name == "vrp" then
+ return VRP.getCharacterData(source)
+ end
+
local playerObj = player.getPlayerObject(source)
if not playerObj then return nil end
function player.setGang(playerObj, gang, grade)
if Framework.name == "qb" or Framework.name == "qbx" then
playerObj.Functions.SetGang(gang, grade)
+ elseif Framework.name == "vrp" then
+ return VRP.setGroupByType(playerObj.source, "gang", gang)
end
end
return exports.gflp10_carkeys:AddCarkey(source, modelHash, plate)
elseif GetResourceState("wasabi_carlock") == "started" then
return exports.wasabi_carlock:GiveKey(source, plate, false)
+ elseif GetResourceState("MrNewbVehicleKeys") == "started" then
+ return exports.MrNewbVehicleKeys:GiveKeysByPlate(source, plate)
end
--- Fallback for clientside exports
return playerObj.get("sex") == 0 and "male" or "female"
elseif Framework.name == 'qb' or Framework.name == 'qbx' then
return playerObj.PlayerData.charinfo.gender == 0 and "male" or "female"
+ elseif Framework.name == 'vrp' then
+ return nil
end
endbridge/shared/inventory.lua
--- ox_inventory, esx_addoninventory and qb-inventory must be last to avoid conflicts
local images = {
{ name = "tgiann-inventory", path = "https://cfx-nui-inventory_images/images/%s.png" },
+ { name = "jaksam_inventory", path = "https://cfx-nui-jaksam_inventory/_images/%s.png" },
{ name = "ak47_inventory", path = "https://cfx-nui-ak47_inventory/web/build/images/%s.png" },
{ name = "ak47_qb_inventory", path = "https://cfx-nui-ak47_qb_inventory/web/build/images/%s.png" },
{ name = "qs-inventory", path = "https://cfx-nui-qs-inventory/html/images/%s.png" },
{ name = "ox_inventory", path = "https://cfx-nui-ox_inventory/web/images/%s.png" },
{ name = "qb-inventory", path = "https://cfx-nui-qb-inventory/html/images/%s.png" },
{ name = "esx_addoninventory", path = "https://cfx-nui-esx_addoninventory/web/images/%s.png" },
- { name="jaksam_inventory", path="https://cfx-nui-jaksam_inventory/_images/%s.png"}
}
if Config.inventory.auto_detect then
end
end
+ if Framework.name == "vrp" then
+ if IsDuplicityVersion() then
+ return Framework.object.adapter.getItems()
+ else
+ return {}
+ end
+ end
+
return {}
endconfig/config.json
"advanced_configuration": {
"disable_map": false,
- "disable_coords_history": true
+ "disable_coords_history": true,
+ "__coords_history_comment__": "This feature consumes a lot of space in the database, use it only for a short period of time and disable it after that."
}
}config/database/queries.d.ts
import type { OfflinePlayer } from '../../../../shared/types/database';
+/**
+ * `queries.js` is evaluated by the Node runtime with `oxmysql`, `Framework`,
+ * `Luxu`, and `GetResourceKvpString` injected into scope.
+ */
declare const queries: {
// -------------------------------------------------------------------------
// Playerconfig/database/queries.js
+function normalizeVrpCharId(charId) {
+ const id = Number(charId);
+ return Number.isFinite(id) ? id : null;
+}
+
+function vrpIdentityName(identity) {
+ if (!identity) return '';
+ return `${identity.firstname || ''} ${identity.name || ''}`.trim();
+}
+
+function vrpLicenseFromIdentifiers(identifiers) {
+ return identifiers.find((identifier) => typeof identifier === 'string' && identifier.startsWith('license:')) || '';
+}
+
+async function getVrpCharacter(charId) {
+ const id = normalizeVrpCharId(charId);
+ if (id === null) return null;
+ return await oxmysql.single('SELECT id, user_id FROM vrp_characters WHERE id = ? LIMIT 1', [id]);
+}
+
+async function getVrpIdentifiersByUserId(userId) {
+ if (!userId) return [];
+ const rows = await oxmysql.query('SELECT identifier FROM vrp_user_ids WHERE user_id = ?', [userId]);
+ return rows.map((row) => row.identifier).filter((identifier) => typeof identifier === 'string');
+}
+
+async function getVrpIdentity(charId) {
+ const id = normalizeVrpCharId(charId);
+ if (id === null) return null;
+ return await oxmysql.single('SELECT * FROM vrp_character_identities WHERE character_id = ? LIMIT 1', [id]);
+}
+
module.exports = {
// ---------------------------------------------------------------------------
// Player
* @returns {Promise<any>}
*/
async getPlayer(charId) {
+ if (Framework.name === 'vrp') {
+ return await getVrpCharacter(charId);
+ }
+
return await oxmysql.scalar('SELECT * FROM players WHERE charId = ?', [charId]);
},
if (Framework.name === 'esx') {
return /** @type {{ timestamp: number }[]} */ (await oxmysql.query('SELECT * FROM users'));
}
+ if (Framework.name === 'vrp') {
+ return [];
+ }
return /** @type {{ timestamp: number }[]} */ (await oxmysql.query('SELECT * FROM luxu_stats_player_retention'));
},
const result = await oxmysql.single(QB_QUERY, [charId, 1]);
if (result) return JSON.parse(result.skin);
}
+ if (Framework.name === 'vrp') {
+ const details = await Luxu.vrp2.getOfflineCharacterDetails(charId);
+ return details?.customization ?? {};
+ }
return {};
} catch {
return {};
[modelHash]
);
}
+ if (Framework.name === 'vrp') {
+ const rows = await oxmysql.query('SELECT id FROM vrp_characters');
+ const result = [];
+ for (const row of rows) {
+ const vehicles = await Luxu.garage.getOwnedVehicles(String(row.id));
+ for (const vehicle of vehicles) {
+ if (GetHashKey(String(vehicle.model)) === modelHash) {
+ result.push({ plate: vehicle.plate, char_id: String(row.id) });
+ }
+ }
+ }
+ return result;
+ }
return await oxmysql.query(
'SELECT pv.plate as plate, pv.citizenid as char_id FROM player_vehicles pv WHERE pv.hash = ?',
[modelHash]
last_seen: result.last_seen || 0,
job,
});
+ } else if (Framework.name === 'vrp') {
+ const character = await getVrpCharacter(charId);
+ if (!character) return null;
+
+ const identity = await getVrpIdentity(charId);
+ const identifiers = await getVrpIdentifiersByUserId(character.user_id);
+ const license = vrpLicenseFromIdentifiers(identifiers);
+ const details = await Luxu.vrp2.getOfflineCharacterDetails(charId);
+
+ return /** @type {OfflinePlayer} */ ({
+ name: vrpIdentityName(identity) || `Character ${charId}`,
+ uniqueId: license.split(':')[1] || String(character.user_id || ''),
+ charId: String(character.id),
+ identifiers,
+ accounts: details?.accounts || {},
+ last_seen: 0,
+ job: details?.job
+ ? {
+ name: details.job.name || '',
+ label: details.job.label || details.job.name || '',
+ grade: typeof details.job.grade === 'number' ? details.job.grade : 0,
+ }
+ : { name: '', label: '', grade: 0 },
+ gang: details?.gang
+ ? {
+ name: details.gang.name || '',
+ label: details.gang.label || details.gang.name || '',
+ grade: typeof details.gang.grade === 'number' ? details.gang.grade : details.gang.grade?.level || 0,
+ }
+ : undefined,
+ });
} else {
const sql = `
SELECT
rawPosition = await oxmysql.scalar('SELECT position FROM users WHERE identifier = ?', [charId]);
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
rawPosition = await oxmysql.scalar('SELECT position FROM players WHERE citizenid = ?', [charId]);
+ } else if (Framework.name === 'vrp') {
+ const details = await Luxu.vrp2.getOfflineCharacterDetails(charId);
+ rawPosition = details?.position ?? null;
}
if (!rawPosition) return null;
identifiers,
};
});
+ } else if (Framework.name === 'vrp') {
+ const sql = `
+ SELECT
+ c.id,
+ c.user_id,
+ i.firstname,
+ i.name,
+ i.phone,
+ GROUP_CONCAT(uid.identifier SEPARATOR '\n') as identifiers
+ FROM vrp_characters c
+ LEFT JOIN vrp_character_identities i ON i.character_id = c.id
+ LEFT JOIN vrp_user_ids uid ON uid.user_id = c.user_id
+ WHERE (
+ CAST(c.id AS CHAR) LIKE ? OR
+ CAST(c.user_id AS CHAR) LIKE ? OR
+ i.firstname LIKE ? OR
+ i.name LIKE ? OR
+ i.phone LIKE ? OR
+ uid.identifier LIKE ?
+ )
+ GROUP BY c.id, c.user_id, i.firstname, i.name, i.phone
+ LIMIT 10
+ `;
+ const results = await oxmysql.query(sql, [
+ searchTerm,
+ searchTerm,
+ searchTerm,
+ searchTerm,
+ searchTerm,
+ searchTerm,
+ ]);
+ return results.map((row) => ({
+ name: `${row.firstname || ''} ${row.name || ''}`.trim() || `Character ${row.id}`,
+ charId: String(row.id),
+ identifiers: row.identifiers ? String(row.identifiers).split('\n').filter(Boolean) : [],
+ }));
}
},
return (await oxmysql.update('DELETE FROM users WHERE identifier = ?', [charId])) === 1;
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
return (await oxmysql.update('DELETE FROM players WHERE citizenid = ?', [charId])) === 1;
+ } else if (Framework.name === 'vrp') {
+ return (await oxmysql.update('DELETE FROM vrp_characters WHERE id = ?', [charId])) === 1;
}
return false;
},
queries.push({ query: 'DELETE FROM player_vehicles WHERE citizenid = ?', values: [charId] });
queries.push({ query: 'DELETE FROM players WHERE citizenid = ?', values: [charId] });
+ } else if (Framework.name === 'vrp') {
+ queries.push({ query: 'DELETE FROM luxu_admin_jail WHERE char_id = ?', values: [charId] });
+ queries.push({ query: 'DELETE FROM vrp_characters WHERE id = ?', values: [charId] });
} else {
return false;
}
[firstname, lastname, charId]
)) === 1
);
+ } else if (Framework.name === 'vrp') {
+ return (
+ (await oxmysql.update('UPDATE vrp_character_identities SET firstname = ?, name = ? WHERE character_id = ?', [
+ firstname,
+ lastname ?? '',
+ charId,
+ ])) === 1
+ );
}
return false;
},
[job, grade, charId]
)) === 1
);
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.setOfflineGroupByType(charId, 'job', job);
}
return false;
},
[gang, grade, charId]
)) === 1
);
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.setOfflineGroupByType(charId, 'gang', gang);
}
return false;
},
charId,
])) === 1
);
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.setOfflineAccounts(charId, accounts);
}
return false;
},
(await oxmysql.update('UPDATE players SET money = ? WHERE citizenid = ?', [JSON.stringify(money), charId])) ===
1
);
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.setOfflineAccount(
+ charId,
+ account === 'cash' || account === 'money' ? 'wallet' : account,
+ amount
+ );
}
return false;
},
} catch {
return [];
}
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.getOfflineInventory(charId);
}
return null;
},
return (await oxmysql.update('UPDATE users SET inventory = ? WHERE identifier = ?', [json, charId])) === 1;
} else if (Framework.name === 'qb' || Framework.name === 'qbx') {
return (await oxmysql.update('UPDATE players SET inventory = ? WHERE citizenid = ?', [json, charId])) === 1;
+ } else if (Framework.name === 'vrp') {
+ return await Luxu.vrp2.setOfflineInventory(charId, items);
}
return false;
},
} catch {
return [];
}
+ } else if (Framework.name === 'vrp') {
+ const rows = await oxmysql.query(`
+ SELECT
+ c.id,
+ i.firstname,
+ i.name
+ FROM vrp_characters c
+ LEFT JOIN vrp_character_identities i ON i.character_id = c.id
+ `);
+
+ const result = [];
+ for (const row of rows) {
+ const inventory = await Luxu.vrp2.getOfflineInventory(String(row.id));
+ const amount = Array.isArray(inventory)
+ ? inventory.reduce((total, item) => {
+ if (!item || item.name !== itemName) return total;
+ return total + (Number(item.amount ?? item.count) || 0);
+ }, 0)
+ : 0;
+
+ if (amount > 0) {
+ result.push({
+ charId: String(row.id),
+ name: vrpIdentityName(row) || `Character ${row.id}`,
+ amount,
+ });
+ }
+ }
+
+ return result.sort(
+ /** @param {{ amount: number }} a @param {{ amount: number }} b */ (a, b) => b.amount - a.amount
+ );
}
return [];
},
}
})
.filter(/** @param {any} r */ (r) => r !== null);
+ } else if (Framework.name === 'vrp') {
+ const rows = await oxmysql.query(`
+ SELECT
+ c.id,
+ i.firstname,
+ i.name
+ FROM vrp_characters c
+ LEFT JOIN vrp_character_identities i ON i.character_id = c.id
+ `);
+
+ const result = [];
+ for (const row of rows) {
+ const accounts = await Luxu.vrp2.getOfflineAccounts(String(row.id));
+ const money =
+ (Number(accounts?.bank) || 0) + (Number(accounts?.wallet ?? accounts?.money ?? accounts?.cash) || 0);
+ if (money > 0) {
+ result.push({
+ name: vrpIdentityName(row) || `Character ${row.id}`,
+ money,
+ charId: String(row.id),
+ });
+ }
+ }
+
+ return result
+ .sort(/** @param {{ money: number }} a @param {{ money: number }} b */ (a, b) => b.money - a.money)
+ .slice(0, 100);
}
return [];
},
rows.forEach((row) => {
distribution[row.job] = row.count;
});
+ } else if (Framework.name === 'vrp') {
+ const rows = await oxmysql.query('SELECT id FROM vrp_characters');
+
+ for (const row of rows) {
+ const details = await Luxu.vrp2.getOfflineCharacterDetails(String(row.id));
+ const jobName = details?.job?.name;
+ if (jobName) distribution[jobName] = (distribution[jobName] || 0) + 1;
+ }
}
return distribution;
},
-
/**
* Returns the total number of distinct players (by license) ever registered.
* @returns {Promise<number>}
)
);
return result[0]?.count ?? 0;
+ } else if (Framework.name === 'vrp') {
+ const result = /** @type {{ count: number }[]} */ (
+ await oxmysql.query('SELECT COUNT(*) as count FROM vrp_users')
+ );
+ return result[0]?.count ?? 0;
}
return 0;
},
} else if (Framework.name === 'esx') {
const result = /** @type {{ count: number }[]} */ (await oxmysql.query('SELECT COUNT(*) as count FROM users'));
return result[0]?.count ?? 0;
+ } else if (Framework.name === 'vrp') {
+ const result = /** @type {{ count: number }[]} */ (
+ await oxmysql.query('SELECT COUNT(*) as count FROM vrp_characters')
+ );
+ return result[0]?.count ?? 0;
}
return 0;
},
)
);
return result[0]?.total ?? 0;
+ } else if (Framework.name === 'vrp') {
+ const rows = await oxmysql.query('SELECT id FROM vrp_characters');
+ let total = 0;
+
+ for (const row of rows) {
+ const accounts = await Luxu.vrp2.getOfflineAccounts(String(row.id));
+ total += Number(accounts?.bank) || 0;
+ }
+
+ return total;
}
return 0;
},fxmanifest.lua
author 'luxu-gg'
description 'Admin menu for FiveM'
-version '1.14.4'
+version '1.14.5'
dependencies {
'oxmysql',
server_scripts { "config/hooks/server.lua", "server/utils/*.lua", 'server/modules/*.lua', 'server/sv_main.lua', 'server/index.js', 'config/anticheat/server.lua' }
-files { 'config/config.json', 'config/themes.css', 'config/permissions.json', 'config/sounds/*', 'config/help.lua', 'config/constants/*.json', 'config/logging/cl_logs.lua', 'config/teleports.lua', 'config/commands/cl_commands.lua', 'config/custom_functions/client.lua', 'locales/*.json', "web/index.html", 'web/**/*', "bridge/framework.lua", "bridge/shared/**/*.lua", "bridge/client/**/*.lua" }
+files { 'config/config.json', 'config/themes.css', 'config/permissions.json', 'config/sounds/*', 'config/help.lua', 'config/constants/*.json', 'config/logging/cl_logs.lua', 'config/teleports.lua', 'config/commands/cl_commands.lua', 'config/custom_functions/client.lua', 'locales/*.json', "web/index.html", 'web/**/*', "bridge/framework.lua", "bridge/shared/**/*.lua", "bridge/client/**/*.lua", "bridge/vrp2/**/*.lua" }
escrow_ignore { 'config/**/*', 'bridge/**/*' }Full list of files changed
| Change | File |
|---|---|
| Modified | bridge/framework.lua |
| Modified | bridge/server/garage.lua |
| Modified | bridge/server/inventory.lua |
| Modified | bridge/server/job.lua |
| Modified | bridge/server/player.lua |
| Modified | bridge/shared/inventory.lua |
| Added | bridge/vrp2/server.lua |
| Modified | client/modules/actions.lua |
| Modified | client/modules/signaling.lua |
| Modified | client/modules/nui.lua |
| Modified | config/config.json |
| Modified | config/database/queries.d.ts |
| Modified | config/database/queries.js |
| Modified | fxmanifest.lua |
| Modified | server/sv_main.lua |
| Modified | server/utils/sv_functions.lua |
| Modified | web/ |
| Modified | server/index.js |