Configuration
Overview
shared/config.lua: Core settings, features, the recycler definition, recipes, upgrades, durability, fuel, fixed stationsshared/data/item_models.lua: 3D prop mappings for the sinking animationserver/config_server.lua: Discord webhooks and rate limiting
The recipes and upgrades shipped in shared/config.lua are working examples. Replace them with the items your server uses.
Most values can also be changed in game from the staff panel. See Staff Panel below.
General
1Config.Debug = false
2Config.Language = 'en'
3Config.MaxRecyclersPerPlayer = 1
4Config.DuiResolution = { width = 1920, height = 1080 }
5Config.InteractionDistance = 3.0
6Config.PlacementDistance = 5.0Debug: Console output for troubleshootingLanguage: Locale code. See thelocales/folder.MaxRecyclersPerPlayer: Maximum player-placed recyclers per characterDuiResolution: Render resolution of the screen on the prop.960x540is lighter,1920x1080is sharperInteractionDistance: Range in meters to interact with a placed machinePlacementDistance: Maximum distance from the player a new machine can be placed
Staff Panel
1Config.Admin = {
2 command = 'recyclers',
3 acePermission = nil,
4 minimizeKey = 'F9',
5}command: Chat command that opens the panel.falseremoves itacePermission: ACE checked before opening, such as'ml_recycler.admin'.niluses the framework admin permissionminimizeKey: Key that hides the open panel and brings it back. F1 to F12, or one letter or digit
The panel has six tabs: Map, List, Stations, Recipes, Upgrades and Settings. Changes made in Stations, Recipes, Upgrades and Settings are stored in the ml_recycler_config table and apply without a restart. Each of those tabs has a Reset to config button that drops the panel values.
A value saved in the panel replaces the same value from shared/config.lua until it is reset from the panel. Editing the file does not change a value the panel already overrides.
Debug, language, theme, screen resolution, camera, sounds, sinking items, rarity colors, prop and screen distances, the base prop list of PlacementRestriction, the ml_fuel jerrycan settings and the zombie resource name are read only from shared/config.lua.
The panel refuses changes that would cost players items: deleting a recipe while that item is processing, deleting an upgrade that is installed or required by another upgrade, and any change to slots or upgrades that would hide slots holding items.
Placement Rules
1Config.PlacementRestriction = {
2 enabled = false,
3 radius = 25.0,
4 requiredProps = {},
5}
6
7Config.PickupCooldown = {
8 enabled = true,
9 seconds = 300,
10}PlacementRestriction.enabled: Whentrue, a recycler can only be placed withinradiusmeters of one of the prop model hashes inrequiredProps. The placement preview checks it on the clientPickupCooldown.enabled: locks pickup forsecondsafter placing
Feature Toggles
1Config.Features = {
2 bulkRecycling = true,
3 queueSystem = true,
4 fuelSystem = true,
5 breakdownAnimations = true,
6 ambientSounds = true,
7 propSinking = true,
8 lootReveal = true,
9 upgradeSystem = true,
10 durabilitySystem = true,
11}bulkRecycling: Insert a stack of the same item in one actionqueueSystem: Items queue when all slots are busy and auto-promote as slots free upfuelSystem: Fuel is consumed while processing. Whenfalse, the fuel bar is hidden and refuel/consumption are disabledbreakdownAnimations: Particle effects during processingambientSounds: 3D loop sound through xsound while runningpropSinking: Inserted item models sink into the slot as they processlootReveal: after a collection, show the loot reveal with the collector's name to nearby playersupgradeSystem: Allow upgrade installationdurabilitySystem: Degradation, slowdown, malfunction and repair
Sound, Camera and Sinking Items
1Config.SoundDefaults = {
2 file = './sounds/recycler_loop.ogg',
3 volume = 0.6,
4 maxDistance = 30.0,
5}
6
7Config.Camera = {
8 offsetX = 0.15,
9 offsetY = -1.02,
10 offsetZ = 0.60,
11 lookZ = -0.64,
12 fov = 51.0,
13}
14
15Config.PropSinking = {
16 startZ = 0.45,
17 endZ = -0.1,
18 propScale = 0.8,
19 rotationSpeed = 8.0,
20 fallbackProp = 'hei_prop_heist_box',
21 slotOffsets = {
22 [1] = { x = -0.10, y = 0.10 },
23 [2] = { x = 0.10, y = 0.10 },
24 [3] = { x = -0.10, y = -0.10 },
25 [4] = { x = 0.10, y = -0.10 },
26 [5] = { x = 0.00, y = 0.15 },
27 [6] = { x = 0.00, y = -0.15 },
28 },
29 itemModels = {},
30}SoundDefaults: loop file, volume and hearing distance of the ambient soundCamera: position of the camera that zooms on the screen, relative to the machine, and its field of viewstartZ,endZ: start and end height of an item sinking into a slotpropScale,rotationSpeed: size and spin of the sinking itemfallbackProp: model used when an item has no model inshared/data/item_models.luaslotOffsets: position of each slot on the machineitemModels: leave empty. Item models are set inshared/data/item_models.lua
Loot Collection
1Config.Queue = {
2 AllowSteal = true,
3}AllowSteal:truelets any player who can use the recycler collect its loot. Setfalseto reserve each slot's results for the character who deposited the items.
This setting applies to player-placed recyclers and fixed stations. Reservations survive reconnects and resource restarts.
The screen shows a lock on ready loot reserved for another character. Machine access permissions still apply.
Deposits without a recorded owner stay public.
The Recycler
1Config.Recycler = {
2 label = _L('recycler_label'),
3 item = 'recycler',
4 baseSlots = 3,
5 maxSlots = 6,
6 processSpeedMultiplier = 1.0,
7 spawnDistance = 50.0,
8 screenDistance = 10.0,
9}label: Name shown in the UI and notificationsitem: Inventory item used to place the recyclerbaseSlots: Slots available before upgrades (each slot processes one item)maxSlots: Absolute slot cap. Base plus a slot-expansion upgrade can never exceed thisprocessSpeedMultiplier: Base speed.1.0is normal, below1.0is fasterspawnDistance: Distance at which the prop streams in for nearby playersscreenDistance: Distance at which the screen becomes visible
Recipes can gate outputs and bonuses behind a recycler tier. Every machine runs at tier 1 by default. An upgrade with effects = { tier = 2 } raises the machine it is installed on, and tier = 2 on Config.Recycler raises every machine. The highest value applies.
Fuel
Read only when Config.Features.fuelSystem = true.
1Config.Fuel = {
2 capacity = 100,
3 perMinute = 2,
4 items = {
5 ['industrial_battery'] = 25,
6 ['fuel_canister'] = 50,
7 },
8 mlFuel = {
9 enabled = true,
10 hintItems = {},
11 acceptedFuelTypes = { 'diesel' },
12 typeLabels = { ['diesel'] = 'Diesel', ['fuel'] = 'Gasoline' },
13 litersToUnits = 5,
14 },
15}capacity: Tank size in units. Theextra_tankupgrade raises itperMinute: Units burned per minute of active processingitems: One-shot fuel items.['item'] = units. The item is removed on usemlFuel.enabled: Accept reusable jerrycans fromml_fuel. Any item carryingmetadata.fuelAmountandmetadata.fuelTypeis treated as a jerrycan: it is kept in the inventory and only emptied, never consumedmlFuel.acceptedFuelTypes: Fuel types the machine accepts. Must match keys inml_fuelConfig.FuelTypesmlFuel.typeLabels: UI label per fuel typemlFuel.litersToUnits: How many recycler fuel units one liter ofml_fuelbecomes
Recipes
The key is the input item. Each recipe defines the time and the possible outputs.
1Config.RecycleRecipes = {
2 ['broken_phone'] = {
3 baseTime = 20,
4 outputs = {
5 { item = 'glass', min = 1, max = 2, chance = 100 },
6 { item = 'copper', min = 1, max = 2, chance = 60 },
7 { item = 'microchip', min = 1, max = 1, chance = 15, tierRequired = 3 },
8 },
9 tierBonuses = {
10 [2] = { chanceBoost = 10, quantityBoost = 1 },
11 [3] = { chanceBoost = 25, quantityBoost = 2 },
12 },
13 },
14}baseTime: processing time in seconds. Effective time isbaseTime * count * processSpeedMultiplierwithbulkRecycling, then upgrade and durability modifiersoutputs: Possible results. Each hasitem,min/maxquantity, andchance(0-100). AddtierRequired = 2to gate an output behind a recycler tier, andraritywith a key ofConfig.Rarityto color it (default'common')tierBonuses: Applied automatically by recycler tier.chanceBoostadds chance points to every output,quantityBoostadds to each outputmax
Rarity
Colors and labels for the loot preview.
1Config.Rarity = {
2 common = { color = '#9ca3af', label = _L('rarity_common') },
3 uncommon = { color = '#22c55e', label = _L('rarity_uncommon') },
4 rare = { color = '#3b82f6', label = _L('rarity_rare') },
5 epic = { color = '#a855f7', label = _L('rarity_epic') },
6 legendary = { color = '#f59e0b', label = _L('rarity_legendary') },
7}Upgrades
1Config.MaxUpgradesPerRecycler = 3
2
3Config.Upgrades = {
4 ['turbo_motor'] = { label = _L('upgrade_turbo_motor'), item = 'turbo_motor', icon = 'turbo_motor', effects = { processSpeedBonus = -0.20 } },
5 ['extra_tank'] = { label = _L('upgrade_extra_tank'), item = 'extra_tank', icon = 'extra_tank', effects = { fuelCapacityBonus = 0.50 } },
6 ['silencer_kit'] = { label = _L('upgrade_silencer_kit'), item = 'silencer_kit', icon = 'silencer_kit', effects = { soundDistanceMultiplier = 0.40 } },
7 ['solar_panel'] = { label = _L('upgrade_solar_panel'), item = 'solar_panel', icon = 'solar_panel', effects = { fuelRegenPerMinute = 1 } },
8}MaxUpgradesPerRecycler: Upgrade slots per machineeffects.processSpeedBonus: Multiplier change to processing time.-0.20is 20% fastereffects.fuelCapacityBonus: Fraction added to tank size.0.50is +50%effects.soundDistanceMultiplier: multiplies the zombie noise radius.0.40makes it 60% smallereffects.fuelRegenPerMinute: Passive fuel units generated per minuteeffects.slotBonus: Extra slots, capped atmaxSlotseffects.tier: Machine tier while installed. Unlocks outputs withtierRequiredand the matchingtierBonusesrequires: Upgrade key, or list of keys, that must be installed first. Example:requires = 'slot_expansion_1'. Removal is refused while another installed upgrade requires it
Upgrades are consumed on install and returned to the player on removal, carrying their state. To make a slot-expansion module, add an upgrade with effects = { slotBonus = N }, give it an item, and the machine unlocks N extra slots up to maxSlots. The upgrade_slot_expansion label is already in the locale files.
Upgrades are permanent by default. Add a wear table to an upgrade definition to give it a durability curve that drains on use and auto-removes the module at zero:
1['turbo_motor'] = {
2 label = _L('upgrade_turbo_motor'), item = 'turbo_motor', icon = 'turbo_motor',
3 effects = { processSpeedBonus = -0.20 },
4 wear = { enabled = true, startDurability = 100, degradePerProcess = 1 },
5}Durability
1Config.Durability = {
2 degradePerProcess = 2,
3 slowdownThreshold = 50,
4 slowdownMultiplier = 1.20,
5 malfunctionThreshold = 25,
6 malfunctionChance = 15,
7 malfunctionCooldown = 600,
8 brokenThreshold = 0,
9}degradePerProcess: Durability points lost per completed slotslowdownThreshold: Below this percent, processing runs atslowdownMultipliertimemalfunctionThreshold: at or below this percent, inserting items can fail with a jammalfunctionChance: Malfunction probability (0-100)malfunctionCooldown: seconds after a malfunction during which the machine cannot jam againbrokenThreshold: At this value the machine stops until repaired
Repair Items
1Config.RepairItems = {
2 ['repair_kit'] = { restore = 100, label = _L('repair_item_repair_kit') },
3 ['patch_kit'] = { restore = 50, label = _L('repair_item_patch_kit') },
4 ['lubricant'] = { restore = 25, label = _L('repair_item_lubricant') },
5}restore: Durability points the item restores on use
Zombie Noise
1Config.ZombieNoise = {
2 enabled = false,
3 resourceName = 'ml_zombies',
4 defaultRadius = 50.0,
5 defaultDuration = 0,
6}enabled: A running machine attracts zombies withindefaultRadiusresourceName: Zombie resource to signal. The hook only fires if it is starteddefaultDuration:0keeps the noise active until processing stops
The silencer_kit upgrade multiplies defaultRadius down through soundDistanceMultiplier.
Fixed Stations
Permanent recyclers you place in the world. Players cannot pick them up.
1Config.FixedStations = {
2 {
3 id = 'unique_station_id',
4 coords = vec3(0.0, 0.0, 0.0),
5 heading = 0.0,
6 infiniteFuel = false,
7 infiniteDurability = false,
8 permanentUpgrades = { 'turbo_motor', 'silencer_kit' },
9 },
10}id: Unique station id, stored in the databaseinfiniteFuel: Whentrue, this station never runs out of fuelinfiniteDurability: Whentrue, this station never wears down: no slowdown, no malfunctions, no repair items needed. Player-placed recyclers keep degrading normallypermanentUpgrades: Upgrades installed for good. Players cannot remove them and the wear system ignores them
Stations can also be placed, moved, renamed and removed from the Stations tab of the staff panel. Removing a station that comes from Config.FixedStations hides it permanently without editing the file; place a new station from the panel to use the spot again.
Permissions
1Config.DefaultPermissions = {
2 onlyOwnerCanUse = false,
3 onlyOwnerCanRemove = true,
4 onlyOwnerCanRefuel = false,
5 onlyOwnerCanUpgrade = false,
6}- Applies to every player-placed recycler. Fixed stations ignore these permissions: anyone can use, refuel and upgrade them, and nobody can pick them up
UI Theme
1Config.UITheme = {
2 preset = nil,
3 colors = {
4 primary = nil,
5 accent = nil,
6 background = nil,
7 surface = nil,
8 text = nil,
9 },
10}preset: theme preset of the panels.nilfollows the shared theme set inml_bridgecolors: CSS hex values for single colors. Anynilvalue follows the shared theme
Discord Webhooks
1ServerConfig.DiscordWebhook = ''
2ServerConfig.RecyclingWebhook = ''
3ServerConfig.PlacementWebhook = ''
4ServerConfig.ErrorWebhook = ''
5ServerConfig.ExploitsWebhook = ''
6ServerConfig.AdminWebhook = ''
7ServerConfig.RateLimitCooldown = 1000DiscordWebhook: Fallback webhook used by any channel left emptyRecyclingWebhook: Recycling started, collected, refueled, repaired, upgrade install/removePlacementWebhook: Recycler placed and picked upErrorWebhook: Database failures and rollback eventsExploitsWebhook: Rate-limit spam, payload type mismatches, distance and origin checksAdminWebhook: Staff panel actions and config changes made in the panelRateLimitCooldown: Minimum milliseconds between actions per player
Leave a channel empty to use DiscordWebhook. Leave all of them empty to disable logging.