API Reference

Server exports, server events, config hooks and drug-resource exports for Oxide Drugselling — live settings, heat, phone lookups, evidence reports, orders, and reference tables for callbacks, network events and NUI messages.

Exports, events and hooks for developers who want other resources to work with oxide-drugselling. Everything here is server-side.

To send seized-phone reports to your own MDT, see Evidence Bridge.

Server Exports

Settings

GetSetting

Reads a live setting (the value the resource is using right now, from the database).

local selling = exports['oxide-drugselling']:GetSetting('Selling')
print(selling.minPoliceOnline)
ParameterTypeDescription
keystringA top-level Config key, like 'Selling', 'Heat' or 'Phone'

Returns: any — the setting's current value, or nil if there's no such key.


SetSetting

Changes a setting, saves it to the database and sends it to every player's game, like saving it in the menu.

exports['oxide-drugselling']:SetSetting('Witnesses', {
    enabled = true, radius = 15.0, crowdLimit = 6, snitchChance = 0.05,
})
ParameterTypeDescription
keystringA top-level Config key
valueanyThe whole new value for that key

Returns: boolean — true when the setting was saved, false when it was refused.

This export doesn't run the settings menu's checks. Pass the complete value in the right shape: a table replaces the whole setting, not just the fields you include. It refuses, logs and returns false for a key that isn't a setting, for the file-only settings (they stay in the config files), and for a value containing a broken number (NaN or infinity).

Heat

GetHeat

Reads current heat, after decay.

local here = exports['oxide-drugselling']:GetHeat('area', GetEntityCoords(GetPlayerPed(src)))
local dealer = exports['oxide-drugselling']:GetHeat('player', charId)
ParameterTypeDescription
kindstring'area', 'corner', 'player' or 'number'
keyanyFor 'area': coords (vector3) or a map-square key like '2:-7'. For 'player': the character identifier. For 'number': a phone number like '555-1234'. For 'corner': 'c:' plus the map-square key of the spot where the corner was started, like 'c:2:-7'

Returns: number — the heat, from 0 to Config.Heat.max.


AddHeat

Adds (or, with a negative amount, removes) heat. Does nothing while heat is switched off.

-- A shootout nearby draws attention
exports['oxide-drugselling']:AddHeat('area', coords, 10)
ParameterTypeDescription
kindstring'area', 'corner', 'player' or 'number'
keyanySame as GetHeat
amountnumberHeat to add. The result stays between 0 and Config.Heat.max

Returns: boolean — true.

Phones and evidence

GetPhoneByNumber

Finds a trap phone by its number.

local phone = exports['oxide-drugselling']:GetPhoneByNumber('555-1234')
if phone and not phone.burned then
    print(phone.phoneId)
end
ParameterTypeDescription
numberstringThe phone number, with its dash

Returns: table|nil — { phoneId = string, number = string, burned = boolean }, or nil if no phone has that number. After a SIM swap, the old number returns nil.


GetPhoneIdFromItem

Gets the phone ID from a trap_phone inventory item.

local phoneId = exports['oxide-drugselling']:GetPhoneIdFromItem(item)
ParameterTypeDescription
itemtableAn inventory item row with a metadata table

Returns: string|nil — the phone ID, or nil for a blank phone that hasn't been used yet.


GetPhoneRecord

The phone's saved details.

local record = exports['oxide-drugselling']:GetPhoneRecord(phoneId)
ParameterTypeDescription
phoneIdstringThe phone ID

Returns: table|nil:

FieldTypeDescription
phoneIdstringThe phone ID
numberstringIts current number
burnedbooleantrue if it was destroyed
poweredbooleantrue if it's switched on
batterynumberBattery right now, 0 to 100
ownerstring|nilThe registered owner's name (its first user)
ownerCharIdstring|nilThe registered owner's character identifier
reputationnumberThe phone's reputation

This returns the owner whatever Config.Evidence.trace.showOwner is set to. Keep it away from players if you want burners to stay anonymous.


GetPhoneMessages

A phone's texts. Reading them this way doesn't mark them as read.

-- Every conversation
local threads = exports['oxide-drugselling']:GetPhoneMessages(phoneId)

-- One conversation
local messages = exports['oxide-drugselling']:GetPhoneMessages(phoneId, { thread = 'n:555-9876' })
ParameterTypeDescription
phoneIdstringThe phone ID
optstable|nil{ thread = string } for one conversation

Returns:

  • Without opts.thread: a list of up to 30 conversations, newest first: { title, thread, messages }.
  • With opts.thread: that conversation's last 40 messages, oldest first.

Each message is { dir = 'in'|'out', text = string, time = number|nil }, where time is the in-game minute of the day it was sent (0 to 1439).

Conversation IDs (thread): 'c:<clientId>' for an NPC client, 'n:<number>' for another burner, 'sys' for network notices.


GetPhoneContacts

The phone's contacts, as its Contacts screen shows them.

local contacts = exports['oxide-drugselling']:GetPhoneContacts(phoneId)
ParameterTypeDescription
phoneIdstringThe phone ID

Returns: table — a list sorted by name. NPC clients look like:

{
    id = 42,              -- client ID
    alias = 'Tasha',
    kind = 'client',      -- 'client' or 'bulk' (undercover buyers also show as 'client')
    persona = 'Regular',
    trust = 'Good',       -- the translated trust label
    hours = 'Evenings',
    last = 'fire',        -- what they thought of the last batch, or ''
    deals = 3,
    knowsNumber = true,
}

Burner numbers the phone has texted look like { number = '555-9876', alias = '555-9876', kind = 'number' }.


TraceNumber

Where a burner number's phone is, blurred like a police trace. No cooldown, and no blip is shown to anyone.

local coords = exports['oxide-drugselling']:TraceNumber('555-9876')
ParameterTypeDescription
numberstringThe number to trace

Returns: vector3|nil — a point within Config.Evidence.trace.radius of the phone, or nil if the phone is off, dead, not carried by an online player, or inside a no-signal area (Config.Phone.indoorsNoSignal doesn't stop a trace).


BuildEvidenceReport

Everything on a phone, as data and as plain text. This is the same report officers send from Evidence View.

local report = exports['oxide-drugselling']:BuildEvidenceReport(phoneId)
if report then
    print(report.text)
end
ParameterTypeDescription
phoneIdstringThe phone ID

Returns: table|nil — the report (see Evidence Bridge → The report for every field), or nil if the phone doesn't exist or was destroyed.

Orders

GetActiveOrders

Orders a phone is working on: ones it took, and meets passed to it from another phone.

local orders = exports['oxide-drugselling']:GetActiveOrders(phoneId)
for _, order in ipairs(orders) do
    print(order.id, order.status, order.data.quantity, order.data.label)
end
ParameterTypeDescription
phoneIdstringThe phone ID

Returns: table — a list of { id, status, clientId, data }.

statusMeaning
openThe client texted and is waiting for an answer
bookingThe price is agreed and the client is waiting for a time
bookedA meet is booked
droppedThe package is at the dead drop, waiting for the client

Useful data fields: label (what the client called it), drug, items, quantity, offer (the agreed or current price), bulk (true for bulk buyers), and meet once booked (type = 'handoff' or 'drop', minute = in-game minute of the day, street, coords). A police sting order also has data.sting. Don't show that to players.

Server Events

All events are plain server events (TriggerEvent, not network events), so players can't fire them. Listen with AddEventHandler in any server script.

oxide-drugselling:drugSold

Fired after every completed sale, whichever way it was made, once the sale is settled. For a drop sale that's when the client collects the package; with Left at the drop payment the cash may still be waiting at the drop. Hand-overs to an undercover buyer don't fire it.

AddEventHandler('oxide-drugselling:drugSold', function(src, saleData)
    -- saleData.method: 'street' | 'corner' | 'phone' | 'drop' | 'bulk'
    print(('%s sold %d x %s for $%d'):format(src, saleData.quantity, saleData.itemName, saleData.finalPrice))
end)
saleData fieldTypeDescription
sourcenumberThe seller's server ID
charIdstringThe seller's character identifier
drugTypestringThe drug's key in Config.Drugs
itemNamestringThe item sold
quantitynumberUnits sold
xpUnitsnumberProgression units for this sale: quantity times the item's xpUnits, or quantity when that row leaves it out
finalPricenumberThe sale price, after any gang territory bonus. For a dead drop, the cash paid or left at the drop
metaKeystringThe stack's group key
modestringConfig.DirtyMoney.mode ('auto', 'account' or 'item')
methodstring'street', 'corner', 'phone', 'drop' or 'bulk'

For a drop sale, source is the player carrying the phone that handled the meet when the client collected (or, if nobody is, whoever holds the phone that took the order). If neither is online, the sale isn't reported.


oxide-drugselling:evidence:phoneExamined

Fired each time an officer opens a seized phone in Evidence View.

AddEventHandler('oxide-drugselling:evidence:phoneExamined', function(src, report)
    -- src: the officer. report: see the Evidence Bridge page
end)

oxide-drugselling:evidence:numberTraced

Fired when an officer traces a number from a seized phone.

AddEventHandler('oxide-drugselling:evidence:numberTraced', function(src, number, found)
    -- found: true if the trace came back with a location
end)

oxide-drugselling:evidence:reportSent

Fired when an officer sends a report from a seized phone, whether or not an MDT accepted it.

AddEventHandler('oxide-drugselling:evidence:reportSent', function(src, caseRef, report, ok)
    -- caseRef: the case number the officer typed. ok: true if the MDT bridge accepted it
end)

oxide-drugselling:stingCompleted

Fired when a dealer hands over to an undercover buyer at a hand-off meet, or when an undercover buyer collects a dealer's dead drop.

AddEventHandler('oxide-drugselling:stingCompleted', function(orderId, dealerSrc, coords)
    -- dealerSrc: the dealer's server ID. For a dead drop it's whoever carries the phone that
    -- handled the meet when it was collected, or nil if nobody does. coords: where it happened
end)

oxide-drugselling:orderClosed

Fired whenever a phone order ends.

AddEventHandler('oxide-drugselling:orderClosed', function(orderId, status)
    -- status: 'done' | 'failed' | 'expired' | 'declined'
end)
statusWhen
doneSold: a hand-off, a collected dead drop, or a sting hand-off or drop
failedCancelled, a no-show, a robbery, a drop bag taken by anyone (the dealer included), or the phone was destroyed
expiredThe client gave up waiting, or the player said "Not now"
declinedThe player declined or said "Don't got it", or the client walked after too many counters

Config Hooks

Two functions in shared/config.lua run on the server and get the same data as the events above. They're never stored in the database, so edits to them apply after a restart.

HookArgumentsSame data as
Config.OnDrugSold(saleData)oxide-drugselling:drugSold
Config.OnPhoneExamined(src, report)oxide-drugselling:evidence:phoneExamined

Both are run safely: an error in your code is logged and doesn't stop the sale or the phone opening. shared/config.lua is also sent to players' games, so keep secrets such as webhook URLs out of it and use the events from your own resource instead.

shared/config.lua
Config.OnDrugSold = function(saleData)
    local turfId = exports['op-crime']:getPlayerTurfZone(saleData.source)
    if turfId then
        TriggerEvent('op-crime:drugSold', saleData.source, turfId, saleData.finalPrice)
    end
end

Exports Your Drug Resource Can Provide

oxide-drugselling calls these on a drug's own resource when its entry in shared/config/drugs.lua asks for them. If the resource isn't running, the call is skipped and the sale goes through.

GetPlayerLevel

Named by progression.levelExport. Used for requiredLevel, which is checked on every way of selling.

exports('GetPlayerLevel', function(source)
    return 3 -- the player's level for this drug
end)

Return a number. Anything else counts as level 1. If the export throws an error, a warning is logged and the sale is allowed.

AddXP

Named by progression.addXpExport. Called after every sale with xpPerUnit * quantity * xpUnits, rounded to a whole number. xpUnits comes from the sold item's row and is 1 when the row doesn't set it (see Configuration).

exports('AddXP', function(source, amount)
end)

AddSold

Named by progression.addSoldExport. Called after every sale with the number of items sold (quantity), whatever the item's xpUnits.

exports('AddSold', function(source, quantity)
end)

GetStrainData (trait_bonus)

Named by a trait_bonus modifier's exportName. Gets the value of the item's sourceField, and returns a table whose traitsPath field holds the traits.

exports('GetStrainData', function(strainId)
    return {
        traits = { thc = 85, potency = 72 },
    }
end)

PriceFor (export_multiplier)

Named by an export_multiplier modifier's exportName. oxide-pills provides one called PriceFor.

exports('PriceFor', function(metadata, context)
    -- metadata: the item's metadata
    -- context: { source = seller, coords = sale position, npcModel = buyer model hash when there is a buyer }
    local multiplier = 1.2
    local details = {
        { label = 'Club kid', value = 'loves it', bonus = 0.2, color = 'text-emerald-400', type = 'variant' },
    }
    return multiplier, details -- details are optional tags for the sell menu
end)

A missing export, an error, or a return that isn't a number of 0 or more counts as 1.0, and is logged once.

Server Callbacks (Reference)

These are the requests the resource's own interface sends to the server, registered with o-link's callbacks. They're listed so you know what exists; other resources shouldn't call them.

CallbackUsed forRate limit
oxide-drugselling:server:getSellableItemsOpening a street sale: checks the buyer and police, returns the sell menu items3 s
oxide-drugselling:server:initSaleA street offer5 s
oxide-drugselling:server:acceptCounterAccepting a street counter-offer5 s
oxide-drugselling:corners:start / :stopStarting and stopping work on a corner2 s (start)
oxide-drugselling:corners:arrived / :dismissA corner buyer arriving or being sent away—
oxide-drugselling:corners:offer / :acceptCounterA corner offer and counter-offer3 s / 5 s
oxide-drugselling:phone:actionEvery button on the phone and in Evidence View0.25 s (texts 1.5 s)
oxide-drugselling:sting:actionEvery button on the sting phone3 s (posting)
oxide-drugselling:meet:open / :complete / :cancelPanelA meet hand-off2 s
oxide-drugselling:meets:mineThe meets for the phones a player carries—
oxide-drugselling:drop:open / :place / :take / :cancelPanelLeaving and taking dead drop bags1.5 s
oxide-drugselling:evidence:extractedFinishing a timed phone extraction—
oxide-drugselling:settings:fetch / :save / :resetThe settings menu (admins only, checked on the server)—

The server also asks a player's game three things through client callbacks: oxide-drugselling:client:clock (the in-game time), oxide-drugselling:client:findPoint (a pavement point for a meet) and oxide-drugselling:client:describePoint (the street name at a drop spot).

Network Events (Reference)

The resource's own network events, listed so you know what exists. Other resources shouldn't trigger them.

DirectionEventUsed for
Client → serveroxide-drugselling:server:syncNpcActionAsks the server to replay a buyer's movement or animation on nearby players' games. Only NPCs the sender controls and stands next to are relayed, at most 30 a second, with a payload the server rebuilds
Server → clientoxide-drugselling:client:replayNpcActionThe replay itself, sent to players within 75 m of the seller
Client → serveroxide-drugselling:phone:quickOpenThe Open trap phone key (rate limit 1 s)
Client → serveroxide-drugselling:phone:indoorsWhether the player is inside an interior, for No signal indoors
Client → serveroxide-drugselling:drops:resyncA player who just loaded asks for their drop list again
Server → clientoxide-drugselling:drops:listThe dead drop bags and cash within Visible within + 40 m of that player, sent when that set changes
Server → clientoxide-drugselling:client:dispatchRelayA server-built police alert, handed to a police officer's game for a dispatch resource that only takes alerts there
Server → clientoxide-drugselling:corners:buyer / :buyerGone / :endedA corner buyer is on the way, left, or the corner closed
Server → clientoxide-drugselling:meet:spawn / :despawnA meet client arrives or leaves
Server → clientoxide-drugselling:phone:open / :push / :ring / :chargingThe phone screen, a change to refresh, a ring nearby, a battery pack in use
Server → clientoxide-drugselling:evidence:extract / :traceA timed extraction, a trace on the map
Server → clientoxide-drugselling:sting:heatmapThe sting phone's heat map
Server → clientoxide:drugselling:openSettingsOpens the settings menu, sent by /drugselling settings, corners and drops
Client → serveroxide-drugselling:modelUnavailableA model the player's game couldn't load, passed on to online admins

NUI Messages (Reference)

For reference when reading the interface. Other resources don't need these.

DirectionNamePurpose
Lua → UIopenSellPanelOpens the sell menu: { items, isGangNpc, panelTitle, bands, request?, deal? }
Lua → UIsellThinking / sellResult / closeThe waiting state, the outcome, and closing
Lua → UIphoneOpen / phonePush / phoneCloseOpening the phone, a change to refresh, closing
Lua → UIphonePeekA text that arrived while the phone was put away
Lua → UImarkersThe dead drop markers to draw on screen
Lua → UIopenSettings / closeSettingsThe settings menu
UI → LuasellConfirm / sellAcceptCounter / sellCloseSell menu buttons
UI → LuaphoneAction / phoneClose / phoneClock / phoneGps / phoneMovePhone buttons, closing, the clock, setting a waypoint, walking while the phone is open
UI → LuasaveSettings / resetSetting / closeSettings / getItemCatalog / startPlacement / startZoneEdit / teleportToSettings menu actions
UI → LuagetLocaleReturns { locale } from the ox:locale convar

Integration Examples

Pay a gang for sales on their turf

my-turfs stands for your own turf resource and its exports.

AddEventHandler('oxide-drugselling:drugSold', function(src, saleData)
    if saleData.method == 'phone' or saleData.method == 'drop' then return end
    local coords = GetEntityCoords(GetPlayerPed(src))
    local turfId = exports['my-turfs']:GetTurfAt(coords)
    if turfId then
        exports['my-turfs']:AddTurfIncome(turfId, math.floor(saleData.finalPrice * 0.1))
    end
end)

Raise heat when shots are fired

my-shotspotter:shotsFired stands for any server event your server fires with a position.

AddEventHandler('my-shotspotter:shotsFired', function(coords)
    exports['oxide-drugselling']:AddHeat('area', coords, 15)
end)

Look up a number from your own police tool

local function DescribeNumber(number)
    local phone = exports['oxide-drugselling']:GetPhoneByNumber(number)
    if not phone then return 'No such number' end
    if phone.burned then return 'Phone destroyed' end
    local record = exports['oxide-drugselling']:GetPhoneRecord(phone.phoneId)
    return ('Battery %d%%, %s'):format(math.floor(record.battery), record.powered and 'on' or 'off')
end

Adding your own drug

  1. Register your item names in the inventory o-link uses.
  2. Put metadata on the items that matches your Config.Drugs entry.
  3. Add progression exports if you want level gating and XP.
  4. Add the drug to shared/config/drugs.lua, and restart. See Configuration → Adding your own drug.

Next Steps