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
 end

bridge/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
 end

bridge/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
 end

bridge/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
 end

bridge/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 {}
 end

config/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: {
   // -------------------------------------------------------------------------
   // Player

config/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