Home › Documentation › Custom Labs · origen_ilegalv2
origen_ilegalv2 ships a generic Lab Process Runner that handles all the boilerplate for a production lab: interaction points, markers, progress bars, animations, server-side validation, item consumption, and upgrade modifiers. To add a new drug lab you only need to write a config table and call one function.
The runner has two sides that mirror each other:
| Side | File | Responsibility |
|---|---|---|
| Client | client/lab_process_runner.lua | lib.points, markers, HelpText, progress bars, animations, lifecycle events |
| Server | server/lab_process_runner.lua | Callback validation, item checks, item consumption/delivery, upgrade modifiers, cooldowns |
Both sides register under the same lab type key and communicate via auto-named ox_lib callbacks:
origen_gang:labs:{labType}:stage_{stageId}Config.LabHeroin = {
Enabled = true,
-- Ordered production stages
Stages = {
{ id = 'mix', label_key = 'mixing', help_key = 'mix', done_key = 'mix_done', timer = 10000 },
{ id = 'filter', label_key = 'filtering', help_key = 'filter', done_key = 'filter_done', timer = 8000 },
{ id = 'bag', label_key = 'bagging', help_key = 'bag', done_key = 'bag_done', timer = 6000 },
},
ProductionCooldown = 600000, -- 10 minutes between full cycles (ms)
Interactions = {
PointDistance = 40.0, -- lib.points render radius
DrawDistance = 8.0, -- marker draw distance
InteractDistance = 1.8, -- HelpText / [E] distance
},
-- World coords for each stage (interior coords inside the lab bucket)
Coords = {
mix = vector3(1006.09, -3200.59, -38.52),
filter = vector3(1007.89, -3201.17, -38.99),
bag = vector3(1014.25, -3194.93, -38.99),
},
-- Progress bar durations per stage (ms)
Timers = {
mix = 10000,
filter = 8000,
bag = 6000,
},
-- Items consumed/delivered per stage
-- inputs: consumed when the stage starts
-- outputs: delivered when the stage completes
-- requires: validated but NOT consumed (tools)
Recipe = {
mix = {
inputs = {
{ item = 'opium_raw', amount = 2 },
{ item = 'acetic_acid', amount = 1 },
},
},
bag = {
inputs = { { item = 'empty_bag', amount = 3 } },
outputs = { { item = 'heroin_bag', amount = 3 } },
},
},
}Stages order is the mandatory execution order. The runner enforces it — a player cannot jump to stage N+1 without completing stage N first.
if not Config.LabHeroin or not Config.LabHeroin.Enabled then return end
LabProcessRunner.Register('heroin', Config.LabHeroin)
-- Optional: expose a reset export for external resources
exports('HeroinLabResetPlayerState', function(source)
LabProcessRunner.ResetPlayerState('heroin', source)
end)if not Config.LabHeroin or not Config.LabHeroin.Enabled then return end
LabProcessRunner.Register('heroin', Config.LabHeroin)Config.LabTypesThe runner does not create the lab in the DB — you need to register a lab type so the admin panel can create instances of it:
Config.LabTypes = {
-- ...existing types...
heroin = {
label = 'Heroin Lab',
interior = {
spawn = vector4(997.17, -3200.64, -37.39, 270.0),
laptop = vector4(1002.01, -3194.89, -38.99, 356.98),
}
},
}fxmanifest.luashared_scripts {
-- ...
'config/lab_heroin.lua',
}
server_scripts {
-- ...
'server/lab_heroin.lua',
}
client_scripts {
-- ...
'client/lab_heroin.lua',
}For each stage you need three locale keys (in locales/translations/en.lua):
-- Progress bar label
['notify.heroin_mixing'] = 'Mixing...',
['notify.heroin_filtering'] = 'Filtering...',
['notify.heroin_bagging'] = 'Packaging...',
-- HelpText (shown when in range)
['notify.heroin_help_mix'] = 'Press [E] to mix chemicals',
['notify.heroin_help_filter'] = 'Press [E] to filter the mixture',
['notify.heroin_help_bag'] = 'Press [E] to bag the product',
-- Stage-done notifications
['notify.heroin_mix_done'] = 'Mixture ready',
['notify.heroin_filter_done'] = 'Filtration complete',
['notify.heroin_bag_done'] = 'Product packaged', -- last stage receives output amount as arg
-- Error messages used by the runner
['notify.heroin_not_in_lab'] = 'You are not inside the lab',
['notify.heroin_wrong_lab_type'] = 'Wrong lab type',
['notify.heroin_on_cooldown'] = 'Wait before starting another cycle',
['notify.heroin_already_in_progress'] = 'Production already in progress',
['notify.heroin_wrong_stage'] = 'Complete the previous step first',
['notify.heroin_requires_basic_upgrade'] = 'Upgrade the lab before processing',Stages arrayEach entry defines one production step:
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✅ | Unique stage identifier. Used in Coords, Timers, Markers, Animations, and Recipe keys |
label_key | string | ✅ | Locale suffix for the progress bar: notify.{labType}_{label_key} |
help_key | string | — | Locale suffix for HelpText: notify.{labType}_help_{help_key}. Defaults to label_key |
done_key | string | — | Locale suffix for stage-complete notification. Defaults to {label_key}_done |
timer | number | — | Stage timer in ms (informational — the runner uses Timers[id] at runtime) |
Recipe tableIndexed by stage_id. Each entry supports:
| Field | Type | Description |
|---|---|---|
inputs | { item, amount }[] | Consumed when the stage starts |
outputs | { item, amount }[] | Delivered when the stage completes |
requires | { item, amount }[] | Validated but NOT consumed (tools, licenses) |
Only the stages that consume or produce items need an entry. Stages without a Recipe entry are "free" transitions.
Markers tableIndexed by stage_id. All fields are optional — the runner falls back to a default marker:
Markers = {
mix = {
type = 22,
offset = { x = 0.0, y = 0.0, z = 0.0 },
scale = { x = 0.25, y = 0.25, z = 0.25 },
color = { r = 0, g = 0, b = 0, a = 180 },
bobUpAndDown = true,
faceCamera = true,
rotationOrder = 2,
rotate = false,
},
},Animations tableIndexed by stage_id. Two modes:
Simple (TaskPlayAnim):
mix = {
networked = false,
dict = 'anim@amb@business@meth@meth_monitoring_cooking@',
clip = 'idle_a',
flag = 49,
blendIn = 4.0,
blendOut = -8.0,
freeze = false,
},Networked (NetworkCreateSynchronisedScene) — with props:
cook = {
networked = true,
dict = 'anim@amb@business@meth@meth_monitoring_cooking@cooking@',
clip = 'chemical_pour_short_cooker',
flag = 49,
blendIn = 1.5,
blendOut = -4.0,
freeze = true,
offset = vector3(4.79, 2.13, -0.41),
rotation = vector3(0.0, 0.0, 0.0),
objects = {
{ hash = 'bkr_prop_meth_sacid', clip = 'chemical_pour_short_sacid' },
{ hash = 'bkr_prop_meth_ammonia', clip = 'chemical_pour_short_ammonia' },
},
},RequiresUpgrade flagBy default, the first stage of any runner-based lab requires a style module upgrade to be unlocked in that lab instance. To disable this gate (useful while testing or for free-access labs):
Config.LabHeroin = {
RequiresUpgrade = false, -- skip the upgrade gate
-- ...
}MaxDistPerStageGlobal or per-stage maximum distance (meters) for server-side interaction validation:
-- Global: same distance for every stage
MaxDistPerStage = 4.0,
-- Per-stage override
MaxDistPerStage = {
mix = 3.0,
filter = 5.0,
bag = 3.0,
},When a lab has style module upgrades unlocked (from origen_gang_lab_upgrade_unlocks), the runner automatically applies:
| Modifier | Effect | Stacking |
|---|---|---|
process_time_mult | Multiplies all stage timers (min 0.70 — max 30% reduction) | Multiplicative |
output_bonus_pct | % chance to give +1 unit on the last stage output (max 25%) | Max of all variants |
These are computed server-side at the start of each cycle and returned to the client as timers and modifiers in the first-stage callback response.
The weed lab (Config.LabWeed) does not use the generic runner. It has a custom flow:
| Step | Action | Items |
|---|---|---|
| 0 — Plants | Pick 14 plants (respawn after PlantRespawnTime s) | → wetcannabis ×1 each |
| 1 — Drying | Place wetcannabis in one of 4 independent dry spots | wetcannabis → drycannabis after DryWait ms |
| 2 — Grind | Use the grinder (requires weedgrinder, not consumed) | drycannabis → grindedweed |
| 3 — Bag | Use the bagger | grindedweed + bluntwrap → blunt |
Steps 1–3 are independent — there is no enforced sequence between them. You only need the required input item.
Dry slot timing is enforced server-side (ready_at timestamp) — the client timer is visual only.