Bay 12 Games Forum

Please login or register.

Login with username, password and session length
Advanced search  

Author Topic: Milo's Reactions: Reaction hook code for doing cool things.  (Read 5494 times)

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile

This thread will (eventually) have lots of bits of Lua code for making reactions do unnatural acts :)


Next Up: Suggest something!

If you have a suggestion I want to hear it!
« Last Edit: July 20, 2015, 10:45:21 am by milo christiansen »
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #1 on: July 18, 2015, 10:52:04 am »

I'll start off with a (very simple) solution for making grown wood items:

Usage is simple, just place the following line in your raw/onload.init file with "MY_REACTION" replaced with your reaction name and "thisscript" replaced with whatever you name this script.

`modtools/reaction-product-trigger -reactionName MY_REACTION -command [ thisscript \\OUTPUT_ITEMS ]`

The script itself:
Code: [Select]
for _, id in ipairs({...}) do
local item = df.item.find(id)
if item ~= nil then
item.flags2.grown
end
end
« Last Edit: July 18, 2015, 10:55:17 am by milo christiansen »
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #2 on: July 20, 2015, 10:44:09 am »

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"):
Code: [Select]
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")
Code: [Select]
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")
Code: [Select]
_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.
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS

expwnent

  • Bay Watcher
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #3 on: July 21, 2015, 01:48:40 am »

I haven't read in detail but you may be interested in persist-table.
Logged

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #4 on: July 21, 2015, 04:42:52 pm »

Maybe, once it's actually in DFHack. My wrapper is just a way to make sure the key exists, simple and stupid, but it works.
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS

expwnent

  • Bay Watcher
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #5 on: July 22, 2015, 01:50:22 am »

Not sure what the antecedent of "it" is in that sentence, but the script I linked is in the current version of DFHack.
Logged

Meph

  • Bay Watcher
    • View Profile
    • worldbicyclist
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #6 on: July 22, 2015, 04:13:48 am »

Just to be clear: There is no difference in grown-wood items that the player might notice, with the only exception being that elven caravans accept them in trade. Correct?
Logged
::: ☼Meph Tileset☼☼Map Tileset☼- 32x graphic sets with TWBT :::
::: ☼MASTERWORK DF☼ - A comprehensive mod pack now on Patreon - 250.000+ downloads and counting :::
::: WorldBicyclist.com - Follow my bike tours around the world - 148 countries visited :::

Roses

  • Bay Watcher
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #7 on: July 22, 2015, 09:52:37 am »

persist-table is most definitely in. It's how I handle all of my class, civilization, and event systems, as well as spells and a large plethora of over things. Even made a script that allows dfhack.timeout to be persistent across save/reload.
Logged

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #8 on: July 22, 2015, 09:54:39 am »

Just to be clear: There is no difference in grown-wood items that the player might notice, with the only exception being that elven caravans accept them in trade. Correct?

You are correct! (well, they do have the "grown" prefix added to the item name)

persist-table is most definitely in. It's how I handle all of my class, civilization, and event systems, as well as spells and a large plethora of over things. Even made a script that allows dfhack.timeout to be persistent across save/reload.

I must have missed that, oh well.
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS

PeridexisErrant

  • Bay Watcher
  • Dai stihó, Hrasht.
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #9 on: July 29, 2015, 04:48:17 am »

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.

Awesome!  Any chance of getting this as a single script that can be called with "enable balanced-melting"?  If so I'd love to put it in the Starter Pack as an option, but for that it needs to be the kind of thing that can be cleanly enabled in the init file.
Logged
I maintain the DF Starter Pack - over a million downloads and still counting!
 Donations here.

milo christiansen

  • Bay Watcher
  • Something generic here
    • View Profile
Re: Milo's Reactions: Reaction hook code for doing cool things.
« Reply #10 on: July 30, 2015, 02:30:21 pm »

I could write a client that works that way, but you would still need to have the modules installed.

A word of warning: disabling the vanilla melt item job does not work due to a eventful bug, someone needs to fix that before you can really use this as a replacement in the vanilla furnace. I use this system in First Landing, but there I replace the furnace entirely with a set of custom workshops.
Logged
Rubble 8 - The most powerful modding suite in existence!
After all, coke is for furnaces, not for snorting.
You're not true dwarven royalty unless you own the complete 'Signature Collection' baby-bone bedroom set from NOKEAS