v4.0.0
A rewrite of the framework bridge, plus the bug hunt that came with reading every module again.
MSK.GetPlayer() returns a different shape, ox_core is gone, and the event payloads changed. Read the migration notes before you update, and replace the whole resource, not single files.
Added
Qbox is a framework of its own
Until 4.0.0 a Qbox server ended up in the QBCore branch, because qbx_core/fxmanifest.lua declares provide 'qb-core' and the detection asked GetResourceState('qb-core'). That worked by accident and cost the one thing Qbox has and QBCore does not: multijob. PlayerData.jobs is a table<string, integer>, and the qb compatibility layer flattens it to a single job.
Detection now asks for each framework under its own resource name and checks qbx_core first. See Frameworks.
provide hit is a fallback, never a detectionFiveM changed how provided names answer resource-state lookups twice during 2026, and for two weeks GetResourceState returned missing for a provided name without a single line of script code having changed.
One player shape on every framework
MSK.GetPlayer(id) returns the same fields whether ESX, QBCore or Qbox is running: source, identifier, license, name, firstName, lastName, dob, sex, phone, group, job, jobs, gang, gangs, money, metadata, position.
job and gang always look the same too: name, label, grade, gradeName, gradeLabel, salary, isBoss, onDuty.
jobs and gangs are filled on every framework. On Qbox that is the real multijob map, on ESX and QBCore it holds the single job the player has, so consumer code can read player.jobs without asking which framework it is on.
MSK.GetPlayer takes whatever you have
A server id, an identifier or citizenid, or a table ({source = }, {identifier = }, {citizenid = }, {phone = }, {userId = }). Passing a plain number used to raise attempt to index a number value.
The player object has methods again, and this time they arrive
SetJob, SetDuty, AddJob, RemoveJob, HasJob, AddMoney, RemoveMoney, GetMoney, SetMoney, GetMeta, SetMeta, AddItem, RemoveItem, HasItem, CanCarryItem, Notify, Kick, Save, Refresh and more. See the migration notes for why they did not before.
Job and gang definitions
MSK.GetJobs() and MSK.GetGangs(), client and server. Every framework keeps its list somewhere else: ESX behind ESX.GetJobs(), QBCore in QBCore.Shared.Jobs, Qbox behind its own export. A script that only wanted to fill a dropdown had to know all three and usually covered two.
The grades come back normalised into a list sorted by grade number, no matter how the framework stored them. MSK.GetGangs() is empty on ESX, which has no gangs.
MSK.VehicleStore
One shape over the framework's owned-vehicle table: GetSchema, GetByPlate, CountByPlate, Insert, Update, Delete, ClearJob and a paginated, filtered Browse. See Vehicle Store.
The tables differ by more than their names. ESX keeps every property as JSON in owned_vehicles.vehicle, QBCore and Qbox keep the spawn name in player_vehicles.vehicle and the properties in mods, so a name-to-name mapping is not enough. The two columns QBCore and Qbox do not have, job and type, are added on start with ALTER TABLE ... ADD COLUMN IF NOT EXISTS, which leaves existing rows untouched.
Vehicle properties are handed back in the format the running framework wrote them in. They are deliberately not unified, because every other garage on the server reads the same column.
More
MSK.IsPlayerDead()as a function of its own, including thevisn_areandosp_ambulancespecial cases. It used to sit on the client player object where consumers could not reach it.MSK.GetPlayerJobs,MSK.GetPlayerGangand, on the client,MSK.GetPlayerData,MSK.IsPlayerLoaded,MSK.GetPlayerJob,MSK.GetPlayerGang,MSK.GetPlayerJobs.MSK.Offline.GetPlayerTable()returns the character table of the running framework together with its key column:usersandidentifieron ESX,playersandcitizenidon QBCore and Qbox. Scripts that hang their own column on that table were guessingusers, which is right on one framework out of three.player.ClearInventory(), with aclearentry in every inventory adapter.- Company accounts follow the banking resource, not the framework.
MSK.Societydetects Renewed-Banking, qb-banking, qb-management or esx_addonaccount. Before this it branched on the framework, so a Qbox server got a hard0back no matter what was installed.MSK.Society.GetProvider()reports which one was found. MSK.OfflineandMSK.GetModelFromPlateknow the Qbox tables.
Changed
Framework and inventory are separate axes
No framework bridge carries item code any more, and no inventory adapter carries framework code. All item handling lives in inventories/server/*.lua, including the new default.lua for the inventory built into the running framework. FunctionOverride, which glued item functions onto the player object after the fact, is gone.
CanCarryItem may answer nil
nil means the running inventory cannot check that, which is not the same as false. The old code answered true without checking on QBCore and Qbox, and an item handed out on that promise ends up on the floor.
The client has no framework adapter any more
The server normalises player data and sends it with every msk_core:* event, and bridge/client.lua receives it. One shape, one place. The client also cannot write player data back any more, which was an exploit path.
More
MSK.HasItem(playerId, item, count, metadata)takes an optional minimum count. Passing metadata as the third argument still works.- The bridge no longer writes to the framework's own objects. In 3.x
MSK.GetPlayer()replacedxPlayer.jobwith the bridge's own shape, for every resource on the server, and setlabelto the technical job name.
Fixed
The load and logout events never fired
bridge/*/server.lua and client.lua registered handlers for msk_core:playerLoaded, :playerLogout, :setJob and :setPlayerData, but nothing in msk_core ever triggered them and no framework event was ever bound. MSK.LoadedPlayers stayed empty on every framework, client player data was only ever filled on a resource restart, and consumers waiting for a load event waited forever.
A single unban disabled the ban system
UnbanPlayer set an array slot to nil instead of removing the entry, and MSK.IsPlayerBanned walks 1..#bannedPlayers. The next player to connect hit that hole, the playerConnecting handler died before reaching its CancelEvent(), and every banned player could join again until the next restart. A ban row with an unreadable time value did the same.
Deleting a cron job disabled cron
Same nil-in-an-array cause, same effect on the two tick loops. On top of that, msk_core:createCron was a net event, so any client could schedule jobs on the server, and a job created from a plain timestamp ended the scheduler on its first run.
Any client could read any player's data
The msk_core:getPlayerData callback returned the complete player table for whatever id it was asked about: cash, bank, every metadata field, the licence. It now answers with identity and job for another player, and with everything only for the caller themselves. It was also registered twice, and the later registration silently replaced the other.
The client player mirror could die and freeze
getPlayerDeath() went through MSK.Call, which raised on timeout rather than returning nil, so one slow answer from an ambulance script ended the thread and ped, vehicle, seat and weapon stayed at their last value for the rest of the session. MSK.Call now returns nil instead of raising, which is what its own comment always claimed it did.
More
MSK.Player.serverIdcould stay wrong for a whole session.GetPlayerServerIdanswers-1until the session is up, and the value was read once at resource start and never corrected.- A closed input or numpad left its caller waiting forever. Both build a promise for the blocking call and both cleared only the callback on close, so escape or a resource stop left the promise unresolved.
- The menu stopped accepting keys.
Menu.ShowcallsMenu.Hide,HidecallsonClose, and if that callback yields, the input thread ends itself and clears the flag that would have let a new one start. - Callback timeouts. All three call sites rejected a promise they never awaited, which FiveM reports as an unhandled rejection, and returned a silent
nil. The request-id collision check compared a number against entries stored under a string key, so it could never match. sexwas inverted on QBCore and Qbox:gender == 1was read as male, while the character creator writes0for male.- The client bridge for QBCore read a field that does not exist. It asked for
self.PlayerData.citizenidwhereselfis already the PlayerData. MSK.Timeouttracked cancelled ids instead of pending ones, so aClearafter the callback had run left an entry nothing removed again.Points.Remove(id)called its handler withoutselfand could not work at all.Progressstopped animations withanim.clip, a field that is never set.Coords.CopyindexedMSK.Player[id]unchecked.ContextandMenubuilt inline ids fromGetGameTimer()and never cleaned them up.AdvancedNotificationturned an explicitflash = falseback on.Check.Dependencycompared againstnilwhen the two versions had a different number of parts, and the "should not be renamed" warning was printed even when the name was right.MSK.AddWebhookfired HTTP requests at an empty URL and said nothing.
Removed
ox_core
The branch carried no guarantee, was never finished (its GetPlayerData was an empty function) and is gone. Config.Framework = 'OXCore' now stops with an explicit message instead of failing later.
Migration
MSK.GetPlayer() returns the unified shape
Field names changed:
| 3.x | 4.0.0 |
|---|---|
grade_name | gradeName |
grade_label | gradeLabel |
grade_salary | salary |
dateofbirth | dob |
sex is 'male' or 'female' on every framework. Money is player.money.cash and player.money.bank, and GetMoney('cash') works everywhere.
The methods work in your resource now
They are built by modules/Player, which import.lua compiles into the consumer. Before, msk_core built the object and handed it over an export, where functions do not survive: every method arrived as nil and only the data half was usable. Reaching them through MSK.GetPlayer(...) is unchanged.
local player = MSK.GetPlayer(source) -- full object, methods included
local data = exports.msk_core:GetPlayerData(source) -- data only
MSK.Bridge is a real table in a consumer
It used to fall through to the export proxy and become a function, so MSK.Bridge.Framework.Type raised attempt to index a function value. Scripts carrying if MSK.Bridge and MSK.Bridge.Framework and ... now do what they always meant to.
MSK.Bridge.Framework.Core stays inside msk_core: the raw framework object is not handed across the export boundary.
Event payloads carry the unified shape
msk_core:playerLoaded hands over a player table, msk_core:setJob a job table. Two events were added: msk_core:setGang and msk_core:setDuty. The full list is on the Frameworks page.
Config.Framework = 'OXCore'
Has to be changed to 'AUTO' or a supported framework.
Changed files
Replace the whole resource.
fxmanifest.lua
config.lua
aliases.lua
init/shared.lua
init/server.lua
bridge/shared.lua
bridge/server.lua (new)
bridge/client.lua (new)
bridge/esx/server.lua
bridge/qbcore/server.lua
bridge/qbox/server.lua (new)
bridge/esx/client.lua (removed)
bridge/qbcore/client.lua (removed)
bridge/oxcore/ (removed)
inventories/server/default.lua (new)
inventories/server/ox_inventory.lua
inventories/server/jaksam_inventory.lua
inventories/server/core_inventory.lua
inventories/server/custom.lua
inventories/server/hasitem_server.lua
inventories/server/registeritems.lua
inventories/client/hasitem_client.lua
modules/Bridge/shared.lua (new)
modules/VehicleStore/server.lua (new)
modules/Player/server.lua
modules/Player/client.lua
modules/Ban/server.lua
modules/Cron/server.lua
modules/Check/server.lua
modules/Callback/shared.lua
modules/Callback/client.lua
modules/Callback/server.lua
modules/Society/server.lua
modules/Offline/server.lua
modules/Command/server.lua
modules/Notify/client.lua
modules/Vehicle/server.lua
modules/Timeout/shared.lua
modules/World/server.lua
modules/Coords/server.lua
modules/Input/client.lua
modules/Numpad/client.lua
modules/Progress/client.lua
modules/Points/client.lua
modules/Context/
modules/Menu/