Melting metal items, source of one of the biggest free item exploits in the game, plus if you ever make a mod that changes smithing (which is possible via the wonders of DFHack) the situation can quickly become much worse.
My solution is simple: A melt item reaction hook that keeps track of fractional bars in much more detail, and provides a way to specify exact returns for every item type in the game (subtypes too!). Wafer metals receive special handling, so adamantine items melt without loss. Stacks are properly handled, so a stack of five coins melt the the same amount of metal five individual coins.
This comes in three parts:
Code to handle the reaction
Code to disable the vanilla melt job
An incidental module need for persistence
To install these scripts as intended you will need
this loader.
Reaction handler (Install to: "raw/dfhack/libs_dfhack_melt_item.start.lua"):
local eventful = require 'plugins.eventful'
local persist = rubble.require("libs_dfhack_persist")
-- This table is normally automatically generated by Rubble.
-- It is VERY IMPORTANT that the order of this table NOT be changed!
-- For items that are produced in multiples (boots, coins, bolts, etc) the costs listed must be for a single item!
itemSizes = {
-- ID (unused), bars, wafers, subtype table (optional)
{"BAR", 1, 1, nil},
{"SMALLGEM", 0, 0, nil},
{"BLOCKS", 1, 1, nil},
{"ROUGH", 0, 0, nil},
{"BOULDER", 0, 0, nil},
{"WOOD", 0, 0, nil},
{"DOOR", 3, 9, nil},
{"FLOODGATE", 3, 9, nil},
{"BED", 0, 0, nil},
{"CHAIR", 3, 9, nil},
{"CHAIN", 1, 4, nil},
{"FLASK", 0.333, 0.333, nil},
{"GOBLET", 0.333, 0.333, nil},
{"INSTRUMENT", 1, 1, nil},
{"TOY", 1, 1, nil},
{"WINDOW", 0, 0, nil},
{"CAGE", 3, 6, nil},
{"BARREL", 3, 9, nil},
{"BUCKET", 1, 3, nil},
{"ANIMALTRAP", 1, 3, nil},
{"TABLE", 3, 9, nil},
{"COFFIN", 3, 9, nil},
{"STATUE", 3, 9, nil},
{"CORPSE", 0, 0, nil},
{"WEAPON", 1, 3, {
-- subtype ID = bars, wafers
["ITEM_WEAPON_AXE_BATTLE"] = {1, 4},
["ITEM_WEAPON_PICK"] = {1, 4},
}},
{"ARMOR", 1, 1, {
["ITEM_ARMOR_BREASTPLATE"] = {3, 9},
["ITEM_ARMOR_MAIL_SHIRT"] = {2, 6},
}},
{"SHOES", 1, 1, {
["ITEM_SHOES_BOOTS"] = {0.5, 2},
["ITEM_SHOES_BOOTS_LOW"] = {0.5, 1},
}},
{"SHIELD", 1, 1, {
["ITEM_SHIELD_SHIELD"] = {1, 4},
["ITEM_SHIELD_BUCKLER"] = {1, 2},
}},
{"HELM", 1, 1, {
["ITEM_HELM_HELM"] = {1, 2},
["ITEM_HELM_CAP"] = {1, 1},
}},
{"GLOVES", 0.5, 2, nil},
{"BOX", 3, 9, nil},
{"BIN", 3, 9, nil},
{"ARMORSTAND", 3, 9, nil},
{"WEAPONRACK", 3, 9, nil},
{"CABINET", 3, 9, nil},
{"FIGURINE", 0.333, 0.333, nil},
{"AMULET", 0.333, 0.333, nil},
{"SCEPTER", 0.333, 0.333, nil},
{"AMMO", 0.04, 0.04, nil},
{"CROWN", 0.333, 0.333, nil},
{"RING", 0.333, 0.333, nil},
{"EARRING", 0.333, 0.333, nil},
{"BRACELET", 0.333, 0.333, nil},
{"GEM", 0, 0, nil},
{"ANVIL", 3, 9, nil},
{"CORPSEPIECE", 0, 0, nil},
{"REMAINS", 0, 0, nil},
{"MEAT", 0, 0, nil},
{"FISH", 0, 0, nil},
{"FISH_RAW", 0, 0, nil},
{"VERMIN", 0, 0, nil},
{"PET", 0, 0, nil},
{"SEEDS", 0, 0, nil},
{"PLANT", 0, 0, nil},
{"SKIN_TANNED", 0, 0, nil},
{"PLANT_GROWTH", 0, 0, nil},
{"THREAD", 0, 0, nil},
{"CLOTH", 0, 0, nil},
{"TOTEM", 0, 0, nil},
{"PANTS", 1, 1, {
["ITEM_PANTS_GREAVES"] = {2, 6},
["ITEM_PANTS_LEGGINGS"] = {1, 5},
}},
{"BACKPACK", 0, 0, nil},
{"QUIVER", 0, 0, nil},
{"CATAPULTPARTS", 0, 0, nil},
{"BALLISTAPARTS", 0, 0, nil},
{"SIEGEAMMO", 3, 4, nil},
{"BALLISTAARROWHEAD", 3, 4, nil},
{"TRAPPARTS", 1, 3, nil},
{"TRAPCOMP", 1, 1, {
["ITEM_TRAPCOMP_GIANTAXEBLADE"] = {1, 5},
["ITEM_TRAPCOMP_ENORMOUSCORKSCREW"] = {1, 5},
["ITEM_TRAPCOMP_SPIKEDBALL"] = {1, 4},
["ITEM_TRAPCOMP_LARGESERRATEDDISC"] = {1, 4},
["ITEM_TRAPCOMP_MENACINGSPIKE"] = {1, 5},
}},
{"DRINK", 0, 0, nil},
{"POWDER_MISC", 0, 0, nil},
{"CHEESE", 0, 0, nil},
{"FOOD", 0, 0, nil},
{"LIQUID_MISC", 0, 0, nil},
{"COIN", 0.002, 0.002, nil},
{"GLOB", 0, 0, nil},
{"ROCK", 0, 0, nil},
{"PIPE_SECTION", 3, 9, nil},
{"HATCH_COVER", 3, 9, nil},
{"GRATE", 3, 9, nil},
{"QUERN", 0, 0, nil},
{"MILLSTONE", 0, 0, nil},
{"SPLINT", 3, 2, nil},
{"CRUTCH", 3, 3, nil},
{"TRACTION_BENCH", 3, 9, nil},
{"ORTHOPEDIC_CAST", 0, 0, nil},
{"TOOL", 1, 1, {
["ITEM_TOOL_MINECART"] = {2, 6},
["ITEM_TOOL_WHEELBARROW"] = {2, 6},
["ITEM_TOOL_STEPLADDER"] = {2, 6},
}},
{"SLAB", 0, 0, nil},
{"EGG", 0, 0, nil},
{"BOOK", 0, 0, nil},
}
-- List your melt reactions here.
reactions = {
--"SMELTER_MELT_METAL_ITEM",
}
--[[
Example reaction code:
[REACTION:SMELTER_MELT_METAL_ITEM]
[NAME:melt metal item]
[BUILDING:SMELTER:CUSTOM_M]
[REAGENT:item_melt:1:NONE:NONE:NONE:NONE]
[PRODUCT:100:0:ROCK:NONE:INORGANIC:NONE]
[SKILL:SMELT]
[FUEL]
]]
local function createBar(mat)
local item = df['item_barst']:new()
item.id = df.global.item_next_id
df.global.world.items.all:insert('#',item)
df.global.item_next_id = df.global.item_next_id+1
item:setMaterial(mat.type)
item:setMaterialIndex(mat.index)
item:setMakerRace(df.global.ui.race_id)
item:categorize(true)
item:setDimension(150)
return item
end
function meltMetalItem(item)
local bars = {}
local mat = dfhack.matinfo.decode(item)
if mat == nil then
return {}
end
local matstring = mat.type.."|"..mat.index
local wafers = false
if mat.mode == "inorganic" then
wafers = mat.inorganic.flags.WAFERS
end
local item_type = item:getType()
local item_stype = item:getSubtype()
local item_mat_size = nil
if item_stype ~= -1 then
if itemSizes[item_type + 1][4] ~= nil then
if itemSizes[item_type + 1][4][item.subtype.id] ~= nil then
if wafers then
item_mat_size = itemSizes[item_type + 1][4][item.subtype.id][2]
else
item_mat_size = itemSizes[item_type + 1][4][item.subtype.id][1]
end
end
end
end
if item_mat_size == nil then
if wafers then
item_mat_size = itemSizes[item_type + 1][3]
else
item_mat_size = itemSizes[item_type + 1][2]
end
end
if item.stack_size > 1 then
item_mat_size = item_mat_size * item.stack_size
end
local product_number = 0
local extra_parts = 0
product_number, extra_parts = math.modf(item_mat_size)
-- Adjust extra_parts to be a positive number between 0 and 999
extra_parts, _ = math.modf(extra_parts * 1000)
-- Create the specified number of bars
if product_number > 0 then
for p = 1, product_number, 1 do
local bar = createBar(mat)
bar.flags.removed = false
table.insert(bars, bar)
end
end
local parts_table = persist.GetAsCode("libs_dfhack_melt_item")
-- Take care of any tail-ender bars
local existing_parts = 0
if parts_table ~= nil then
existing_parts = parts_table[matstring] or 0
else
parts_table = {}
end
local parts = existing_parts + extra_parts
-- 333 * 3 = 999
if parts >= 999 then
parts = parts - 1000
if parts < 0 then parts = 0 end
local bar = createBar(mat)
bar.flags.removed = false
table.insert(bars, bar)
end
parts_table[matstring] = parts
local out = "return {\n"
for k, v in pairs(parts_table) do
out = out..'\t["'..k..'"] = '..v..',\n'
end
out = out.."}"
persist.Save("libs_dfhack_melt_item", out)
--print("Melt item debug:")
--print(" Bars produced (before part calculations): "..product_number)
--print(" Parts left from last reaction: "..existing_parts)
--print(" Parts produced by this reaction: "..extra_parts)
--print(" Parts left from this reaction: "..parts)
return bars
end
-- This custom item melt reaction is a little more balanced than the vanilla one.
-- Instead of always producing a minimum of 1/10 of a bar a minimum of 1/1000 of a bar is produced.
-- Stacks of items are properly handled, a stack of 5 coins will produce exactly the same amount
-- of metal as 5 individual coins.
-- Bar returns are hard coded in a table instead of using a weird algorithm that has nothing to do
-- with the number of bars required to actually produce the item (which is what vanilla seems to do).
-- This makes most (if not all) melt-item exploits impossible.
-- Partial bars are shared globally by all smelters, so there is no need to restrict melting to one
-- smelter or anything like that. It is probably possible to use the hardcoded "melt_remainder" vector
-- for storing partial bars, but only for furnaces (and I need to use this with workshops).
function meltMetalItemHook(reaction, reaction_product, unit, in_items, in_reag, out_items, call_native)
call_native.value = false
for i = 0, #in_reag - 1, 1 do
if string.match(in_reag[i].code, '%_melt$') then
local bars = meltMetalItem(in_items[i])
for _, bar in ipairs(bars) do
out_items:insert('#', bar)
end
end
end
end
-- Register all melt reactions with eventful.
for _, r in ipairs(reactions) do
eventful.registerReaction(r, meltMetalItemHook)
end
-- This finds the melt reactions and sets them to only accept melt designated items.
for _, reaction in ipairs(df.global.world.raws.reactions) do
for _, r in ipairs(reactions) do
if reaction.code == r then
for i = 0, #reaction.reagents - 1, 1 do
if string.match(reaction.reagents[i].code, '%_melt$') then
reaction.reagents[i].flags2.melt_designated = true
reaction.reagents[i].flags2.allow_melt_dump = true
end
end
end
end
end
Vanilla job disabler (install to: "raw/dfhack/user_dfhack_melt_item.start.lua")
local eventful = require 'plugins.eventful'
-- This version of the script has some stuff related to powered workshops trimmed.
-- Remove the vanilla melt item job.
-- This does not appear to work in the current DFHack (40.24-r3), as this function is not called for
-- the smelter (maybe not any furnace).
eventful.postWorkshopFillSidebarMenu.User_DFHack_Melt_Item = function(wshop)
if wshop:getType() == df.building_type.Furnace then
if wshop:getSubtype() == df.furnace_type.Smelter or wshop:getSubtype() == df.furnace_type.MagmaSmelter then
local wjob = df.global.ui_sidebar_menus.workshop_job
for i = 0, #wjob.choices_all - 1, 1 do
if wjob.choices_all[i].job_type == df.job_type.MeltMetalObject then
wjob.choices_all:delete(i)
wjob.choices_visible:delete(i)
return
end
end
end
end
end
rubble.unloaders.User_DFHack_Melt_Item = function()
eventful.postWorkshopFillSidebarMenu.User_DFHack_Melt_Item = nil
end
Persistence wrapper module (install to: "raw/dfhack/libs_dfhack_persist.mod.lua")
_ENV = rubble.mkmodule("libs_dfhack_persist")
-- This is a simple and easy to use wrapper for the DFHack persistence API.
-- Basically these functions take care of ensuring the existence of the desired key.
-- Get the underlying structure from the persistence API.
function GetRaw(key)
local raw = dfhack.persistent.get(key)
if raw == nil then
raw, _ = dfhack.persistent.save({key = key})
end
return raw
end
-- Save a value using the persistence API.
function Save(key, value)
local raw = GetRaw(key)
raw.value = value
raw:save()
end
-- Get a value from the persistence API.
function Get(key)
return GetRaw(key).value
end
-- Get a value from the persistence API and run it as code (returning any return value).
-- Returns nil if there is an error when loading the code.
-- If there is an error it is logged to the DFHack console.
function GetAsCode(key)
local code = Get(key)
local f, err = load(code)
if f == nil then
dfhack.printerr(err)
return nil
end
return f()
end
return _ENV
This code is tested and working.