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.
How it works
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}Minimum viable lab
1. Create the config file
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.
2. Register on the server
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)3. Register on the client
if not Config.LabHeroin or not Config.LabHeroin.Enabled then return end
LabProcessRunner.Register('heroin', Config.LabHeroin)4. Add the lab type to Config.LabTypes
The 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),
}
},
}5. Add to fxmanifest.lua
shared_scripts {
-- ...
'config/lab_heroin.lua',
}
server_scripts {
-- ...
'server/lab_heroin.lua',
}
client_scripts {
-- ...
'client/lab_heroin.lua',
}6. Add locale keys
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',Full config reference
Stages array
Each 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 table
Indexed 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 table
Indexed 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 table
Indexed 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 flag
By 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
-- ...
}MaxDistPerStage
Global 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,
},Upgrade modifiers
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.
Weed lab: special flow
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.