Glitched Scripts
Metal DetectingAdvanced Configuration

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
end

Notify 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
end

CanUseDetector 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
end

drop 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
FieldDefaultEffect
scanClarityBonus0.0Added to detector rangeMultiplier. 0.2 is +20% reach. Used when the session starts.
drainMultiplier1.0Scales drainPerTick. 0.8 is 20% less drain. 0.0 stops drain. Used on each drain tick.
digCooldownMsnilIf a number, replaces Config.Scan.digCooldownMs for that player.
lootWeightMultipliersnilTable 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),
        },
    }
end

OnSkillProgress

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
end

Add 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":

FunctionWhenWhat to return / do
GetSlotItem(src, slot)Read chargeItem table with .name, .slot, and .metadata or .info (charge lives there). nil if empty.
GetSlotsWithItem(src, itemName)Find detectors / batteriesList of those slot tables.
SetItemMetadata(src, slot, metadata)Write chargePersist metadata.charge (0 to 100).
RemoveItem(src, itemName, count, slot)Battery consumetrue if removed.
CanCarryItem(src, itemName, count)Before lootfalse if inventory is full.
AddItem(src, itemName, count)Loottrue if added.
GetItemLabel(itemName)Found notifyDisplay 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.

On this page