The RSG Inventory system features a sophisticated time-based decay mechanism that simulates realistic item deterioration. Food spoils, materials degrade, and items lose quality over time - adding depth and realism to your RedM economy and encouraging active gameplay.Version: 2.7.3+ (Updated November 2024)
Item decay runs automatically on the server-side based on real-world time, not playtime. You can now control whether items decay offline!
New in v2.7.3: Offline decay control, custom stash decay rates, and default item metadata!
-- SERVER SIDE-- Create a freezer stash with 0% decayRegisterNetEvent('house:server:openFreezer', function(houseId) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local freezerId = 'freezer_'.. houseId ..'_decay0' -- 0% decay exports['rsg-inventory']:OpenInventory(src, freezerId, { label = 'Freezer', maxweight = 50000, slots = 20 })end)
Result: Items in this stash never decay (perfect for long-term storage)
-- SERVER SIDE-- Create a refrigerator with 20% decay rateRegisterNetEvent('house:server:openFridge', function(houseId) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local fridgeId = 'fridge_'.. houseId ..'_decay20' -- 20% decay exports['rsg-inventory']:OpenInventory(src, fridgeId, { label = 'Refrigerator', maxweight = 30000, slots = 15 })end)
-- SERVER SIDE-- Root cellar with 50% decay (moderate preservation)RegisterNetEvent('house:server:openCellar', function(houseId) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local cellarId = 'cellar_'.. houseId ..'_decay50' -- 50% decay exports['rsg-inventory']:OpenInventory(src, cellarId, { label = 'Root Cellar', maxweight = 40000, slots = 25 })end)
-- SERVER SIDE-- Composter that rapidly breaks down organic matterRegisterNetEvent('farming:server:openComposter', function(composterId) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local stashId = 'composter_'.. composterId ..'_decay5000' -- 5000% decay exports['rsg-inventory']:OpenInventory(src, stashId, { label = 'Composter', maxweight = 10000, slots = 10 })end)
Result: Items decay 50x faster than normal
Normal bread takes 5 hours to decay
Bread in composter decays in 6 minutes (5 hours ÷ 50)
Use for converting organic matter to compost quickly
-- SERVER SIDE-- Gang stash with decay rate based on upgradesRegisterNetEvent('gang:server:openStash', function() local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local gang = Player.PlayerData.gang.name if gang == 'none' then return end -- Check gang upgrades from database local result = MySQL.query.await('SELECT * FROM gang_upgrades WHERE gangname = ?', { gang }) local decayRate = 100 -- Default: normal decay if result and result[1] then if result[1].has_freezer then decayRate = 0 -- Freezer upgrade elseif result[1].has_cooler then decayRate = 30 -- Cooler upgrade end end local stashId = 'gang_'.. gang ..'_decay'.. decayRate exports['rsg-inventory']:OpenInventory(src, stashId, { label = gang ..' Gang Stash', maxweight = 500000, slots = 50 })end)
itemInfo (table|nil) - Item definition from RSGCore.Shared.Items (optional)
currentTime (number|nil) - Unix timestamp (optional, defaults to os.time())
decayRateModifier (number|nil) - Decay rate multiplier (optional, defaults to 1.0)
Returns:
shouldUpdate (boolean) - Whether item metadata was changed
newQuality (number|nil) - New quality value after decay
shouldDelete (boolean) - Whether item should be removed
Example:
Copy
-- SERVER SIDElocal Player = RSGCore.Functions.GetPlayer(source)local breadItem = exports['rsg-inventory']:GetItemByName(source, 'bread')if breadItem then local updated, quality, shouldDelete = Inventory.CheckItemDecay(breadItem) if updated then print('Bread quality updated to: '.. math.floor(quality) ..'%') if shouldDelete and quality <= 0 then print('Bread has spoiled and will be removed') exports['rsg-inventory']:RemoveItem(source, 'bread', 1, breadItem.slot, 'spoiled') end endend
Check and update all items in a player’s inventory.
Copy
Inventory.CheckPlayerItemsDecay(player)
Parameters:
player (table) - Player object from RSGCore.Functions.GetPlayer
Example:
Copy
-- SERVER SIDERegisterCommand('checkdecay', function(source) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end Inventory.CheckPlayerItemsDecay(Player) TriggerClientEvent('ox_lib:notify', source, { description = 'Checked all items for decay', type = 'info' })end, false)
This function is automatically called when inventory is loaded. Manual calls are rarely needed.
decayRateModifier (number|nil) - Decay rate multiplier (optional, defaults to 1.0)
Returns:
needsUpdate (boolean) - Whether any items were updated
removedItems (table) - Table of items that should be deleted
Example:
Copy
-- SERVER SIDElocal stashItems = exports['rsg-inventory']:GetInventory('gang_odriscoll')if stashItems and stashItems.items then local needsUpdate, removedItems = Inventory.CheckItemsDecay(stashItems.items) if needsUpdate then print('Stash items were updated') -- Remove spoiled items for slot, item in pairs(removedItems) do print('Removed spoiled '.. item.name ..' from slot '.. slot) stashItems.items[slot] = nil end -- Save stash exports['rsg-inventory']:SaveStash('gang_odriscoll') endend
-- Pseudocode of automatic decay checkingfunction OnInventoryLoad(player) for slot, item in pairs(player.items) do if item.decay then CheckItemDecay(item) if item.info.quality <= 0 and item.delete then RemoveItem(item) end end endend
Decay is calculated based on real-world time, not in-game time or playtime. Items continue to decay even when the server is offline!
-- SERVER SIDERSGCore.Functions.CreateUseableItem('bread', function(source, item) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end local quality = item.info.quality or 100 local hungerRestore = 0 local healthEffect = 0 -- Fresh bread (75-100%) if quality >= 75 then hungerRestore = 25 healthEffect = 5 -- Small health bonus -- Decent bread (50-74%) elseif quality >= 50 then hungerRestore = 20 healthEffect = 0 -- Stale bread (25-49%) elseif quality >= 25 then hungerRestore = 10 healthEffect = -5 -- Mild stomach ache TriggerClientEvent('ox_lib:notify', source, { description = 'This bread tastes stale', type = 'warning' }) -- Moldy bread (1-24%) elseif quality > 0 then hungerRestore = 5 healthEffect = -15 -- Food poisoning -- 50% chance of getting sick if math.random(100) <= 50 then Player.Functions.SetMetaData('stress', math.min(100, Player.PlayerData.metadata.stress + 10)) TriggerClientEvent('ox_lib:notify', source, { description = 'The spoiled bread made you sick!', type = 'error' }) end else -- Quality is 0, shouldn't reach here as item would be deleted return end -- Apply effects if exports['rsg-inventory']:RemoveItem(source, 'bread', 1, item.slot, 'consumed') then Player.Functions.SetMetaData('hunger', math.min(100, Player.PlayerData.metadata.hunger + hungerRestore)) if healthEffect ~= 0 then local currentHealth = Player.PlayerData.metadata.health Player.Functions.SetMetaData('health', math.max(0, math.min(600, currentHealth + healthEffect))) end TriggerClientEvent('ox_lib:notify', source, { description = 'You ate bread (Quality: '.. math.floor(quality) ..'%)', type = 'success' }) endend)
-- SERVER SIDE-- Pickaxe effectiveness based on qualityRegisterNetEvent('mining:server:minOre', function(oreType) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end -- Check for pickaxe local pickaxe = exports['rsg-inventory']:GetItemByName(src, 'pickaxe') if not pickaxe then TriggerClientEvent('ox_lib:notify', src, { description = 'You need a pickaxe', type = 'error' }) return end local quality = pickaxe.info.quality or 100 -- Tool broken if quality <= 0 then TriggerClientEvent('ox_lib:notify', src, { description = 'Your pickaxe is broken!', type = 'error' }) return end -- Success chance based on quality local baseChance = 70 local qualityBonus = (quality / 100) * 30 -- Up to 30% bonus local successChance = baseChance + qualityBonus if math.random(100) <= successChance then -- Success - give ore local oreAmount = math.random(1, 3) exports['rsg-inventory']:AddItem(src, oreType, oreAmount, nil, nil, 'mining') TriggerClientEvent('ox_lib:notify', src, { description = 'You mined '.. oreAmount ..'x '.. oreType, type = 'success' }) else TriggerClientEvent('ox_lib:notify', src, { description = 'Mining failed', type = 'error' }) end -- Degrade tool slightly pickaxe.info.quality = math.max(0, quality - 1) pickaxe.info.lastUpdate = os.time() Player.Functions.SetPlayerData('items', Player.PlayerData.items)end)
-- SERVER SIDE-- Portable cooler with 30% decay rateRSGCore.Functions.CreateUseableItem('cooler', function(source, item) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end -- Use decay rate in stash ID local coolerId = 'cooler_'.. Player.PlayerData.citizenid ..'_decay30' exports['rsg-inventory']:OpenInventory(source, coolerId, { label = 'Portable Cooler', maxweight = 10000, slots = 10 })end)
Result: Items in portable cooler decay at 30% of normal rate (built-in system handles it automatically!)Before v2.7.3: Required complex manual timestamp resetting
After v2.7.3: Just add _decay30 to the stash ID!
-- SERVER SIDERegisterNetEvent('shop:server:sellItem', function(itemName, slot) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local item = Player.PlayerData.items[slot] if not item or item.name ~= itemName then return end local basePrice = 10 local quality = item.info.quality or 100 local priceMultiplier = quality / 100 local finalPrice = math.floor(basePrice * priceMultiplier) -- Refuse items below 10% quality if quality < 10 then TriggerClientEvent('ox_lib:notify', src, { description = 'This item is too damaged to sell', type = 'error' }) return end if exports['rsg-inventory']:RemoveItem(src, itemName, 1, slot, 'sold-to-shop') then Player.Functions.AddMoney('cash', finalPrice, 'item-sale') TriggerClientEvent('ox_lib:notify', src, { description = 'Sold for $'.. finalPrice ..' (Quality: '.. math.floor(quality) ..'%)', type = 'success' }) endend)
-- SERVER SIDERegisterNetEvent('repair:server:repairItem', function(slot) local src = source local Player = RSGCore.Functions.GetPlayer(src) if not Player then return end local item = Player.PlayerData.items[slot] if not item then return end local itemInfo = RSGCore.Shared.Items[item.name:lower()] if not itemInfo or not itemInfo.decay then TriggerClientEvent('ox_lib:notify', src, { description = 'This item cannot be repaired', type = 'error' }) return end local currentQuality = item.info.quality or 100 if currentQuality >= 100 then TriggerClientEvent('ox_lib:notify', src, { description = 'This item is already in perfect condition', type = 'info' }) return end local repairCost = math.floor((100 - currentQuality) * 0.5) if Player.Functions.GetMoney('cash') < repairCost then TriggerClientEvent('ox_lib:notify', src, { description = 'Not enough money (need $'.. repairCost ..')', type = 'error' }) return end if Player.Functions.RemoveMoney('cash', repairCost, 'item-repair') then item.info.quality = 100 item.info.lastUpdate = os.time() Player.Functions.SetPlayerData('items', Player.PlayerData.items) TriggerClientEvent('ox_lib:notify', src, { description = 'Repaired '.. item.label ..' for $'.. repairCost, type = 'success' }) endend)
-- SERVER SIDE-- Salt preserves meat, doubling its decay timeRSGCore.Functions.CreateUseableItem('salt', function(source, item) local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end -- Check for meat in inventory local meat = exports['rsg-inventory']:GetItemByName(source, 'fresh_meat') if not meat then TriggerClientEvent('ox_lib:notify', source, { description = 'You need fresh meat to preserve', type = 'error' }) return end -- Remove salt and meat if exports['rsg-inventory']:RemoveItem(source, 'salt', 1, item.slot, 'used-for-preservation') then if exports['rsg-inventory']:RemoveItem(source, 'fresh_meat', 1, meat.slot, 'preserved') then -- Give preserved meat with better quality retention local preservedInfo = { quality = meat.info.quality or 100, lastUpdate = os.time(), preserved = true -- Custom flag } exports['rsg-inventory']:AddItem(source, 'preserved_meat', 1, nil, preservedInfo, 'preservation') TriggerClientEvent('ox_lib:notify', source, { description = 'You preserved the meat with salt', type = 'success' }) end endend)-- Preserved meat (in shared/items.lua)['preserved_meat'] = { name = 'preserved_meat', label = 'Preserved Meat', weight = 500, type = 'item', decay = 480, -- 8 hours instead of 1 hour delete = true, description = 'Salted meat that lasts longer'},
-- CLIENT SIDE-- Show quality color in inventory tooltipRegisterNetEvent('inventory:client:showItemInfo', function(item) local quality = item.info and item.info.quality or 100 local color = GetQualityColor(quality) local label = GetQualityLabel(quality) -- Send to NUI SendNUIMessage({ action = 'showTooltip', item = item.label, quality = math.floor(quality), qualityColor = color, qualityLabel = label, description = item.description })end)function GetQualityColor(quality) if quality >= 75 then return '#00ff00' end -- Green if quality >= 50 then return '#ffff00' end -- Yellow if quality >= 25 then return '#ff9900' end -- Orange return '#ff0000' -- Redendfunction GetQualityLabel(quality) if quality >= 75 then return 'Fresh' end if quality >= 50 then return 'Decent' end if quality >= 25 then return 'Poor' end return 'Spoiled'end
-- Admin command to simulate time passageRegisterCommand('testdecay', function(source, args) if not RSGCore.Functions.HasPermission(source, 'admin') then return end local hoursAhead = tonumber(args[1]) or 1 local secondsAhead = hoursAhead * 3600 local Player = RSGCore.Functions.GetPlayer(source) if not Player then return end -- Simulate time passage for slot, item in pairs(Player.PlayerData.items) do if item.info and item.info.lastUpdate then item.info.lastUpdate = item.info.lastUpdate - secondsAhead end end -- Recalculate decay Inventory.CheckPlayerItemsDecay(Player) TriggerClientEvent('ox_lib:notify', source, { description = 'Simulated '.. hoursAhead ..' hours of decay', type = 'info' })end, false)
-- Helper function from shared/helpers.luaHelpers.ParseDecayRate = function(name) local num = name and string.match(name:lower(), "decay(%d+)") return num and (tonumber(num) / 100) or falseend-- Examples:Helpers.ParseDecayRate("freezer_decay0") -- Returns: 0.0Helpers.ParseDecayRate("fridge-decay30") -- Returns: 0.3Helpers.ParseDecayRate("composter_decay5000") -- Returns: 50.0Helpers.ParseDecayRate("normal_stash") -- Returns: false (1.0 used as default)