server/edit.lua
Owner-editable server hooks: inventory, job gates, skill, session events.
Do not rename these globals. Core server code calls them by name. Keep the if not Framework and Config.Framework ~= 'custom' then return end guard at the top.
Config.Framework = "custom" skips the built-in core object. Config.Inventory = "custom" skips ox and the framework inventory. Fill every custom branch you need. Leaving GetPlayer as return nil means detectors will not register or start.
Session state bag (replicated): Player(src).state.gs_metaldetect is false while unequipped, or { item, slot, charge } while scanning.
Client UX hooks: client/edit.lua.
Notify, GetPlayer, CreateUsableItem
function Notify(src, nType, description)
TriggerClientEvent('ox_lib:notify', src, { type = nType, description = description })
end
function GetPlayer(source)
-- qbcore / qbox: Framework.Functions.GetPlayer(source)
-- esx: Framework.GetPlayerFromId(source)
-- custom: return exports['your_core']:GetPlayer(source)
end
function CreateUsableItem(itemName, cb)
-- Registers each Config.Detectors[].item as UseDetector
-- and Config.Battery.item as UseBattery
-- custom: register itemName, then cb(source, item) with .name and .slot
endNotify is server-originated toasts (found loot, battery installed, blocked use). Match ClientNotify if you swapped notify resources.
GetPlayer must return a live player object for that server id, or equip / loot / battery all no-op.
CreateUsableItem runs at resource start for every detector and the battery. For "custom" framework, register the item with your inventory and call cb(source, { name = itemName, slot = slot }). ox_inventory / QBCore / ESX branches already ship filled.
Job gates
Checked before a session starts or a battery is consumed. Return true to allow. Return false to block. Optional second return is sent through Notify (error).
function CanUseDetector(src, player, item)
-- item.name, item.slot
-- example: only on-duty hunter
-- local job = player.PlayerData and player.PlayerData.job
-- if not job or job.name ~= 'hunter' then
-- return false, 'You need the hunter job'
-- end
return true
end
function CanUseBattery(src, player, item)
return true
endCanUseDetector is the place for job, grade, ACE, or gang checks. CanUseBattery can use the same rules, or stay open so anyone holding a detector can recharge.
The client still runs CanStartScan after this (vehicles, downed). Both must pass.
Session hooks
function OnDetectorEquipped(src, player, session)
-- session.item, session.slot, session.charge
end
function OnDetectorUnequipped(src, reason)
-- toggle, client, death, export, no_detector, drop
enddrop means the player lost the detector item mid-scan (inventory change). export is exports['gs_metaldetect']:Stop(src). Use these for logs or to clear a busy flag. Do not give loot here; that is OnSkillProgress after a successful dig.
GetSkillProfile
Called on start, identify, collect, drain, and cooldown. Return a table every time. Missing keys fall back to defaults.
function GetSkillProfile(source, player, context)
-- context.event 'start' | 'identify' | 'collect' | 'drain' | 'cooldown'
-- context.detectorItem spawn name when known
-- context.siteId site id on identify / collect
return {
scanClarityBonus = 0.0,
drainMultiplier = 1.0,
digCooldownMs = nil,
lootWeightMultipliers = nil,
}
end| Field | Default | Effect |
|---|---|---|
scanClarityBonus | 0.0 | Added to detector rangeMultiplier. 0.2 is +20% reach. Used when the session starts. |
drainMultiplier | 1.0 | Scales drainPerTick. 0.8 is 20% less drain. 0.0 stops drain. Used on each drain tick. |
digCooldownMs | nil | If a number, replaces Config.Scan.digCooldownMs for that player. |
lootWeightMultipliers | nil | Table of extra multipliers for Config.Loot.bands keys: common, uncommon, valuable, hotspotValuableBonus. valuable = 2.0 doubles valuable odds for that roll. |
Example: scale valuable finds from a crafting skill, and slow drain at high level:
function GetSkillProfile(source, player, context)
local level = 0
-- level = exports['your_skills']:GetLevel(source, 'detecting') or 0
return {
scanClarityBonus = level * 0.02,
drainMultiplier = math.max(0.5, 1.0 - (level * 0.02)),
digCooldownMs = nil,
lootWeightMultipliers = {
valuable = 1.0 + (level * 0.05),
},
}
endOnSkillProgress
Runs after a successful dig has already given the item.
function OnSkillProgress(source, player, result)
-- result.item loot spawn name
-- result.count amount given
-- result.rare true if the pool was valuable
-- result.siteId
-- result.detectorItem which detector was used
endAdd XP, reputation, or a framework skill here. Do not AddItem the loot again; the core already did.
Inventory (only if you need custom)
Used to read detector charge, consume batteries, and give loot. ox_inventory and QBCore/Qbox/ESX branches already work when Config.Inventory / Config.Framework are auto. Fill these for "custom":
| Function | When | What to return / do |
|---|---|---|
GetSlotItem(src, slot) | Read charge | Item table with .name, .slot, and .metadata or .info (charge lives there). nil if empty. |
GetSlotsWithItem(src, itemName) | Find detectors / batteries | List of those slot tables. |
SetItemMetadata(src, slot, metadata) | Write charge | Persist metadata.charge (0 to 100). |
RemoveItem(src, itemName, count, slot) | Battery consume | true if removed. |
CanCarryItem(src, itemName, count) | Before loot | false if inventory is full. |
AddItem(src, itemName, count) | Loot | true if added. |
GetItemLabel(itemName) | Found notify | Display name, or the spawn name. |
Charge is metadata.charge on ox_inventory and info.charge on QBCore. The core also writes a description string (Battery charge: N%).
Wiring examples: Integrations. Config values those hooks read: shared/config.lua.
