Crafting System for s&box Games
Build a server-authoritative crafting system with recipe tables, material checks, output items, and crafting XP. This works for survival games, factory games, m...
# Crafting System for s&box Games
Build a server-authoritative crafting system with recipe tables, material checks, output items, and crafting XP. This works for survival games, factory games, mining games, and RPG crafting benches.
The client sends only a `recipeId` and quantity. Network Storage looks up the recipe, verifies materials, spends inputs, grants outputs, and awards XP.
## Collection: `player_crafting`
Create `collections/player_crafting.collection.yml`:
```yml
sourceVersion: "1"
kind: collection
name: player_crafting
description: Player materials, crafted items, and crafting XP.
collectionType: per-steamid
accessMode: endpoint
maxRecords: 1
allowRecordDelete: false
requireSaveVersion: true
rateLimits:
mode: none
rateLimitAction: reject
schema:
type: object
properties:
materials:
type: object
additionalProperties:
type: number
items:
type: object
additionalProperties:
type: number
craftingXp:
type: number
min: 0
_ledger: true
lifetimeCrafts:
type: number
min: 0
```
## Game Values: recipe table
Create `collections/game_values.collection.yml`:
```yml
sourceVersion: "1"
kind: collection
name: game_values
description: Crafting recipe definitions.
collectionType: game_values
accessMode: endpoint
schema: {}
tables:
- id: crafting_recipes
name: Crafting Recipes
columns:
- key: id
type: string
- key: displayName
type: string
- key: ingredientA
type: string
- key: amountA
type: number
- key: ingredientB
type: string
- key: amountB
type: number
- key: outputItem
type: string
- key: outputQty
type: number
- key: xpReward
type: number
rows:
- id: iron_plate
displayName: Iron Plate
ingredientA: iron_ore
amountA: 3
ingredientB: coal
amountB: 1
outputItem: iron_plate
outputQty: 1
xpReward: 10
- id: repair_kit
displayName: Repair Kit
ingredientA: iron_plate
amountA: 2
ingredientB: cloth
amountB: 4
outputItem: repair_kit
outputQty: 1
xpReward: 25
- id: gold_circuit
displayName: Gold Circuit
ingredientA: gold_ore
amountA: 2
ingredientB: copper_wire
amountB: 5
outputItem: gold_circuit
outputQty: 1
xpReward: 60
```
## Workflow: validate recipe materials
Create `workflows/crafting_validate_recipe.workflow.yml`:
```yml
sourceVersion: "1"
kind: workflow
id: crafting_validate_recipe
name: Crafting Validate Recipe
description: Checks recipe existence, quantity, and both material requirements.
params:
player:
type: object
recipe:
type: object
quantity:
type: number
steps:
- id: recipe_exists
type: condition
check:
field: "{{recipe.id}}"
op: exists
onFail:
status: 404
errorCode: RECIPE_NOT_FOUND
message: Unknown recipe.
- id: craft_qty
type: transform
expression: "clamp(floor({{quantity}}), 1, 99)"
- id: need_a
type: transform
expression: "{{recipe.amountA}} * {{craft_qty}}"
- id: need_b
type: transform
expression: "{{recipe.amountB}} * {{craft_qty}}"
- id: have_a
type: transform
expression: "max(0, {{num(player.materials.{{recipe.ingredientA}}, 0)}})"
- id: have_b
type: transform
expression: "max(0, {{num(player.materials.{{recipe.ingredientB}}, 0)}})"
- id: enough_a
type: condition
check:
field: "{{have_a}}"
op: ">="
value: "{{need_a}}"
onFail:
status: 409
errorCode: MISSING_MATERIAL_A
message: "Need {{need_a}}x {{recipe.ingredientA}}."
- id: enough_b
type: condition
check:
field: "{{have_b}}"
op: ">="
value: "{{need_b}}"
onFail:
status: 409
errorCode: MISSING_MATERIAL_B
message: "Need {{need_b}}x {{recipe.ingredientB}}."
- id: output_qty
type: transform
expression: "{{recipe.outputQty}} * {{craft_qty}}"
- id: xp
type: transform
expression: "{{recipe.xpReward}} * {{craft_qty}}"
returns:
quantity: "{{craft_qty}}"
needA: "{{need_a}}"
needB: "{{need_b}}"
outputQty: "{{output_qty}}"
xp: "{{xp}}"
```
## Endpoint: `craft-item`
Create `endpoints/craft-item.endpoint.yml`:
```yml
sourceVersion: "1"
kind: endpoint
name: Craft Item
slug: craft-item
method: POST
enabled: true
input:
type: object
properties:
recipeId:
type: string
quantity:
type: number
required:
- recipeId
- quantity
steps:
- id: player
type: read
collection: player_crafting
key: "{{playerKey}}"
- id: recipe
type: lookup
source: values
table: crafting_recipes
where:
field: id
op: "=="
value: "{{input.recipeId}}"
- id: validation
type: workflow
workflow: crafting_validate_recipe
params:
player: "{{player}}"
recipe: "{{recipe}}"
quantity: "{{input.quantity}}"
- id: spend_a
type: transform
expression: "0 - {{validation.needA}}"
- id: spend_b
type: transform
expression: "0 - {{validation.needB}}"
- id: apply
type: write
collection: player_crafting
key: "{{playerKey}}"
ops:
- op: inc
path: "materials.{{recipe.ingredientA}}"
value: "{{spend_a}}"
- op: inc
path: "materials.{{recipe.ingredientB}}"
value: "{{spend_b}}"
- op: inc
path: "items.{{recipe.outputItem}}"
value: "{{validation.outputQty}}"
- op: inc
path: craftingXp
value: "{{validation.xp}}"
source: crafting
reason: "Crafted {{recipe.displayName}}"
- op: inc
path: lifetimeCrafts
value: "{{validation.quantity}}"
response:
status: 200
body:
ok: true
recipeId: "{{recipe.id}}"
outputItem: "{{recipe.outputItem}}"
outputQty: "{{validation.outputQty}}"
craftingXpAwarded: "{{validation.xp}}"
```
## Optional endpoint: gather materials
Create `endpoints/gather-materials.endpoint.yml` for P2P mining, harvesting, or loot pickups. The endpoint awards materials to the authenticated caller and clamps the amount per call.
```yml
sourceVersion: "1"
kind: endpoint
name: Gather Materials
slug: gather-materials
method: POST
enabled: true
input:
type: object
properties:
materialId:
type: string
amount:
type: number
source:
type: string
required:
- materialId
- amount
steps:
- id: safe_amount
type: transform
expression: "clamp(floor({{input.amount}}), 0, 1000)"
- id: positive
type: condition
check:
field: "{{safe_amount}}"
op: ">"
value: 0
onFail:
status: 400
errorCode: INVALID_AMOUNT
- id: apply
type: write
collection: player_crafting
key: "{{playerKey}}"
ops:
- op: inc
path: "materials.{{input.materialId}}"
value: "{{safe_amount}}"
source: material_grant
reason: "{{input.source}}"
response:
status: 200
body:
ok: true
materialId: "{{input.materialId}}"
amount: "{{safe_amount}}"
```
## C# call from s&box
```csharp
var crafted = await NetworkStorage.CallEndpoint( "craft-item", new
{
recipeId = "repair_kit",
quantity = 1
} );
if ( crafted.HasValue )
{
Log.Info( $"Crafted {crafted.Value.String( "outputItem" )}" );
}
```
## Recommended rate limits
| Rule | Collection | Field | Scope | Window | Limit | Action |
|------|------------|-------|-------|--------|-------|--------|
| `crafting_xp_daily` | `player_crafting` | `craftingXp` | per_player | perDay | 50000 | clamp |
| `material_grant_hourly` | `player_crafting` | `materials` | per_player | perHour | 100000 | flag |
For recipes with more than two ingredients, add `ingredientC`/`amountC` columns or move recipe ingredients into a separate table and call an internal flow per ingredient.