The RSG Framework uses FiveMโs StateBags system to efficiently sync player metadata between server and client in real-time. StateBags provide a performant alternative to constantly triggering events, allowing instant access to player data on both sides.Version: 2.3.6+
StateBags automatically sync between server and client without requiring events. Changes are instant and bidirectional!
When a player logs in, their metadata is loaded and synced to StateBags:
Copy
-- SERVER SIDE (rsg-core/server/player.lua:528-552)function Player.Functions.InitializeStateBags() local metadata = self.PlayerData.metadata local keys = { "hunger", "thirst", "cleanliness", "stress", "health" } local state = Player(self.PlayerData.source).state for _, key in ipairs(keys) do if metadata[key] ~= nil then state[key] = metadata[key] -- Syncs to client automatically end end -- Set login state state.isLoggedIn = trueend
When player data is saved, StateBag values are persisted back to metadata:
Copy
-- SERVER SIDE (rsg-core/server/player.lua:554-568)function Player.Functions.PersistStateBags() local metadata = {} local keys = { "hunger", "thirst", "cleanliness", "stress", "health" } local state = Player(self.PlayerData.source).state for _, key in ipairs(keys) do if state[key] ~= nil then metadata[key] = state[key] end end if next(metadata) then self.Functions.SetMetaData(metadata) -- Saves to database endend
-- SERVER SIDE-- Get player statelocal state = Player(source).state-- Read valueslocal hunger = state.hunger or 100local thirst = state.thirst or 100local health = state.health or 600print('Player hunger: '.. hunger)-- Update values (syncs to client automatically)state.hunger = 50state.thirst = 75
-- CLIENT SIDE-- Read your own statelocal hunger = LocalPlayer.state.hunger or 100local thirst = LocalPlayer.state.thirst or 100local health = LocalPlayer.state.health or 600local isLoggedIn = LocalPlayer.state.isLoggedInif hunger < 20 then print('You are very hungry!')end-- Read another player's statelocal targetPlayerId = GetPlayerFromServerId(targetServerId)local targetState = Player(targetPlayerId).statelocal targetHunger = targetState.hungerlocal targetHealth = targetState.healthprint('Target player hunger: '.. targetHunger)
-- CLIENT SIDE-- Watch for hunger changesAddStateBagChangeHandler('hunger', nil, function(bagName, key, value) -- Only react to your own state changes local playerBagName = ('player:%s'):format(GetPlayerServerId(PlayerId())) if bagName ~= playerBagName then return end print('Your hunger changed to: '.. value) -- Update UI SendNUIMessage({ action = 'updateHunger', hunger = value }) -- Show warning if low if value < 20 then lib.notify({ description = 'You are very hungry!', type = 'warning' }) endend)-- Watch for thirst changesAddStateBagChangeHandler('thirst', nil, function(bagName, key, value) local playerBagName = ('player:%s'):format(GetPlayerServerId(PlayerId())) if bagName ~= playerBagName then return end print('Your thirst changed to: '.. value) -- Apply debuffs based on thirst level if value < 10 then -- Severe dehydration effects SetPlayerHealthRechargeMultiplier(PlayerId(), 0.5) elseif value < 30 then -- Mild dehydration effects SetPlayerHealthRechargeMultiplier(PlayerId(), 0.75) else -- Normal health regeneration SetPlayerHealthRechargeMultiplier(PlayerId(), 1.0) endend)-- Watch for login state changesAddStateBagChangeHandler('isLoggedIn', nil, function(bagName, key, value) local playerBagName = ('player:%s'):format(GetPlayerServerId(PlayerId())) if bagName ~= playerBagName then return end if value then print('Player logged in') -- Initialize client-side systems TriggerEvent('player:client:initialize') else print('Player logged out') -- Clean up client-side systems TriggerEvent('player:client:cleanup') endend)
-- SERVER SIDE-- Watch for state changes on all playersAddStateBagChangeHandler('hunger', nil, function(bagName, key, value) -- Extract player ID from bagName format: "player:12" local playerId = tonumber(bagName:gsub('player:', '')) if not playerId then return end local Player = RSGCore.Functions.GetPlayer(playerId) if not Player then return end print('Player '.. playerId ..' hunger changed to: '.. value) -- Trigger effects based on hunger if value <= 0 then -- Player is starving - apply damage TriggerClientEvent('player:client:takeDamage', playerId, 5, 'starvation') endend)
-- CLIENT SIDElocal function UpdateHUD() local hunger = LocalPlayer.state.hunger or 100 local thirst = LocalPlayer.state.thirst or 100 local health = LocalPlayer.state.health or 600 local stress = LocalPlayer.state.stress or 0 local cleanliness = LocalPlayer.state.cleanliness or 100 -- Send to NUI SendNUIMessage({ action = 'updateHud', stats = { hunger = hunger, thirst = thirst, health = health, stress = stress, cleanliness = cleanliness } })end-- Update HUD periodicallyCreateThread(function() while true do Wait(1000) if LocalPlayer.state.isLoggedIn then UpdateHUD() end endend)-- Also update immediately when any stat changeslocal stats = { 'hunger', 'thirst', 'health', 'stress', 'cleanliness' }for _, stat in ipairs(stats) do AddStateBagChangeHandler(stat, nil, function(bagName, key, value) local playerBagName = ('player:%s'):format(GetPlayerServerId(PlayerId())) if bagName ~= playerBagName then return end UpdateHUD() end)end
-- CLIENT SIDECreateThread(function() while true do Wait(5000) -- Check every 5 seconds if not LocalPlayer.state.isLoggedIn then goto continue end local hunger = LocalPlayer.state.hunger or 100 local thirst = LocalPlayer.state.thirst or 100 local ped = PlayerPedId() -- Hunger effects if hunger < 10 then -- Severe hunger: health drain, stamina drain SetPedSuffersCriticalHits(ped, true) lib.notify({ description = 'You are starving!', type = 'error' }) elseif hunger < 30 then -- Moderate hunger: reduced stamina RestorePlayerStamina(PlayerId(), -25.0) end -- Thirst effects if thirst < 10 then -- Severe dehydration: visual effects, health drain SetTimecycleModifier('spectator5') lib.notify({ description = 'You are severely dehydrated!', type = 'error' }) elseif thirst < 30 then -- Moderate thirst: reduced stamina RestorePlayerStamina(PlayerId(), -25.0) else -- Normal hydration ClearTimecycleModifier() end ::continue:: endend)
-- SERVER SIDE-- Gradually decrease hunger and thirst over timeCreateThread(function() while true do Wait(60000) -- Every minute for _, playerId in pairs(RSGCore.Functions.GetPlayers()) do local Player = RSGCore.Functions.GetPlayer(playerId) if Player then local state = Player(playerId).state -- Decrease hunger local currentHunger = state.hunger or 100 state.hunger = math.max(0, currentHunger - 1) -- Decrease thirst (faster than hunger) local currentThirst = state.thirst or 100 state.thirst = math.max(0, currentThirst - 1.5) -- Decrease cleanliness (very slow) local currentCleanliness = state.cleanliness or 100 state.cleanliness = math.max(0, currentCleanliness - 0.5) end end endend)
-- SERVER SIDE-- Bread restores hungerRSGCore.Functions.CreateUseableItem('bread', function(source, item) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end if exports['rsg-inventory']:RemoveItem(source, 'bread', 1, item.slot, 'consumed') then local state = Player(source).state local currentHunger = state.hunger or 100 -- Restore 25 hunger (capped at 100) state.hunger = math.min(100, currentHunger + 25) TriggerClientEvent('ox_lib:notify', source, { description = 'You ate bread and feel less hungry', type = 'success' }) endend)-- Water restores thirstRSGCore.Functions.CreateUseableItem('water', function(source, item) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end if exports['rsg-inventory']:RemoveItem(source, 'water', 1, item.slot, 'consumed') then local state = Player(source).state local currentThirst = state.thirst or 100 -- Restore 30 thirst (capped at 100) state.thirst = math.min(100, currentThirst + 30) TriggerClientEvent('ox_lib:notify', source, { description = 'You drank water and feel refreshed', type = 'success' }) endend)
-- SERVER SIDERegisterNetEvent('medic:server:healPlayer', function(targetId) local src = source local Medic = RSGCore.Functions.GetPlayer(src) local Target = RSGCore.Functions.GetPlayer(targetId) if not Medic or not Target then return end -- Check if source is a medic if Medic.PlayerData.job.type ~= 'medic' then TriggerClientEvent('ox_lib:notify', src, { description = 'You are not a medic', type = 'error' }) return end -- Heal target using StateBag local targetState = Player(targetId).state targetState.health = 600 -- Full health targetState.hunger = 100 -- Full hunger targetState.thirst = 100 -- Full thirst TriggerClientEvent('ox_lib:notify', targetId, { description = 'You were healed by a doctor', type = 'success' }) TriggerClientEvent('ox_lib:notify', src, { description = 'You healed the patient', type = 'success' })end)
When you use Player.Functions.SetMetaData(), it automatically updates StateBags:
Copy
-- SERVER SIDElocal Player = RSGCore.Functions.GetPlayer(source)-- This updates both PlayerData.metadata AND StateBagPlayer.Functions.SetMetaData('hunger', 50)Player.Functions.SetMetaData('thirst', 75)Player.Functions.SetMetaData('health', 600)-- The StateBag is automatically synced to client-- No events needed!
You can also update StateBags directly (faster, but bypasses metadata tracking):
Copy
-- SERVER SIDElocal state = Player(source).state-- Direct update (syncs to client immediately)state.hunger = 50state.thirst = 75-- This is faster but doesn't update PlayerData.metadata-- Use PersistStateBags() when saving to update metadata
-- CLIENT SIDE-- Read state directly when neededlocal hunger = LocalPlayer.state.hunger or 100-- Use state change handlers for reactionsAddStateBagChangeHandler('hunger', nil, function(bagName, key, value) if bagName ~= ('player:%s'):format(GetPlayerServerId(PlayerId())) then return end UpdateHungerUI(value)end)
Bad Practice:
Copy
-- CLIENT SIDE-- DON'T poll for changes in a loopCreateThread(function() while true do Wait(0) -- Every frame! TriggerServerEvent('player:getHunger') -- Event spam! endend)
Only these keys are automatically persisted to database:
hunger, thirst, cleanliness, stress, health
Other custom states can be created but wonโt persist:
Copy
-- Custom state (won't persist to database)state.customFlag = truestate.tempValue = 123-- These work fine for temporary runtime data-- but won't survive server restart or player relog