Configuration
Configure orbit-craftingsystem shared recipes, stations, benches, blueprints, images, webhooks, and logging hooks.
Configuration
orbit-craftingsystem has two public config files. Use shared/config.lua for gameplay and recipes. Use server/config.lua for server-only logging, image paths, and Discord webhooks.
Do not mix the files
Put craftable items, benches, stations, blueprints, XP, and access rules in shared/config.lua. Put webhook URLs and logging callbacks in server/config.lua.
File Map
Prop
Type
Setup Workflow
Choose where the item belongs
Use a station item when the recipe should be available at a fixed world location. Use a bench non-blueprint item when the recipe should always be available on a portable bench. Use a blueprint item when players must own or install a blueprint first.
Add the inventory items first
Every final item and every required material must exist in your inventory or framework item list before testing. If the inventory does not know metalscrap, steel, or weapon_pistol, the crafting system cannot add or remove them correctly.
Add one recipe and test it
Add one recipe to shared/config.lua, restart the resource, open the station or bench, and test one craft. After that works, duplicate the same structure for the rest of your recipes.
Enable logging
Add your Discord webhook URL in server/config.lua, then keep or customize Config.Server.OnCraft and Config.Server.SuspiciousActivity.
Shared Config Shape
shared/config.lua is read in this order. Keeping the same order makes it much easier to compare your file with the shipped config when you update the resource.
Config = Config or {}
Config.DeveloperMode = false
Config.CraftingWithWheel = true
Config.NuiState = function(bool)
if not Config.DeveloperMode then return end
print("NUI Open:", bool)
end
Config.Celebration = {
animDict = "anim@mp_player_intcelebrationmale@air_guitar",
animName = "air_guitar"
}
Config.BenchAnimations = {
putting_down_bench = {
duration = 5000,
animDict = "amb@world_human_hammering@male@base",
anim = "base",
flag = 1
},
moving_bench = {
duration = 1000,
animDict = "amb@world_human_hammering@male@base",
anim = "base",
flag = 1
}
}
Config.LevelValues = {
xpGainMultiplier = 10,
levelUpXPMultiplier = 100,
loseMultiplier = 0.5
}
Config.MainRoutingBucket = 0
Config.Benches = {}
Config.Blueprints = {}
Config.CustomBlueprints = {}
Config.Stations = {}The empty tables above are only an outline. In your real file, Config.Benches, Config.Blueprints, and Config.Stations contain the actual bench types, blueprint recipes, fixed stations, recipes, targets, and access groups.
Add Bench Items To Inventory
Every key inside Config.Benches.benchTypes must also exist as an inventory item. The default config includes weapon-crafting_bench and medical_bench, so those item names need to be added before players can place those benches.
Bench item names must match
If the bench type is ["weapon-crafting_bench"], the inventory item must also be weapon-crafting_bench. Bench item names cannot start with weapon_ and must include bench.
Add the bench item inside your inventory resource. For ESX servers using ox_inventory, this is usually ox_inventory/data/items.lua.
["weapon-crafting_bench"] = {
label = "Weapon Crafting Bench",
weight = 1000,
stack = false,
close = true,
description = "Places a weapon crafting bench."
},
["medical_bench"] = {
label = "Medical Crafting Bench",
weight = 1000,
stack = false,
close = true,
description = "Places a medical crafting bench."
},If your ESX server uses a different inventory, add the same item names using that inventory's item format. The important part is that the item name exactly matches the bench key in Config.Benches.benchTypes.
Add the bench item in QBCore's shared item file, not inside the crafting resource.
["weapon-crafting_bench"] = {
name = "weapon-crafting_bench",
label = "Weapon Crafting Bench",
weight = 1000,
type = "item",
image = "weapon-crafting_bench.png",
unique = true,
useable = true,
shouldClose = true,
combinable = nil,
description = "Places a weapon crafting bench."
},
["medical_bench"] = {
name = "medical_bench",
label = "Medical Crafting Bench",
weight = 1000,
type = "item",
image = "medical_bench.png",
unique = true,
useable = true,
shouldClose = true,
combinable = nil,
description = "Places a medical crafting bench."
},unique = true is recommended because benches can carry metadata, especially when Config.Blueprints.store = "bench".
Add the bench item inside your inventory resource. For most Qbox/QBX servers this is the inventory item file, for example ox_inventory/data/items.lua.
["weapon-crafting_bench"] = {
label = "Weapon Crafting Bench",
weight = 1000,
stack = false,
close = true,
description = "Places a weapon crafting bench."
},
["medical_bench"] = {
label = "Medical Crafting Bench",
weight = 1000,
stack = false,
close = true,
description = "Places a medical crafting bench."
},If you rename or add a bench type in Config.Benches.benchTypes, add the same item name here too.
The crafting system registers each bench type as a usable item through orbit-lib. That means players use the inventory item, the resource previews the bench prop, and then the bench is placed if the player confirms the position.
Where To Add Items
Craftable items can live in three places. The recipe shape is mostly the same, but the location decides how the player accesses it.
Put fixed-location recipes inside Config.Stations[stationId].items.
Config.Stations = {
HideStationDistance = 50,
["weapon_station"] = {
title = "Weapon Crafting Station",
secondaryTitle = "Orbit Studios",
blip = {
haveBlip = true,
sprite = 110,
color = 1,
scale = 0.8,
coords = vector3(502.64, -1338.17, 29.31)
},
target = {
type = "npc",
model = "s_m_m_ammucountry",
coords = vector3(502.64, -1338.17, 29.31),
heading = 37.18
},
groups = {
jobs = {},
gangs = {}
},
items = {
["weapon_pistol"] = {
type = "weapon",
itemLevel = 1,
baseOdds = 50,
maxOdds = 90,
amountToCraft = 1,
requirements = {
{ item = "plastic", amount = 5, removeOnSuccess = true, removeOnFail = true },
{ item = "iron", amount = 5, removeOnSuccess = true, removeOnFail = false }
}
}
}
}
}Leave groups.jobs and groups.gangs empty for a public station. Add job or gang names only when the station should be restricted.
Put bench recipes that do not need blueprints inside Config.Benches.benchTypes[benchItemName].nonBlueprintItems.
Config.Benches = {
onlyOwnerCanMove = true,
enableBenches = true,
enablePersistentBenches = true,
HideBenchDistance = 50,
benchTypes = {
["weapon-crafting_bench"] = {
benchProp = "prop_tool_bench02",
benchLabel = "Weapon Bench",
title = "Weapon Crafting Bench",
secondaryTitle = "Orbit Studios",
nonBlueprintItems = {
["weapon_knife"] = {
type = "weapon",
itemLevel = 1,
baseOdds = 50,
maxOdds = 90,
amountToCraft = 1,
requirements = {
{ item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true },
{ item = "steel", amount = 5, removeOnSuccess = true, removeOnFail = false }
}
}
}
}
}
}The bench type key, such as weapon-crafting_bench, must match the bench inventory item name. Bench item names cannot start with weapon_ and must include bench.
Put blueprint-locked recipes inside Config.Blueprints.items.
Config.Blueprints = {
store = "bench",
items = {
["weapon_pistol"] = {
bench = "weapon-crafting_bench",
type = "weapon",
itemLevel = 1,
baseOdds = 60,
maxOdds = 90,
amountToCraft = 1,
requirements = {
{ item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true },
{ item = "steel", amount = 5, removeOnSuccess = true, removeOnFail = false }
}
}
}
}store = "bench" stores learned blueprints on the bench. store = "player" stores learned blueprints on the player. The bench value must match a key in Config.Benches.benchTypes.
Use Config.CustomBlueprints when the physical inventory item has a different name from the recipe key.
Config.Blueprints = {
store = "bench",
items = {
["weapon_pistol"] = {
bench = "weapon-crafting_bench",
type = "weapon",
itemLevel = 1,
baseOdds = 60,
maxOdds = 90,
requirements = {
{ item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true }
}
}
}
}
Config.CustomBlueprints = {
["weapon_pistol"] = {
customBlueprintName = "blueprint_weapon_pistol"
}
}In this example, the blueprint content is still weapon_pistol, but the inventory item given to the player is blueprint_weapon_pistol.
Recipe Fields
Every recipe uses the same core fields whether it is placed in a station, bench, or blueprint list.
Prop
Type
["firstaid"] = {
itemLevel = 2,
baseOdds = 40,
maxOdds = 90,
amountToCraft = 2,
requirements = {
{ item = "bandage", amount = 5, removeOnSuccess = true, removeOnFail = true },
{ item = "painkillers", amount = 2, removeOnSuccess = true, removeOnFail = true }
}
}Set removeOnSuccess and removeOnFail explicitly for each requirement while learning the config. That makes it obvious whether materials disappear on a successful craft, a failed craft, or both.
Core Shared Options
Prop
Type
Crafting Wheel
Config.CraftingWithWheel decides how strict the crafting level system is.
Config.CraftingWithWheel = trueWhen this is true, players use the success-percentage crafting wheel. itemLevel, baseOdds, and maxOdds decide how difficult the craft is and how much the chance can improve.
When this is false, crafting becomes level-gated. Players can craft items at their level or below their level, and the percentage wheel is not used.
NUI State Callback
Config.NuiState runs whenever the crafting UI opens or closes. Use it when another resource needs to know that the player is inside the crafting interface.
Config.NuiState = function(bool)
if not Config.DeveloperMode then return end
print("NUI Open:", bool)
endbool = true means the UI opened. bool = false means it closed.
Set it to nil if you do not need this callback:
Config.NuiState = nilCelebration Animation
Config.Celebration is the animation played after a successful craft.
Config.Celebration = {
animDict = "anim@mp_player_intcelebrationmale@air_guitar",
animName = "air_guitar"
}To change the animation, replace both values with a valid GTA animation dictionary and animation name. To disable the celebration, set both values to nil.
Config.Celebration = {
animDict = nil,
animName = nil
}Bench Animations
Config.BenchAnimations is used by the orbit-lib progress integration when a player places or moves a bench.
Config.BenchAnimations = {
putting_down_bench = {
duration = 5000,
animDict = "amb@world_human_hammering@male@base",
anim = "base",
flag = 1
},
moving_bench = {
duration = 1000,
animDict = "amb@world_human_hammering@male@base",
anim = "base",
flag = 1
}
}duration is in milliseconds. animDict, anim, and flag are passed to the progress animation. If you make the duration very short, bench placement will feel instant and players may not understand that the bench is being placed.
Level Values
Config.LevelValues = {
xpGainMultiplier = 10,
levelUpXPMultiplier = 100,
loseMultiplier = 0.5
}loseMultiplier = 0.5 means a failed craft gives half of the XP the craft would normally give.
Bench Placement
Bench placement controls where portable benches can be placed.
Blacklist mode allows bench placement everywhere except inside listed zones.
Config.Benches.benchPlacement = {
enabled = true,
mode = "blacklist",
zones = {
{
vectors = {
vector3(494.22, -1328.00, 29.00),
vector3(494.70, -1333.96, 29.00),
vector3(500.11, -1335.09, 29.00)
},
height = 10
}
}
}Whitelist mode blocks bench placement everywhere except inside listed zones.
Config.Benches.benchPlacement = {
enabled = true,
mode = "whitelist",
zones = {
{
vectors = {
vector3(493.93, -1335.25, 29.00)
},
radius = 5.0
}
}
}Use vectors plus height for polygon zones. Use vectors plus radius for sphere zones. Use vectors plus size = vec3(...) for box zones.
{
vectors = {
vector3(493.93, -1335.25, 29.00)
},
size = vec3(2, 2, 2)
}Station Access
Access groups live inside each station. Leave both tables empty for public use.
groups = {
jobs = {},
gangs = {}
}Restrict by job or gang name with a minimum grade:
groups = {
jobs = {
["police"] = 0,
["ambulance"] = 2
},
gangs = {
["lostmc"] = 1
}
}["ambulance"] = 2 means the player must have the ambulance job at grade 2 or higher. Job and gang names must match the data returned by your framework through orbit-lib.
Server Config
resources/[orbit]/orbit-craftingsystem/server/config.lua is server-only. This is where inventory image URLs, Discord webhooks, and logging callbacks live.
Prop
Type
Inventory Image URLs
Use Config.Server.CustomImageURL when item images are missing in the crafting UI.
Config.Server.CustomImageURL = falseFor ox_inventory, this is a common value:
Config.Server.CustomImageURL = "cfx-nui-ox_inventory/web/images/"The image file name should match the inventory item name. For example, weapon_pistol normally needs an image named like weapon_pistol.png in the configured image directory.
Webhooks
Add your Discord webhook URL in server/config.lua. Do not put webhook URLs in shared/config.lua, because shared files can be loaded client-side.
Config.Server.Webhook = {
url = "https://discord.com/api/webhooks/...",
username = nil,
avatar = nil
}Use username and avatar only if you want to override the Discord webhook defaults:
Config.Server.Webhook = {
url = "https://discord.com/api/webhooks/...",
username = "Orbit Crafting Logs",
avatar = "https://example.com/logo.png"
}The included sender uses Discord embeds:
function Config.Server.SendWebhook(embed)
local webhook = Config.Server.Webhook
if not webhook.url or webhook.url == "" then return end
PerformHttpRequest(
webhook.url,
function() end,
"POST",
json.encode({
username = webhook.username,
avatar_url = webhook.avatar,
embeds = { embed }
}),
{ ["Content-Type"] = "application/json" }
)
endCrafting Log Webhook
Config.Server.OnCraft runs after a valid crafting attempt. It receives the player, station or bench ID, item name, crafted amount, item level, and whether the craft succeeded.
Config.Server.Colors = {
success = "#2ecc71",
failed = "#e74c3c",
suspicious = "#ff0000"
}
local function HexToDecimal(hex)
return tonumber(hex:gsub("#", ""), 16)
end
Config.Server.OnCraft = function(source, stationId, itemName, craftedAmount, itemLevel, isWin)
local playerName = GetPlayerName(source) or "Unknown"
local playerId = source
local embed = {
title = isWin and "Crafting Successful" or "Crafting Failed",
description = isWin and "The player successfully crafted an item." or "The crafting attempt failed.",
color = HexToDecimal(isWin and Config.Server.Colors.success or Config.Server.Colors.failed),
author = {
name = playerName .. " (" .. playerId .. ")"
},
fields = {
{
name = "Station or Bench",
value = tostring(stationId),
inline = false
},
{
name = "Item",
value = tostring(itemName),
inline = true
},
{
name = "Amount",
value = tostring(craftedAmount),
inline = true
},
{
name = "Item Level",
value = tostring(itemLevel),
inline = true
}
},
footer = {
text = "Orbit Crafting System"
},
timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
}
Config.Server.SendWebhook(embed)
endSet Config.Server.OnCraft = nil if you want to disable normal crafting logs entirely:
Config.Server.OnCraft = nilSuspicious Activity Webhook
Config.Server.SuspiciousActivity runs when the server detects an invalid crafting attempt. This is useful for exploit attempts, invalid items, missing requirements, or other rejected craft requests.
Config.Server.SuspiciousActivity = function(source, stationId, itemName, craftedAmount, reason, isWin)
local playerName = GetPlayerName(source) or "Unknown"
local playerId = source
local embed = {
title = "Suspicious Crafting Activity",
description = "Potential exploit or invalid crafting attempt detected.",
color = HexToDecimal(Config.Server.Colors.suspicious),
author = {
name = playerName .. " (" .. playerId .. ")"
},
fields = {
{
name = "Station or Bench",
value = tostring(stationId),
inline = false
},
{
name = "Item",
value = tostring(itemName),
inline = true
},
{
name = "Attempted Amount",
value = tostring(craftedAmount),
inline = true
},
{
name = "Reason",
value = tostring(reason),
inline = false
}
},
footer = {
text = "Orbit Crafting System - monitor recommended"
},
timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
}
Config.Server.SendWebhook(embed)
endSet Config.Server.SuspiciousActivity = nil if you want to disable suspicious activity logs:
Config.Server.SuspiciousActivity = nilWebhook Testing
Add the webhook URL
Paste the Discord webhook URL into Config.Server.Webhook.url in server/config.lua.
Keep OnCraft enabled
Make sure Config.Server.OnCraft is a function, not nil.
Craft one test item
Join the server, open a station or bench, and craft an item. Discord should receive a success or failure embed.
Check the server console
If no message arrives, confirm the URL is not empty, outbound HTTP is allowed, and the config file is saved on the server.