Developer

4 min readUpdated 2 weeks ago

Overview

Three integration layers: server exports for progression, blueprints and bench registration; a named hook system any resource can attach to; and editable handler stubs in the open/ folder.

Server Exports

Progression

  • getPlayerLevel(src): Player's crafting level (from ml_skills when external skills are enabled)
  • getPlayerCategoryLevel(src, categoryId): Level for one recipe category
  • addXp(src, amount): Award crafting XP

Blueprints

  • getPlayerBlueprints(src): Map of blueprint id to { type = 'permanent' | 'item' }
  • givePlayerBlueprint(src, blueprintId): Permanently unlock a blueprint (alias: unlockPermanentBlueprint)
  • removePlayerBlueprint(src, blueprintId): Remove a permanent unlock

Benches

  • setEntityAsBench(entity, benchTypeId): Register an existing entity as a usable bench. Automatically cleared when the registering resource stops
  • removeEntityAsBench(entity): Unregister it
  • setCoordAsBench(coord, benchTypeId, routingBucket?): Create a bench at coordinates (coord.w = heading). Returns { success, id, remove } where remove() deletes it again

Data & Queue

  • getCategories(): All recipe categories
  • getRecipe(recipeId): One recipe with ingredients, results and blueprints
  • createRecipe(data): Create a recipe programmatically
  • getPlayerQueue(src): The player's queued crafts
  • getQueuedCrafts(benchLocationId): Queue of one bench

Hook System

Hooks let another resource validate, modify or observe crafting without touching ml_crafting. Register from any server script:

your_script/server.lua
1local hookId = exports.ml_crafting:registerHook('crafting:preCraft', {
2    priority = 10,
3    fn = function(ctx)
4        -- return false                                to block
5        -- return { success = false, message = 'msg' } to block with a reason
6        -- return { quantity = 1 }                     to modify the context
7        return true
8    end
9})
10
11exports.ml_crafting:unregisterHook('crafting:preCraft', hookId)

Hooks run in ascending priority. A table return without success is merged into the context. Hooks registered by a resource are removed automatically when that resource stops.

Hook points

  • crafting:canInteract: Player opens a bench. ctx: src, benchType, benchLocation
  • crafting:enter: After access is granted, before the menu data is built. Same ctx
  • crafting:getCategories: Filter or extend the category list. ctx adds categories
  • crafting:getRecipes: Filter or extend a category's recipe list. ctx adds categoryId, recipes
  • crafting:getRecipe: Modify a single recipe before display. ctx adds recipe
  • crafting:preCraft: Before ingredients are taken. ctx: src, recipe, quantity, benchType, benchLocation. Can change quantity or block the craft
  • crafting:postCraft: After results are delivered. ctx adds results. Observe-only
  • crafting:exit: Player leaves the bench. ctx: src, benchType, benchLocation. Observe-only

Open Handlers

The open/ folder is never escrowed. The shipped stubs are pre-registered into the hook system at priority 50.

Server

open/server.lua
1Handlers.CanPlayerUseBench = function(src, benchType, benchLocation)
2    return true -- return false (optionally with a message) to block
3end
4
5Handlers.OnBeforeCraft = function(src, recipe, quantity)
6    return true -- return false to block the craft
7end
8
9Handlers.OnAfterCraft = function(src, recipe, quantity, results)
10end
11
12Handlers.OnBlueprintUnlocked = function(src, blueprintId)
13end
14
15Handlers.OnLevelUp = function(src, category, newLevel)
16end
17
18Handlers.ModifyCraftResults = function(src, recipe, results)
19    return nil -- return a modified results table to replace the output
20end
  • OnLevelUp fires only with the internal progression (Config.ExternalSkills.Enabled = false)
  • ModifyCraftResults runs right before items are handed out, last chance to add, remove or retag results

Client

open/client.lua
1Handlers.CanInteractWithBench = function(benchType, entity)
2    return true -- return false to hide the interaction
3end
4
5Handlers.OnCraftStarted = function(recipe)
6end
7
8Handlers.GetCustomAnimation = function(recipe)
9    return nil -- or { dict = '...', clip = '...', flag = 49 }
10end
11
12Handlers.OnBenchEnter = function(benchType, entity)
13end
14
15Handlers.OnBenchExit = function(benchType, entity)
16end
17
18Handlers.OnCraftCompleted = function(recipe, results)
19end

Skill Bonuses

Recipes can carry skill bonuses, set in the recipe editor. When the player has the bound ml_skills skill unlocked, the bonus applies automatically. Four bonus types are supported; values are percentages, and multiple bonuses of the same type stack additively:

  • time_reduction: Shorter craft time, capped at 90%
  • extra_xp: More XP from the craft
  • extra_quantity: Chance-scaled extra output quantity
  • ingredient_reduction: Chance to consume fewer ingredients

Examples

your_script/server.lua
1local bench = exports.ml_crafting:setCoordAsBench(
2    vector4(-261.95, -956.42, 31.22, 90.0),
3    'weapons_bench'
4)
5
6if bench.success then
7    print('Bench created with id ' .. bench.id)
8    -- later: bench.remove()
9end
open/server.lua
1Handlers.CanPlayerUseBench = function(src, benchType, benchLocation)
2    if benchType.id == 'medic_bench' and not Bridge.HasJob(src, 'ambulance') then
3        return false, 'error_not_authorized'
4    end
5    return true
6end
your_script/server.lua
1exports.ml_crafting:registerHook('crafting:postCraft', {
2    priority = 10,
3    fn = function(ctx)
4        -- ctx.src, ctx.recipe, ctx.quantity, ctx.results
5        TriggerEvent('myeconomy:craftTax', ctx.src, ctx.recipe.id, ctx.quantity)
6    end
7})
open/client.lua
1Handlers.GetCustomAnimation = function(recipe)
2    if recipe.category_id == 'cooking' then
3        return { dict = 'amb@prop_human_bbq@male@base', clip = 'base', flag = 49 }
4    end
5    return nil
6end
7
8Handlers.OnCraftCompleted = function(recipe, results)
9end
open/client.lua
1Handlers.OnBenchEnter = function(benchType, entity)
2    local coords = GetEntityCoords(cache.ped)
3    print(('Entered bench at %.1f %.1f %.1f'):format(coords.x, coords.y, coords.z))
4end
5
6Handlers.OnCraftCompleted = function(recipe, results)
7end