Exports & API Reference

Server and client exports, the o-link medical namespace, replicated statebags, and events provided by oxide-medical.

The developer surface of oxide-medical — what other resources can call, read and listen to.

oxide-medical provides the o-link medical namespace. If you are writing a framework-agnostic resource, use that. The direct exports below are the full surface, including the few functions the bridge does not carry.

The Condition Object

Returned everywhere a condition is handed out:

{
    id,         -- number, database row id
    category,   -- 'fracture' | 'disease' | 'sickness' | 'wound'
    type,       -- catalog key, e.g. 'broken_leg_left', 'flu', 'gunshot'
    bodypart,   -- one of the 15 body parts, or nil
    severity,   -- 1 minor | 2 moderate | 3 severe | 4 critical
    onset,      -- when it started
    expiresAt,  -- when it clears on its own, or nil
    data,       -- provider pass-through: { weaponHash, class, bullet, source } or nil
}

"Active" means not treated and not expired. Every read returns active conditions only.

The fifteen body parts: HEAD, NECK, SPINE, UPPER_BODY, LOWER_BODY, LARM, LHAND, LFINGER, LLEG, LFOOT, RARM, RHAND, RFINGER, RLEG, RFOOT.

Server Exports

local M = exports['oxide-medical']

Reading the record

ExportReturns
M:GetRecord(src){ charId, bloodType, dnaHash, immune, allergies, immunities, chronic } or nil
M:GetBloodType(src)'O+' etc., or nil
M:GetDNA(src)The DNA hash string, or nil
M:GetImmunity(src)0100, or nil when no record is loaded
M:GetOfflineRecord(charId){ record, conditions } read straight from the database, or nil

GetOfflineRecord takes a character id string, not a server id, and works whether or not the player is online. It is the right call for an MDT, a medical-records lookup or anything reading a character who is not connected.

Reading conditions

ExportReturns
M:GetConditions(src)Array of active condition objects
M:HasCondition(src, typeOrCategory)boolean. Matches either a condition type or a whole category
M:GetVitals(src){ pulse, systolic, diastolic, spo2, trend, arrest? }, or nil before the first tick
if exports['oxide-medical']:HasCondition(src, 'fracture') then ... end
if exports['oxide-medical']:HasCondition(src, 'cardiac_arrest') then ... end

Changing conditions

ExportReturns
M:AddCondition(src, payload)The new condition's id, or false
M:TreatCondition(src, idOrType, treaterSrc?)boolean
M:RemoveCondition(src, idOrType)boolean
local id = exports['oxide-medical']:AddCondition(src, {
    category = 'sickness',       -- required
    type = 'food_poisoning',     -- required
    bodypart = nil,              -- optional
    severity = 2,                -- optional, defaults to 1, clamped 1-4
    expiresInSec = 540,          -- optional; omit for permanent
    data = { source = 'my-resource' },  -- optional, stored as JSON
})

TreatCondition vs RemoveCondition. Treating marks it treated, records who did it, and grants a disease's immunity window — it is the medical outcome. Removing deletes it with no trace and no immunity — it is a correction. Pick deliberately.

treaterSrc is optional; passing it records that player's character id as the treater.

byEmsOnly does not apply here. That flag only stops a player self-treating with an item. These exports treat anything.

Immunity

ExportReturns
M:SetImmunity(src, value)The clamped value, or false
M:ModifyImmunity(src, delta)The clamped value, or false

Both refresh the player's statebag. Lower immunity means illness takes hold more easily.

Suspension

ExportReturns
M:SetExempt(src, bool)true
M:IsExempt(src)boolean

While exempt, the player's conditions, physical effects and symptoms all blank out (their blood type and immune stat are kept), every trigger and the damage tick skip them, and no wound reports are accepted. Turning it off restores everything immediately.

IsExempt is also true whenever the player's replicated godmode flag is set, with no dependency on the resource that sets it.

Client Exports

exports['oxide-medical']:GetConditions()  --> array of condition objects (own player)
exports['oxide-medical']:IsInjured()      --> boolean

Both read from the local statebag cache, so they are cheap and safe in a loop.

The framework-agnostic path, and what you should use in a released resource.

Server:

olink.medical.GetResourceName()             --> 'oxide-medical', or 'none' when absent
olink.medical.GetRecord(src)
olink.medical.GetBloodType(src)
olink.medical.GetDNA(src)
olink.medical.GetImmunity(src)
olink.medical.GetConditions(src)
olink.medical.GetVitals(src)
olink.medical.HasCondition(src, typeOrCategory)
olink.medical.AddCondition(src, payload)
olink.medical.TreatCondition(src, idOrType, treaterSrc?)
olink.medical.RemoveCondition(src, idOrType)
olink.medical.GetOfflineRecord(charId)

Client:

olink.medical.GetConditions()
olink.medical.IsInjured()

Not on the bridge: SetImmunity, ModifyImmunity, SetExempt and IsExempt. Call those through the direct exports if you need them.

Detecting whether it is actually installed

olink.supports() is not enough here. The o-link stubs are callable and answer with defaults, so a bare install passes the check and then silently returns nothing.

Gate on the provider name instead:

local function MedicalAvailable()
    local fn = olink.medical and olink.medical.GetResourceName
    if not fn then return false end
    local ok, name = pcall(fn)
    return ok and type(name) == 'string' and name ~= '' and name ~= 'none'
end

This is exactly what oxide-ems does before offering any treatment.

Statebags

oxide:medical — the condition view

Replicated, so any client can read it for any player.

Player(src).state['oxide:medical']        -- server
LocalPlayer.state['oxide:medical']        -- client, own player
{
    bloodType = 'O+',
    immune = 100,
    conditions = {                        -- active only, compact form
        { id, category, type, bodypart, severity },
    },
    flags = {
        movementPenalty = 0.0,            -- 0-1, worst active fracture
        limp = false,
        aimSway = false,
        bleeding = 0,                     -- 0-5, summed severity of bleeding wounds
        hasBullet = false,                -- a retained bullet is present
    },
    symptoms = { cough = true, queasy = true },   -- aggregate set, O(1) lookup
}

Watch it on the client:

AddStateBagChangeHandler('oxide:medical', ('player:%s'):format(cache.serverId), function(_, _, value)
    -- value is the view above, or nil
end)

flags is the cheapest way to ask "is this player bleeding / limping / carrying a bullet" without a callback. symptoms is a set, not an array — check symptoms.cough, don't iterate looking for it.

oxide:medical:vitals — the vitals feed

A separate bag on purpose. Anything watching oxide:medical re-runs the whole client effects pipeline on change, and a value that ticks every two seconds must not live there.

{
    pulse = 76,        -- bpm, quantized
    systolic = 120,    -- mmHg, quantized
    diastolic = 80,
    spo2 = 98,         -- %, quantized
    trend = 'steady',  -- 'rising' | 'falling' | 'steady'
    arrest = true,     -- present only during cardiac arrest; absent otherwise
}

Published only when a quantized value actually changes, so an idle player costs nothing. nil before the first tick after login, and cleared on disconnect.

A dead player reads 0/0/0 with spo2 = 0 — a flatline.

Events

Condition changes

AddEventHandler('oxide:medical:conditionAdded',   function(src, condition) end)
AddEventHandler('oxide:medical:conditionTreated', function(src, conditionId) end)
AddEventHandler('oxide:medical:conditionRemoved', function(src, conditionId) end)
AddEventHandler('oxide:medical:conditionUpdated', function(src, condition) end)

conditionUpdated fires when severity or data changes on an existing condition — disease progression is the main source.

Server-side only.

One event carrying all four, for consumers that would rather subscribe once:

AddEventHandler('olink:server:medicalConditionChanged', function(src, data)
    -- data = { action = 'added'|'treated'|'removed'|'updated', condition = { ... } }
end)

This is what oxide-ems listens to for its triage state.

Blood evidence

AddEventHandler('oxide:medical:bleed', function(src, data)
    -- data = { coords, charId, dnaHash, bodypart, severity }
end)

AddEventHandler('olink:server:medicalBleed', function(src, data) end)   -- same payload

Fires when a player bleeds from a non-firearm source: falls, fire, blades and animal attacks. Rate-limited per player.

Firearm and melee blood is deliberately excluded — oxide-police drops that itself from the weapon-damage event, and emitting here as well would create the evidence twice.

Internal events — do not trigger these

These carry the resource's own state between its server and client sides. They validate their input, so triggering them from outside will not corrupt anything, but they are not an API:

oxide:medical:applyDamage, oxide:medical:treatStart, oxide:medical:reportWound, oxide:medical:treatComplete, oxide:medical:treatCancel, oxide:medical:reportContagion, oxide:medical:reportExposure.

Usable Items

Every distinct cure item across all four catalogs is registered as a usable item through o-link at startup. Using one runs a progress bar and, on completion, treats the worst-severity active condition that item cures. The server re-validates the item and the condition before consuming anything.

Items whose only match is a medic-only condition tell the player a medic is needed rather than being consumed.

Registering the same item name yourself elsewhere will conflict — check the catalogs first.

Adding Your Own Conditions

You do not need code. Add an entry to a catalog in shared/config/ and everything else follows: persistence, the statebag view, symptoms, notifications, treatment, damage over time, the admin tools and the exports all read from the catalogs.

The one thing that will not happen automatically is a new symptom token having a visible effect — map it in shared/config/symptoms.lua, or it is silently ignored.

If you add a condition from another resource via AddCondition with a type that is not in any catalog, it works — it persists, appears in the statebag, shows in /med record and can be treated — but it has no symptoms, no cure item and no notification pool. Add a catalog entry to give it those.